├── .gitignore ├── .jshintignore ├── .jshintrc ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── build.js ├── out ├── simplewebrtc-with-adapter.bundle.js └── simplewebrtc.bundle.js ├── package-lock.json ├── package.json ├── src ├── peer.js ├── simplewebrtc.js ├── socketioconnection.js └── webrtc.js └── test ├── index.html ├── run-selenium └── selenium ├── index.js ├── p2p.js └── three.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | 5 | browsers 6 | firefox*.bz2 7 | 8 | # JetBrains IDEs 9 | .idea 10 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.bundle.js 3 | socket.io.js 4 | latest-v2.js 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "asi": false, 3 | "expr": true, 4 | "loopfunc": true, 5 | "curly": false, 6 | "evil": true, 7 | "white": true, 8 | "undef": true, 9 | "browser": true, 10 | "node": true, 11 | "trailing": true, 12 | "indent": 4, 13 | "latedef": true, 14 | "newcap": true, 15 | "predef": [ 16 | "require", 17 | "__dirname", 18 | "process", 19 | "exports", 20 | "console", 21 | "Buffer", 22 | "module" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: node_js 3 | node_js: 4 | - 6 5 | sudo: false 6 | 7 | addons: 8 | apt: 9 | sources: 10 | - ubuntu-toolchain-r-test 11 | packages: 12 | - g++-4.8 13 | - pulseaudio 14 | 15 | env: 16 | global: 17 | - CXX=g++-4.8 18 | matrix: 19 | - BVER=stable 20 | - BVER=beta 21 | 22 | before_script: 23 | - BROWSER=chrome ./node_modules/travis-multirunner/setup.sh 24 | - BROWSER=firefox ./node_modules/travis-multirunner/setup.sh 25 | - export DISPLAY=:99.0 26 | - sh -e /etc/init.d/xvfb start 27 | - pulseaudio --start 28 | 29 | script: 30 | - npm run test-travis 31 | 32 | after_failure: 33 | - for file in *.log; do echo $file; echo "======================"; cat $file; done || true 34 | 35 | notifications: 36 | email: 37 | - 38 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Issues 4 | 5 | Feel free to submit bug reports or pull requests. 6 | 7 | If you have a security related issue, please see the [Security Guidelines](SECURITY.md) first. 8 | 9 | ## Pull Requests 10 | 11 | For any code changes, please ensure that: 12 | 13 | - The latest master branch has been incorporated in your branch 14 | - JSHint is happy 15 | - All current tests pass 16 | - All new tests (there are new tests, right?) pass 17 | 18 | The git pre-commit hook should catch most of the above issues if you run `npm install` first to set it up. 19 | 20 | ## Licensing 21 | 22 | All contributions MUST be submitted under the MIT license. Please do not contribute code that you did not write, 23 | unless you are certain you have the authorization to both do so and release it under MIT. 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Written by Henrik Joreteg. 2 | Copyright © 2013 by &yet, LLC. 3 | Released under the terms of the MIT License: 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 18 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 19 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | The open-source version of SimpleWebRTC has been deprecated. This repository will remain as-is but is no longer actively maintained. You can find the old website in the [gh-pages](https://github.com/andyet/SimpleWebRTC/tree/gh-pages) branch. 4 | Read more about the "new" SimpleWebRTC (which is an entirely different thing) on https://simplewebrtc.com 5 | 6 | 7 | # SimpleWebRTC - World's easiest WebRTC lib 8 | 9 | 10 | Want to see it in action? 11 | Check out the demo: https://simplewebrtc.com/demo.html 12 | 13 | Want to run it locally? 14 | 1. Install all dependencies and run the test page 15 | ```bash 16 | npm install && npm run test-page 17 | ``` 18 | 19 | 2. open your browser to https://0.0.0.0:8443/test/ 20 | 21 | ## It's so easy: 22 | 23 | ### 1. Some basic html 24 | 25 | ```html 26 | 27 | 28 | 29 | 30 | 38 | 39 | 40 | 41 |
42 | 43 | 44 | 45 | ``` 46 | 47 | ### Installing through NPM 48 | ```sh 49 | npm install --save simplewebrtc 50 | 51 | # for yarn users 52 | yarn add simplewebrtc 53 | ``` 54 | After that simply import simplewebrtc into your project 55 | ```js 56 | import SimpleWebRTC from 'simplewebrtc'; 57 | ``` 58 | 59 | ### 2. Create our WebRTC object 60 | 61 | ```js 62 | var webrtc = new SimpleWebRTC({ 63 | // the id/element dom element that will hold "our" video 64 | localVideoEl: 'localVideo', 65 | // the id/element dom element that will hold remote videos 66 | remoteVideosEl: 'remoteVideos', 67 | // immediately ask for camera access 68 | autoRequestMedia: true 69 | }); 70 | ``` 71 | 72 | ### 3. Tell it to join a room when ready 73 | 74 | ```js 75 | // we have to wait until it's ready 76 | webrtc.on('readyToCall', function () { 77 | // you can name it anything 78 | webrtc.joinRoom('your awesome room name'); 79 | }); 80 | ``` 81 | 82 | ### Available options 83 | 84 | 85 | `peerConnectionConfig` - Set this to specify your own STUN and TURN servers. By 86 | default, SimpleWebRTC uses Google's public STUN server 87 | (`stun.l.google.com:19302`), which is intended for public use according to: 88 | https://twitter.com/HenrikJoreteg/status/354105684591251456 89 | 90 | Note that you will most likely also need to run your own TURN servers. See 91 | http://www.html5rocks.com/en/tutorials/webrtc/infrastructure/ for a basic 92 | tutorial. 93 | 94 | ## Filetransfer 95 | Sending files between individual participants is supported. See 96 | http://simplewebrtc.com/filetransfer.html for a demo. 97 | 98 | Note that this is not file sharing between a group which requires a completely 99 | different approach. 100 | 101 | ## It's not always that simple... 102 | 103 | Sometimes you need to do more advanced stuff. See 104 | http://simplewebrtc.com/notsosimple.html for some examples. 105 | 106 | ## API 107 | 108 | ### Constructor 109 | 110 | `new SimpleWebRTC(options)` 111 | 112 | - `object options` - options object provided to constructor consisting of: 113 | - `string url` - *required* url for signaling server. Defaults to signaling 114 | server URL which can be used for development. You must use your own signaling 115 | server for production. 116 | - `object socketio` - *optional* object to be passed as options to the signaling 117 | server connection. 118 | - `Connection connection` - *optional* connection object for signaling. See 119 | `Connection` below. Defaults to a new SocketIoConnection 120 | - `bool debug` - *optional* flag to set the instance to debug mode 121 | - `[string|DomElement] localVideoEl` - ID or Element to contain the local video 122 | element 123 | - `[string|DomElement] remoteVideosEl` - ID or Element to contain the 124 | remote video elements 125 | - `bool autoRequestMedia` - *optional(=false)* option to automatically request 126 | user media. Use `true` to request automatically, or `false` to request media 127 | later with `startLocalVideo` 128 | - `bool enableDataChannels` *optional(=true)* option to enable/disable data 129 | channels (used for volume levels or direct messaging) 130 | - `bool autoRemoveVideos` - *optional(=true)* option to automatically remove 131 | video elements when streams are stopped. 132 | - `bool adjustPeerVolume` - *optional(=false)* option to reduce peer volume 133 | when the local participant is speaking 134 | - `number peerVolumeWhenSpeaking` - *optional(=.0.25)* value used in 135 | conjunction with `adjustPeerVolume`. Uses values between 0 and 1. 136 | - `object media` - media options to be passed to `getUserMedia`. Defaults to 137 | `{ video: true, audio: true }`. Valid configurations described 138 | [on MDN](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) 139 | with official spec 140 | [at w3c](http://w3c.github.io/mediacapture-main/#dom-mediadevices-getusermedia). 141 | - `object receiveMedia` - *optional* RTCPeerConnection options. Defaults to 142 | `{ offerToReceiveAudio: 1, offerToReceiveVideo: 1 }`. 143 | - `object localVideo` - *optional* options for attaching the local video 144 | stream to the page. Defaults to 145 | ```javascript 146 | { 147 | autoplay: true, // automatically play the video stream on the page 148 | mirror: true, // flip the local video to mirror mode (for UX) 149 | muted: true // mute local video stream to prevent echo 150 | } 151 | ``` 152 | - `object logger` - *optional* alternate logger for the instance; any object 153 | that implements `log`, `warn`, and `error` methods. 154 | - `object peerConnectionConfig` - *optional* options to specify own your own STUN/TURN servers. 155 | By default these options are overridden when the signaling server specifies the STUN/TURN server configuration. 156 | Example on how to specify the peerConnectionConfig: 157 | ```javascript 158 | { 159 | "iceServers": [{ 160 | "url": "stun3.l.google.com:19302" 161 | }, 162 | { 163 | "url": "turn:your.turn.servers.here", 164 | "username": "your.turn.server.username", 165 | "credential": "your.turn.server.password" 166 | } 167 | ], 168 | iceTransports: 'relay' 169 | } 170 | ``` 171 | 172 | ### Fields 173 | 174 | `capabilities` - the 175 | [`webrtcSupport`](https://github.com/HenrikJoreteg/webrtcsupport) object that 176 | describes browser capabilities, for convenience 177 | 178 | `config` - the configuration options extended from options passed to the 179 | constructor 180 | 181 | `connection` - the socket (or alternate) signaling connection 182 | 183 | `webrtc` - the underlying WebRTC session manager 184 | 185 | ### Events 186 | 187 | To set up event listeners, use the SimpleWebRTC instance created with the 188 | constructor. Example: 189 | 190 | ```javascript 191 | var webrtc = new SimpleWebRTC(options); 192 | webrtc.on('connectionReady', function (sessionId) { 193 | // ... 194 | }) 195 | ``` 196 | 197 | `'connectionReady', sessionId` - emitted when the signaling connection emits the 198 | `connect` event, with the unique id for the session. 199 | 200 | `'createdPeer', peer` - emitted three times: 201 | 202 | - when joining a room with existing peers, once for each peer 203 | - when a new peer joins a joined room 204 | - when sharing screen, once for each peer 205 | 206 | - `peer` - the object representing the peer and underlying peer connection 207 | 208 | `'channelMessage', peer, channelLabel, {messageType, payload}` - emitted when a broadcast message to all peers is received via dataChannel by using the method sendDirectlyToAll(). 209 | 210 | 211 | `'stunservers', [...args]` - emitted when the signaling connection emits the 212 | same event 213 | 214 | `'turnservers', [...args]` - emitted when the signaling connection emits the 215 | same event 216 | 217 | `'localScreenAdded', el` - emitted after triggering the start of screen sharing 218 | 219 | - `el` the element that contains the local screen stream 220 | 221 | `'joinedRoom', roomName` - emitted after successfully joining a room with the name roomName 222 | 223 | `'leftRoom', roomName` - emitted after successfully leaving the current room, 224 | ending all peers, and stopping the local screen stream 225 | 226 | `'videoAdded', videoEl, peer` - emitted when a peer stream is added 227 | 228 | - `videoEl` - the video element associated with the stream that was added 229 | - `peer` - the peer associated with the stream that was added 230 | 231 | `'videoRemoved', videoEl, peer` - emitted when a peer stream is removed 232 | 233 | - `videoEl` - the video element associated with the stream that was removed 234 | - `peer` - the peer associated with the stream that was removed 235 | 236 | ### Methods 237 | 238 | `createRoom(name, callback)` - emits the `create` event on the connection with 239 | `name` and (if provided) invokes `callback` on response 240 | 241 | `joinRoom(name, callback)` - joins the conference in room `name`. Callback is 242 | invoked with `callback(err, roomDescription)` where `roomDescription` is yielded 243 | by the connection on the `join` event. See [signalmaster](https://github.com/andyet/signalmaster) for more details. 244 | 245 | `startLocalVideo()` - starts the local media with the `media` options provided 246 | in the config passed to the constructor 247 | 248 | `testReadiness()` - tests that the connection is ready and that (if media is 249 | enabled) streams have started 250 | 251 | `mute()` - mutes the local audio stream for all peers (pauses sending audio) 252 | 253 | `unmute()` - unmutes local audio stream for all peers (resumes sending audio) 254 | 255 | `pauseVideo()` - pauses sending video to peers 256 | 257 | `resumeVideo()` - resumes sending video to all peers 258 | 259 | `pause()` - pauses sending audio and video to all peers 260 | 261 | `resume()` - resumes sending audio and video to all peers 262 | 263 | `sendToAll(messageType, payload)` - broadcasts a message to all peers in the 264 | room via the signaling channel (websocket) 265 | 266 | - `string messageType` - the key for the type of message being sent 267 | - `object payload` - an arbitrary value or object to send to peers 268 | 269 | `sendDirectlyToAll(channelLabel, messageType, payload)` - broadcasts a message 270 | to all peers in the room via a dataChannel 271 | 272 | - `string channelLabel` - the label for the dataChannel to send on 273 | - `string messageType` - the key for the type of message being sent 274 | - `object payload` - an arbitrary value or object to send to peers 275 | 276 | `getPeers(sessionId, type)` - returns all peers by `sessionId` and/or `type` 277 | 278 | `shareScreen(callback)` - initiates screen capture request to browser, then 279 | adds the stream to the conference 280 | 281 | `getLocalScreen()` - returns the local screen stream 282 | 283 | `stopScreenShare()` - stops the screen share stream and removes it from the room 284 | 285 | `stopLocalVideo()` - stops all local media streams 286 | 287 | `setVolumeForAll(volume)` - used to set the volume level for all peers 288 | 289 | - `volume` - the volume level, between 0 and 1 290 | 291 | `leaveRoom()` - leaves the currently joined room and stops local screen share 292 | 293 | `disconnect()` - calls `disconnect` on the signaling connection and deletes it 294 | 295 | `handlePeerStreamAdded(peer)` - used internally to attach media stream to the 296 | DOM and perform other setup 297 | 298 | `handlePeerStreamRemoved(peer)` - used internally to remove the video container 299 | from the DOM and emit `videoRemoved` 300 | 301 | `getDomId(peer)` - used internally to get the DOM id associated with a peer 302 | 303 | `getEl(idOrEl)` - helper used internally to get an element where `idOrEl` is 304 | either an element, or an id of an element 305 | 306 | `getLocalVideoContainer()` - used internally to get the container that will hold 307 | the local video element 308 | 309 | `getRemoteVideoContainer()` - used internally to get the container that holds 310 | the remote video elements 311 | 312 | ### Connection 313 | 314 | By default, SimpleWebRTC uses a [socket.io](http://socket.io/) connection to 315 | communicate with the signaling server. However, you can provide an alternate 316 | connection object to use. All that your alternate connection need provide are 317 | four methods: 318 | 319 | - `on(ev, fn)` - A method to invoke `fn` when event `ev` is triggered 320 | - `emit()` - A method to send/emit arbitrary arguments on the connection 321 | - `getSessionId()` - A method to get a unique session Id for the connection 322 | - `disconnect()` - A method to disconnect the connection 323 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | [Report Vulnerability to the Node Security Project](mailto:report@nodesecurity.io?subject=Security%20Issue%20for%20simplewebrtc) 4 | 5 | In the interest of responsible disclosure, please use the above to report any security issues so 6 | that the appropriate patches and advisories can be made. 7 | 8 | ## History 9 | 10 | No security issues have been reported for this project yet. 11 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | const browserify = require('browserify'); 2 | const fs = require('fs'); 3 | const request = require('request'); 4 | const uglify = require('uglify-js'); 5 | 6 | const bundle = browserify({ standalone: 'SimpleWebRTC' }); 7 | bundle.add('./src/simplewebrtc'); 8 | bundle.bundle(function (err, source) { 9 | if (err) { 10 | console.error(err); 11 | } 12 | fs.writeFileSync('out/simplewebrtc.bundle.js', source); 13 | const adapter = fs.readFileSync('node_modules/webrtc-adapter/out/adapter.js').toString(); 14 | fs.writeFileSync('out/simplewebrtc-with-adapter.bundle.js', `${adapter}\n${source}`); 15 | }); 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simplewebrtc", 3 | "version": "3.1.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/andyet/SimpleWebRTC.git" 7 | }, 8 | "main": "./src/simplewebrtc.js", 9 | "description": "World's easiest webrtc", 10 | "dependencies": { 11 | "filetransfer": "^2.0.4", 12 | "localmedia": "^5.0.0", 13 | "rtcpeerconnection": "^8.0.0", 14 | "webrtcsupport": "^2.2.0", 15 | "wildemitter": "^1.2.0", 16 | "socket.io-client": "^1.7.4", 17 | "attachmediastream": "^2.0.0", 18 | "mockconsole": "0.0.1" 19 | }, 20 | "devDependencies": { 21 | "browserify": "^13.1.0", 22 | "precommit-hook": "^3.0.0", 23 | "request": "^2.72.0", 24 | "tape": "^4.0.0", 25 | "testling": "^1.7.1", 26 | "travis-multirunner": "^4.0.0", 27 | "uglify-js": "^2.7.3", 28 | "stupid-server": "^0.2.2", 29 | "webrtc-adapter": "^6.0.0", 30 | "webrtc-testbed": "git+https://github.com/fippo/testbed.git" 31 | }, 32 | "peerDependencies": { 33 | "webrtc-adapter": "^6.0.0" 34 | }, 35 | "license": "MIT", 36 | "scripts": { 37 | "build": "node build.js", 38 | "test-travis": "test/run-selenium", 39 | "updateLatest": "./scripts/updateLatest.sh", 40 | "lint": "jshint src", 41 | "validate": "npm ls", 42 | "test-page": "echo \"open https://0.0.0.0:8443/test/\" && stupid-server -s -h 0.0.0.0", 43 | "test": "node test/selenium/index.js" 44 | }, 45 | "pre-commit": [ 46 | "lint", 47 | "validate" 48 | ], 49 | "false": {} 50 | } 51 | -------------------------------------------------------------------------------- /src/peer.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var webrtcSupport = require('webrtcsupport'); 3 | var PeerConnection = require('rtcpeerconnection'); 4 | var WildEmitter = require('wildemitter'); 5 | var FileTransfer = require('filetransfer'); 6 | 7 | // the inband-v1 protocol is sending metadata inband in a serialized JSON object 8 | // followed by the actual data. Receiver closes the datachannel upon completion 9 | var INBAND_FILETRANSFER_V1 = 'https://simplewebrtc.com/protocol/filetransfer#inband-v1'; 10 | 11 | function isAllTracksEnded(stream) { 12 | var isAllTracksEnded = true; 13 | stream.getTracks().forEach(function (t) { 14 | isAllTracksEnded = t.readyState === 'ended' && isAllTracksEnded; 15 | }); 16 | return isAllTracksEnded; 17 | } 18 | 19 | function Peer(options) { 20 | var self = this; 21 | 22 | // call emitter constructor 23 | WildEmitter.call(this); 24 | 25 | this.id = options.id; 26 | this.parent = options.parent; 27 | this.type = options.type || 'video'; 28 | this.oneway = options.oneway || false; 29 | this.sharemyscreen = options.sharemyscreen || false; 30 | this.browserPrefix = options.prefix; 31 | this.stream = options.stream; 32 | this.enableDataChannels = options.enableDataChannels === undefined ? this.parent.config.enableDataChannels : options.enableDataChannels; 33 | this.receiveMedia = options.receiveMedia || this.parent.config.receiveMedia; 34 | this.channels = {}; 35 | this.sid = options.sid || Date.now().toString(); 36 | // Create an RTCPeerConnection via the polyfill 37 | this.pc = new PeerConnection(this.parent.config.peerConnectionConfig, this.parent.config.peerConnectionConstraints); 38 | this.pc.on('ice', this.onIceCandidate.bind(this)); 39 | this.pc.on('endOfCandidates', function (event) { 40 | self.send('endOfCandidates', event); 41 | }); 42 | this.pc.on('offer', function (offer) { 43 | if (self.parent.config.nick) offer.nick = self.parent.config.nick; 44 | self.send('offer', offer); 45 | }); 46 | this.pc.on('answer', function (answer) { 47 | if (self.parent.config.nick) answer.nick = self.parent.config.nick; 48 | self.send('answer', answer); 49 | }); 50 | this.pc.on('addStream', this.handleRemoteStreamAdded.bind(this)); 51 | this.pc.on('addChannel', this.handleDataChannelAdded.bind(this)); 52 | this.pc.on('removeStream', this.handleStreamRemoved.bind(this)); 53 | // Just fire negotiation needed events for now 54 | // When browser re-negotiation handling seems to work 55 | // we can use this as the trigger for starting the offer/answer process 56 | // automatically. We'll just leave it be for now while this stabalizes. 57 | this.pc.on('negotiationNeeded', this.emit.bind(this, 'negotiationNeeded')); 58 | this.pc.on('iceConnectionStateChange', this.emit.bind(this, 'iceConnectionStateChange')); 59 | this.pc.on('iceConnectionStateChange', function () { 60 | switch (self.pc.iceConnectionState) { 61 | case 'failed': 62 | // currently, in chrome only the initiator goes to failed 63 | // so we need to signal this to the peer 64 | if (self.pc.pc.localDescription.type === 'offer') { 65 | self.parent.emit('iceFailed', self); 66 | self.send('connectivityError'); 67 | } 68 | break; 69 | } 70 | }); 71 | this.pc.on('signalingStateChange', this.emit.bind(this, 'signalingStateChange')); 72 | this.logger = this.parent.logger; 73 | 74 | // handle screensharing/broadcast mode 75 | if (options.type === 'screen') { 76 | if (this.parent.localScreens && this.parent.localScreens[0] && this.sharemyscreen) { 77 | this.logger.log('adding local screen stream to peer connection'); 78 | this.pc.addStream(this.parent.localScreens[0]); 79 | this.broadcaster = options.broadcaster; 80 | } 81 | } else { 82 | this.parent.localStreams.forEach(function (stream) { 83 | self.pc.addStream(stream); 84 | }); 85 | } 86 | 87 | this.on('channelOpen', function (channel) { 88 | if (channel.protocol === INBAND_FILETRANSFER_V1) { 89 | channel.onmessage = function (event) { 90 | var metadata = JSON.parse(event.data); 91 | var receiver = new FileTransfer.Receiver(); 92 | receiver.receive(metadata, channel); 93 | self.emit('fileTransfer', metadata, receiver); 94 | receiver.on('receivedFile', function (file, metadata) { 95 | receiver.channel.close(); 96 | }); 97 | }; 98 | } 99 | }); 100 | 101 | // proxy events to parent 102 | this.on('*', function () { 103 | self.parent.emit.apply(self.parent, arguments); 104 | }); 105 | } 106 | 107 | util.inherits(Peer, WildEmitter); 108 | 109 | Peer.prototype.handleMessage = function (message) { 110 | var self = this; 111 | 112 | this.logger.log('getting', message.type, message); 113 | 114 | if (message.prefix) this.browserPrefix = message.prefix; 115 | 116 | if (message.type === 'offer') { 117 | if (!this.nick) this.nick = message.payload.nick; 118 | delete message.payload.nick; 119 | this.pc.handleOffer(message.payload, function (err) { 120 | if (err) { 121 | return; 122 | } 123 | // auto-accept 124 | self.pc.answer(function (err, sessionDescription) { 125 | //self.send('answer', sessionDescription); 126 | }); 127 | }); 128 | } else if (message.type === 'answer') { 129 | if (!this.nick) this.nick = message.payload.nick; 130 | delete message.payload.nick; 131 | this.pc.handleAnswer(message.payload); 132 | } else if (message.type === 'candidate') { 133 | this.pc.processIce(message.payload); 134 | } else if (message.type === 'connectivityError') { 135 | this.parent.emit('connectivityError', self); 136 | } else if (message.type === 'mute') { 137 | this.parent.emit('mute', {id: message.from, name: message.payload.name}); 138 | } else if (message.type === 'unmute') { 139 | this.parent.emit('unmute', {id: message.from, name: message.payload.name}); 140 | } else if (message.type === 'endOfCandidates') { 141 | this.pc.pc.addIceCandidate(undefined); 142 | } 143 | }; 144 | 145 | // send via signalling channel 146 | Peer.prototype.send = function (messageType, payload) { 147 | var message = { 148 | to: this.id, 149 | sid: this.sid, 150 | broadcaster: this.broadcaster, 151 | roomType: this.type, 152 | type: messageType, 153 | payload: payload, 154 | prefix: webrtcSupport.prefix 155 | }; 156 | this.logger.log('sending', messageType, message); 157 | this.parent.emit('message', message); 158 | }; 159 | 160 | // send via data channel 161 | // returns true when message was sent and false if channel is not open 162 | Peer.prototype.sendDirectly = function (channel, messageType, payload) { 163 | var message = { 164 | type: messageType, 165 | payload: payload 166 | }; 167 | this.logger.log('sending via datachannel', channel, messageType, message); 168 | var dc = this.getDataChannel(channel); 169 | if (dc.readyState != 'open') return false; 170 | dc.send(JSON.stringify(message)); 171 | return true; 172 | }; 173 | 174 | // Internal method registering handlers for a data channel and emitting events on the peer 175 | Peer.prototype._observeDataChannel = function (channel) { 176 | var self = this; 177 | channel.onclose = this.emit.bind(this, 'channelClose', channel); 178 | channel.onerror = this.emit.bind(this, 'channelError', channel); 179 | channel.onmessage = function (event) { 180 | self.emit('channelMessage', self, channel.label, JSON.parse(event.data), channel, event); 181 | }; 182 | channel.onopen = this.emit.bind(this, 'channelOpen', channel); 183 | }; 184 | 185 | // Fetch or create a data channel by the given name 186 | Peer.prototype.getDataChannel = function (name, opts) { 187 | if (!webrtcSupport.supportDataChannel) return this.emit('error', new Error('createDataChannel not supported')); 188 | var channel = this.channels[name]; 189 | opts || (opts = {}); 190 | if (channel) return channel; 191 | // if we don't have one by this label, create it 192 | channel = this.channels[name] = this.pc.createDataChannel(name, opts); 193 | this._observeDataChannel(channel); 194 | return channel; 195 | }; 196 | 197 | Peer.prototype.onIceCandidate = function (candidate) { 198 | if (this.closed) return; 199 | if (candidate) { 200 | var pcConfig = this.parent.config.peerConnectionConfig; 201 | if (webrtcSupport.prefix === 'moz' && pcConfig && pcConfig.iceTransports && 202 | candidate.candidate && candidate.candidate.candidate && 203 | candidate.candidate.candidate.indexOf(pcConfig.iceTransports) < 0) { 204 | this.logger.log('Ignoring ice candidate not matching pcConfig iceTransports type: ', pcConfig.iceTransports); 205 | } else { 206 | this.send('candidate', candidate); 207 | } 208 | } else { 209 | this.logger.log("End of candidates."); 210 | } 211 | }; 212 | 213 | Peer.prototype.start = function () { 214 | var self = this; 215 | 216 | // well, the webrtc api requires that we either 217 | // a) create a datachannel a priori 218 | // b) do a renegotiation later to add the SCTP m-line 219 | // Let's do (a) first... 220 | if (this.enableDataChannels) { 221 | this.getDataChannel('simplewebrtc'); 222 | } 223 | 224 | this.pc.offer(this.receiveMedia, function (err, sessionDescription) { 225 | //self.send('offer', sessionDescription); 226 | }); 227 | }; 228 | 229 | Peer.prototype.icerestart = function () { 230 | var constraints = this.receiveMedia; 231 | constraints.mandatory.IceRestart = true; 232 | this.pc.offer(constraints, function (err, success) { }); 233 | }; 234 | 235 | Peer.prototype.end = function () { 236 | if (this.closed) return; 237 | this.pc.close(); 238 | this.handleStreamRemoved(); 239 | }; 240 | 241 | Peer.prototype.handleRemoteStreamAdded = function (event) { 242 | var self = this; 243 | if (this.stream) { 244 | this.logger.warn('Already have a remote stream'); 245 | } else { 246 | this.stream = event.stream; 247 | 248 | this.stream.getTracks().forEach(function (track) { 249 | track.addEventListener('ended', function () { 250 | if (isAllTracksEnded(self.stream)) { 251 | self.end(); 252 | } 253 | }); 254 | }); 255 | 256 | this.parent.emit('peerStreamAdded', this); 257 | } 258 | }; 259 | 260 | Peer.prototype.handleStreamRemoved = function () { 261 | var peerIndex = this.parent.peers.indexOf(this); 262 | if (peerIndex > -1) { 263 | this.parent.peers.splice(peerIndex, 1); 264 | this.closed = true; 265 | this.parent.emit('peerStreamRemoved', this); 266 | } 267 | }; 268 | 269 | Peer.prototype.handleDataChannelAdded = function (channel) { 270 | this.channels[channel.label] = channel; 271 | this._observeDataChannel(channel); 272 | }; 273 | 274 | Peer.prototype.sendFile = function (file) { 275 | var sender = new FileTransfer.Sender(); 276 | var dc = this.getDataChannel('filetransfer' + (new Date()).getTime(), { 277 | protocol: INBAND_FILETRANSFER_V1 278 | }); 279 | // override onopen 280 | dc.onopen = function () { 281 | dc.send(JSON.stringify({ 282 | size: file.size, 283 | name: file.name 284 | })); 285 | sender.send(file, dc); 286 | }; 287 | // override onclose 288 | dc.onclose = function () { 289 | console.log('sender received transfer'); 290 | sender.emit('complete'); 291 | }; 292 | return sender; 293 | }; 294 | 295 | module.exports = Peer; 296 | -------------------------------------------------------------------------------- /src/simplewebrtc.js: -------------------------------------------------------------------------------- 1 | var WebRTC = require('./webrtc'); 2 | var WildEmitter = require('wildemitter'); 3 | var webrtcSupport = require('webrtcsupport'); 4 | var attachMediaStream = require('attachmediastream'); 5 | var mockconsole = require('mockconsole'); 6 | var SocketIoConnection = require('./socketioconnection'); 7 | 8 | function SimpleWebRTC(opts) { 9 | var self = this; 10 | var options = opts || {}; 11 | var config = this.config = { 12 | url: 'https://sandbox.simplewebrtc.com:443/', 13 | socketio: {/* 'force new connection':true*/}, 14 | connection: null, 15 | debug: false, 16 | localVideoEl: '', 17 | remoteVideosEl: '', 18 | enableDataChannels: true, 19 | autoRequestMedia: false, 20 | autoRemoveVideos: true, 21 | adjustPeerVolume: false, 22 | peerVolumeWhenSpeaking: 0.25, 23 | media: { 24 | video: true, 25 | audio: true 26 | }, 27 | receiveMedia: { 28 | offerToReceiveAudio: 1, 29 | offerToReceiveVideo: 1 30 | }, 31 | localVideo: { 32 | autoplay: true, 33 | mirror: true, 34 | muted: true 35 | } 36 | }; 37 | var item, connection; 38 | 39 | // We also allow a 'logger' option. It can be any object that implements 40 | // log, warn, and error methods. 41 | // We log nothing by default, following "the rule of silence": 42 | // http://www.linfo.org/rule_of_silence.html 43 | this.logger = function () { 44 | // we assume that if you're in debug mode and you didn't 45 | // pass in a logger, you actually want to log as much as 46 | // possible. 47 | if (opts.debug) { 48 | return opts.logger || console; 49 | } else { 50 | // or we'll use your logger which should have its own logic 51 | // for output. Or we'll return the no-op. 52 | return opts.logger || mockconsole; 53 | } 54 | }(); 55 | 56 | // set our config from options 57 | for (item in options) { 58 | if (options.hasOwnProperty(item)) { 59 | this.config[item] = options[item]; 60 | } 61 | } 62 | 63 | // attach detected support for convenience 64 | this.capabilities = webrtcSupport; 65 | 66 | // call WildEmitter constructor 67 | WildEmitter.call(this); 68 | 69 | // create default SocketIoConnection if it's not passed in 70 | if (this.config.connection === null) { 71 | connection = this.connection = new SocketIoConnection(this.config); 72 | } else { 73 | connection = this.connection = this.config.connection; 74 | } 75 | 76 | connection.on('connect', function () { 77 | self.emit('connectionReady', connection.getSessionid()); 78 | self.sessionReady = true; 79 | self.testReadiness(); 80 | }); 81 | 82 | connection.on('message', function (message) { 83 | var peers = self.webrtc.getPeers(message.from, message.roomType); 84 | var peer; 85 | 86 | if (message.type === 'offer') { 87 | if (peers.length) { 88 | peers.forEach(function (p) { 89 | if (p.sid == message.sid) peer = p; 90 | }); 91 | //if (!peer) peer = peers[0]; // fallback for old protocol versions 92 | } 93 | if (!peer) { 94 | peer = self.webrtc.createPeer({ 95 | id: message.from, 96 | sid: message.sid, 97 | type: message.roomType, 98 | enableDataChannels: self.config.enableDataChannels && message.roomType !== 'screen', 99 | sharemyscreen: message.roomType === 'screen' && !message.broadcaster, 100 | broadcaster: message.roomType === 'screen' && !message.broadcaster ? self.connection.getSessionid() : null 101 | }); 102 | self.emit('createdPeer', peer); 103 | } 104 | peer.handleMessage(message); 105 | } else if (peers.length) { 106 | peers.forEach(function (peer) { 107 | if (message.sid) { 108 | if (peer.sid === message.sid) { 109 | peer.handleMessage(message); 110 | } 111 | } else { 112 | peer.handleMessage(message); 113 | } 114 | }); 115 | } 116 | }); 117 | 118 | connection.on('remove', function (room) { 119 | if (room.id !== self.connection.getSessionid()) { 120 | self.webrtc.removePeers(room.id, room.type); 121 | } 122 | }); 123 | 124 | // instantiate our main WebRTC helper 125 | // using same logger from logic here 126 | opts.logger = this.logger; 127 | opts.debug = false; 128 | this.webrtc = new WebRTC(opts); 129 | 130 | // attach a few methods from underlying lib to simple. 131 | ['mute', 'unmute', 'pauseVideo', 'resumeVideo', 'pause', 'resume', 'sendToAll', 'sendDirectlyToAll', 'getPeers'].forEach(function (method) { 132 | self[method] = self.webrtc[method].bind(self.webrtc); 133 | }); 134 | 135 | // proxy events from WebRTC 136 | this.webrtc.on('*', function () { 137 | self.emit.apply(self, arguments); 138 | }); 139 | 140 | // log all events in debug mode 141 | if (config.debug) { 142 | this.on('*', this.logger.log.bind(this.logger, 'SimpleWebRTC event:')); 143 | } 144 | 145 | // check for readiness 146 | this.webrtc.on('localStream', function () { 147 | self.testReadiness(); 148 | }); 149 | 150 | this.webrtc.on('message', function (payload) { 151 | self.connection.emit('message', payload); 152 | }); 153 | 154 | this.webrtc.on('peerStreamAdded', this.handlePeerStreamAdded.bind(this)); 155 | this.webrtc.on('peerStreamRemoved', this.handlePeerStreamRemoved.bind(this)); 156 | 157 | // echo cancellation attempts 158 | if (this.config.adjustPeerVolume) { 159 | this.webrtc.on('speaking', this.setVolumeForAll.bind(this, this.config.peerVolumeWhenSpeaking)); 160 | this.webrtc.on('stoppedSpeaking', this.setVolumeForAll.bind(this, 1)); 161 | } 162 | 163 | connection.on('stunservers', function (args) { 164 | // resets/overrides the config 165 | self.webrtc.config.peerConnectionConfig.iceServers = args; 166 | self.emit('stunservers', args); 167 | }); 168 | connection.on('turnservers', function (args) { 169 | // appends to the config 170 | self.webrtc.config.peerConnectionConfig.iceServers = self.webrtc.config.peerConnectionConfig.iceServers.concat(args); 171 | self.emit('turnservers', args); 172 | }); 173 | 174 | this.webrtc.on('iceFailed', function (peer) { 175 | // local ice failure 176 | }); 177 | this.webrtc.on('connectivityError', function (peer) { 178 | // remote ice failure 179 | }); 180 | 181 | 182 | // sending mute/unmute to all peers 183 | this.webrtc.on('audioOn', function () { 184 | self.webrtc.sendToAll('unmute', {name: 'audio'}); 185 | }); 186 | this.webrtc.on('audioOff', function () { 187 | self.webrtc.sendToAll('mute', {name: 'audio'}); 188 | }); 189 | this.webrtc.on('videoOn', function () { 190 | self.webrtc.sendToAll('unmute', {name: 'video'}); 191 | }); 192 | this.webrtc.on('videoOff', function () { 193 | self.webrtc.sendToAll('mute', {name: 'video'}); 194 | }); 195 | 196 | // screensharing events 197 | this.webrtc.on('localScreen', function (stream) { 198 | var item, 199 | el = document.createElement('video'), 200 | container = self.getRemoteVideoContainer(); 201 | 202 | el.oncontextmenu = function () { return false; }; 203 | el.id = 'localScreen'; 204 | attachMediaStream(stream, el); 205 | if (container) { 206 | container.appendChild(el); 207 | } 208 | 209 | self.emit('localScreenAdded', el); 210 | self.connection.emit('shareScreen'); 211 | 212 | self.webrtc.peers.forEach(function (existingPeer) { 213 | var peer; 214 | if (existingPeer.type === 'video') { 215 | peer = self.webrtc.createPeer({ 216 | id: existingPeer.id, 217 | type: 'screen', 218 | sharemyscreen: true, 219 | enableDataChannels: false, 220 | receiveMedia: { 221 | offerToReceiveAudio: 0, 222 | offerToReceiveVideo: 0 223 | }, 224 | broadcaster: self.connection.getSessionid(), 225 | }); 226 | self.emit('createdPeer', peer); 227 | peer.start(); 228 | } 229 | }); 230 | }); 231 | this.webrtc.on('localScreenStopped', function (stream) { 232 | if (self.getLocalScreen()) { 233 | self.stopScreenShare(); 234 | } 235 | /* 236 | self.connection.emit('unshareScreen'); 237 | self.webrtc.peers.forEach(function (peer) { 238 | if (peer.sharemyscreen) { 239 | peer.end(); 240 | } 241 | }); 242 | */ 243 | }); 244 | 245 | this.webrtc.on('channelMessage', function (peer, label, data) { 246 | if (data.type == 'volume') { 247 | self.emit('remoteVolumeChange', peer, data.volume); 248 | } 249 | }); 250 | 251 | if (this.config.autoRequestMedia) this.startLocalVideo(); 252 | } 253 | 254 | 255 | SimpleWebRTC.prototype = Object.create(WildEmitter.prototype, { 256 | constructor: { 257 | value: SimpleWebRTC 258 | } 259 | }); 260 | 261 | SimpleWebRTC.prototype.leaveRoom = function () { 262 | if (this.roomName) { 263 | this.connection.emit('leave'); 264 | while (this.webrtc.peers.length) { 265 | this.webrtc.peers[0].end(); 266 | } 267 | if (this.getLocalScreen()) { 268 | this.stopScreenShare(); 269 | } 270 | this.emit('leftRoom', this.roomName); 271 | this.roomName = undefined; 272 | } 273 | }; 274 | 275 | SimpleWebRTC.prototype.disconnect = function () { 276 | this.connection.disconnect(); 277 | delete this.connection; 278 | }; 279 | 280 | SimpleWebRTC.prototype.handlePeerStreamAdded = function (peer) { 281 | var self = this; 282 | var container = this.getRemoteVideoContainer(); 283 | var video = attachMediaStream(peer.stream); 284 | 285 | // store video element as part of peer for easy removal 286 | peer.videoEl = video; 287 | video.id = this.getDomId(peer); 288 | 289 | if (container) container.appendChild(video); 290 | 291 | this.emit('videoAdded', video, peer); 292 | 293 | // send our mute status to new peer if we're muted 294 | // currently called with a small delay because it arrives before 295 | // the video element is created otherwise (which happens after 296 | // the async setRemoteDescription-createAnswer) 297 | window.setTimeout(function () { 298 | if (!self.webrtc.isAudioEnabled()) { 299 | peer.send('mute', {name: 'audio'}); 300 | } 301 | if (!self.webrtc.isVideoEnabled()) { 302 | peer.send('mute', {name: 'video'}); 303 | } 304 | }, 250); 305 | }; 306 | 307 | SimpleWebRTC.prototype.handlePeerStreamRemoved = function (peer) { 308 | var container = this.getRemoteVideoContainer(); 309 | var videoEl = peer.videoEl; 310 | if (this.config.autoRemoveVideos && container && videoEl) { 311 | container.removeChild(videoEl); 312 | } 313 | if (videoEl) this.emit('videoRemoved', videoEl, peer); 314 | }; 315 | 316 | SimpleWebRTC.prototype.getDomId = function (peer) { 317 | return [peer.id, peer.type, peer.broadcaster ? 'broadcasting' : 'incoming'].join('_'); 318 | }; 319 | 320 | // set volume on video tag for all peers takse a value between 0 and 1 321 | SimpleWebRTC.prototype.setVolumeForAll = function (volume) { 322 | this.webrtc.peers.forEach(function (peer) { 323 | if (peer.videoEl) peer.videoEl.volume = volume; 324 | }); 325 | }; 326 | 327 | SimpleWebRTC.prototype.joinRoom = function (name, cb) { 328 | var self = this; 329 | this.roomName = name; 330 | this.connection.emit('join', name, function (err, roomDescription) { 331 | console.log('join CB', err, roomDescription); 332 | if (err) { 333 | self.emit('error', err); 334 | } else { 335 | var id, 336 | client, 337 | type, 338 | peer; 339 | for (id in roomDescription.clients) { 340 | client = roomDescription.clients[id]; 341 | for (type in client) { 342 | if (client[type]) { 343 | peer = self.webrtc.createPeer({ 344 | id: id, 345 | type: type, 346 | enableDataChannels: self.config.enableDataChannels && type !== 'screen', 347 | receiveMedia: { 348 | offerToReceiveAudio: type !== 'screen' && self.config.receiveMedia.offerToReceiveAudio ? 1 : 0, 349 | offerToReceiveVideo: self.config.receiveMedia.offerToReceiveVideo 350 | } 351 | }); 352 | self.emit('createdPeer', peer); 353 | peer.start(); 354 | } 355 | } 356 | } 357 | } 358 | 359 | if (cb) cb(err, roomDescription); 360 | self.emit('joinedRoom', name); 361 | }); 362 | }; 363 | 364 | SimpleWebRTC.prototype.getEl = function (idOrEl) { 365 | if (typeof idOrEl === 'string') { 366 | return document.getElementById(idOrEl); 367 | } else { 368 | return idOrEl; 369 | } 370 | }; 371 | 372 | SimpleWebRTC.prototype.startLocalVideo = function () { 373 | var self = this; 374 | this.webrtc.start(this.config.media, function (err, stream) { 375 | if (err) { 376 | self.emit('localMediaError', err); 377 | } else { 378 | attachMediaStream(stream, self.getLocalVideoContainer(), self.config.localVideo); 379 | } 380 | }); 381 | }; 382 | 383 | SimpleWebRTC.prototype.stopLocalVideo = function () { 384 | this.webrtc.stop(); 385 | }; 386 | 387 | // this accepts either element ID or element 388 | // and either the video tag itself or a container 389 | // that will be used to put the video tag into. 390 | SimpleWebRTC.prototype.getLocalVideoContainer = function () { 391 | var el = this.getEl(this.config.localVideoEl); 392 | if (el && el.tagName === 'VIDEO') { 393 | el.oncontextmenu = function () { return false; }; 394 | return el; 395 | } else if (el) { 396 | var video = document.createElement('video'); 397 | video.oncontextmenu = function () { return false; }; 398 | el.appendChild(video); 399 | return video; 400 | } else { 401 | return; 402 | } 403 | }; 404 | 405 | SimpleWebRTC.prototype.getRemoteVideoContainer = function () { 406 | return this.getEl(this.config.remoteVideosEl); 407 | }; 408 | 409 | SimpleWebRTC.prototype.shareScreen = function (cb) { 410 | this.webrtc.startScreenShare(cb); 411 | }; 412 | 413 | SimpleWebRTC.prototype.getLocalScreen = function () { 414 | return this.webrtc.localScreens && this.webrtc.localScreens[0]; 415 | }; 416 | 417 | SimpleWebRTC.prototype.stopScreenShare = function () { 418 | this.connection.emit('unshareScreen'); 419 | var videoEl = document.getElementById('localScreen'); 420 | var container = this.getRemoteVideoContainer(); 421 | 422 | if (this.config.autoRemoveVideos && container && videoEl) { 423 | container.removeChild(videoEl); 424 | } 425 | 426 | // a hack to emit the event the removes the video 427 | // element that we want 428 | if (videoEl) { 429 | this.emit('videoRemoved', videoEl); 430 | } 431 | if (this.getLocalScreen()) { 432 | this.webrtc.stopScreenShare(); 433 | } 434 | this.webrtc.peers.forEach(function (peer) { 435 | if (peer.broadcaster) { 436 | peer.end(); 437 | } 438 | }); 439 | }; 440 | 441 | SimpleWebRTC.prototype.testReadiness = function () { 442 | var self = this; 443 | if (this.sessionReady) { 444 | if (!this.config.media.video && !this.config.media.audio) { 445 | self.emit('readyToCall', self.connection.getSessionid()); 446 | } else if (this.webrtc.localStreams.length > 0) { 447 | self.emit('readyToCall', self.connection.getSessionid()); 448 | } 449 | } 450 | }; 451 | 452 | SimpleWebRTC.prototype.createRoom = function (name, cb) { 453 | this.roomName = name; 454 | if (arguments.length === 2) { 455 | this.connection.emit('create', name, cb); 456 | } else { 457 | this.connection.emit('create', name); 458 | } 459 | }; 460 | 461 | SimpleWebRTC.prototype.sendFile = function () { 462 | if (!webrtcSupport.dataChannel) { 463 | return this.emit('error', new Error('DataChannelNotSupported')); 464 | } 465 | 466 | }; 467 | 468 | module.exports = SimpleWebRTC; 469 | -------------------------------------------------------------------------------- /src/socketioconnection.js: -------------------------------------------------------------------------------- 1 | var io = require('socket.io-client'); 2 | 3 | function SocketIoConnection(config) { 4 | this.connection = io.connect(config.url, config.socketio); 5 | } 6 | 7 | SocketIoConnection.prototype.on = function (ev, fn) { 8 | this.connection.on(ev, fn); 9 | }; 10 | 11 | SocketIoConnection.prototype.emit = function () { 12 | this.connection.emit.apply(this.connection, arguments); 13 | }; 14 | 15 | SocketIoConnection.prototype.getSessionid = function () { 16 | return this.connection.id; 17 | }; 18 | 19 | SocketIoConnection.prototype.disconnect = function () { 20 | return this.connection.disconnect(); 21 | }; 22 | 23 | module.exports = SocketIoConnection; 24 | -------------------------------------------------------------------------------- /src/webrtc.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var webrtcSupport = require('webrtcsupport'); 3 | var mockconsole = require('mockconsole'); 4 | var localMedia = require('localmedia'); 5 | var Peer = require('./peer'); 6 | 7 | function WebRTC(opts) { 8 | var self = this; 9 | var options = opts || {}; 10 | var config = this.config = { 11 | debug: false, 12 | // makes the entire PC config overridable 13 | peerConnectionConfig: { 14 | iceServers: [{'urls': 'stun:stun.l.google.com:19302'}] 15 | }, 16 | peerConnectionConstraints: { 17 | optional: [] 18 | }, 19 | receiveMedia: { 20 | offerToReceiveAudio: 1, 21 | offerToReceiveVideo: 1 22 | }, 23 | enableDataChannels: true 24 | }; 25 | var item; 26 | 27 | // We also allow a 'logger' option. It can be any object that implements 28 | // log, warn, and error methods. 29 | // We log nothing by default, following "the rule of silence": 30 | // http://www.linfo.org/rule_of_silence.html 31 | this.logger = function () { 32 | // we assume that if you're in debug mode and you didn't 33 | // pass in a logger, you actually want to log as much as 34 | // possible. 35 | if (opts.debug) { 36 | return opts.logger || console; 37 | } else { 38 | // or we'll use your logger which should have its own logic 39 | // for output. Or we'll return the no-op. 40 | return opts.logger || mockconsole; 41 | } 42 | }(); 43 | 44 | // set options 45 | for (item in options) { 46 | if (options.hasOwnProperty(item)) { 47 | this.config[item] = options[item]; 48 | } 49 | } 50 | 51 | // check for support 52 | if (!webrtcSupport.support) { 53 | this.logger.error('Your browser doesn\'t seem to support WebRTC'); 54 | } 55 | 56 | // where we'll store our peer connections 57 | this.peers = []; 58 | 59 | // call localMedia constructor 60 | localMedia.call(this, this.config); 61 | 62 | this.on('speaking', function () { 63 | if (!self.hardMuted) { 64 | // FIXME: should use sendDirectlyToAll, but currently has different semantics wrt payload 65 | self.peers.forEach(function (peer) { 66 | if (peer.enableDataChannels) { 67 | var dc = peer.getDataChannel('hark'); 68 | if (dc.readyState != 'open') return; 69 | dc.send(JSON.stringify({type: 'speaking'})); 70 | } 71 | }); 72 | } 73 | }); 74 | this.on('stoppedSpeaking', function () { 75 | if (!self.hardMuted) { 76 | // FIXME: should use sendDirectlyToAll, but currently has different semantics wrt payload 77 | self.peers.forEach(function (peer) { 78 | if (peer.enableDataChannels) { 79 | var dc = peer.getDataChannel('hark'); 80 | if (dc.readyState != 'open') return; 81 | dc.send(JSON.stringify({type: 'stoppedSpeaking'})); 82 | } 83 | }); 84 | } 85 | }); 86 | this.on('volumeChange', function (volume, treshold) { 87 | if (!self.hardMuted) { 88 | // FIXME: should use sendDirectlyToAll, but currently has different semantics wrt payload 89 | self.peers.forEach(function (peer) { 90 | if (peer.enableDataChannels) { 91 | var dc = peer.getDataChannel('hark'); 92 | if (dc.readyState != 'open') return; 93 | dc.send(JSON.stringify({type: 'volume', volume: volume })); 94 | } 95 | }); 96 | } 97 | }); 98 | 99 | // log events in debug mode 100 | if (this.config.debug) { 101 | this.on('*', function (event, val1, val2) { 102 | var logger; 103 | // if you didn't pass in a logger and you explicitly turning on debug 104 | // we're just going to assume you're wanting log output with console 105 | if (self.config.logger === mockconsole) { 106 | logger = console; 107 | } else { 108 | logger = self.logger; 109 | } 110 | logger.log('event:', event, val1, val2); 111 | }); 112 | } 113 | } 114 | 115 | util.inherits(WebRTC, localMedia); 116 | 117 | WebRTC.prototype.createPeer = function (opts) { 118 | var peer; 119 | opts.parent = this; 120 | peer = new Peer(opts); 121 | this.peers.push(peer); 122 | return peer; 123 | }; 124 | 125 | // removes peers 126 | WebRTC.prototype.removePeers = function (id, type) { 127 | this.getPeers(id, type).forEach(function (peer) { 128 | peer.end(); 129 | }); 130 | }; 131 | 132 | // fetches all Peer objects by session id and/or type 133 | WebRTC.prototype.getPeers = function (sessionId, type) { 134 | return this.peers.filter(function (peer) { 135 | return (!sessionId || peer.id === sessionId) && (!type || peer.type === type); 136 | }); 137 | }; 138 | 139 | // sends message to all 140 | WebRTC.prototype.sendToAll = function (message, payload) { 141 | this.peers.forEach(function (peer) { 142 | peer.send(message, payload); 143 | }); 144 | }; 145 | 146 | // sends message to all using a datachannel 147 | // only sends to anyone who has an open datachannel 148 | WebRTC.prototype.sendDirectlyToAll = function (channel, message, payload) { 149 | this.peers.forEach(function (peer) { 150 | if (peer.enableDataChannels) { 151 | peer.sendDirectly(channel, message, payload); 152 | } 153 | }); 154 | }; 155 | 156 | module.exports = WebRTC; 157 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SimpleWebRTC Demo 5 | 6 | 7 |

Start a room

8 | 28 | 29 |

30 |
31 | 32 | 33 |
34 |
35 | 36 |
37 |
38 |
39 | 40 | 41 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /test/run-selenium: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Installs Chrome or Firefox if missing and sets default browser to 4 | # chrome stable. 5 | # 6 | echo -e "\nPreparing Selenium webdriver tests." 7 | 8 | BINDIR=./browsers/bin 9 | export BROWSER=chrome 10 | export BVER=stable 11 | BROWSERBIN=$BINDIR/$BROWSER-$BVER 12 | if [ ! -x $BROWSERBIN ]; then 13 | echo "Installing Chrome" 14 | ./node_modules/travis-multirunner/setup.sh 15 | fi 16 | export BROWSER=firefox 17 | export BVER=stable 18 | BROWSERBIN=$BINDIR/$BROWSER-$BVER 19 | if [ ! -x $BROWSERBIN ]; then 20 | echo "Installing Firefox" 21 | ./node_modules/travis-multirunner/setup.sh 22 | fi 23 | echo "Starting browser." 24 | PATH=$PATH:./node_modules/.bin 25 | 26 | node test/selenium/index.js 27 | -------------------------------------------------------------------------------- /test/selenium/index.js: -------------------------------------------------------------------------------- 1 | // Test 1-1 selenium. 2 | require('./p2p'); 3 | 4 | // Test three-party mesh. 5 | // apparently too much for travis :-( 6 | // TODO: investigate sauce labs 7 | //require('./three'); 8 | -------------------------------------------------------------------------------- /test/selenium/p2p.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var test = require('tape'); 3 | 4 | // https://code.google.com/p/selenium/wiki/WebDriverJs 5 | var seleniumHelpers = require('webrtc-testbed/webdriver'); 6 | var webdriver = require('selenium-webdriver'); 7 | 8 | function doJoin(driver, room) { 9 | return driver.get('file://' + process.cwd() + '/test/index.html?' + room); 10 | } 11 | 12 | function iceConnected(driver) { 13 | return driver.wait(() => { 14 | return driver.executeScript(function () { 15 | return window.webrtc && window.webrtc.getPeers().length === 1 && 16 | (window.webrtc.getPeers()[0].pc.iceConnectionState === 'connected' || 17 | window.webrtc.getPeers()[0].pc.iceConnectionState === 'completed'); 18 | }); 19 | }, 30 * 1000) 20 | } 21 | 22 | const TIMEOUT = 30000; 23 | function waitNPeerConnectionsExist(driver, n) { 24 | return driver.wait(function() { 25 | return driver.executeScript(function(n) { 26 | return webrtc.getPeers().length === n; 27 | }, n); 28 | }, TIMEOUT); 29 | } 30 | 31 | function waitAllPeerConnectionsConnected(driver) { 32 | return driver.wait(function() { 33 | return driver.executeScript(function() { 34 | var peers = webrtc.getPeers(); 35 | var states = []; 36 | peers.forEach(function(peer) { 37 | states.push(peer.pc.iceConnectionState); 38 | }); 39 | return states.length === states.filter((s) => s === 'connected' || s === 'completed').length; 40 | }); 41 | }, TIMEOUT); 42 | } 43 | 44 | function waitNVideosExist(driver, n) { 45 | return driver.wait(function() { 46 | return driver.executeScript(function(n) { 47 | return document.querySelectorAll('video').length === n; 48 | }, n); 49 | }, TIMEOUT); 50 | } 51 | 52 | function waitAllVideosHaveEnoughData(driver) { 53 | return driver.wait(function() { 54 | return driver.executeScript(function() { 55 | var videos = document.querySelectorAll('video'); 56 | var ready = 0; 57 | for (var i = 0; i < videos.length; i++) { 58 | if (videos[i].readyState >= videos[i].HAVE_ENOUGH_DATA) { 59 | ready++; 60 | } 61 | } 62 | return ready === videos.length; 63 | }); 64 | }, TIMEOUT); 65 | } 66 | 67 | function testP2P(browserA, browserB, t) { 68 | const room = 'testing_' + Math.floor(Math.random() * 100000); 69 | 70 | const driverA = seleniumHelpers.buildDriver(browserA, {bver: process.env.BVER}); 71 | const driverB = seleniumHelpers.buildDriver(browserB, {bver: process.env.BVER}); 72 | const drivers = [driverA, driverB]; 73 | 74 | return Promise.all(drivers.map(driver => doJoin(driver, room))) 75 | .then(() => { 76 | t.pass('joined room'); 77 | return Promise.all(drivers.map(driver => waitNPeerConnectionsExist(driver, 1))); 78 | }) 79 | .then(() => { 80 | return Promise.all(drivers.map(driver => waitAllPeerConnectionsConnected(driver))); 81 | }) 82 | .then(() => { 83 | t.pass('P2P connected'); 84 | return Promise.all(drivers.map(driver => waitNVideosExist(driver, 2))); 85 | }) 86 | .then(() => { 87 | return Promise.all(drivers.map(driver => waitAllVideosHaveEnoughData(driver))); 88 | }) 89 | .then(() => { 90 | t.pass('all videos have enough data'); 91 | return Promise.all(drivers.map(driver => driver.quit())); 92 | }) 93 | .then(() => { 94 | t.end(); 95 | }) 96 | .then(null, function (err) { 97 | return Promise.all(drivers.map(driver => driver.quit())) 98 | .then(() => t.fail(err)); 99 | }); 100 | } 101 | 102 | test('P2P, Chrome-Chrome', function (t) { 103 | testP2P('chrome', 'chrome', t); 104 | }); 105 | 106 | test('P2P, Firefox-Firefox', function (t) { 107 | testP2P('firefox', 'firefox', t); 108 | }); 109 | 110 | test('P2P, Chrome-Firefox', function (t) { 111 | testP2P('chrome', 'firefox', t); 112 | }); 113 | 114 | test('P2P, Firefox-Chrome', function (t) { 115 | testP2P('firefox', 'chrome', t); 116 | }); 117 | -------------------------------------------------------------------------------- /test/selenium/three.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var test = require('tape'); 3 | 4 | // https://code.google.com/p/selenium/wiki/WebDriverJs 5 | var webdriver = require('selenium-webdriver'); 6 | var chrome = require('selenium-webdriver/chrome'); 7 | var firefox = require('selenium-webdriver/firefox'); 8 | 9 | function buildDriver(browser) { 10 | // Firefox options. 11 | // http://selenium.googlecode.com/git/docs/api/javascript/module_selenium-webdriver_firefox.html 12 | var profile = new firefox.Profile(); 13 | profile.setPreference('media.navigator.streams.fake', true); 14 | var firefoxOptions = new firefox.Options() 15 | .setProfile(profile); 16 | 17 | // Chrome options. 18 | // http://selenium.googlecode.com/git/docs/api/javascript/module_selenium-webdriver_chrome_class_Options.html#addArguments 19 | var chromeOptions = new chrome.Options() 20 | /* 21 | .addArguments('enable-logging=1') 22 | .addArguments('v=1') 23 | .addArguments('vmodule=*libjingle/source/talk/*=4') 24 | .addArguments('user-data-dir=/some/where') 25 | */ 26 | .addArguments('allow-file-access-from-files') 27 | .addArguments('use-fake-device-for-media-stream') 28 | .addArguments('use-fake-ui-for-media-stream'); 29 | // use-file-for-fake-audio-capture -- see https://code.google.com/p/chromium/issues/detail?id=421054 30 | 31 | return new webdriver.Builder() 32 | .forBrowser(browser || process.env.BROWSER || 'firefox') 33 | .setFirefoxOptions(firefoxOptions) 34 | .setChromeOptions(chromeOptions) 35 | .build(); 36 | } 37 | 38 | function doJoin(driver, room) { 39 | return driver.get('file://' + process.cwd() + '/index.html?' + room); 40 | } 41 | 42 | function test3(browserA, browserB, browserC, t) { 43 | var room = 'testing_' + Math.floor(Math.random() * 100000); 44 | 45 | var userA = buildDriver(browserA); 46 | doJoin(userA, room); 47 | 48 | var userB = buildDriver(browserB); 49 | doJoin(userB, room); 50 | 51 | var userC = buildDriver(browserC); 52 | doJoin(userC, room); 53 | userA.wait(function () { 54 | return userA.executeScript('return (function() {' + 55 | 'var connected = 0;' + 56 | 'webrtc.getPeers().forEach(function (peer) {' + 57 | ' if (peer.pc.iceConnectionState === \'connected\' || peer.pc.iceConnectionState === \'completed\') connected++;' + 58 | '});' + 59 | 'return connected === 2;' + 60 | '})()'); 61 | }, 15 * 1000) 62 | .then(function () { 63 | //return userA.sleep(2000); 64 | }) 65 | .then(function () { 66 | t.pass('Mesh connected'); 67 | userA.quit(); 68 | userB.quit(); 69 | userC.quit().then(function () { 70 | t.end(); 71 | }); 72 | }) 73 | .then(null, function (err) { 74 | t.fail('Mesh failed'); 75 | userA.quit(); 76 | userB.quit(); 77 | userC.quit().then(function () { 78 | t.end(); 79 | }); 80 | }); 81 | } 82 | 83 | test('Mesh, Chrome-Chrome-Chrome', function (t) { 84 | test3('chrome', 'chrome', 'chrome', t); 85 | }); 86 | 87 | test('Mesh, Chrome-Firefox-Firefox', function (t) { 88 | test3('chrome', 'firefox', 'firefox', t); 89 | }); 90 | 91 | test('Mesh, Firefox-Firefox-Chrome', function (t) { 92 | test3('firefox', 'firefox', 'chrome', t); 93 | }); 94 | 95 | test('Mesh, Chrome-Chrome-Firefox', function (t) { 96 | test3('chrome', 'chrome', 'chrome', t); 97 | }); 98 | 99 | test('Mesh, Firefox-Firefox-Firefox', function (t) { 100 | test3('firefox', 'firefox', 'firefox', t); 101 | }); 102 | --------------------------------------------------------------------------------