diff --git a/README.md b/README.md index fa0a31b..c28f308 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ An 'as simple as it gets' WebRTC example. See [https://shanetully.com/2014/09/a-dead-simple-webrtc-example/](https://shanetully.com/2014/09/a-dead-simple-webrtc-example/) for a detailed walkthrough of the code. +Note: This repo is kept updated. The general ideas are there, but the above blog post may be somewhat out of date with the code in this repo. + ### Usage The signaling server uses Node.js and `ws` and can be started as such: @@ -16,9 +18,15 @@ $ npm install ws $ node server/server.js ``` -With the client running, open `client/index.html` in a recent version of either Firefox or Chrome. +With the server running, open a recent version of Firefox or Chrome and visit `https://localhost:8443`. Note the HTTPS! There is no redirect from HTTP to HTTPS! -Note that if using Chrome and opening the file locally, you must run Chrome with the `--allow-file-access-from-files` flag. Or you could serve the files with a webserver (Python's SimpleHTTPServer is a good option). +### TLS + +Recent versions of Chrome require secure websockets for WebRTC. Thus, this example utilizes HTTPS. Included is a self-signed certificate that must be accepted in the browser for the example to work. + +### Problems? + +WebRTC is a rapidly evolving beast. Being an example that I don't check often, I rely on users for reports if something breaks. Issues and pull requests are greatly appreciated. ### License diff --git a/cert.pem b/cert.pem new file mode 100644 index 0000000..bd5ad93 --- /dev/null +++ b/cert.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFhTCCA22gAwIBAgIJALqPiXsVwZGuMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNjA1MDMwODA1 +NDdaFw0xNzA1MDMwODA1NDdaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21l +LVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV +BAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMu4 +8c4IwR1pO7N0H1D1PFFOgveRtLZu7vk8iEjsjAf+DD8CGpZ9BeV1/D4cMmn5Q2bp +y3UpwIiUeuz8fGVN3hhR3vaY2JS97qTK5+83pP09gperD5/+yZqT6CLuRoh6CYp0 +vEHD5CyfUK9HSRhlKVeAnWYrIWD0krN1WRrgZVIcvzlV/pmbj1aKuQ1+cXJxAEfj +2/rhWTZf15hNu2fLWXemBeqQZTMOAl55CFVx5r7wPyYw4i9I2az9aj/fQRPjMQDV +j/d5bdF68t5hxIf5wk/gGo8XBMBmo0uAj60Qp81wHirEr2FFMK7I+48XMFA9SfxR +SahVZhjbz58K0+URD/taJewL3qrkoivJGQfL9waF+NlLJ8DoGQLN/tWNX9nsQIwu +HBijg7z4hDP/cTm2dzIyxaSpSp904r3SE74n/+jI+BOWmZwo3iRJMKzWhx80VtM4 +qXZyHUrxCSf0Q1yqSlcyNAVpe3fCpGO9hR6ao3MX6xx3fgdQ1LUcNyZbmHFWPZ0N +pp+oI8kx+9ksMMtcCJq4KGgoLaCEtJjf4a0TDiCL1eNnym79YSto2hQc8cyvc4my +kerRAy9mcvmpd/e7j/eKQikNi/usWxGbeIOiy/5zG2Xrf9S2fNsc4/TdnLn+OLJX +5pJ4AuVIxkA/zHsE0kcANssoJk6Hgxxek6GizytdAgMBAAGjUDBOMB0GA1UdDgQW +BBSP8O2pIi6niJ4w0gHXR6PzawwRrDAfBgNVHSMEGDAWgBSP8O2pIi6niJ4w0gHX +R6PzawwRrDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQCkxybb3ci2 +NjbzjmvPflqmdTwyGOvreouCXmVHvm5vNL69LSOgZ6PmyEpyywALJqhaEhCXLJrq +yyZ82My9TxYn84rukH20lyBgbjIMFnabamFAOB9LqMqM4jaedT00OEbfY+uEE5Nw +NwOjNdUrrwfD2PQLAF06Orft4wex/L9psHBHaxWybnPkx+UAlA/Moz8kmCWWq9SA +JPvFNXwN3WylgCgAG8kR9PYtLWQEFSp8D9lSezI1C8Ddk0ymTw4aF+p9gHbbjZw4 +XEEpKgRwhpHa7IrcVyeXdQ78/pYc1f7fThL88LLv7lwtJnDWqWYOJrO4HH064IKn +KazLz1v9UT6yegUFiP/y688BH9LTNNf4pxGOee7LiDpfc7wyIkwrAjxoy4gc8xjp +Gp6aAvLC9Wzc+i2P0Hz/CvS5ffhZ9k0XGIvSHGbMRN1G4l9cOEQadyzGZGUryFRC +yBvXGBfSEYdr4OWOhdrelwpZXvl8rLRaAKU2xxIYZT9JtAXmTjWRFnjYLAuJil7e +ywPbGFYsB5d9Buiw+rJLnGF4NOudk1wXBElrhdjoajDS3Miw5vn2TbuHxIleoszC +fojNr80RenHT+EnY+0MecCUaeKfU4xd9nIl9I/1MpvM84zSu7Rhd0xEli6SN3fen +8zRCZGBfwD1y21dDrpgWDPFxjc5Cas63bg== +-----END CERTIFICATE----- diff --git a/client/index.html b/client/index.html index 457cdf4..8282b47 100755 --- a/client/index.html +++ b/client/index.html @@ -1,6 +1,7 @@
+ diff --git a/client/webrtc.js b/client/webrtc.js index 725eff6..ab1ce39 100755 --- a/client/webrtc.js +++ b/client/webrtc.js @@ -1,18 +1,22 @@ var localVideo; var remoteVideo; var peerConnection; -var peerConnectionConfig = {'iceServers': [{'url': 'stun:stun.services.mozilla.com'}, {'url': 'stun:stun.l.google.com:19302'}]}; +var uuid; -navigator.getUserMedia = navigator.getUserMedia || navigator.mozGetUserMedia || navigator.webkitGetUserMedia; -window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; -window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate; -window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription; +var peerConnectionConfig = { + 'iceServers': [ + {'urls': 'stun:stun.services.mozilla.com'}, + {'urls': 'stun:stun.l.google.com:19302'}, + ] +}; function pageReady() { + uuid = uuid(); + localVideo = document.getElementById('localVideo'); remoteVideo = document.getElementById('remoteVideo'); - serverConnection = new WebSocket('wss://' + window.location.hostname + ':3434'); + serverConnection = new WebSocket('wss://' + window.location.hostname + ':8443'); serverConnection.onmessage = gotMessageFromServer; var constraints = { @@ -20,8 +24,8 @@ function pageReady() { audio: true, }; - if(navigator.getUserMedia) { - navigator.getUserMedia(constraints, getUserMediaSuccess, errorHandler); + if(navigator.mediaDevices.getUserMedia) { + navigator.mediaDevices.getUserMedia(constraints).then(getUserMediaSuccess).catch(errorHandler); } else { alert('Your browser does not support getUserMedia API'); } @@ -39,7 +43,7 @@ function start(isCaller) { peerConnection.addStream(localStream); if(isCaller) { - peerConnection.createOffer(gotDescription, errorHandler); + peerConnection.createOffer().then(createdDescription).catch(errorHandler); } } @@ -47,26 +51,34 @@ function gotMessageFromServer(message) { if(!peerConnection) start(false); var signal = JSON.parse(message.data); + + // Ignore messages from ourself + if(signal.uuid == uuid) return; + if(signal.sdp) { - peerConnection.setRemoteDescription(new RTCSessionDescription(signal.sdp), function() { - peerConnection.createAnswer(gotDescription, errorHandler); - }, errorHandler); + peerConnection.setRemoteDescription(new RTCSessionDescription(signal.sdp)).then(function() { + // Only create answers in response to offers + if(signal.sdp.type == 'offer') { + peerConnection.createAnswer().then(createdDescription).catch(errorHandler); + } + }).catch(errorHandler); } else if(signal.ice) { - peerConnection.addIceCandidate(new RTCIceCandidate(signal.ice)); + peerConnection.addIceCandidate(new RTCIceCandidate(signal.ice)).catch(errorHandler); } } function gotIceCandidate(event) { if(event.candidate != null) { - serverConnection.send(JSON.stringify({'ice': event.candidate})); + serverConnection.send(JSON.stringify({'ice': event.candidate, 'uuid': uuid})); } } -function gotDescription(description) { +function createdDescription(description) { console.log('got description'); - peerConnection.setLocalDescription(description, function () { - serverConnection.send(JSON.stringify({'sdp': description})); - }, function() {console.log('set description error')}); + + peerConnection.setLocalDescription(description).then(function() { + serverConnection.send(JSON.stringify({'sdp': peerConnection.localDescription, 'uuid': uuid})); + }).catch(errorHandler); } function gotRemoteStream(event) { @@ -77,3 +89,13 @@ function gotRemoteStream(event) { function errorHandler(error) { console.log(error); } + +// Taken from http://stackoverflow.com/a/105074/515584 +// Strictly speaking, it's not a real UUID, but it gets the job done here +function uuid() { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); + } + + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); +} diff --git a/generate_cert.sh b/generate_cert.sh new file mode 100755 index 0000000..68a4b96 --- /dev/null +++ b/generate_cert.sh @@ -0,0 +1,3 @@ +#! /bin/bash + +openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes diff --git a/key.pem b/key.pem new file mode 100644 index 0000000..bbfa5e8 --- /dev/null +++ b/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +0qgfH8qILf4t1ULeWxPK4n3dHZ49K2E= +-----END PRIVATE KEY----- diff --git a/server/server.js b/server/server.js index cd7108f..666ec33 100644 --- a/server/server.js +++ b/server/server.js @@ -1,26 +1,46 @@ -var ws_cfg = { - ssl: true, - port: 3434, - ssl_key: '/path/to/your/ssl.key', - ssl_cert: '/path/to/your/ssl.bundle.crt' -}; +var HTTPS_PORT = 8443; -var processRequest = function(req, res) { - console.log("Request received.") -}; - -var httpServ = require('https'); var fs = require('fs'); -var app = null; - -app = httpServ.createServer({ - key: fs.readFileSync(ws_cfg.ssl_key), - cert: fs.readFileSync(ws_cfg.ssl_cert) -}, processRequest).listen(ws_cfg.port); - +var https = require('https'); var WebSocketServer = require('ws').Server; -var wss = new WebSocketServer({server: app}); +// Yes, SSL is required +var serverConfig = { + key: fs.readFileSync('key.pem'), + cert: fs.readFileSync('cert.pem'), +}; + +// ---------------------------------------------------------------------------------------- + +// Create a server for the client html page +var handleRequest = function(request, response) { + // Render the single client html file for any request the HTTP server receives + console.log('request received: ' + request.url); + + if(request.url == '/') { + response.writeHead(200, {'Content-Type': 'text/html'}); + response.end(fs.readFileSync('client/index.html')); + } else if(request.url == '/webrtc.js') { + response.writeHead(200, {'Content-Type': 'application/javascript'}); + response.end(fs.readFileSync('client/webrtc.js')); + } +}; + +var httpsServer = https.createServer(serverConfig, handleRequest); +httpsServer.listen(HTTPS_PORT, '0.0.0.0'); + +// ---------------------------------------------------------------------------------------- + +// Create a server for handling websocket calls +var wss = new WebSocketServer({server: httpsServer}); + +wss.on('connection', function(ws) { + ws.on('message', function(message) { + // Broadcast any received message to all clients + console.log('received: %s', message); + wss.broadcast(message); + }); +}); wss.broadcast = function(data) { for(var i in this.clients) { @@ -28,9 +48,4 @@ wss.broadcast = function(data) { } }; -wss.on('connection', function(ws) { - ws.on('message', function(message) { - console.log('received: %s', message); - wss.broadcast(message); - }); -}); +console.log('Server running. Visit https://localhost:' + HTTPS_PORT + ' in Firefox/Chrome (note the HTTPS; there is no HTTP -> HTTPS redirect!)');