├── .gitignore
├── Q&A.md
├── README.md
├── client
├── index.html
└── main.js
└── server
├── pom.xml
└── src
└── main
├── java
└── fr
│ └── mabreizh
│ └── webrtc
│ └── signaling
│ ├── Application.java
│ ├── conf
│ └── WebSocketConfig.java
│ ├── model
│ └── SignalMessage.java
│ └── socket
│ └── SignalingSocketHandler.java
└── resources
└── application.yml
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | target
3 | *.iml
4 |
--------------------------------------------------------------------------------
/Q&A.md:
--------------------------------------------------------------------------------
1 | # Questions and answers
2 |
3 | #### Which share of web users will this implementation address? Should it be increased and how?
4 |
5 | This implementation use WebSocket and of course, the client, webbrowser should be compatible with WebSocket.
6 | According to [caniuse](http://caniuse.com/#feat=websockets), it's widely avalaible now.
7 |
8 | Nevertheless, we could increase it using a Comet fallback mechanism with socket.io.
9 |
10 | But, our purpose is webRTC, so we need it to be available in clients. When RTC is available, WebSocket is. So I think we're goot with WebSockets.
11 |
12 | #### How many users can connect to one server?
13 |
14 | We use 1 webSocket per user so the limit is the maximum number of alive webSocket on the machine, so the max tcp connection, so the max open filedescriptor.
15 |
16 | #### How can the system support more systems?
17 | I guess the question is about more users. More users means more machines and so load balancing.
18 | A problem occur when A and B are on differents nodes (Node 1 and Node 2). Node 1 have no idea of peer B existence. A message queue could be an helpfull solution. Node 1 send the message for B in the queue and the other nodes will receive and have the opportnuity to treat the message.
19 | #### How to reduce the attack surface of the systems?
20 |
21 |
22 | #### Should an authentication mechanism be put in place and if yes, how?
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WebRTC signaling server POC
2 |
3 | This is my own signaling server implementation using Spring boot and WebSockets
4 |
5 | ## How-to
6 |
7 | ### Launch the server the Docker way
8 |
9 | You can launch the server with this one-liner. The server will then listen to port 8080.
10 |
11 | ```
12 | docker run -d \
13 | --name server \
14 | -v ~/.m2:/root/.m2 \
15 | -v $(pwd)/server:/usr/src/app \
16 | -w /usr/src/app \
17 | -p 8080:8080 \
18 | maven:3.3.3-jdk-8 \
19 | mvn clean spring-boot:run
20 | ```
21 |
22 | NB: this launch a maven image (building tool) with a maven plugin to launch the spring-boot standalone tomcat. This is obviously not production ready.
23 |
24 | ### Launch the server the maven way
25 |
26 | The server will then listen to port 8080.
27 |
28 | ```
29 | mvn -f server/pom.xml clean spring-boot:run
30 | ```
31 |
32 | ### Test with the client (the docker way)
33 |
34 | First launch a small nginx server with client source
35 | ```
36 | docker run -d \
37 | --name client \
38 | -v $PWD/client:/usr/share/nginx/html \
39 | -p 8081:80 \
40 | nginx
41 | ```
42 |
43 | Then in a browser open 2 tabs to http://localhost:8081
44 |
45 | In the Chrome console (ctrl+shift+I), in the second tab, type
46 | ```
47 | connect("B");
48 | ```
49 | It will register the client to the signal server, opening a websocket between them
50 |
51 | In the Chrome console, in first tab, type
52 | ```
53 | connect("A");
54 | startRTC();
55 | offer("B");
56 | ```
57 | It will register A, initiate a RTCConnection and ask signal server to connect to B.
58 |
59 | If you go to the second tab you should see 2 video stream, local below remote ("A"). Of course it's the same stream if you are on only one machine.
60 |
61 |
62 | ### Clean up
63 |
64 | ```
65 | docker rm -vf server client
66 | ```
67 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WebRTC client
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/client/main.js:
--------------------------------------------------------------------------------
1 |
2 | navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
3 | var loggedIn = false;
4 | var configuration = {
5 | 'iceServers': [{
6 | 'url': 'stun:stun.example.org'
7 | }]
8 | };
9 | var pc;
10 | var peer;
11 |
12 | function logError(error) {
13 | console.log(error.name + ': ' + error.message);
14 | }
15 |
16 | function connect(username) {
17 | console.log('connect');
18 | var loc = window.location
19 | var uri = "ws://" + loc.hostname + ":8080/signal"
20 | sock = new WebSocket(uri);
21 |
22 | sock.onopen = function(e) {
23 | console.log('open', e);
24 | sock.send(
25 | JSON.stringify(
26 | {
27 | type: "login",
28 | data: username
29 | }
30 | )
31 | );
32 | // should check better here, it could have failed
33 | // moreover not logout implemented
34 | loggedIn = true;
35 | }
36 |
37 | sock.onclose = function(e) {
38 | console.log('close', e);
39 | }
40 |
41 | sock.onerror = function(e) {
42 | console.log('error', e);
43 | }
44 |
45 | sock.onmessage = function(e) {
46 | console.log('message', e.data);
47 | if (!pc) {
48 | startRTC();
49 | }
50 |
51 | var message = JSON.parse(e.data);
52 | if (message.type === 'rtc') {
53 | if (message.data.sdp) {
54 | pc.setRemoteDescription(
55 | new RTCSessionDescription(message.data.sdp),
56 | function () {
57 | // if we received an offer, we need to answer
58 | if (pc.remoteDescription.type == 'offer') {
59 | peer = message.dest;
60 | pc.createAnswer(localDescCreated, logError);
61 | }
62 | },
63 | logError);
64 | }
65 | else {
66 | pc.addIceCandidate(new RTCIceCandidate(message.data.candidate));
67 | }
68 | }
69 | }
70 |
71 | //setConnected(true);
72 | }
73 |
74 | function startRTC() {
75 | pc = new webkitRTCPeerConnection(configuration);
76 |
77 | // send any ice candidates to the other peer
78 | pc.onicecandidate = function (evt) {
79 | if (evt.candidate) {
80 | sendMessage(
81 | {
82 | type: "rtc",
83 | dest: peer,
84 | data: {
85 | 'candidate': evt.candidate
86 | }
87 | }
88 | );
89 | }
90 | };
91 |
92 | // once remote stream arrives, sho480w it in the remote video element
93 | pc.onaddstream = function (evt) {
94 | remoteView.src = URL.createObjectURL(evt.stream);
95 | };
96 |
97 | // get a local stream, show it in a self-view and add it to be sent
98 | navigator.getUserMedia({
99 | 'audio': true,
100 | 'video': true
101 | }, function (stream) {
102 | selfView.src = URL.createObjectURL(stream);
103 | pc.addStream(stream);
104 | }, logError);
105 |
106 | }
107 |
108 | function offer(dest) {
109 | peer = dest;
110 | pc.createOffer(localDescCreated, logError);
111 | }
112 |
113 | function localDescCreated(desc) {
114 | pc.setLocalDescription(desc, function () {
115 | // ici en voyé un obj {type: offer, dest: B, data: desc}
116 | sendMessage(
117 | {
118 | type: "rtc",
119 | dest: peer,
120 | data: {
121 | 'sdp': pc.localDescription
122 | }
123 | }
124 | );
125 | }, logError);
126 | };
127 |
128 | function sendMessage(payload) {
129 | sock.send(JSON.stringify(payload));
130 | }
131 |
132 | function disconnect() {
133 | console.log('disconnect');
134 | if(sock != null) {
135 | sock.close();
136 | }
137 | setConnected(false);
138 | }
139 |
--------------------------------------------------------------------------------
/server/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | fr.mabreizh
8 | webrtc-signaling-server
9 | 1.0-SNAPSHOT
10 |
11 |
12 | org.springframework.boot
13 | spring-boot-starter-parent
14 | 1.4.0.RELEASE
15 |
16 |
17 |
18 |
19 | org.springframework.boot
20 | spring-boot-starter-websocket
21 |
22 |
23 |
24 |
25 |
26 |
27 | org.springframework.boot
28 | spring-boot-maven-plugin
29 |
30 |
31 |
32 | repackage
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/server/src/main/java/fr/mabreizh/webrtc/signaling/Application.java:
--------------------------------------------------------------------------------
1 | package fr.mabreizh.webrtc.signaling;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.web.socket.config.annotation.EnableWebSocket;
6 |
7 | /**
8 | * @author Guillaume Gerbaud
9 | */
10 | @SpringBootApplication
11 | @EnableWebSocket
12 | public class Application {
13 |
14 | public static void main(String[] args) {
15 | SpringApplication.run(Application.class, args);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/server/src/main/java/fr/mabreizh/webrtc/signaling/conf/WebSocketConfig.java:
--------------------------------------------------------------------------------
1 | package fr.mabreizh.webrtc.signaling.conf;
2 |
3 | import fr.mabreizh.webrtc.signaling.socket.SignalingSocketHandler;
4 | import org.springframework.context.annotation.Bean;
5 | import org.springframework.context.annotation.Configuration;
6 | import org.springframework.web.socket.WebSocketHandler;
7 | import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
8 | import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
9 |
10 | /**
11 | * @author Guillaume Gerbaud
12 | */
13 | @Configuration
14 | public class WebSocketConfig implements WebSocketConfigurer {
15 |
16 | @Override
17 | public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
18 | webSocketHandlerRegistry
19 | // handle on "/signal" endpoint
20 | .addHandler(signalingSocketHandler(), "/signal")
21 | // Allow cross origins
22 | .setAllowedOrigins("*");
23 | }
24 |
25 | @Bean
26 | public WebSocketHandler signalingSocketHandler() {
27 | return new SignalingSocketHandler();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/server/src/main/java/fr/mabreizh/webrtc/signaling/model/SignalMessage.java:
--------------------------------------------------------------------------------
1 | package fr.mabreizh.webrtc.signaling.model;
2 |
3 | /**
4 | * @author Guillaume Gerbaud
5 | */
6 | public class SignalMessage {
7 |
8 | private String type;
9 | private String dest;
10 | private Object data;
11 |
12 | public String getType() {
13 | return type;
14 | }
15 |
16 | public void setType(String type) {
17 | this.type = type;
18 | }
19 |
20 | public String getDest() {
21 | return dest;
22 | }
23 |
24 | public void setDest(String dest) {
25 | this.dest = dest;
26 | }
27 |
28 | public Object getData() {
29 | return data;
30 | }
31 |
32 | public void setData(Object data) {
33 | this.data = data;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/server/src/main/java/fr/mabreizh/webrtc/signaling/socket/SignalingSocketHandler.java:
--------------------------------------------------------------------------------
1 | package fr.mabreizh.webrtc.signaling.socket;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import fr.mabreizh.webrtc.signaling.model.SignalMessage;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.web.socket.TextMessage;
8 | import org.springframework.web.socket.WebSocketSession;
9 | import org.springframework.web.socket.handler.TextWebSocketHandler;
10 |
11 | import java.util.HashMap;
12 | import java.util.Map;
13 |
14 | /**
15 | * @author Guillaume Gerbaud
16 | */
17 | public class SignalingSocketHandler extends TextWebSocketHandler {
18 |
19 | private static final Logger LOG = LoggerFactory.getLogger(SignalingSocketHandler.class);
20 |
21 | private static final String LOGIN_TYPE = "login";
22 | private static final String RTC_TYPE = "rtc";
23 |
24 |
25 | // Jackson JSON converter
26 | private ObjectMapper objectMapper = new ObjectMapper();
27 |
28 | // Here is our Directory (MVP way)
29 | // This map saves sockets by usernames
30 | private Map clients = new HashMap();
31 | // Thus map saves username by socket ID
32 | private Map clientIds = new HashMap();
33 |
34 | @Override
35 | protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
36 | LOG.debug("handleTextMessage : {}", message.getPayload());
37 |
38 | SignalMessage signalMessage = objectMapper.readValue(message.getPayload(), SignalMessage.class);
39 |
40 | if (LOGIN_TYPE.equalsIgnoreCase(signalMessage.getType())) {
41 | // It's a login message so we assume data to be a String representing the username
42 | String username = (String) signalMessage.getData();
43 |
44 | WebSocketSession client = clients.get(username);
45 |
46 | // quick check to verify that the username is not already taken and active
47 | if (client == null || !client.isOpen()) {
48 | LOG.debug("Login {} : OK", username);
49 | // saves socket and username
50 | clients.put(username, session);
51 | clientIds.put(session.getId(), username);
52 | } else {
53 | LOG.debug("Login {} : KO", username);
54 | }
55 |
56 | } else if (RTC_TYPE.equalsIgnoreCase(signalMessage.getType())) {
57 |
58 | // with the dest username, we can find the targeted socket, if any
59 | String dest = signalMessage.getDest();
60 | WebSocketSession destSocket = clients.get(dest);
61 | // if the socket exists and is open, we go on
62 | if (destSocket != null && destSocket.isOpen()) {
63 |
64 | // We write the message to send to the dest socket (it's our propriatary format)
65 |
66 | SignalMessage out = new SignalMessage();
67 | // still an RTC type
68 | out.setType(RTC_TYPE);
69 | // we use the dest field to specify the actual exp., but it will be the next dest.
70 | out.setDest(clientIds.get(session.getId()));
71 | // The data stays as it is
72 | out.setData(signalMessage.getData());
73 |
74 | // Convert our object back to JSON
75 | String stringifiedJSONmsg = objectMapper.writeValueAsString(out);
76 |
77 | LOG.debug("send message {}", stringifiedJSONmsg);
78 |
79 | destSocket.sendMessage(new TextMessage(stringifiedJSONmsg));
80 | }
81 | }
82 |
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/server/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | logging:
2 | level:
3 | ROOT: INFO
4 | fr.mabreizh: DEBUG
--------------------------------------------------------------------------------