├── .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 --------------------------------------------------------------------------------