├── .gitignore ├── README.md ├── config ├── express.js └── settings.js ├── lib ├── router.js └── sockets.js ├── package.json ├── public ├── app.js ├── index.html ├── style.css └── videochat.js └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #webRTC Video Chat (still working on the peer connection API) 2 | Project that uses node.js, web sockets, and the peer connection API to handle real-time HTML video chat. -------------------------------------------------------------------------------- /config/express.js: -------------------------------------------------------------------------------- 1 | module.exports = function(connect, express, path, server, settings) { 2 | 3 | server.configure(function() { 4 | server.set('views', path + '/views'); 5 | server.set('view engine', 'jade'); 6 | server.set('view options', { layout: false }); 7 | server.use(connect.bodyParser()); 8 | server.use(connect.static(path + '/public')); 9 | server.use(express.cookieParser()); 10 | server.use(express.session({secret: settings.sessionSecret})); 11 | server.use(server.router); 12 | }); 13 | 14 | server.listen(settings.port, null); 15 | }; -------------------------------------------------------------------------------- /config/settings.js: -------------------------------------------------------------------------------- 1 | var settings = { 2 | sessionSecret: 'sh44FSFEIPANVPOEANVh5h55h66h7h7x01hhh!', 3 | port: 8081, 4 | debug: (process.env.NODE_ENV !== 'production'), 5 | theme: 'default', 6 | themes: { 7 | default: { 8 | errors: { 9 | notfound: 'themes/default/errors/404' 10 | }, 11 | layout: 'themes/default/layout', 12 | index: 'themes/default/index' 13 | } 14 | } 15 | }; 16 | 17 | settings.uri = 'http://localhost:' + 8081; 18 | 19 | if (process.env.NODE_ENV == 'production') { 20 | settings.uri = 'http://videochat.jit.su'; 21 | settings.port = process.env.PORT || 80; 22 | } 23 | 24 | module.exports = settings; -------------------------------------------------------------------------------- /lib/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function(express, server, settings) { 2 | 3 | //Show all errors and keep search engines from using robots.txt 4 | server.configure('development', function() { 5 | server.use(express.errorHandler({ 6 | 'dumpExceptions': true, 7 | 'showStack': true 8 | })); 9 | server.all('/robots.txt', function(req,res) { 10 | res.send('User-agent: *\nDisallow: /', {'Content-Type': 'text/plain'}); 11 | }); 12 | }); 13 | 14 | //Suppress errors, allow all search engines 15 | server.configure('production', function() { 16 | server.use(express.errorHandler()); 17 | server.all('/robots.txt', function(req,res) { 18 | res.send('User-agent: *', {'Content-Type': 'text/plain'}); 19 | }); 20 | }); 21 | 22 | server.get('/', function(req, res) { 23 | res.render(settings.themes[settings.theme].index, { 24 | title: 'Index' 25 | }); 26 | }); 27 | 28 | //404 29 | server.get('*', function(req, res) { 30 | if(req.accepts('html')) { 31 | res.status(404); 32 | res.render(settings.themes[settings.theme].errors.notfound, { 33 | title: 'Not Found' 34 | }); 35 | } 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /lib/sockets.js: -------------------------------------------------------------------------------- 1 | module.exports = function Server(io, server) { 2 | var socket = io.listen(server); 3 | 4 | socket.on('connection', function(socket) { 5 | 6 | console.log('Client Connected'); 7 | socket.broadcast.emit('connected', {}); 8 | 9 | socket.on('message', function(data) { 10 | socket.broadcast.emit('server_message', data); 11 | socket.emit('server_message', data); 12 | }); 13 | 14 | socket.on('disconnect', function() { 15 | console.log('Client Disconnected.'); 16 | }); 17 | }); 18 | 19 | return this; 20 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videochat", 3 | "subdomain": "videochat", 4 | "description": "video chatting", 5 | "scripts": { 6 | "start": "node server.js" 7 | }, 8 | "version": "0.0.1-6", 9 | "engines": { 10 | "node": "v0.8.x" 11 | }, 12 | "authors": [ 13 | "Chris Abrams " 14 | ], 15 | "dependencies": { 16 | "connect": "2.4.4", 17 | "express": "https://github.com/chrisabrams/express2/tarball/master", 18 | "jade": "0.27.2", 19 | "moment": "1.7.0", 20 | "mongoose": "3.0.3", 21 | "socket.io": "0.9.10", 22 | "stylus": "0.29.0" 23 | }, 24 | "devDependencies": { 25 | "mocha": "1.4.0", 26 | "should": "1.1.0" 27 | } 28 | } -------------------------------------------------------------------------------- /public/app.js: -------------------------------------------------------------------------------- 1 | var app = new VideoChat; 2 | 3 | app.init(); -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |
22 |
23 | 25 |
26 |
28 |
29 |
30 | 32 |
33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul, li { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | 50 | body { 51 | background-color: #000; 52 | } 53 | 54 | a:link { color: #ffffff; } 55 | a:visited {color: #ffffff; } 56 | html, body { 57 | background-color: #000000; 58 | height: 100%; 59 | font-family:Verdana, Arial, Helvetica, sans-serif; 60 | } 61 | body { 62 | margin: 0; 63 | padding: 0; 64 | } 65 | #container { 66 | position: relative; 67 | min-height: 100%; 68 | width: 100%; 69 | margin: 0px auto; 70 | -webkit-perspective: 1000; 71 | } 72 | #card { 73 | -webkit-transition-property: rotation; 74 | -webkit-transition-duration: 2s; 75 | } 76 | #local { 77 | position: absolute; 78 | width: 100%; 79 | -webkit-transform: scale(-1, 1); 80 | -webkit-backface-visibility: hidden; 81 | } 82 | #remote { 83 | position: absolute; 84 | width: 100%; 85 | -webkit-transform: rotateY(180deg); 86 | -webkit-backface-visibility: hidden; 87 | } 88 | #mini { 89 | position: absolute; 90 | height: 30%; 91 | width: 30%; 92 | bottom: 32px; 93 | right: 4px; 94 | -webkit-transform: scale(-1, 1); 95 | opacity: 1.0; 96 | //-webkit-transform: rotateY(180deg); 97 | //-webkit-backface-visibility: hidden; 98 | } 99 | #localVideo { 100 | opacity: 0; 101 | -webkit-transition-property: opacity; 102 | -webkit-transition-duration: 2s; 103 | } 104 | #remoteVideo { 105 | opacity: 0; 106 | -webkit-transition-property: opacity; 107 | -webkit-transition-duration: 2s; 108 | } 109 | #miniVideo { 110 | opacity: 0; 111 | -webkit-transition-property: opacity; 112 | -webkit-transition-duration: 2s; 113 | } 114 | #footer { 115 | spacing: 4px; 116 | position: absolute; 117 | bottom: 0; 118 | width: 100%; 119 | height: 28px; 120 | background-color: #3F3F3F; 121 | color: rgb(255, 255, 255); 122 | font-size:13px; font-weight: bold; 123 | line-height: 28px; 124 | text-align: center; 125 | } 126 | #hangup { 127 | font-size:13px; font-weight:bold; 128 | color:#FFFFFF; 129 | width:128px; 130 | height:24px; 131 | background-color:#808080; 132 | border-style:solid; 133 | border-color:#FFFFFF; 134 | margin:2px; 135 | } 136 | #logo { 137 | display: block; 138 | top:4; 139 | right:4; 140 | position:absolute; 141 | float:right; 142 | opacity: 0.5; 143 | } 144 | -------------------------------------------------------------------------------- /public/videochat.js: -------------------------------------------------------------------------------- 1 | var VideoChat = function() { 2 | this.localVideo; 3 | this.miniVideo; 4 | this.pc; 5 | this.remoteVideo; 6 | }; 7 | 8 | VideoChat.prototype.init = function() { 9 | var _this = this; 10 | 11 | var socket = io.connect("http://videochat.jit.su"); 12 | 13 | socket.on('connected', function(data) { 14 | console.log('Client Connected'); 15 | _this.maybeStart(); 16 | }); 17 | 18 | setTimeout(function() { 19 | console.log("Initializing"); 20 | card = document.getElementById("card"); 21 | _this.localVideo = document.getElementById("localVideo"); 22 | _this.miniVideo = document.getElementById("miniVideo"); 23 | _this.remoteVideo = document.getElementById("remoteVideo"); 24 | _this.resetStatus(); 25 | _this.getUserMedia(); 26 | }, 100); 27 | }; 28 | 29 | VideoChat.prototype.createPeerConnection = function() { 30 | var _this = this; 31 | 32 | try { 33 | _this.pc = new webkitPeerConnection00("STUN stun.l.google.com:19302", _this.onIceCandidate); 34 | console.log("Created webkitPeerConnnection00 with config \"STUN stun.l.google.com:19302\"."); 35 | } catch (e) { 36 | console.log("Failed to create PeerConnection, exception: " + e.message); 37 | alert("Cannot create PeerConnection object; Is the 'PeerConnection' flag enabled in about:flags?"); 38 | return; 39 | } 40 | 41 | _this.pc.onconnecting = _this.onSessionConnecting; 42 | _this.pc.onopen = _this.onSessionOpened; 43 | _this.pc.onaddstream = _this.onRemoteStreamAdded; 44 | _this.pc.onremovestream = _this.onRemoteStreamRemoved; 45 | }; 46 | 47 | VideoChat.prototype.doCall = function() { 48 | console.log("Send offer to peer"); 49 | var offer = this.pc.createOffer({audio:true, video:true}); 50 | this.pc.setLocalDescription(pc.SDP_OFFER, offer); 51 | this.sendMessage({type: 'offer', sdp: offer.toSdp()}); 52 | this.pc.startIce(); 53 | }; 54 | 55 | VideoChat.prototype.doAnswer = function() { 56 | console.log("Send answer to peer"); 57 | var offer = this.pc.remoteDescription; 58 | var answer = this.pc.createAnswer(offer.toSdp(), {audio:true,video:true}); 59 | this.pc.setLocalDescription(this.pc.SDP_ANSWER, answer); 60 | this.sendMessage({type: 'answer', sdp: answer.toSdp()}); 61 | this.pc.startIce(); 62 | }; 63 | 64 | VideoChat.prototype.enterFullScreen = function() { 65 | remote.webkitRequestFullScreen(); 66 | }; 67 | 68 | VideoChat.prototype.getUserMedia = function() { 69 | var _this = this; 70 | 71 | try { 72 | navigator.webkitGetUserMedia({audio:true, video:true}, _this.onUserMediaSuccess, _this.onUserMediaError); 73 | console.log("Requested access to local media with new syntax."); 74 | } catch (e) { 75 | try { 76 | navigator.webkitGetUserMedia("video,audio", _this.onUserMediaSuccess, _this.onUserMediaError); 77 | console.log("Requested access to local media with old syntax."); 78 | } catch (e) { 79 | alert("webkitGetUserMedia() failed. Is the MediaStream flag enabled in about:flags?"); 80 | console.log("webkitGetUserMedia failed with exception: " + e.message); 81 | } 82 | } 83 | }; 84 | 85 | VideoChat.prototype.maybeStart = function() { 86 | var _this = this; 87 | 88 | if (!started && localStream) { 89 | _this.setStatus("Connecting..."); 90 | console.log("Creating PeerConnection."); 91 | _this.createPeerConnection(); 92 | console.log("Adding local stream."); 93 | _this.pc.addStream(localStream); 94 | started = true; 95 | // Caller initiates offer to peer. 96 | if (initiator) { 97 | console.log("I am the initiator") 98 | _this.doCall(); 99 | } 100 | 101 | } 102 | }; 103 | 104 | VideoChat.prototype.onChannelMessage = function(message) { 105 | console.log('S->C: ' + message.data); 106 | this.processSignalingMessage(message.data); 107 | }; 108 | 109 | VideoChat.prototype.onChannelOpened = function() { 110 | var _this = this; 111 | 112 | console.log('Channel opened.'); 113 | channelReady = true; 114 | if (initiator) _this.maybeStart(); 115 | }; 116 | 117 | VideoChat.prototype.onHangup = function() { 118 | console.log("Hanging up."); 119 | started = false; // Stop processing any message 120 | this.transitionToDone(); 121 | this.pc.close(); 122 | // will trigger BYE from server 123 | socket.close(); 124 | this.pc = null; 125 | //socket = null; 126 | }; 127 | 128 | VideoChat.prototype.onIceCandidate = function(candidate, moreToFollow) { 129 | var _this = this; 130 | 131 | if (candidate) { 132 | _this.sendMessage({type: 'candidate', label: candidate.label, candidate: candidate.toSdp()}); 133 | } 134 | 135 | if (!moreToFollow) { 136 | console.log("End of candidates."); 137 | } 138 | }; 139 | 140 | VideoChat.prototype.onRemoteHangup = function() { 141 | console.log('Session terminated.'); 142 | started = false; // Stop processing any message 143 | this.transitionToWaiting(); 144 | this.pc.close(); 145 | this.pc = null; 146 | initiator = 0; 147 | } 148 | 149 | VideoChat.prototype.onRemoteStreamAdded = function(event) { 150 | var _this = this; 151 | 152 | console.log("Remote stream added."); 153 | var url = webkitURL.createObjectURL(event.stream); 154 | _this.miniVideo.src = _this.localVideo.src; 155 | _this.remoteVideo.src = url; 156 | _this.waitForRemoteVideo(); 157 | }; 158 | 159 | VideoChat.prototype.onRemoteStreamRemoved = function(event) { 160 | console.log("Remote stream removed."); 161 | }; 162 | 163 | VideoChat.prototype.onSessionConnecting = function(message) { 164 | console.log("Session connecting."); 165 | }; 166 | 167 | VideoChat.prototype.onSessionOpened = function(message) { 168 | console.log("Session opened."); 169 | }; 170 | 171 | VideoChat.prototype.onUserMediaError = function(error) { 172 | console.log("Failed to get access to local media. Error code was " + error.code); 173 | alert("Failed to get access to local media. Error code was " + error.code + "."); 174 | }; 175 | 176 | VideoChat.prototype.onUserMediaSuccess = function(stream) { 177 | var _this = this; 178 | 179 | console.log("User has granted access to local media."); 180 | var url = webkitURL.createObjectURL(stream); 181 | _this.localVideo.style.opacity = 1; 182 | _this.localVideo.src = url; 183 | localStream = stream; 184 | // Caller creates PeerConnection. 185 | if (initiator) _this.maybeStart(); 186 | }; 187 | 188 | VideoChat.prototype.processSignalingMessage = function(message) { 189 | var _this = this; 190 | 191 | var msg = JSON.parse(message); 192 | 193 | if (msg.type === 'offer') { 194 | // Callee creates PeerConnection 195 | if (!initiator && !started) 196 | _this.maybeStart(); 197 | 198 | _this.pc.setRemoteDescription(_this.pc.SDP_OFFER, new SessionDescription(msg.sdp)); 199 | _this.doAnswer(); 200 | } else if (msg.type === 'answer' && started) { 201 | _this.pc.setRemoteDescription(_this.pc.SDP_ANSWER, new SessionDescription(msg.sdp)); 202 | } else if (msg.type === 'candidate' && started) { 203 | var candidate = new IceCandidate(msg.label, msg.candidate); 204 | _this.pc.processIceMessage(candidate); 205 | } else if (msg.type === 'bye' && started) { 206 | _this.onRemoteHangup(); 207 | } 208 | }; 209 | 210 | VideoChat.prototype.resetStatus = function() { 211 | var _this = this; 212 | 213 | if (!initiator) { 214 | _this.setStatus("Waiting for someone to join: "+window.location.origin+""); 215 | } else { 216 | _this.setStatus("Initializing..."); 217 | } 218 | }; 219 | 220 | VideoChat.prototype.sendMessage = function(message) { 221 | var msgString = JSON.stringify(message); 222 | console.log('C->S: ' + msgString); 223 | path = '/'; 224 | var xhr = new XMLHttpRequest(); 225 | xhr.open('POST', path, true); 226 | xhr.send(msgString); 227 | }; 228 | 229 | VideoChat.prototype.setStatus = function(state) { 230 | footer.innerHTML = state; 231 | }; 232 | 233 | VideoChat.prototype.transitionToActive = function() { 234 | var _this = thisl 235 | 236 | _this.remoteVideo.style.opacity = 1; 237 | card.style.webkitTransform = "rotateY(180deg)"; 238 | setTimeout(function() { _this.localVideo.src = ""; }, 500); 239 | setTimeout(function() { _this.miniVideo.style.opacity = 1; }, 1000); 240 | _this.setStatus(""); 241 | }; 242 | 243 | VideoChat.prototype.transitionToDone = function() { 244 | this.localVideo.style.opacity = 0; 245 | this.remoteVideo.style.opacity = 0; 246 | this.miniVideo.style.opacity = 0; 247 | this.setStatus("You have left the call. Click here to rejoin."); 248 | }; 249 | 250 | VideoChat.prototype.transitionToWaiting = function() { 251 | var _this = this; 252 | 253 | card.style.webkitTransform = "rotateY(0deg)"; 254 | setTimeout(function() { _this.localVideo.src = _this.miniVideo.src; _this.miniVideo.src = ""; _this.remoteVideo.src = "" }, 500); 255 | _this.miniVideo.style.opacity = 0; 256 | _this.remoteVideo.style.opacity = 0; 257 | _this.resetStatus(); 258 | }; 259 | 260 | VideoChat.prototype.waitForRemoteVideo = function() { 261 | var _this = this; 262 | 263 | if (_this.remoteVideo.currentTime > 0) { 264 | _this.transitionToActive(); 265 | } else { 266 | setTimeout(_this.waitForRemoteVideo, 100); 267 | } 268 | }; -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var connect = require('connect'), 2 | express = require('express'), 3 | http = require('http'), 4 | io = require('socket.io'), 5 | mongoose = require('mongoose'), 6 | path = __dirname, 7 | settings = require('./config/settings'); 8 | 9 | //Express 10 | var server = module.exports = express.createServer(); 11 | 12 | require('./config/express')(connect, express, path, server, settings); 13 | 14 | //Socket.io 15 | require('./lib/sockets')(io, server); 16 | 17 | //Routes 18 | require('./lib/router')(express, server, settings); 19 | 20 | console.log('Running in ' + (process.env.NODE_ENV || 'development') + ' mode @ ' + settings.uri); --------------------------------------------------------------------------------