├── .gitignore ├── .travis.yml ├── README.md ├── lib ├── builder.js ├── io.js └── webrtc.io.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webrtc.io [![Build Status](https://travis-ci.org/webRTC/webrtc.io-client.png?branch=master)](https://travis-ci.org/webRTC/webrtc.io-client) 2 | 3 | A library that is to webRTC like socket.io is to WebSockets. 4 | 5 | This will eventually be bundled with the [server code](https://github.com/webRTC/webRTC.io). 6 | 7 | ## Installation 8 | 9 | Currently, webrtc.io depends on socket.io, so include the socket.io client as well. After including socket.io-client, drop in `lib/io.js`. You'll also need a webrtc.io server running. 10 | 11 | Now you can start using webRTC commands with our abstraction. 12 | 13 | 14 | ## Usage 15 | 16 | ```javascript 17 | rtc.createStream({"video": true, "audio":true}, function(stream){ 18 | // get local stream for manipulation 19 | } 20 | rtc.connect('ws://yourserveraddress:8001', optionalRoom); 21 | //then a bunch of callbacks are available 22 | ``` 23 | 24 | You can set the STUN server by calling 25 | rtc.SERVER = "STUN stun.l.google.com:19302" and set your server. The default STUN server used by the library is one from google. 26 | -------------------------------------------------------------------------------- /lib/builder.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | var builder = module.exports = function(callback) { 4 | fs.readFile(__dirname + '/webrtc.io.js', function(err, content) { 5 | if (err) { 6 | return callback(err); 7 | } 8 | 9 | return callback(null, content); 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/io.js: -------------------------------------------------------------------------------- 1 | var io = module.exports; 2 | 3 | io.builder = require('./builder'); 4 | 5 | -------------------------------------------------------------------------------- /lib/webrtc.io.js: -------------------------------------------------------------------------------- 1 | //CLIENT 2 | 3 | // Fallbacks for vendor-specific variables until the spec is finalized. 4 | 5 | var PeerConnection = (window.PeerConnection || window.webkitPeerConnection00 || window.webkitRTCPeerConnection || window.mozRTCPeerConnection); 6 | var URL = (window.URL || window.webkitURL || window.msURL || window.oURL); 7 | var getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia); 8 | var nativeRTCIceCandidate = (window.mozRTCIceCandidate || window.RTCIceCandidate); 9 | var nativeRTCSessionDescription = (window.mozRTCSessionDescription || window.RTCSessionDescription); // order is very important: "RTCSessionDescription" defined in Nighly but useless 10 | 11 | var sdpConstraints = { 12 | 'mandatory': { 13 | 'OfferToReceiveAudio': true, 14 | 'OfferToReceiveVideo': true 15 | } 16 | }; 17 | 18 | if (navigator.webkitGetUserMedia) { 19 | if (!webkitMediaStream.prototype.getVideoTracks) { 20 | webkitMediaStream.prototype.getVideoTracks = function() { 21 | return this.videoTracks; 22 | }; 23 | webkitMediaStream.prototype.getAudioTracks = function() { 24 | return this.audioTracks; 25 | }; 26 | } 27 | 28 | // New syntax of getXXXStreams method in M26. 29 | if (!webkitRTCPeerConnection.prototype.getLocalStreams) { 30 | webkitRTCPeerConnection.prototype.getLocalStreams = function() { 31 | return this.localStreams; 32 | }; 33 | webkitRTCPeerConnection.prototype.getRemoteStreams = function() { 34 | return this.remoteStreams; 35 | }; 36 | } 37 | } 38 | 39 | (function() { 40 | 41 | var rtc; 42 | if ('undefined' === typeof module) { 43 | rtc = this.rtc = {}; 44 | } else { 45 | rtc = module.exports = {}; 46 | } 47 | 48 | // Toggle debug mode (console.log) 49 | rtc.debug = false; 50 | 51 | // Holds a connection to the server. 52 | rtc._socket = null; 53 | 54 | // Holds identity for the client. 55 | rtc._me = null; 56 | 57 | // Holds callbacks for certain events. 58 | rtc._events = {}; 59 | 60 | rtc.on = function(eventName, callback) { 61 | rtc._events[eventName] = rtc._events[eventName] || []; 62 | rtc._events[eventName].push(callback); 63 | }; 64 | 65 | rtc.fire = function(eventName, _) { 66 | var events = rtc._events[eventName]; 67 | var args = Array.prototype.slice.call(arguments, 1); 68 | 69 | if (!events) { 70 | return; 71 | } 72 | 73 | for (var i = 0, len = events.length; i < len; i++) { 74 | events[i].apply(null, args); 75 | } 76 | }; 77 | 78 | // Holds the STUN/ICE server to use for PeerConnections. 79 | rtc.SERVER = function() { 80 | if (navigator.mozGetUserMedia) { 81 | return { 82 | "iceServers": [{ 83 | "url": "stun:23.21.150.121" 84 | }] 85 | }; 86 | } 87 | return { 88 | "iceServers": [{ 89 | "url": "stun:stun.l.google.com:19302" 90 | }] 91 | }; 92 | }; 93 | 94 | 95 | // Reference to the lone PeerConnection instance. 96 | rtc.peerConnections = {}; 97 | 98 | // Array of known peer socket ids 99 | rtc.connections = []; 100 | // Stream-related variables. 101 | rtc.streams = []; 102 | rtc.numStreams = 0; 103 | rtc.initializedStreams = 0; 104 | 105 | 106 | // Reference to the data channels 107 | rtc.dataChannels = {}; 108 | 109 | // PeerConnection datachannel configuration 110 | rtc.dataChannelConfig = { 111 | "optional": [{ 112 | "RtpDataChannels": true 113 | }, { 114 | "DtlsSrtpKeyAgreement": true 115 | }] 116 | }; 117 | 118 | rtc.pc_constraints = { 119 | "optional": [{ 120 | "DtlsSrtpKeyAgreement": true 121 | }] 122 | }; 123 | 124 | 125 | // check whether data channel is supported. 126 | rtc.checkDataChannelSupport = function() { 127 | try { 128 | // raises exception if createDataChannel is not supported 129 | var pc = new PeerConnection(rtc.SERVER(), rtc.dataChannelConfig); 130 | var channel = pc.createDataChannel('supportCheck', { 131 | reliable: false 132 | }); 133 | channel.close(); 134 | return true; 135 | } catch (e) { 136 | return false; 137 | } 138 | }; 139 | 140 | rtc.dataChannelSupport = rtc.checkDataChannelSupport(); 141 | 142 | 143 | /** 144 | * Connects to the websocket server. 145 | */ 146 | rtc.connect = function(server, room) { 147 | room = room || ""; // by default, join a room called the blank string 148 | rtc._socket = new WebSocket(server); 149 | 150 | rtc._socket.onopen = function() { 151 | 152 | rtc._socket.send(JSON.stringify({ 153 | "eventName": "join_room", 154 | "data": { 155 | "room": room 156 | } 157 | })); 158 | 159 | rtc._socket.onmessage = function(msg) { 160 | var json = JSON.parse(msg.data); 161 | rtc.fire(json.eventName, json.data); 162 | }; 163 | 164 | rtc._socket.onerror = function(err) { 165 | console.error('onerror'); 166 | console.error(err); 167 | }; 168 | 169 | rtc._socket.onclose = function(data) { 170 | var id = rtc._socket.id; 171 | rtc.fire('disconnect stream', id); 172 | if (typeof(rtc.peerConnections[id]) !== 'undefined') 173 | rtc.peerConnections[id].close(); 174 | delete rtc.peerConnections[id]; 175 | delete rtc.dataChannels[id]; 176 | delete rtc.connections[id]; 177 | }; 178 | 179 | rtc.on('get_peers', function(data) { 180 | rtc.connections = data.connections; 181 | rtc._me = data.you; 182 | if (rtc.offerSent) { // 'ready' was fired before 'get_peers' 183 | rtc.createPeerConnections(); 184 | rtc.addStreams(); 185 | rtc.addDataChannels(); 186 | rtc.sendOffers(); 187 | } 188 | // fire connections event and pass peers 189 | rtc.fire('connections', rtc.connections); 190 | }); 191 | 192 | rtc.on('receive_ice_candidate', function(data) { 193 | var candidate = new nativeRTCIceCandidate(data); 194 | rtc.peerConnections[data.socketId].addIceCandidate(candidate); 195 | rtc.fire('receive ice candidate', candidate); 196 | }); 197 | 198 | rtc.on('new_peer_connected', function(data) { 199 | var id = data.socketId; 200 | rtc.connections.push(id); 201 | delete rtc.offerSent; 202 | 203 | var pc = rtc.createPeerConnection(id); 204 | for (var i = 0; i < rtc.streams.length; i++) { 205 | var stream = rtc.streams[i]; 206 | pc.addStream(stream); 207 | } 208 | }); 209 | 210 | rtc.on('remove_peer_connected', function(data) { 211 | var id = data.socketId; 212 | rtc.fire('disconnect stream', id); 213 | if (typeof(rtc.peerConnections[id]) !== 'undefined') 214 | rtc.peerConnections[id].close(); 215 | delete rtc.peerConnections[id]; 216 | delete rtc.dataChannels[id]; 217 | delete rtc.connections[id]; 218 | }); 219 | 220 | rtc.on('receive_offer', function(data) { 221 | rtc.receiveOffer(data.socketId, data.sdp); 222 | rtc.fire('receive offer', data); 223 | }); 224 | 225 | rtc.on('receive_answer', function(data) { 226 | rtc.receiveAnswer(data.socketId, data.sdp); 227 | rtc.fire('receive answer', data); 228 | }); 229 | 230 | rtc.fire('connect'); 231 | }; 232 | }; 233 | 234 | 235 | rtc.sendOffers = function() { 236 | for (var i = 0, len = rtc.connections.length; i < len; i++) { 237 | var socketId = rtc.connections[i]; 238 | rtc.sendOffer(socketId); 239 | } 240 | }; 241 | 242 | rtc.onClose = function(data) { 243 | rtc.on('close_stream', function() { 244 | rtc.fire('close_stream', data); 245 | }); 246 | }; 247 | 248 | rtc.createPeerConnections = function() { 249 | for (var i = 0; i < rtc.connections.length; i++) { 250 | rtc.createPeerConnection(rtc.connections[i]); 251 | } 252 | }; 253 | 254 | rtc.createPeerConnection = function(id) { 255 | 256 | var config = rtc.pc_constraints; 257 | if (rtc.dataChannelSupport) config = rtc.dataChannelConfig; 258 | 259 | var pc = rtc.peerConnections[id] = new PeerConnection(rtc.SERVER(), config); 260 | pc.onicecandidate = function(event) { 261 | if (event.candidate) { 262 | rtc._socket.send(JSON.stringify({ 263 | "eventName": "send_ice_candidate", 264 | "data": { 265 | "label": event.candidate.sdpMLineIndex, 266 | "candidate": event.candidate.candidate, 267 | "socketId": id 268 | } 269 | })); 270 | } 271 | rtc.fire('ice candidate', event.candidate); 272 | }; 273 | 274 | pc.onopen = function() { 275 | // TODO: Finalize this API 276 | rtc.fire('peer connection opened'); 277 | }; 278 | 279 | pc.onaddstream = function(event) { 280 | // TODO: Finalize this API 281 | rtc.fire('add remote stream', event.stream, id); 282 | }; 283 | 284 | if (rtc.dataChannelSupport) { 285 | pc.ondatachannel = function(evt) { 286 | if (rtc.debug) console.log('data channel connecting ' + id); 287 | rtc.addDataChannel(id, evt.channel); 288 | }; 289 | } 290 | 291 | return pc; 292 | }; 293 | 294 | rtc.sendOffer = function(socketId) { 295 | var pc = rtc.peerConnections[socketId]; 296 | 297 | var constraints = { 298 | "optional": [], 299 | "mandatory": { 300 | "MozDontOfferDataChannel": true 301 | } 302 | }; 303 | // temporary measure to remove Moz* constraints in Chrome 304 | if (navigator.webkitGetUserMedia) { 305 | for (var prop in constraints.mandatory) { 306 | if (prop.indexOf("Moz") != -1) { 307 | delete constraints.mandatory[prop]; 308 | } 309 | } 310 | } 311 | constraints = mergeConstraints(constraints, sdpConstraints); 312 | 313 | pc.createOffer(function(session_description) { 314 | session_description.sdp = preferOpus(session_description.sdp); 315 | pc.setLocalDescription(session_description); 316 | rtc._socket.send(JSON.stringify({ 317 | "eventName": "send_offer", 318 | "data": { 319 | "socketId": socketId, 320 | "sdp": session_description 321 | } 322 | })); 323 | }, null, sdpConstraints); 324 | }; 325 | 326 | rtc.receiveOffer = function(socketId, sdp) { 327 | var pc = rtc.peerConnections[socketId]; 328 | rtc.sendAnswer(socketId, sdp); 329 | }; 330 | 331 | rtc.sendAnswer = function(socketId, sdp) { 332 | var pc = rtc.peerConnections[socketId]; 333 | pc.setRemoteDescription(new nativeRTCSessionDescription(sdp)); 334 | pc.createAnswer(function(session_description) { 335 | pc.setLocalDescription(session_description); 336 | rtc._socket.send(JSON.stringify({ 337 | "eventName": "send_answer", 338 | "data": { 339 | "socketId": socketId, 340 | "sdp": session_description 341 | } 342 | })); 343 | //TODO Unused variable!? 344 | var offer = pc.remoteDescription; 345 | }, null, sdpConstraints); 346 | }; 347 | 348 | 349 | rtc.receiveAnswer = function(socketId, sdp) { 350 | var pc = rtc.peerConnections[socketId]; 351 | pc.setRemoteDescription(new nativeRTCSessionDescription(sdp)); 352 | }; 353 | 354 | 355 | rtc.createStream = function(opt, onSuccess, onFail) { 356 | var options; 357 | onSuccess = onSuccess || function() {}; 358 | onFail = onFail || function() {}; 359 | 360 | options = { 361 | video: !! opt.video, 362 | audio: !! opt.audio 363 | }; 364 | 365 | if (getUserMedia) { 366 | rtc.numStreams++; 367 | getUserMedia.call(navigator, options, function(stream) { 368 | rtc.streams.push(stream); 369 | rtc.initializedStreams++; 370 | onSuccess(stream); 371 | if (rtc.initializedStreams === rtc.numStreams) { 372 | rtc.fire('ready'); 373 | } 374 | }, function(error) { 375 | alert("Could not connect stream."); 376 | onFail(error); 377 | }); 378 | } else { 379 | alert('webRTC is not yet supported in this browser.'); 380 | } 381 | }; 382 | 383 | rtc.addStreams = function() { 384 | for (var i = 0; i < rtc.streams.length; i++) { 385 | var stream = rtc.streams[i]; 386 | for (var connection in rtc.peerConnections) { 387 | rtc.peerConnections[connection].addStream(stream); 388 | } 389 | } 390 | }; 391 | 392 | rtc.attachStream = function(stream, element) { 393 | if (typeof(element) === "string") 394 | element = document.getElementById(element); 395 | if (navigator.mozGetUserMedia) { 396 | if (rtc.debug) console.log("Attaching media stream"); 397 | element.mozSrcObject = stream; 398 | element.play(); 399 | } else { 400 | element.src = webkitURL.createObjectURL(stream); 401 | } 402 | }; 403 | 404 | 405 | rtc.createDataChannel = function(pcOrId, label) { 406 | if (!rtc.dataChannelSupport) { 407 | //TODO this should be an exception 408 | alert('webRTC data channel is not yet supported in this browser,' + 409 | ' or you must turn on experimental flags'); 410 | return; 411 | } 412 | 413 | var id, pc; 414 | if (typeof(pcOrId) === 'string') { 415 | id = pcOrId; 416 | pc = rtc.peerConnections[pcOrId]; 417 | } else { 418 | pc = pcOrId; 419 | id = undefined; 420 | for (var key in rtc.peerConnections) { 421 | if (rtc.peerConnections[key] === pc) id = key; 422 | } 423 | } 424 | 425 | if (!id) throw new Error('attempt to createDataChannel with unknown id'); 426 | 427 | if (!pc || !(pc instanceof PeerConnection)) throw new Error('attempt to createDataChannel without peerConnection'); 428 | 429 | // need a label 430 | label = label || 'fileTransfer' || String(id); 431 | 432 | // chrome only supports reliable false atm. 433 | var options = { 434 | reliable: false 435 | }; 436 | 437 | var channel; 438 | try { 439 | if (rtc.debug) console.log('createDataChannel ' + id); 440 | channel = pc.createDataChannel(label, options); 441 | } catch (error) { 442 | if (rtc.debug) console.log('seems that DataChannel is NOT actually supported!'); 443 | throw error; 444 | } 445 | 446 | return rtc.addDataChannel(id, channel); 447 | }; 448 | 449 | rtc.addDataChannel = function(id, channel) { 450 | 451 | channel.onopen = function() { 452 | if (rtc.debug) console.log('data stream open ' + id); 453 | rtc.fire('data stream open', channel); 454 | }; 455 | 456 | channel.onclose = function(event) { 457 | delete rtc.dataChannels[id]; 458 | delete rtc.peerConnections[id]; 459 | delete rtc.connections[id]; 460 | if (rtc.debug) console.log('data stream close ' + id); 461 | rtc.fire('data stream close', channel); 462 | }; 463 | 464 | channel.onmessage = function(message) { 465 | if (rtc.debug) console.log('data stream message ' + id); 466 | rtc.fire('data stream data', channel, message.data); 467 | }; 468 | 469 | channel.onerror = function(err) { 470 | if (rtc.debug) console.log('data stream error ' + id + ': ' + err); 471 | rtc.fire('data stream error', channel, err); 472 | }; 473 | 474 | // track dataChannel 475 | rtc.dataChannels[id] = channel; 476 | return channel; 477 | }; 478 | 479 | rtc.addDataChannels = function() { 480 | if (!rtc.dataChannelSupport) return; 481 | 482 | for (var connection in rtc.peerConnections) 483 | rtc.createDataChannel(connection); 484 | }; 485 | 486 | 487 | rtc.on('ready', function() { 488 | rtc.createPeerConnections(); 489 | rtc.addStreams(); 490 | rtc.addDataChannels(); 491 | rtc.sendOffers(); 492 | rtc.offerSent = true; 493 | }); 494 | 495 | }).call(this); 496 | 497 | function preferOpus(sdp) { 498 | var sdpLines = sdp.split('\r\n'); 499 | var mLineIndex = null; 500 | // Search for m line. 501 | for (var i = 0; i < sdpLines.length; i++) { 502 | if (sdpLines[i].search('m=audio') !== -1) { 503 | mLineIndex = i; 504 | break; 505 | } 506 | } 507 | if (mLineIndex === null) return sdp; 508 | 509 | // If Opus is available, set it as the default in m line. 510 | for (var j = 0; j < sdpLines.length; j++) { 511 | if (sdpLines[j].search('opus/48000') !== -1) { 512 | var opusPayload = extractSdp(sdpLines[j], /:(\d+) opus\/48000/i); 513 | if (opusPayload) sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], opusPayload); 514 | break; 515 | } 516 | } 517 | 518 | // Remove CN in m line and sdp. 519 | sdpLines = removeCN(sdpLines, mLineIndex); 520 | 521 | sdp = sdpLines.join('\r\n'); 522 | return sdp; 523 | } 524 | 525 | function extractSdp(sdpLine, pattern) { 526 | var result = sdpLine.match(pattern); 527 | return (result && result.length == 2) ? result[1] : null; 528 | } 529 | 530 | function setDefaultCodec(mLine, payload) { 531 | var elements = mLine.split(' '); 532 | var newLine = []; 533 | var index = 0; 534 | for (var i = 0; i < elements.length; i++) { 535 | if (index === 3) // Format of media starts from the fourth. 536 | newLine[index++] = payload; // Put target payload to the first. 537 | if (elements[i] !== payload) newLine[index++] = elements[i]; 538 | } 539 | return newLine.join(' '); 540 | } 541 | 542 | function removeCN(sdpLines, mLineIndex) { 543 | var mLineElements = sdpLines[mLineIndex].split(' '); 544 | // Scan from end for the convenience of removing an item. 545 | for (var i = sdpLines.length - 1; i >= 0; i--) { 546 | var payload = extractSdp(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i); 547 | if (payload) { 548 | var cnPos = mLineElements.indexOf(payload); 549 | if (cnPos !== -1) { 550 | // Remove CN payload from m line. 551 | mLineElements.splice(cnPos, 1); 552 | } 553 | // Remove CN line in sdp 554 | sdpLines.splice(i, 1); 555 | } 556 | } 557 | 558 | sdpLines[mLineIndex] = mLineElements.join(' '); 559 | return sdpLines; 560 | } 561 | 562 | function mergeConstraints(cons1, cons2) { 563 | var merged = cons1; 564 | for (var name in cons2.mandatory) { 565 | merged.mandatory[name] = cons2.mandatory[name]; 566 | } 567 | merged.optional.concat(cons2.optional); 568 | return merged; 569 | } 570 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webrtc.io-client", 3 | "version": "0.0.2-2", 4 | "description": "Drop-in client code for webrtc.io", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/sarenji/webrtc.io-client.git" 8 | }, 9 | "main": "./lib/io.js", 10 | "keywords": [ 11 | "webrtc" 12 | ], 13 | "author": "David Peter ", 14 | "contributors": [ 15 | { 16 | "name": "David Peter", 17 | "email": "david.a.peter@gmail.com" 18 | }, 19 | { 20 | "name": "Ben Brittain", 21 | "email": "ben@brittain.org" 22 | }, 23 | { 24 | "name": "Dennis Mårtensson", 25 | "email": "me@dennis.is" 26 | } 27 | ], 28 | "license": "MIT", 29 | "devDependencies": { 30 | "jshint": "~0.9.1" 31 | }, 32 | "scripts": { 33 | "test": "jshint lib/" 34 | } 35 | } 36 | --------------------------------------------------------------------------------