├── .gitignore ├── .travis.yml ├── Gruntfile.coffee ├── Readme.md ├── build ├── compiled-dev.js └── compiled-prod.js ├── coffeelint.json ├── package.json ├── src ├── broadcast_bridge.coffee ├── browser_detect.coffee ├── call.coffee ├── chrome_screen_sharer.coffee ├── config.coffee ├── debug.coffee ├── firefox_screen_sharer.coffee ├── main.coffee ├── nearest_server.coffee ├── peer_connection_factory.coffee ├── screen_share_base.coffee ├── screen_sharer.coffee ├── signaling_connection.coffee └── vendor │ ├── primus.js │ └── uuid.js └── test ├── broadcast_bridge_test.coffee ├── call_test.coffee ├── helpers ├── fake_media_stream.coffee ├── fake_peer_connection.coffee ├── setup_and_teardown.coffee ├── stub_create_object_url.coffee ├── stub_primus.coffee └── stub_user_media.coffee ├── main_test.coffee ├── nearest_server_test.coffee ├── peer_connection_factory_test.coffee ├── runner.html ├── signaling_connection_test.coffee └── test_helper.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | .floo 3 | .env 4 | 5 | compiled/* 6 | 7 | ignored/ 8 | 9 | node_modules/* 10 | example/node_modules/* 11 | npm-debug.log 12 | 13 | tmp/ 14 | 15 | *.sublime-project 16 | *.sublime-workspace 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | 2 | module.exports = (grunt) -> 3 | grunt.initConfig 4 | pkg: grunt.file.readJSON("package.json") 5 | 6 | browserify: 7 | development: 8 | files: 9 | 'build/compiled-dev.js': ['src/main.coffee'] 10 | options: 11 | browserifyOptions: 12 | extensions: ['.coffee', '.js'] 13 | transform: ['coffeeify', ['envify', NODE_ENV: 'development']] 14 | 15 | production: 16 | files: 17 | 'build/compiled-prod.js': ['src/main.coffee'] 18 | options: 19 | browserifyOptions: 20 | extensions: ['.coffee', '.js'] 21 | transform: ['coffeeify', ['envify', NODE_ENV: 'production']] 22 | 23 | tests: 24 | files: 25 | 'compiled/all-tests.js': ['src/main.coffee', 'test/test_helper.coffee', 'test/*.coffee'] 26 | options: 27 | browserifyOptions: 28 | extensions: ['.coffee', '.js'] 29 | transform: ['coffeeify'] 30 | 31 | uglify: 32 | production: 33 | files: 34 | 'build/compiled-prod.js': ['build/compiled-prod.js'] 35 | 36 | watch: 37 | grunt: 38 | files: ["Gruntfile.coffee"] 39 | 40 | main: 41 | files: ["src/*.coffee"] 42 | tasks: ["compile"] 43 | 44 | tests: 45 | files: ["test/*.coffee", "test/**/*.coffee", "src/*.coffee", "src/**/*.js"] 46 | tasks: ["browserify:tests"] 47 | 48 | trimtrailingspaces: 49 | development: 50 | src: ['build/compiled-dev.js'] 51 | 52 | mocha: 53 | all: 54 | src: ['test/runner.html'] 55 | options: 56 | run: true 57 | log: true 58 | reporter: 'Spec' 59 | 60 | grunt.loadNpmTasks('grunt-browserify') 61 | grunt.loadNpmTasks('grunt-contrib-coffee') 62 | grunt.loadNpmTasks('grunt-mocha') 63 | grunt.loadNpmTasks("grunt-contrib-watch") 64 | grunt.loadNpmTasks('grunt-contrib-uglify') 65 | grunt.loadNpmTasks('grunt-trimtrailingspaces') 66 | 67 | grunt.registerTask "compile:production", ["browserify:production", "uglify"] 68 | grunt.registerTask "compile:development", ["browserify:development", "trimtrailingspaces:development"] 69 | grunt.registerTask "compile", ["compile:development", "compile:production"] 70 | 71 | grunt.registerTask "test", ["browserify:tests", "mocha"] 72 | 73 | grunt.registerTask "default", ["compile", "browserify:tests", "watch"] 74 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # [cine.io](https://www.cine.io) Peer JS SDK 2 | 3 | [](https://travis-ci.org/cine-io/peer-js-sdk) 4 | 5 | The JavaScript SDK for [cine.io](https://www.cine.io) [peer-to-peer](https://www.cine.io/products/peer) communication. 6 | 7 | 8 | ## Installation 9 | 10 | ```html 11 | 12 | ``` 13 | 14 | ## Usage 15 | 16 | The `CineIOPeer` object is used for real-time communication between two "peers". It supports video-chat using a [webcam and microphone](#camera-and-microphone) and also allows for sharing a [desktop screen](#screen-sharing). It's possible to [make and recieve calls](#calling) within an application or to [join chat rooms](#rooms). It can also supports [sending data](#sending-data-to-peers) between connected peers. 17 | 18 | ### Initialize 19 | 20 | Start off by initializing CineIOPeer with your public key. 21 | 22 | ```JavaScript 23 | CineIOPeer.init(CINE_IO_PUBLIC_KEY); 24 | ``` 25 | **CINE_IO_PUBLIC_KEY** 26 | This is your public key for a [cine.io](https://www.cine.io) project. 27 | 28 | ### Camera and microphone 29 | 30 | #### Turning on and off the camera and microphone 31 | 32 | CineIOPeer has functions for turning on and off your camera and/or microphone. 33 | 34 | ```JavaScript 35 | CineIOPeer.startCameraAndMicrophone(optionalCallback); 36 | CineIOPeer.stopCameraAndMicrophone(optionalCallback); 37 | CineIOPeer.startCamera(optionalCallback); 38 | CineIOPeer.stopCamera(optionalCallback); 39 | CineIOPeer.startMicrophone(optionalCallback); 40 | CineIOPeer.stopMicrophone(optionalCallback); 41 | ``` 42 | 43 | A common workflow is to start by calling `CineIOPeer.startCameraAndMicrophone` and using `CineIOPeer.stopMicrophone` for muting audio. The same can be done with `CineIOPeer.stopCamera`. 44 | 45 | Accessing the camera and microphone may result in a native browser popup asking the user for permission to the camera and microphone. As such, to avoid duplicate permission-asks, it is best to use the most appropriate camera and microphone initialization request. 46 | 47 | #### Camera and microphone status 48 | 49 | CineIOPeer has helpful functions to check the status of the camera and microphone. 50 | 51 | ```JavaScript 52 | CineIOPeer.cameraRunning(); 53 | CineIOPeer.microphoneRunning(); 54 | ``` 55 | 56 | ### Screen Sharing 57 | 58 | Screen Sharing does work but the general state of screen sharing over WebRTC is generally broken all around. Chrome requires an [extension](https://chrome.google.com/webstore/detail/cineio-screen-sharing/ancoeogeclfnhienkmfmeeomadmofhmi). Firefox requires updating browser permissions, or an extension that does it for you (coming soon), which whitelists specific domains. If you need assistance contact [cine.io support](http://support.cine.io). 59 | 60 | #### Checking support 61 | 62 | To check if your browser supports screen sharing 63 | 64 | ```JavaScript 65 | var supported = CineIOPeer.screenShareSupported() 66 | ``` 67 | 68 | #### Turning on and off screen sharing 69 | 70 | CineIOPeer has functions for turning on and off your desktop screen share. 71 | 72 | ```JavaScript 73 | CineIOPeer.startScreenShare(optionalCallback); 74 | CineIOPeer.stopScreenShare(optionalCallback); 75 | ``` 76 | 77 | #### Screen Share Status 78 | 79 | CineIOPeer has helpful functions to check the status of the desktop screen share. 80 | 81 | ```JavaScript 82 | CineIOPeer.screenShareRunning(); 83 | ``` 84 | 85 | ### Creating connections between two or more peers 86 | 87 | CineIOPeer can join users together by either rooms or individual calling. 88 | 89 | #### Rooms 90 | 91 | Rooms are one of the easiest ways to get up and running. When two or more users join a room and they will begin communicating instantly. If the first user leaves the room, the remaining users will still remain in the room and other users can still join. If you join multiple rooms at the same time, the same running streams (camera, microphone, screen) will be sent to all connected peers. 92 | 93 | ```JavaScript 94 | var room = "the-best-room-ever"; 95 | CineIOPeer.join(room, optionalCallback); 96 | ``` 97 | 98 | Leaving a room will close the connection between the user and all of the room users. To leave a room: 99 | 100 | ```JavaScript 101 | var room = 'the-best-room-ever'; 102 | CineIOPeer.leave(room, optionalCallback); 103 | ``` 104 | 105 | There is no built-in room authorization. All rooms are public. Room names are unique per project. 106 | 107 | #### Calling 108 | 109 | Calling is a super neat feature! But it is a bit more complex to setup. Calling allows users to `identify` and call another user. Other users can be invited to join the conversation. Calling is split up into two sections: `identify` and `call`. 110 | 111 | ##### Identifying a user 112 | 113 | Identifying is done with a secure token generated using your **CINE_IO_SECRET_KEY**. 114 | We don't want anybody to impersonate a different user and therefore we require a secure timestamped generated hash. 115 | This part must be done on your server as it requires your **CINE_IO_SECRET_KEY**. 116 | 117 | The pseudo code for generating a secure signature is: 118 | 119 | ```java 120 | Integer timestamp = getSecondsFromEpoch(); 121 | String signatureString = "identity=" + identity + "×tamp=" + timestamp + secretKey; 122 | String signature = sha1.hexdigest(signatureString); 123 | KeyValueStore response = {"identity": identity, "signature": signature, "timestamp": timestamp}; 124 | ``` 125 | 126 | You can find language specific implemtations in our: 127 | 128 | * [Ruby Gem](https://github.com/cine-io/cineio-ruby#identity-signature-generation) 129 | * [Node Package](https://github.com/cine-io/cineio-node#identity-signature-generation) 130 | * [Python Egg](https://github.com/cine-io/cineio-python#identity-signature-generation) 131 | 132 | This response can now be used in `CineIOPeer`. Identifying a user: 133 | 134 | ```JavaScript 135 | CineIOPeer.identify(identity, timestamp, signature); 136 | ``` 137 | 138 | Identities are unique per project. Common identity names are user ids. 139 | 140 | ##### Calling another user 141 | 142 | Calling is the easy part. Calling is as simple as: 143 | 144 | ```JavaScript 145 | CineIOPeer.call(otherIdentity); 146 | ``` 147 | 148 | ##### Call Object 149 | 150 | When a user makes or recieves a call. They will get, via event callback, a `Call` object. See [Events](#cineiopeer-events). 151 | 152 | The Call object provides the following interface: 153 | 154 | ```JavaScript 155 | callObject.answer() // answer a call 156 | callObject.reject() // reject a call 157 | callObject.invite(identity) // invite another user to join this call 158 | callObject.hangup() // hangup on the call. This will keep the remaining users in the call 159 | ``` 160 | 161 | #### Sending data to peers 162 | 163 | `CineIOPeer` allows users to send arbitrary json data between the peers. 164 | 165 | ```JavaScript 166 | CineIOPeer.sendDataToAll(data) 167 | ``` 168 | 169 | ### CineIOPeer Events 170 | 171 | ```JavaScript 172 | 173 | // Media Events 174 | // if the user was asked to grant permission to the camera/microphone/screen share 175 | // This event only fires if the user was prompted for permission and we are waiting for the user to approve the permission. If there is no user approval step, this event does not fire. 176 | CineIOPeer.on('media-request', function(data) { 177 | if (data.type === 'screen'){ 178 | // requested screen share 179 | } else{ 180 | // requested camera/microphone 181 | } 182 | }); 183 | 184 | // The user rejected the permission to access camera/microphone 185 | CineIOPeer.on('media-rejected', function(data) { 186 | if (data.type === 'screen'){ 187 | // rejected screen share 188 | } else{ 189 | // rejected camera/microphone 190 | } 191 | }); 192 | 193 | // when local or remote media is added 194 | CineIOPeer.on('media-added', function(data) { 195 | var videoDOMNode = data.videoElement; 196 | if (data.local) { 197 | // local video 198 | if (data.type === 'screen') { 199 | // screen share video 200 | } else { 201 | // camera stream 202 | } 203 | } else { 204 | // remote video 205 | } 206 | }); 207 | 208 | // when local or remote media is removed 209 | CineIOPeer.on('media-removed', function(data) { 210 | var videoDOMNode = data.videoElement; 211 | if (data.local) { 212 | // local video 213 | if (data.type === 'screen') { 214 | // screen share video 215 | } else { 216 | // camera stream 217 | } 218 | } else { 219 | // remote video 220 | } 221 | }); 222 | 223 | 224 | // Calling Events 225 | // when a new call comes in 226 | CineIOPeer.on('call', function(data) { 227 | var call = data.call; 228 | // handle call (See CallObject above) 229 | }); 230 | 231 | // when a call was initiated by this user 232 | CineIOPeer.on('call-placed', function(data) { 233 | var call = data.call 234 | // handle call (See CallObject above) 235 | }); 236 | 237 | // when a call was rejecected by the user 238 | CineIOPeer.on('call-reject', function(data) { 239 | var call = data.call 240 | // handle call (See CallObject above) 241 | }); 242 | 243 | 244 | // Data Events 245 | // Processing raw json data sent between peers 246 | CineIOPeer.on('peer-data', function(data) { 247 | // process json data 248 | }); 249 | 250 | 251 | // Misc Events 252 | CineIOPeer.on('error', function(err) { 253 | if (typeof(err.support) != "undefined" && !err.support) { 254 | alert("This browser does not support WebRTC.") 255 | } else if (err.msg) { 256 | alert(err.msg) 257 | } 258 | }); 259 | 260 | ``` 261 | 262 | ### WebRTC Broadcast 263 | 264 | Cine.io provides a WebRTC to RTMP/HLS bridge to enhance your conference applications with broadcast capabilities. 265 | 266 | #### Broadcast your webcam and microphone 267 | 268 | To broadcast your webcam and microphone to a cine.io live stream, start by requesting camera and microphone access. 269 | 270 | ```javascript 271 | 272 | CineIOPeer.startCameraAndMicrophone(optionalCallback) 273 | 274 | ``` 275 | 276 | ##### Starting a webcam and microphone broadcast 277 | 278 | ```javascript 279 | var streamId = "cine.io stream id"; 280 | var password = "stream password"; 281 | var optionalCallback = function(error){ 282 | console.log("broadcasting"); 283 | }; 284 | CineIOPeer.broadcastCameraAndMicrophone(streamId, password, optionalCallback) 285 | ``` 286 | 287 | >The camera and microphone stream must already be started. Here is a complete example: 288 | ```javascript 289 | CineIOPeer.startCameraAndMicrophone(function(err) { 290 | if (err) { return console.log("camera/mic error", err); } 291 | var streamId = "cine.io stream id"; 292 | var password = "stream password"; 293 | CineIOPeer.broadcastCameraAndMicrophone(streamId, password, function(err) { 294 | if (err) { return console.log("broadcasting error", err); } 295 | }); 296 | }); 297 | ``` 298 | 299 | ##### Stopping a webcam and microphone broadcast 300 | 301 | ```javascript 302 | var optionalCallback = function(error){ 303 | console.log("broadcasting"); 304 | }; 305 | CineIOPeer.stopCameraAndMicrophoneBroadcast(optionalCallback) 306 | ``` 307 | 308 | ##### Checking the status of your camera and microphone broadcast 309 | 310 | ```javasript 311 | CineIOPeer.isBroadcastingCameraAndMicrophone(); 312 | ``` 313 | 314 | #### Broadcast your screen share 315 | 316 | To broadcast your desktop to a cine.io live stream, start by requesting screen share access. 317 | 318 | ```javascript 319 | 320 | CineIOPeer.startScreenShare(optionalCallback) 321 | 322 | ``` 323 | 324 | ##### Starting a screen share broadcast 325 | 326 | ```javascript 327 | var streamId = "cine.io stream id"; 328 | var password = "stream password"; 329 | var optionalCallback = function(error){ 330 | console.log("broadcasting"); 331 | }; 332 | CineIOPeer.broadcastScreenShare(streamId, password, optionalCallback) 333 | ``` 334 | 335 | >The screen share stream must already be started. Here is a complete example: 336 | ```javascript 337 | CineIOPeer.startScreenShare(function(err) { 338 | if (err) { return console.log("screen share error", err); } 339 | var streamId = "cine.io stream id"; 340 | var password = "stream password"; 341 | CineIOPeer.broadcastScreenShare(streamId, password, function(err) { 342 | if (err) { return console.log("broadcasting error", err); } 343 | }); 344 | }); 345 | ``` 346 | 347 | ##### Stopping a screen share broadcast 348 | 349 | ```javascript 350 | var optionalCallback = function(error){ 351 | console.log("broadcasting"); 352 | }; 353 | CineIOPeer.stopScreenShareBroadcast(optionalCallback) 354 | ``` 355 | 356 | ##### Checking the status of your screen share broadcast 357 | 358 | ```javasript 359 | CineIOPeer.isBroadcastingScreenShare(); 360 | ``` 361 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "coffeescript_error": { 3 | "level": "error" 4 | }, 5 | "arrow_spacing": { 6 | "name": "arrow_spacing", 7 | "level": "ignore" 8 | }, 9 | "no_tabs": { 10 | "name": "no_tabs", 11 | "level": "error" 12 | }, 13 | "no_trailing_whitespace": { 14 | "name": "no_trailing_whitespace", 15 | "level": "error", 16 | "allowed_in_comments": false, 17 | "allowed_in_empty_lines": false 18 | }, 19 | "max_line_length": { 20 | "name": "max_line_length", 21 | "value": 80, 22 | "level": "ignore", 23 | "limitComments": true 24 | }, 25 | "line_endings": { 26 | "name": "line_endings", 27 | "level": "ignore", 28 | "value": "unix" 29 | }, 30 | "no_trailing_semicolons": { 31 | "name": "no_trailing_semicolons", 32 | "level": "error" 33 | }, 34 | "indentation": { 35 | "name": "indentation", 36 | "value": 2, 37 | "level": "error" 38 | }, 39 | "camel_case_classes": { 40 | "name": "camel_case_classes", 41 | "level": "error" 42 | }, 43 | "colon_assignment_spacing": { 44 | "name": "colon_assignment_spacing", 45 | "level": "ignore", 46 | "spacing": { 47 | "left": 0, 48 | "right": 0 49 | } 50 | }, 51 | "no_implicit_braces": { 52 | "name": "no_implicit_braces", 53 | "level": "ignore", 54 | "strict": true 55 | }, 56 | "no_plusplus": { 57 | "name": "no_plusplus", 58 | "level": "ignore" 59 | }, 60 | "no_throwing_strings": { 61 | "name": "no_throwing_strings", 62 | "level": "error" 63 | }, 64 | "no_backticks": { 65 | "name": "no_backticks", 66 | "level": "error" 67 | }, 68 | "no_implicit_parens": { 69 | "name": "no_implicit_parens", 70 | "level": "ignore" 71 | }, 72 | "no_empty_param_list": { 73 | "name": "no_empty_param_list", 74 | "level": "error" 75 | }, 76 | "no_stand_alone_at": { 77 | "name": "no_stand_alone_at", 78 | "level": "error" 79 | }, 80 | "space_operators": { 81 | "name": "space_operators", 82 | "level": "ignore" 83 | }, 84 | "duplicate_key": { 85 | "name": "duplicate_key", 86 | "level": "error" 87 | }, 88 | "empty_constructor_needs_parens": { 89 | "name": "empty_constructor_needs_parens", 90 | "level": "ignore" 91 | }, 92 | "cyclomatic_complexity": { 93 | "name": "cyclomatic_complexity", 94 | "value": 10, 95 | "level": "ignore" 96 | }, 97 | "newlines_after_classes": { 98 | "name": "newlines_after_classes", 99 | "value": 3, 100 | "level": "ignore" 101 | }, 102 | "no_unnecessary_fat_arrows": { 103 | "name": "no_unnecessary_fat_arrows", 104 | "level": "error" 105 | }, 106 | "missing_fat_arrows": { 107 | "name": "missing_fat_arrows", 108 | "level": "ignore" 109 | }, 110 | "non_empty_constructor_needs_parens": { 111 | "name": "non_empty_constructor_needs_parens", 112 | "level": "ignore" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Cine.io team", 3 | "name": "cineio-peer", 4 | "domains": [ 5 | "cine.io" 6 | ], 7 | "version": "0.0.9", 8 | "description": "peer client for cine.io", 9 | "scripts": { 10 | "test": "coffeelint src/ test/ && grunt test" 11 | }, 12 | "dependencies": { 13 | "attachmediastream": "1.0.1", 14 | "backbone-events-standalone": "0.2.4", 15 | "getusermedia": "1.1.0", 16 | "jsonp": "0.1.0", 17 | "rtcpeerconnection": "2.6.6", 18 | "webrtcsupport": "1.1.0" 19 | }, 20 | "devDependencies": { 21 | "async": "0.9.0", 22 | "chai": "1.10.0", 23 | "coffee-script": "1.7.1", 24 | "coffeeify": "1.0.0", 25 | "coffeelint": "1.5.2", 26 | "envify": "3.2.0", 27 | "grunt": "0.4.5", 28 | "grunt-browserify": "3.2.1", 29 | "grunt-cli": "0.1.13", 30 | "grunt-contrib-coffee": "0.12.0", 31 | "grunt-contrib-uglify": "0.6.0", 32 | "grunt-contrib-watch": "0.6.1", 33 | "grunt-mocha": "0.4.11", 34 | "grunt-trimtrailingspaces": "1.1.0", 35 | "mocha": "2.0.1", 36 | "sinon": "1.12.1", 37 | "type-of": "2.0.1" 38 | }, 39 | "license": "Copyrighted / All Rights Reserved", 40 | "engines": { 41 | "node": "0.10.x" 42 | }, 43 | "private": true 44 | } 45 | -------------------------------------------------------------------------------- /src/broadcast_bridge.coffee: -------------------------------------------------------------------------------- 1 | uuid = require('./vendor/uuid') 2 | PeerConnectionFactory = require('./peer_connection_factory') 3 | Primus = require('./vendor/primus') 4 | nearestServer = require('./nearest_server') 5 | debug = require('./debug')('cine:peer:broadcast_bridge') 6 | 7 | noop = -> 8 | 9 | connectToCineBroadcastBridge = (broadcastUrl)-> 10 | Primus.connect(broadcastUrl) 11 | 12 | class Connection 13 | constructor: (@broadcastBridge, ns)-> 14 | @myUUID = uuid() 15 | @peerConnections = {} 16 | @calls = {} 17 | @connected = false 18 | 19 | connectToCineBroadcastBridge: (ns)-> 20 | @primus = connectToCineBroadcastBridge(ns) 21 | @primus.on 'open', @_onConnectionOpen 22 | @primus.on 'data', @_signalHandler 23 | @primus.on 'end', @_connectionEnded 24 | @connected = true 25 | 26 | write: (data)=> 27 | data.client = "cineio-peer-js version-#{CineIOPeer.version}" 28 | data.publicKey = CineIOPeer.config.publicKey 29 | data.uuid = @myUUID 30 | debug("Writing", data) 31 | @primus.write(arguments...) 32 | 33 | startBroadcast: (streamType, streamId, streamKey, mediaStream, callback)-> 34 | debug("ensuring ready") 35 | @_ensureReady => 36 | debug("ready") 37 | peerConnection = PeerConnectionFactory.create() 38 | @peerConnections[streamType] = peerConnection 39 | peerConnection.addStream(mediaStream) 40 | debug("waiting for ice") 41 | 42 | peerConnection.on 'close', (event)=> 43 | @_onCloseOfPeerConnection(peerConnection) 44 | delete @peerConnections[streamType] 45 | 46 | @_createOffer peerConnection, (err, offer)=> 47 | debug("MADE OFFER", err, offer) 48 | return callback(err) if err 49 | peerConnection.on 'endOfCandidates', (candidate)=> 50 | debug("got all candidates") 51 | # you have to do the offer first 52 | # but if you wait till the end of candidates 53 | # then your offer actually changes 54 | # and includes all of the candidates 55 | # so you can send it in a single bundle 56 | data = 57 | streamType: streamType 58 | action: 'broadcast-start' 59 | offer: peerConnection.pc.localDescription 60 | streamId: streamId 61 | streamKey: streamKey 62 | @write data 63 | callback() 64 | 65 | stopBroadcast: (streamType, callback)-> 66 | @peerConnections[streamType].close() if @peerConnections[streamType] 67 | data = 68 | streamType: streamType 69 | action: 'broadcast-stop' 70 | @write data 71 | callback() 72 | 73 | _onConnectionOpen: => 74 | @write action: 'auth' 75 | 76 | _connectionEnded: -> 77 | debug("Connection closed") 78 | 79 | _signalHandler: (data)=> 80 | debug("got data", data) 81 | switch data.action 82 | # BASE 83 | when 'error' 84 | CineIOPeer.trigger('error', data) 85 | 86 | when 'ack' 87 | debug("ack") 88 | # END BASE 89 | 90 | # RTC 91 | when 'rtc-answer' 92 | debug('got answer', data) 93 | pc = @peerConnections[data.streamType] 94 | pc.handleAnswer(data.answer) 95 | # END RTC 96 | # else 97 | # debug("UNKNOWN DATA", data) 98 | 99 | _createOffer: (peerConnection, callback)-> 100 | response = (err, offer)-> 101 | if err || !offer 102 | debug("FATAL ERROR in offer", err, offer) 103 | return CineIOPeer.trigger("error", kind: 'offer', fatal: true, err: err) 104 | debug('offering', err, offer) 105 | callback(err, offer) 106 | # av = CineIOPeer.localStreams().length == 0 107 | constraints = 108 | mandatory: 109 | OfferToReceiveAudio: true 110 | OfferToReceiveVideo: true 111 | # if peerConnection.mainDataChannel 112 | # constraints.optional = [{RtpDataChannels: true}] 113 | peerConnection.offer constraints, response 114 | 115 | _onCloseOfPeerConnection: (peerConnection)-> 116 | 117 | _ensureReady: (callback)=> 118 | @_ensureIce callback 119 | 120 | _ensureIce: (callback)=> 121 | return setTimeout callback if @broadcastBridge.iceReady 122 | CineIOPeer.once 'gotIceServers', callback 123 | 124 | 125 | module.exports = class BroadcastBridge 126 | constructor: (@CineIOPeer)-> 127 | @CineIOPeer.on 'gotIceServers', (data)=> 128 | @iceReady = true 129 | @connection = new Connection(this) 130 | 131 | startBroadcast: (streamType, mediaStream, streamId, streamKey, callback=noop)-> 132 | @_ensureConnection => 133 | @connection.startBroadcast(streamType, streamId, streamKey, mediaStream, callback) 134 | 135 | stopBroadcast: (streamType, callback)-> 136 | return callback() unless @connection.connected 137 | @connection.stopBroadcast(streamType, callback) 138 | 139 | _ensureConnection: (callback=noop)-> 140 | if @connection.connected 141 | return setTimeout -> 142 | callback() 143 | nearestServer (err, ns)=> 144 | debug("HERE I AM", err, ns) 145 | return callback(err) if err 146 | @connection.connectToCineBroadcastBridge(ns.rtcPublish) 147 | callback() 148 | -------------------------------------------------------------------------------- /src/browser_detect.coffee: -------------------------------------------------------------------------------- 1 | check = (string)-> 2 | navigator.userAgent.indexOf(string) != -1 3 | 4 | exports.isOpera = check("OPR") 5 | exports.isChrome = check("Chrome") && !exports.isOpera 6 | exports.isFirefox = check("Firefox") && !exports.isOpera 7 | exports.isMSIE = check("MSIE") 8 | -------------------------------------------------------------------------------- /src/call.coffee: -------------------------------------------------------------------------------- 1 | BackboneEvents = require("backbone-events-standalone") 2 | noop = -> 3 | 4 | INITIATED = 0 5 | IN_CALL = 1 6 | ENDED = 2 7 | 8 | class Participant 9 | constructor: (@otherIdentity, @room)-> 10 | @state = INITIATED 11 | # initiator methods 12 | 13 | call: -> 14 | @state = IN_CALL 15 | options = 16 | action: 'call' 17 | room: @room 18 | # recipient 19 | otheridentity: @otherIdentity 20 | 21 | CineIOPeer._signalConnection.write options 22 | 23 | cancel: -> 24 | @state = ENDED 25 | options = 26 | action: 'call-cancel' 27 | room: @room 28 | # recipient 29 | otheridentity: @otherIdentity 30 | 31 | CineIOPeer._signalConnection.write options 32 | 33 | left: -> 34 | @state = ENDED 35 | 36 | joined: -> 37 | @state = IN_CALL 38 | 39 | module.exports = class CallObject 40 | constructor: (@room, @options={})-> 41 | @state = if @options.initiated then IN_CALL else INITIATED 42 | @participants = {} 43 | 44 | @_createParticipant(@options.called) if @options.called 45 | 46 | # global call functions 47 | answer: (callback=noop)-> 48 | @state = IN_CALL 49 | CineIOPeer.join(@room, callback) 50 | 51 | isInCall: -> 52 | @state == IN_CALL 53 | 54 | isEnded: -> 55 | @state == ENDED 56 | 57 | reject: (callback=noop)-> 58 | @state = ENDED 59 | options = 60 | action: 'call-reject' 61 | room: @room 62 | 63 | CineIOPeer._signalConnection.write options 64 | callback() 65 | 66 | hangup: (callback=noop)-> 67 | @state = ENDED 68 | CineIOPeer.leave @room, callback 69 | @_cancelOutgoingCalls() 70 | 71 | left: (otherIdentity)-> 72 | participant = @participants[otherIdentity] 73 | return unless participant 74 | participant.left() 75 | 76 | joined: (otherIdentity)-> 77 | participant = @_createParticipant(otherIdentity) 78 | participant.joined() 79 | # end global call functions 80 | 81 | # individual connection actions 82 | invite: (otherIdentity, callback=noop)-> 83 | participant = @_createParticipant(otherIdentity) 84 | participant.call() 85 | callback() 86 | 87 | cancel: (otherIdentity, callback=noop)-> 88 | participant = @participants[otherIdentity] 89 | return callback("participant not in room: #{otherIdentity}") unless participant 90 | participant.cancel() 91 | callback() 92 | 93 | _cancelOutgoingCalls: -> 94 | participant.cancel() for otherIdentity, participant of @participants 95 | # maybe kick... 96 | # end individual connection actions 97 | 98 | _createParticipant: (otherIdentity)-> 99 | existingParticipant = @participants[otherIdentity] 100 | return existingParticipant if existingParticipant 101 | @participants[otherIdentity] = new Participant(otherIdentity, @room) 102 | 103 | BackboneEvents.mixin CallObject:: 104 | 105 | CineIOPeer = require('./main') 106 | -------------------------------------------------------------------------------- /src/chrome_screen_sharer.coffee: -------------------------------------------------------------------------------- 1 | Config = require('./config') 2 | ssBase = require('./screen_share_base') 3 | ScreenSharer = ssBase.ScreenSharer 4 | ScreenShareError = ssBase.ScreenShareError 5 | debug = require('./debug')('cine:peer:chrome_screen_sharer') 6 | 7 | class ChromeScreenSharer extends ScreenSharer 8 | constructor: -> 9 | super() 10 | @_extensionInstalled = false 11 | @_extensionReplyTries = 0 12 | 13 | # Add a listener for "message" events on the window, then send a message 14 | # to see if the extension is installed. If it is, it will post a message 15 | # named "cineScreenShareHasExtension" (see _receiveMessage method). 16 | window.addEventListener("message", @_receiveMessage.bind(this), false) 17 | window.postMessage({ name: "cineScreenShareCheckForExtension" }, "*") 18 | 19 | share: (options, callback)-> 20 | super(options, callback) 21 | @_shareAfterExtensionReplies() 22 | 23 | _shareAfterExtensionReplies: -> 24 | return @_callback( 25 | new ScreenShareError( 26 | "Screen sharing in chrome requires the cine.io Screen Sharing extension.", 27 | extensionRequired: true 28 | type: 'chrome' 29 | url: Config.chromeExtension) 30 | ) unless @_extensionInstalled or (++@_extensionReplyTries < 3) 31 | 32 | if @_extensionInstalled 33 | window.postMessage({ name: "cineScreenShare" }, "*") 34 | else 35 | debug "Waiting for the screen sharing extension reply ..." 36 | setTimeout(@_shareAfterExtensionReplies.bind(this), 100) 37 | 38 | _receiveMessage: (event)-> 39 | debug "received:", event 40 | switch event.data.name 41 | when "cineScreenShareHasExtension" 42 | debug "cine.io screen share extension is installed." 43 | @_extensionInstalled = true 44 | return 45 | when "cineScreenShareResponse" 46 | return @_onScreenShareResponse(event.data.id) 47 | 48 | _onScreenShareResponse: (id)=> 49 | return @_callback(new ScreenShareError("Screen access rejected.")) unless id 50 | debug "ossr id =", id 51 | screenShareOptions = 52 | # audio sharing with desktop is not allowed in chrome 53 | # https://code.google.com/p/chromium/issues/detail?id=223639 54 | audio: false 55 | 56 | video: 57 | mandatory: 58 | chromeMediaSource: "desktop" 59 | chromeMediaSourceId: id 60 | navigator.webkitGetUserMedia(screenShareOptions, @_onStreamReceived.bind(this), @_onError.bind(this)) 61 | 62 | module.exports = ChromeScreenSharer 63 | -------------------------------------------------------------------------------- /src/config.coffee: -------------------------------------------------------------------------------- 1 | protocol = if location.protocol == 'https:' then 'https' else 'http' 2 | 3 | if process.env.NODE_ENV == 'production' 4 | exports.signalingServer = "#{protocol}://signaling.cine.io" 5 | if process.env.NODE_ENV == 'development' 6 | exports.signalingServer = "https://localhost.cine.io:8443" 7 | 8 | exports.chromeExtension = "https://chrome.google.com/webstore/detail/cineio-screen-sharing/ancoeogeclfnhienkmfmeeomadmofhmi" 9 | -------------------------------------------------------------------------------- /src/debug.coffee: -------------------------------------------------------------------------------- 1 | if process.env.NODE_ENV == 'development' 2 | module.exports = (value)-> 3 | return (messages...)-> 4 | console.log(value, messages...) 5 | else 6 | module.exports = -> 7 | return -> 8 | -------------------------------------------------------------------------------- /src/firefox_screen_sharer.coffee: -------------------------------------------------------------------------------- 1 | ssBase = require('./screen_share_base') 2 | ScreenSharer = ssBase.ScreenSharer 3 | ScreenShareError = ssBase.ScreenShareError 4 | debug = require('./debug')('cine:peer:firefox_screen_sharer') 5 | 6 | class FirefoxScreenSharer extends ScreenSharer 7 | share: (options, callback)-> 8 | super(options, callback) 9 | debug "requesting screen share (moz) ..." 10 | constraints = 11 | audio: @options.audio 12 | video: 13 | mediaSource: "screen" 14 | navigator.mozGetUserMedia(constraints, @_onStreamReceived.bind(this), @_onError.bind(this)) 15 | 16 | module.exports = FirefoxScreenSharer 17 | -------------------------------------------------------------------------------- /src/main.coffee: -------------------------------------------------------------------------------- 1 | getUserMedia = require('getusermedia') 2 | attachMediaStream = require('attachmediastream') 3 | webrtcSupport = require('webrtcsupport') 4 | BackboneEvents = require("backbone-events-standalone") 5 | debug = require('./debug')('cine:peer:main') 6 | 7 | noop = -> 8 | defaultOptions = 9 | video: true 10 | audio: true 11 | autoplay: true 12 | mirror: true 13 | muted: true 14 | 15 | userOrDefault = (userOptions, key)-> 16 | if Object.prototype.hasOwnProperty.call(userOptions, key) then userOptions[key] else defaultOptions[key] 17 | 18 | CineIOPeer = 19 | version: "0.0.9" 20 | reset: -> 21 | CineIOPeer.config = {rooms: [], videoElements: {}} 22 | 23 | init: (publicKey)-> 24 | CineIOPeer.config.publicKey = publicKey 25 | CineIOPeer._signalConnection ||= signalingConnection.connect() 26 | CineIOPeer._broadcastBridge = new BroadcastBridge(this) 27 | setTimeout CineIOPeer._checkSupport 28 | 29 | identify: (identity, timestamp, signature)-> 30 | debug('identifying as', identity) 31 | CineIOPeer.config.identity = 32 | identity: identity 33 | timestamp: timestamp 34 | signature: signature 35 | CineIOPeer._sendIdentity() 36 | 37 | _sendIdentity: -> 38 | CineIOPeer._signalConnection.write action: 'identify', identity: CineIOPeer.config.identity.identity, timestamp: CineIOPeer.config.identity.timestamp, signature: CineIOPeer.config.identity.signature, publicKey: CineIOPeer.config.publicKey, client: 'web' 39 | 40 | sendDataToAll: (data)-> 41 | CineIOPeer._signalConnection.sendDataToAllPeers(data) 42 | 43 | call: (otheridentity, room=null, callback=noop)-> 44 | if typeof room == 'function' 45 | callback = room 46 | room = null 47 | options = 48 | action: 'call' 49 | otheridentity: otheridentity 50 | options.identity = CineIOPeer.config.identity.identity if CineIOPeer.config.identity 51 | options.room = room if room 52 | debug('calling', otheridentity) 53 | CineIOPeer._signalConnection.write options 54 | callPlacedCallback = (data)-> 55 | if data.otheridentity == otheridentity 56 | CineIOPeer.off 'call-placed', callPlacedCallback 57 | callback(null, call: data.call) 58 | CineIOPeer.on 'call-placed', callPlacedCallback 59 | 60 | join: (room, callback=noop)-> 61 | debug('Joining', room) 62 | CineIOPeer.config.rooms.push(room) 63 | CineIOPeer._sendJoinRoom(room) 64 | setTimeout callback 65 | 66 | leave: (room, callback=noop)-> 67 | index = CineIOPeer.config.rooms.indexOf(room) 68 | if index < 0 69 | CineIOPeer.trigger('error', msg: 'not connected to room', room: room) 70 | return setTimeout callback 71 | 72 | CineIOPeer.config.rooms.splice(index, 1) 73 | CineIOPeer._signalConnection.write action: 'room-leave', room: room 74 | setTimeout callback 75 | 76 | startCameraAndMicrophone: (callback=noop)-> 77 | CineIOPeer._startMedia(video: true, audio: true, callback) 78 | 79 | stopCameraAndMicrophone: (callback=noop)-> 80 | if CineIOPeer.microphoneStream 81 | CineIOPeer._removeStream(CineIOPeer.microphoneStream, 'camera') 82 | delete CineIOPeer.microphoneStream 83 | if CineIOPeer.cameraStream 84 | CineIOPeer._removeStream(CineIOPeer.cameraStream, 'camera') 85 | delete CineIOPeer.cameraStream 86 | if CineIOPeer.cameraAndMicrophoneStream 87 | CineIOPeer._removeStream(CineIOPeer.cameraAndMicrophoneStream, 'camera') 88 | delete CineIOPeer.cameraAndMicrophoneStream 89 | callback() 90 | 91 | startMicrophone: (callback=noop)-> 92 | if CineIOPeer._audioCapableStreams().length > 0 93 | CineIOPeer._unmuteAudio() 94 | return callback() 95 | if CineIOPeer.cameraStream && !CineIOPeer.mutedCamera 96 | CineIOPeer._removeStream(CineIOPeer.cameraStream, 'camera', silent: true) 97 | delete CineIOPeer.cameraStream 98 | return CineIOPeer.startCameraAndMicrophone(callback) 99 | CineIOPeer._startMedia(video: false, audio: true, callback) 100 | 101 | stopMicrophone: (callback=noop)-> 102 | if CineIOPeer.microphoneStream 103 | CineIOPeer._removeStream(CineIOPeer.microphoneStream, 'camera') 104 | delete CineIOPeer.microphoneStream 105 | if CineIOPeer.cameraAndMicrophoneStream 106 | # if the camera is muted, remove the stream all together 107 | if CineIOPeer.mutedCamera 108 | CineIOPeer._removeStream(CineIOPeer.cameraAndMicrophoneStream, 'camera') 109 | delete CineIOPeer.cameraAndMicrophoneStream 110 | # the camera is still on, keep the stream around and just remove the video 111 | else 112 | CineIOPeer._muteAudio() 113 | callback() 114 | 115 | startCamera: (callback=noop)-> 116 | if CineIOPeer._cameraCapableStreams().length > 0 117 | CineIOPeer._unmuteCamera() 118 | return callback() 119 | if CineIOPeer.microphoneStream && !CineIOPeer.mutedMicrophone 120 | CineIOPeer._removeStream(CineIOPeer.microphoneStream, 'camera', silent: true) 121 | delete CineIOPeer.microphoneStream 122 | return CineIOPeer.startCameraAndMicrophone(callback) 123 | CineIOPeer._startMedia(video: true, audio: false, callback) 124 | 125 | stopCamera: (callback=noop)-> 126 | if CineIOPeer.cameraStream 127 | CineIOPeer._removeStream(CineIOPeer.cameraStream, 'camera') 128 | delete CineIOPeer.cameraStream 129 | if CineIOPeer.cameraAndMicrophoneStream 130 | # if the microphone is muted, remove the stream all together 131 | if CineIOPeer.mutedMicrophone 132 | CineIOPeer._removeStream(CineIOPeer.cameraAndMicrophoneStream, 'camera') 133 | delete CineIOPeer.cameraAndMicrophoneStream 134 | # the microphone is still on, keep the stream around and just remove the video 135 | else 136 | CineIOPeer._muteCamera() 137 | callback() 138 | 139 | cameraRunning: -> 140 | return true if CineIOPeer.cameraStream 141 | CineIOPeer.cameraAndMicrophoneStream && !CineIOPeer.mutedCamera 142 | 143 | screenShareRunning: -> 144 | CineIOPeer.screenShareStream? 145 | 146 | microphoneRunning: -> 147 | return true if CineIOPeer.microphoneStream? 148 | CineIOPeer._audioCapableStreams().length > 0 && !CineIOPeer.mutedMicrophone 149 | 150 | screenShareSupported: -> 151 | browserDetect.isChrome || browserDetect.isFirefox 152 | 153 | startScreenShare: (options={}, callback=noop)-> 154 | if typeof options == 'function' 155 | callback = options 156 | options = {} 157 | CineIOPeer._screenSharer ||= screenSharer.get() 158 | 159 | requestTimeout = setTimeout CineIOPeer._mediaNotReady('screen'), 1000 160 | 161 | onStreamReceived = (err, screenShareStream)=> 162 | clearTimeout requestTimeout 163 | if err 164 | if err.extensionRequired 165 | CineIOPeer.trigger 'extension-required', 166 | url: err.url 167 | type: err.type 168 | return callback() 169 | else 170 | CineIOPeer.trigger 'media-rejected', 171 | type: 'screen' 172 | local: true 173 | return callback(err) 174 | videoEl = @_createVideoElementFromStream(screenShareStream, mirror: false) 175 | CineIOPeer.screenShareStream = screenShareStream 176 | CineIOPeer._signalConnection.addLocalStream(screenShareStream) 177 | CineIOPeer.trigger 'media-added', 178 | videoElement: videoEl 179 | stream: screenShareStream 180 | type: 'screen' 181 | local: true 182 | callback() 183 | 184 | CineIOPeer._screenSharer.share(options, onStreamReceived) 185 | 186 | stopScreenShare: (callback=noop)-> 187 | return callback() unless CineIOPeer.screenShareRunning() 188 | CineIOPeer._removeStream(CineIOPeer.screenShareStream, 'screen') 189 | delete CineIOPeer.screenShareStream 190 | callback() 191 | 192 | broadcastCameraAndMicrophone: (streamId, streamKey, callback=noop)-> 193 | if CineIOPeer.isBroadcastingCameraAndMicrophone() 194 | return setTimeout -> 195 | callback("cannot broadcast to multiple endpoints") 196 | stream = CineIOPeer.cameraAndMicrophoneStream 197 | unless stream 198 | return setTimeout -> 199 | callback("stream not started") 200 | CineIOPeer._isBroadcastingCameraAndMicrophone = true 201 | CineIOPeer._broadcastBridge.startBroadcast('camera', stream, streamId, streamKey, callback) 202 | 203 | stopCameraAndMicrophoneBroadcast: (callback=noop)-> 204 | delete CineIOPeer._isBroadcastingCameraAndMicrophone 205 | CineIOPeer._broadcastBridge.stopBroadcast('camera', callback) 206 | 207 | isBroadcastingCameraAndMicrophone: -> 208 | CineIOPeer._isBroadcastingCameraAndMicrophone? 209 | 210 | broadcastScreenShare: (streamId, streamKey, callback=noop)-> 211 | if CineIOPeer.isBroadcastingScreenShare() 212 | return setTimeout -> 213 | callback("cannot broadcast to multiple endpoints") 214 | stream = CineIOPeer.screenShareStream 215 | unless stream 216 | return setTimeout -> 217 | callback("stream not started") 218 | CineIOPeer._isBroadcastingScreenShare = true 219 | CineIOPeer._broadcastBridge.startBroadcast('screen', stream, streamId, streamKey, callback) 220 | 221 | stopScreenShareBroadcast: (callback=noop)-> 222 | delete CineIOPeer._isBroadcastingScreenShare 223 | CineIOPeer._broadcastBridge.stopBroadcast('screen', callback) 224 | 225 | isBroadcastingScreenShare: -> 226 | CineIOPeer._isBroadcastingScreenShare? 227 | 228 | _muteAudio: -> 229 | CineIOPeer._muteStreamAudio(stream) for stream in CineIOPeer.localStreams() 230 | CineIOPeer.mutedMicrophone = true 231 | 232 | _muteCamera: -> 233 | CineIOPeer._muteStreamVideo(stream) for stream in CineIOPeer._cameraCapableStreams() 234 | CineIOPeer.mutedCamera = true 235 | 236 | _unmuteAudio: -> 237 | unmuteStream = CineIOPeer._audioCapableStreams()[0] 238 | if unmuteStream 239 | CineIOPeer._unmuteStreamAudio(unmuteStream) 240 | else 241 | CineIOPeer.startMicrophone() 242 | delete CineIOPeer.mutedMicrophone 243 | 244 | _unmuteCamera: -> 245 | unmuteStream = CineIOPeer._cameraCapableStreams()[0] 246 | if unmuteStream 247 | CineIOPeer._unmuteStreamVideo(unmuteStream) 248 | else 249 | CineIOPeer.startCamera() 250 | delete CineIOPeer.mutedCamera 251 | 252 | _removeStream: (stream, type, options={})-> 253 | stream.stop() 254 | CineIOPeer._signalConnection.removeLocalStream(stream, options) 255 | CineIOPeer.trigger('media-removed', local: true, type: type, videoElement: CineIOPeer.config.videoElements[stream.id]) 256 | delete CineIOPeer.config.videoElements[stream.id] 257 | 258 | _muteStreamAudio: (stream)-> 259 | return unless stream 260 | CineIOPeer._disableTracks(stream.getAudioTracks()) 261 | 262 | _unmuteStreamAudio: (stream)-> 263 | return unless stream 264 | CineIOPeer._enableTracks(stream.getAudioTracks()) 265 | 266 | _muteStreamVideo: (stream)-> 267 | return unless stream 268 | CineIOPeer._disableTracks(stream.getVideoTracks()) 269 | 270 | _unmuteStreamVideo: (stream)-> 271 | return unless stream 272 | CineIOPeer._enableTracks(stream.getVideoTracks()) 273 | 274 | _enableTracks: (tracks)-> 275 | track.enabled = true for track in tracks 276 | _disableTracks: (tracks)-> 277 | track.enabled = false for track in tracks 278 | 279 | localStreams: -> 280 | streams = [] 281 | streams.push CineIOPeer.cameraAndMicrophoneStream if CineIOPeer.cameraAndMicrophoneStream 282 | streams.push CineIOPeer.cameraStream if CineIOPeer.cameraStream 283 | streams.push CineIOPeer.microphoneStream if CineIOPeer.microphoneStream 284 | streams.push CineIOPeer.screenShareStream if CineIOPeer.screenShareStream 285 | streams 286 | 287 | _cameraCapableStreams: -> 288 | streams = [] 289 | streams.push CineIOPeer.cameraAndMicrophoneStream if CineIOPeer.cameraAndMicrophoneStream 290 | streams.push CineIOPeer.cameraStream if CineIOPeer.cameraStream 291 | streams 292 | 293 | _audioCapableStreams: -> 294 | streams = [] 295 | streams.push CineIOPeer.cameraAndMicrophoneStream if CineIOPeer.cameraAndMicrophoneStream 296 | streams.push CineIOPeer.microphoneStream if CineIOPeer.microphoneStream 297 | streams 298 | 299 | _startMedia: (options, callback=noop)-> 300 | if CineIOPeer.cameraAndMicrophoneStream && options.video && options.audio 301 | return setTimeout(callback) 302 | if CineIOPeer.cameraStream && options.video 303 | return setTimeout(callback) 304 | if CineIOPeer.microphoneStream && options.audio 305 | return setTimeout(callback) 306 | requestTimeout = setTimeout CineIOPeer._mediaNotReady('camera'), 1000 307 | CineIOPeer._askForMedia options, (err, response)-> 308 | clearTimeout requestTimeout 309 | if err 310 | # did not grant permission 311 | CineIOPeer.trigger 'media-rejected', 312 | type: 'camera' 313 | local: true 314 | debug("ERROR", err) 315 | return callback(err) 316 | if options.video && options.audio 317 | CineIOPeer.cameraAndMicrophoneStream = response.stream 318 | delete CineIOPeer.mutedMicrophone 319 | delete CineIOPeer.mutedCamera 320 | else if options.video 321 | CineIOPeer.cameraStream = response.stream 322 | delete CineIOPeer.mutedCamera 323 | else if options.audio 324 | CineIOPeer.microphoneStream = response.stream 325 | delete CineIOPeer.mutedMicrophone 326 | 327 | CineIOPeer.trigger 'media-added', 328 | videoElement: response.videoElement 329 | stream: response.stream 330 | type: 'camera' 331 | video: options.video 332 | audio: options.audio 333 | local: true 334 | CineIOPeer._signalConnection.addLocalStream(response.stream) 335 | callback() 336 | 337 | _checkSupport: -> 338 | if webrtcSupport.support 339 | CineIOPeer.trigger 'info', support: true 340 | else 341 | CineIOPeer.trigger 'error', support: false 342 | 343 | _sendJoinRoom: (room)-> 344 | CineIOPeer._signalConnection.write action: 'room-join', room: room 345 | 346 | _mediaNotReady: (type)-> 347 | -> 348 | CineIOPeer.trigger('media-request', local: true, type: type) 349 | 350 | _askForMedia: (options={}, callback)-> 351 | if typeof options == 'function' 352 | callback = options 353 | options = {} 354 | streamDoptions = 355 | video: userOrDefault(options, 'video') 356 | audio: userOrDefault(options, 'audio') 357 | debug('fetching media', options) 358 | 359 | CineIOPeer._unsafeGetUserMedia streamDoptions, (err, stream)=> 360 | return callback(err) if err 361 | videoEl = @_createVideoElementFromStream(stream, options) 362 | callback(null, videoElement: videoEl, stream: stream) 363 | 364 | _unsafeGetUserMedia: (options, callback)-> 365 | getUserMedia options, callback 366 | 367 | _createVideoElementFromStream: (stream, options={})-> 368 | videoOptions = 369 | autoplay: userOrDefault(options, 'autoplay') 370 | mirror: userOrDefault(options, 'mirror') 371 | muted: userOrDefault(options, 'muted') 372 | videoEl = attachMediaStream(stream, null, videoOptions) 373 | CineIOPeer.config.videoElements[stream.id] = videoEl 374 | videoEl 375 | 376 | _getVideoElementFromStream: (stream)-> 377 | CineIOPeer.config.videoElements[stream.id] 378 | 379 | 380 | CineIOPeer.reset() 381 | BackboneEvents.mixin CineIOPeer 382 | 383 | window.CineIOPeer = CineIOPeer if typeof window isnt 'undefined' 384 | 385 | module.exports = CineIOPeer 386 | 387 | Config = require('./config') 388 | signalingConnection = require('./signaling_connection') 389 | screenSharer = require('./screen_sharer') 390 | browserDetect = require('./browser_detect') 391 | BroadcastBridge = require('./broadcast_bridge') 392 | -------------------------------------------------------------------------------- /src/nearest_server.coffee: -------------------------------------------------------------------------------- 1 | jsonp = require('jsonp') 2 | BASE_SERVER_URL = "https://www.cine.io/api/1/-/nearest-server?default=ok" 3 | 4 | nearestServer = null 5 | fetchingNearestServer = null 6 | nearestServerCallbacks = null 7 | 8 | 9 | module.exports = (callback)-> 10 | return callback(null, nearestServer) if nearestServer 11 | return nearestServerCallbacks.push(callback) if fetchingNearestServer 12 | fetchingNearestServer = true 13 | module.exports._makeJsonpCall BASE_SERVER_URL, (err, data)-> 14 | nearestServer = data 15 | for cb in nearestServerCallbacks 16 | cb(err, nearestServer) 17 | nearestServerCallbacks = [] 18 | callback(err, nearestServer) 19 | 20 | module.exports._makeJsonpCall = (url, callback)-> 21 | jsonp url, callback 22 | 23 | module.exports._reset = -> 24 | nearestServer = null 25 | fetchingNearestServer = false 26 | nearestServerCallbacks = [] 27 | 28 | module.exports._reset() 29 | # nearestServer = {rtcPublish: "https://docker-local.cine.io"} if process.env.NODE_ENV == 'development' 30 | -------------------------------------------------------------------------------- /src/peer_connection_factory.coffee: -------------------------------------------------------------------------------- 1 | PeerConnection = require('rtcpeerconnection') 2 | 3 | iceServers = null 4 | 5 | exports.create = -> 6 | return null unless iceServers 7 | exports._actuallyCreatePeerConnection(iceServers: iceServers) 8 | 9 | exports._actuallyCreatePeerConnection = (options)-> 10 | new PeerConnection(options) 11 | 12 | exports._reset = -> 13 | iceServers = null 14 | 15 | Main = require('./main') 16 | Main.on 'gotIceServers', (data)-> 17 | iceServers = data 18 | -------------------------------------------------------------------------------- /src/screen_share_base.coffee: -------------------------------------------------------------------------------- 1 | webrtcSupport = require('webrtcsupport') 2 | debug = require('./debug')('cine:peer:screen_share_base') 3 | 4 | 5 | class ScreenShareError 6 | constructor: (@msg, data)-> 7 | for k, v of data 8 | this[k] = v 9 | 10 | 11 | class ScreenSharer 12 | share: (@options, @_callback)-> 13 | return @_callback(new ScreenShareError("Screen sharing requires a browser environment!")) unless window and navigator 14 | return @_callback(new ScreenShareError("Screen sharing not implemented in this browser / environment.")) unless webrtcSupport.screenSharing 15 | 16 | _onStreamReceived: (stream)-> 17 | debug "Received local stream:", stream 18 | stream.onended = @_onStreamEnded.bind(this) 19 | return @_callback(null, stream) 20 | 21 | _onStreamEnded: -> 22 | debug "Screen share ended." 23 | CineIOPeer.stopScreenShare() 24 | return 25 | 26 | _onError: (err)-> 27 | errMsg = if err.name then err.name + (if err.message then " (#{err.message})" else "") else err 28 | errMsg = "Screen share failed: #{errMsg}" 29 | console.dir err 30 | debug errMsg 31 | return @_callback(new ScreenShareError(errMsg)) 32 | 33 | 34 | module.exports = 35 | ScreenShareError: ScreenShareError 36 | ScreenSharer: ScreenSharer 37 | 38 | CineIOPeer = require('./main') 39 | -------------------------------------------------------------------------------- /src/screen_sharer.coffee: -------------------------------------------------------------------------------- 1 | ScreenShareError = require('./screen_share_base').ScreenShareError 2 | browserDetect = require('./browser_detect') 3 | 4 | ScreenSharer = 5 | get: (options={}, cb)-> 6 | options.audio = false unless options.hasOwnProperty("audio") 7 | 8 | if browserDetect.isChrome 9 | ChromeScreenSharer = require('./chrome_screen_sharer') 10 | return new ChromeScreenSharer(options, cb) 11 | else if browserDetect.isFirefox 12 | FirefoxScreenSharer = require('./firefox_screen_sharer') 13 | return new FirefoxScreenSharer(options, cb) 14 | else 15 | return cb(new ScreenShareError("Screen sharing not implemented in this browser / environment.")) 16 | 17 | 18 | module.exports = ScreenSharer 19 | -------------------------------------------------------------------------------- /src/signaling_connection.coffee: -------------------------------------------------------------------------------- 1 | uuid = require('./vendor/uuid') 2 | PeerConnectionFactory = require('./peer_connection_factory') 3 | debug = require('./debug')('cine:peer:signaling_connection') 4 | 5 | Primus = require('./vendor/primus') 6 | Config = require('./config') 7 | 8 | noop = -> 9 | connectToCineSignaling = -> 10 | Primus.connect(Config.signalingServer) 11 | 12 | PENDING = 1 13 | 14 | sendToDataChannel = (dataChannel, data)-> 15 | return dataChannel.send(JSON.stringify(data)) if dataChannel.readyState == 'open' 16 | dataChannel.dataToSend.push data 17 | 18 | setSparkIdOnPeerConnection = (peerConnection, otherClientSparkId)-> 19 | peerConnection.otherClientSparkId = otherClientSparkId 20 | 21 | class Connection 22 | constructor: (@options)-> 23 | @myUUID = uuid() 24 | @iceServers = null 25 | @fetchedIce = false 26 | @peerConnections = {} 27 | @calls = {} 28 | @primus = connectToCineSignaling() 29 | @primus.on 'open', @_onConnectionOpen 30 | @primus.on 'data', @_signalHandler 31 | @primus.on 'end', @_connectionEnded 32 | 33 | write: (data)=> 34 | data.client = "cineio-peer-js version-#{CineIOPeer.version}" 35 | data.publicKey = CineIOPeer.config.publicKey 36 | data.uuid = @myUUID 37 | data.identity = CineIOPeer.config.identity.identity if CineIOPeer.config.identity 38 | data.support = 39 | trickleIce: true 40 | debug("Writing", data) 41 | @primus.write(arguments...) 42 | 43 | addLocalStream: (stream, options={})=> 44 | for otherClientUUID, peerConnection of @peerConnections 45 | debug "adding local stream #{stream.id} to #{otherClientUUID}" 46 | peerConnection.addStream(stream) 47 | # need to reoffer every time there's a new stream 48 | # http://stackoverflow.com/questions/16015022/webrtc-how-to-add-stream-after-offer-and-answer 49 | @_sendOffer(peerConnection) unless options.silent 50 | 51 | removeLocalStream: (stream, options={})=> 52 | for otherClientUUID, peerConnection of @peerConnections 53 | debug "removing local stream #{stream.id} from #{otherClientUUID}" 54 | peerConnection.removeStream(stream) 55 | @_sendOffer(peerConnection) unless options.silent 56 | 57 | sendDataToAllPeers: (data)-> 58 | for otherClientUUID, peerConnection of @peerConnections 59 | debug "sending data #{data} to #{otherClientUUID}" 60 | @_sendDataToPeer(peerConnection, data) 61 | 62 | _sendDataToPeer: (peerConnection, data)-> 63 | unless peerConnection.mainDataChannel 64 | peerConnection.mainDataChannel = @_newDataChannel(peerConnection) 65 | @_sendOffer(peerConnection) 66 | 67 | sendToDataChannel peerConnection.mainDataChannel, action: 'userData', data: data 68 | 69 | _newDataChannel: (peerConnection)-> 70 | dataChannel = peerConnection.createDataChannel 'CINE', 71 | ordered: false # do not guarantee order 72 | maxRetransmitTime: 3000 # in milliseconds 73 | @_prepareDataChannel(peerConnection, dataChannel) 74 | dataChannel 75 | 76 | _prepareDataChannel: (peerConnection, dataChannel)-> 77 | dataChannel.dataToSend = [] 78 | dataChannel.onopen = (event)-> 79 | debug("ON OPEN", event) 80 | if dataChannel.readyState == "open" 81 | for data in dataChannel.dataToSend 82 | debug("Actually sending data", data) 83 | sendToDataChannel(dataChannel, data) 84 | delete dataChannel.dataToSend 85 | 86 | dataChannel.onmessage = (event)-> 87 | if event && event.data 88 | data = JSON.parse(event.data) 89 | CineIOPeer.trigger('peer-data', data.data) if data.action == 'userData' 90 | 91 | dataChannel 92 | 93 | _onConnectionOpen: => 94 | @write action: 'auth' 95 | CineIOPeer._sendIdentity() if CineIOPeer.config.identity 96 | CineIOPeer._sendJoinRoom(room) for room in CineIOPeer.config.rooms 97 | 98 | _connectionEnded: -> 99 | debug("Connection closed") 100 | 101 | _callFromRoom: (room, options)-> 102 | @calls[room] ||= new CallObject(room, options) 103 | @calls[room] 104 | 105 | _signalHandler: (data)=> 106 | # debug("got data") 107 | switch data.action 108 | # BASE 109 | when 'error' 110 | CineIOPeer.trigger('error', data) 111 | 112 | when 'rtc-servers' 113 | debug('setting config', data) 114 | @iceServers = data.data 115 | @fetchedIce = true 116 | CineIOPeer.trigger('gotIceServers', data.data) 117 | 118 | when 'ack' 119 | if data.source == 'call' 120 | CineIOPeer.config.rooms.push(data.room) 121 | callObj = @_callFromRoom(data.room, initiated: true, called: data.otheridentity) 122 | CineIOPeer.trigger('call-placed', call: callObj, otheridentity: data.otheridentity) 123 | # END BASE 124 | 125 | # CALLING 126 | when 'call' 127 | # debug('got incoming call', data) 128 | CineIOPeer.trigger('call', identity: data.identity, call: @_callFromRoom(data.room)) 129 | 130 | # created from initiator 131 | when 'call-cancel' 132 | # debug('got incoming call', data) 133 | @_callFromRoom(data.room).trigger('call-cancel', identity: data.identity) 134 | 135 | # created from recipient 136 | when 'call-reject' 137 | # debug('got incoming call', data) 138 | @_callFromRoom(data.room).trigger('call-reject', identity: data.identity) 139 | # END CALLING 140 | 141 | # ROOMS 142 | when 'room-leave' 143 | debug('room-leave', data) 144 | @_callFromRoom(data.room).left(data.identity) if data.identity 145 | @write action: 'room-goodbye', sparkId: data.sparkId, data.room 146 | @_closePeerConnection(data) 147 | 148 | when 'room-join' 149 | debug('room-join', data) 150 | @_callFromRoom(data.room).joined(data.identity) if data.identity 151 | @_ensurePeerConnection data, offer: true, support: data.support 152 | @write action: 'room-announce', sparkId: data.sparkId, room: data.room 153 | 154 | when 'room-announce' 155 | debug('room-announce', data) 156 | @_ensurePeerConnection data, offer: false, support: data.support 157 | 158 | when 'room-goodbye' 159 | debug("room-goodbye", data) 160 | @_closePeerConnection(data) 161 | # END ROOMS 162 | 163 | # RTC 164 | when 'rtc-ice' 165 | # debug('got remote ice', data) 166 | return unless data.sparkId 167 | @_ensurePeerConnection data, offer: false, support: data.support, (err, pc)-> 168 | pc.processIce(data.candidate) 169 | 170 | when 'rtc-offer' 171 | debug('got offer', data) 172 | @_ensurePeerConnection data, offer: false, support: data.support, (err, pc)=> 173 | pc.handleOffer data.offer, (err)=> 174 | debug('handled offer', err) 175 | handleAnswer = (err, answer)=> 176 | actuallySendAnswer = => 177 | # no harm in always overwriting the offer sdp with the local description 178 | answer.sdp = pc.pc.localDescription.sdp 179 | @write action: 'rtc-answer', answer: answer, sparkId: data.sparkId 180 | # debug('handling answer', answer, pc.pc.localDescription.sdp) 181 | 182 | # if the peer connection has not gotten the list of candidates 183 | # and it does not support trickle ice, 184 | # then wait for the ice and then send the answer 185 | if !pc.gotEndOfCandidates && pc.support.trickleIce == false 186 | console.log("waiting for endOfCandidates") 187 | pc.once 'endOfCandidates', actuallySendAnswer 188 | else 189 | console.log("not waiting for end of candidates") 190 | actuallySendAnswer() 191 | 192 | pc.answer handleAnswer 193 | 194 | 195 | when 'rtc-answer' 196 | # debug('got answer', data) 197 | @_ensurePeerConnection data, offer: false, support: data.support, (err, pc)-> 198 | pc.handleAnswer(data.answer) 199 | # END RTC 200 | # else 201 | # debug("UNKNOWN DATA", data) 202 | _closePeerConnection: (data)=> 203 | otherClientUUID = data.sparkUUID 204 | return unless @peerConnections[otherClientUUID] 205 | return if @peerConnections[otherClientUUID] == PENDING 206 | @peerConnections[otherClientUUID].close() 207 | delete @peerConnections[otherClientUUID] 208 | 209 | _sendOffer: (peerConnection)=> 210 | response = (err, offer)=> 211 | otherClientSparkId = peerConnection.otherClientSparkId 212 | if err || !offer 213 | debug("FATAL ERROR in offer", err, offer) 214 | return CineIOPeer.trigger("error", kind: 'offer', fatal: true, err: err) 215 | 216 | reallySendOffer = => 217 | debug('offering', err, otherClientSparkId, offer) 218 | # no harm in always overwriting the offer sdp with the local description 219 | offer.sdp = peerConnection.pc.localDescription.sdp 220 | @write action: 'rtc-offer', offer: offer, sparkId: otherClientSparkId 221 | 222 | # if the peer connection has not gotten the list of candidates 223 | # and it does not support trickle ice, 224 | # then wait for the ice and then send the offer 225 | if !peerConnection.gotEndOfCandidates && peerConnection.support.trickleIce == false 226 | peerConnection.once 'endOfCandidates', reallySendOffer 227 | else 228 | reallySendOffer() 229 | 230 | # av = CineIOPeer.localStreams().length == 0 231 | constraints = 232 | mandatory: 233 | OfferToReceiveAudio: true 234 | OfferToReceiveVideo: true 235 | if peerConnection.mainDataChannel 236 | constraints.optional = [{RtpDataChannels: true}] 237 | peerConnection.offer constraints, response 238 | 239 | _onCloseOfPeerConnection: (peerConnection)-> 240 | # debug("remote closed", event) 241 | return unless peerConnection.videoEls 242 | for videoEl in peerConnection.videoEls 243 | CineIOPeer.trigger 'media-removed', 244 | peerConnection: peerConnection 245 | videoElement: videoEl 246 | remote: true 247 | delete peerConnection.videoEls 248 | 249 | _newMember: (otherClientUUID, otherClientSparkId, options, callback)=> 250 | # we must be pending to get ice candidates, do not create a new pc 251 | if @peerConnections[otherClientUUID] 252 | return @_ensureReady => 253 | callback(null, @peerConnections[otherClientUUID]) 254 | 255 | @peerConnections[otherClientUUID] = PENDING 256 | @_ensureReady => 257 | debug("CREATING NEW PEER CONNECTION!!", otherClientUUID, options) 258 | peerConnection = PeerConnectionFactory.create() 259 | @peerConnections[otherClientUUID] = peerConnection 260 | peerConnection.videoEls = [] 261 | 262 | peerConnection.support = options.support || {} 263 | 264 | setSparkIdOnPeerConnection(peerConnection, otherClientSparkId) 265 | peerConnection.addStream(stream) for stream in CineIOPeer.localStreams() 266 | 267 | peerConnection.on 'addStream', (event)-> 268 | debug("got remote stream", event) 269 | videoEl = CineIOPeer._createVideoElementFromStream(event.stream, muted: false, mirror: false) 270 | peerConnection.videoEls.push videoEl 271 | CineIOPeer.trigger 'media-added', 272 | peerConnection: peerConnection 273 | videoElement: videoEl 274 | remote: true 275 | 276 | peerConnection.on 'removeStream', (event)-> 277 | debug("removing remote stream", event) 278 | videoEl = CineIOPeer._getVideoElementFromStream(event.stream) 279 | index = peerConnection.videoEls.indexOf(videoEl) 280 | peerConnection.videoEls.splice(index, 1) if index > -1 281 | 282 | CineIOPeer.trigger 'media-removed', 283 | peerConnection: peerConnection 284 | videoElement: videoEl 285 | remote: true 286 | 287 | peerConnection.on 'addChannel', (dataChannel)=> 288 | debug("GOT A NEW DATA CHANNEL", dataChannel) 289 | peerConnection.mainDataChannel = @_prepareDataChannel(peerConnection, dataChannel) 290 | 291 | peerConnection.on 'ice', (candidate)=> 292 | return if peerConnection.support.trickleIce == false 293 | # debug('got my ice', candidate.candidate.candidate) 294 | @write action: 'rtc-ice', candidate: candidate, sparkId: peerConnection.otherClientSparkId 295 | 296 | # unlikely there will be a mainDataChannel but good to check as we would want to offer 297 | if options.offer && CineIOPeer.localStreams().length > 0 || peerConnection.mainDataChannel 298 | @_sendOffer(peerConnection) 299 | peerConnection.on 'endOfCandidates', (event)-> 300 | debug("got end of candidates") 301 | peerConnection.gotEndOfCandidates = true 302 | peerConnection.on 'close', (event)=> 303 | @_onCloseOfPeerConnection(peerConnection) 304 | delete @peerConnections[otherClientUUID] 305 | 306 | callback(null, peerConnection) 307 | CineIOPeer.trigger("peerConnectionMade") 308 | 309 | _ensurePeerConnection: (data, options, callback=noop)=> 310 | otherClientSparkId = data.sparkId 311 | otherClientUUID = data.sparkUUID 312 | candidate = @peerConnections[otherClientUUID] 313 | if candidate && candidate != PENDING 314 | # the sparkID might have changed because the other client reconnected 315 | setSparkIdOnPeerConnection(candidate, otherClientSparkId) 316 | return setTimeout -> 317 | callback null, candidate 318 | @_newMember(otherClientUUID, otherClientSparkId, options, callback) 319 | 320 | _ensureReady: (callback)=> 321 | @_ensureIce callback 322 | 323 | _ensureIce: (callback)=> 324 | return setTimeout callback if @fetchedIce 325 | CineIOPeer.once 'gotIceServers', callback 326 | 327 | exports.connect = (options)-> 328 | new Connection(options) 329 | 330 | CineIOPeer = require('./main') 331 | CallObject = require('./call') 332 | -------------------------------------------------------------------------------- /src/vendor/uuid.js: -------------------------------------------------------------------------------- 1 | // https://gist.github.com/jed/982883 2 | function b( 3 | a // placeholder 4 | ){ 5 | return a // if the placeholder was passed, return 6 | ? ( // a random number from 0 to 15 7 | a ^ // unless b is 8, 8 | Math.random() // in which case 9 | * 16 // a random number from 10 | >> a/4 // 8 to 11 11 | ).toString(16) // in hexadecimal 12 | : ( // or otherwise a concatenated string: 13 | [1e7] + // 10000000 + 14 | -1e3 + // -1000 + 15 | -4e3 + // -4000 + 16 | -8e3 + // -80000000 + 17 | -1e11 // -100000000000, 18 | ).replace( // replacing 19 | /[018]/g, // zeroes, ones, and eights with 20 | b // random hex digits 21 | ) 22 | } 23 | 24 | module.exports = b; 25 | -------------------------------------------------------------------------------- /test/broadcast_bridge_test.coffee: -------------------------------------------------------------------------------- 1 | async = require('async') 2 | CineIOPeer = require('../src/main') 3 | BroadcastBridge = require('../src/broadcast_bridge') 4 | nearestServer = require('../src/nearest_server') 5 | PeerConnectionFactory = require('../src/peer_connection_factory') 6 | setupAndTeardown = require('./helpers/setup_and_teardown') 7 | stubPrimus = require('./helpers/stub_primus') 8 | FakePeerConnection = require('./helpers/fake_peer_connection') 9 | FakeMediaStream = require('./helpers/fake_media_stream') 10 | debug = require('../src/debug')('cine:peer:broadcast_bridge_test') 11 | 12 | describe 'BroadcastBridge', -> 13 | setupAndTeardown() 14 | stubPrimus() 15 | 16 | beforeEach -> 17 | sinon.stub PeerConnectionFactory, 'create', => 18 | if @fakeConnection 19 | debug("ugh fakeConnection") 20 | throw new Error("Two connections made!!!") 21 | @fakeConnection = new FakePeerConnection() 22 | 23 | afterEach -> 24 | PeerConnectionFactory.create.restore() 25 | PeerConnectionFactory._reset() 26 | 27 | afterEach -> 28 | if @fakeConnection 29 | debug("deleting fakeConnection", @fakeConnection) 30 | delete @fakeConnection 31 | 32 | describe 'constructor', -> 33 | it 'waits for ice servers', -> 34 | bb = new BroadcastBridge(CineIOPeer) 35 | CineIOPeer.trigger('gotIceServers', some: 'ice data') 36 | expect(bb.iceReady).to.be.true 37 | 38 | it 'creates a connection', -> 39 | bb = new BroadcastBridge(CineIOPeer) 40 | CineIOPeer.trigger('gotIceServers', some: 'ice data') 41 | expect(bb.connection).to.be.ok 42 | expect(bb.connection.connected).to.be.false 43 | 44 | describe 'methods', -> 45 | beforeEach -> 46 | @jsonpStub = sinon.stub nearestServer, '_makeJsonpCall' 47 | @jsonpStub.callsArgWith 1, null, {rtcPublish: 'http://some-broadcast-bridge-url'} 48 | 49 | afterEach -> 50 | @jsonpStub.restore() 51 | nearestServer._reset() 52 | 53 | beforeEach -> 54 | CineIOPeer.config.publicKey = 'project-public-key' 55 | 56 | beforeEach -> 57 | @subject = new BroadcastBridge(CineIOPeer) 58 | 59 | describe '#startBroadcast', -> 60 | beforeEach -> 61 | CineIOPeer.trigger('gotIceServers', some: 'ice data') 62 | 63 | beforeEach (done)-> 64 | streamType = 'camera' 65 | @mediaStream = new FakeMediaStream 66 | streamId = 'the stream id' 67 | streamKey = 'the stream key' 68 | @subject.startBroadcast streamType, @mediaStream, streamId, streamKey, done 69 | 70 | fakeConnectionMade = false 71 | testFunction = -> fakeConnectionMade 72 | checkFunction = (callback)=> 73 | if @fakeConnection && @fakeConnection.offered 74 | @subject.connection.primus.trigger 'open' 75 | fakeConnectionMade = true 76 | setTimeout(callback, 10) 77 | async.until testFunction, checkFunction, (err)=> 78 | return done(err) if err 79 | @fakeConnection.trigger('endOfCandidates', 'some fake candidate') 80 | 81 | it 'fetches the nearest server', -> 82 | expect(@jsonpStub.calledOnce).to.be.true 83 | 84 | it 'triggers auth on the connection', -> 85 | expect(@primusStub.write.calledTwice).to.be.true 86 | args = @primusStub.write.firstCall.args 87 | expect(args).to.have.length(1) 88 | expect(args[0].action).to.equal('auth') 89 | expect(args[0].publicKey).to.equal("project-public-key") 90 | 91 | it 'adds the media stream to the peer connection', -> 92 | expect(@fakeConnection.streams).to.deep.equal([@mediaStream]) 93 | 94 | it 'creates an offer to the broadcast bridge server', -> 95 | expect(@fakeConnection.offer.calledOnce).to.be.true 96 | 97 | it 'sends the broadcast-start action', -> 98 | expect(@primusStub.write.calledTwice).to.be.true 99 | args = @primusStub.write.secondCall.args 100 | expect(args).to.have.length(1) 101 | expect(args[0].streamType).to.equal('camera') 102 | expect(args[0].action).to.equal('broadcast-start') 103 | expect(args[0].offer).to.equal('full local description') 104 | expect(args[0].streamId).to.equal('the stream id') 105 | expect(args[0].streamKey).to.equal('the stream key') 106 | 107 | it 'consumes an answer from the broadcast bridge server', -> 108 | @subject.connection.primus.trigger('data', action: 'rtc-answer', streamType: 'camera', answer: 'the hello answer') 109 | expect(@fakeConnection.handleAnswer.calledOnce).to.be.true 110 | expect(@fakeConnection.handleAnswer.firstCall.args[0]).to.equal('the hello answer') 111 | 112 | describe '#stopBroadcast', -> 113 | it 'does nothing when there is no connection', (done)-> 114 | @subject.stopBroadcast 'camera', (err)-> 115 | expect(err).to.be.undefined 116 | done() 117 | 118 | describe 'with an open connection', -> 119 | beforeEach -> 120 | CineIOPeer.trigger('gotIceServers', some: 'ice data') 121 | 122 | beforeEach (done)-> 123 | streamType = 'camera' 124 | @mediaStream = new FakeMediaStream 125 | streamId = 'the stream id' 126 | streamKey = 'the stream key' 127 | @subject.startBroadcast streamType, @mediaStream, streamId, streamKey, done 128 | 129 | fakeConnectionMade = false 130 | testFunction = -> fakeConnectionMade 131 | checkFunction = (callback)=> 132 | if @fakeConnection && @fakeConnection.offered 133 | @subject.connection.primus.trigger 'open' 134 | fakeConnectionMade = true 135 | setTimeout(callback, 10) 136 | async.until testFunction, checkFunction, (err)=> 137 | return done(err) if err 138 | @fakeConnection.trigger('endOfCandidates', 'some fake candidate') 139 | 140 | it 'does not error when when there is no stream for that type', (done)-> 141 | @subject.stopBroadcast 'camera', (err)=> 142 | expect(err).to.be.undefined 143 | expect(@fakeConnection.close.calledOnce).to.be.true 144 | done() 145 | 146 | describe 'without a stream for that type', -> 147 | 148 | it 'closes the peer connection', (done)-> 149 | @subject.stopBroadcast 'screen', (err)=> 150 | expect(err).to.be.undefined 151 | expect(@fakeConnection.close.calledOnce).to.be.false 152 | done() 153 | 154 | it 'tells the broadcast bridge', (done)-> 155 | @subject.stopBroadcast 'screen', (err)=> 156 | expect(@primusStub.write.calledThrice).to.be.true 157 | expect(err).to.be.undefined 158 | args = @primusStub.write.thirdCall.args 159 | expect(args).to.have.length(1) 160 | expect(args[0].streamType).to.equal('screen') 161 | expect(args[0].action).to.equal('broadcast-stop') 162 | done() 163 | 164 | describe 'with a stream for that type', -> 165 | 166 | it 'closes the peer connection', (done)-> 167 | @subject.stopBroadcast 'camera', (err)=> 168 | expect(err).to.be.undefined 169 | expect(@fakeConnection.close.calledOnce).to.be.true 170 | done() 171 | 172 | it 'tells the broadcast bridge', (done)-> 173 | @subject.stopBroadcast 'camera', (err)=> 174 | expect(@primusStub.write.calledThrice).to.be.true 175 | expect(err).to.be.undefined 176 | args = @primusStub.write.thirdCall.args 177 | expect(args).to.have.length(1) 178 | expect(args[0].streamType).to.equal('camera') 179 | expect(args[0].action).to.equal('broadcast-stop') 180 | done() 181 | -------------------------------------------------------------------------------- /test/call_test.coffee: -------------------------------------------------------------------------------- 1 | setupAndTeardown = require('./helpers/setup_and_teardown') 2 | CineIOPeer = require('../src/main') 3 | CallObject = require('../src/call') 4 | stubPrimus = require('./helpers/stub_primus') 5 | stubUserMedia = require('./helpers/stub_user_media') 6 | 7 | describe 'Call', -> 8 | setupAndTeardown() 9 | 10 | stubPrimus() 11 | 12 | stubUserMedia() 13 | 14 | beforeEach (done)-> 15 | @dataTrigger = (data)-> 16 | done() 17 | CineIOPeer.on 'info', @dataTrigger 18 | CineIOPeer.on 'error', @dataTrigger 19 | CineIOPeer.init('the-public-key') 20 | 21 | afterEach -> 22 | CineIOPeer.off 'info', @dataTrigger 23 | CineIOPeer.off 'error', @dataTrigger 24 | 25 | beforeEach -> 26 | @call = new CallObject('Hogwarts Express') 27 | 28 | describe '#answer', -> 29 | it 'joins the room', (done)-> 30 | @call.answer (err)=> 31 | expect(@primusStub.write.calledOnce).to.be.true 32 | args = @primusStub.write.firstCall.args 33 | expect(args).to.have.length(1) 34 | expect(args[0].action).to.equal('room-join') 35 | expect(args[0].room).to.equal('Hogwarts Express') 36 | expect(args[0].publicKey).to.equal('the-public-key') 37 | done() 38 | 39 | describe '#reject', -> 40 | it 'sends a rejection', -> 41 | @call.reject() 42 | expect(@primusStub.write.calledOnce).to.be.true 43 | args = @primusStub.write.firstCall.args 44 | expect(args).to.have.length(1) 45 | expect(args[0].action).to.equal('call-reject') 46 | expect(args[0].room).to.equal('Hogwarts Express') 47 | expect(args[0].publicKey).to.equal('the-public-key') 48 | -------------------------------------------------------------------------------- /test/helpers/fake_media_stream.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class FakeMediaStream 2 | constructor: -> 3 | @ended = false 4 | @id = @label = "stream-id" 5 | -------------------------------------------------------------------------------- /test/helpers/fake_peer_connection.coffee: -------------------------------------------------------------------------------- 1 | BackboneEvents = require("backbone-events-standalone") 2 | 3 | class FakeBrowserPeerConnection 4 | constructor: -> 5 | @localDescription = null 6 | offered: -> 7 | @localDescription = 'full local description' 8 | module.exports = class FakePeerConnection 9 | constructor: (@options)-> 10 | sinon.stub this, 'close' 11 | sinon.spy this, 'answer' 12 | sinon.spy this, 'offer' 13 | sinon.spy this, 'handleAnswer' 14 | @streams = [] 15 | @offered = false 16 | @pc = new FakeBrowserPeerConnection 17 | close: -> 18 | addStream: (stream)-> 19 | @streams.push(stream) 20 | removeStream: (stream)-> 21 | index = @streams.indexOf(stream) 22 | @streams.splice(index, 1) if index > -1 23 | processIce: (@remoteIce)-> 24 | handleOffer: (@remoteOffer, callback)-> 25 | setTimeout -> 26 | callback(null) 27 | handleAnswer: (@remoteAnswer)-> 28 | offer: (constraints, callback)-> 29 | if typeof constraints == 'function' 30 | callback = constraints 31 | constraints = {} 32 | setTimeout => 33 | @offered = true 34 | @pc.offered() 35 | callback(null, "some-offer-string") 36 | answer: (callback)-> 37 | setTimeout -> 38 | callback(null, "some-answer-string") 39 | 40 | BackboneEvents.mixin FakePeerConnection:: 41 | -------------------------------------------------------------------------------- /test/helpers/setup_and_teardown.coffee: -------------------------------------------------------------------------------- 1 | module.exports = -> 2 | beforeEach -> 3 | CineIOPeer.reset() 4 | 5 | afterEach -> 6 | delete CineIOPeer._signalConnection 7 | delete CineIOPeer.microphoneStream 8 | delete CineIOPeer.cameraStream 9 | delete CineIOPeer.cameraAndMicrophoneStream 10 | delete CineIOPeer.screenShareStream 11 | -------------------------------------------------------------------------------- /test/helpers/stub_create_object_url.coffee: -------------------------------------------------------------------------------- 1 | inPhantom = typeof window.URL == 'undefined' 2 | module.exports = (identifier="identifier")-> 3 | if inPhantom 4 | beforeEach -> 5 | window.URL = {createObjectURL: ->} 6 | 7 | afterEach -> 8 | window.URL 9 | 10 | beforeEach -> 11 | sinon.stub window.URL, 'createObjectURL', (mediaStream)-> 12 | return "blob:http%3A//#{window.location.host}/#{identifier}" 13 | 14 | afterEach -> 15 | window.URL.createObjectURL.restore() 16 | -------------------------------------------------------------------------------- /test/helpers/stub_primus.coffee: -------------------------------------------------------------------------------- 1 | Primus = require('../../src/vendor/primus') 2 | BackboneEvents = require("backbone-events-standalone") 3 | 4 | class PrimusStub 5 | write: -> 6 | 7 | BackboneEvents.mixin PrimusStub:: 8 | 9 | module.exports = -> 10 | beforeEach -> 11 | @primusConnectStub = sinon.stub Primus, 'connect', (url)=> 12 | @primusStub = new PrimusStub 13 | 14 | sinon.stub @primusStub, 'write' 15 | 16 | @primusStub 17 | 18 | afterEach -> 19 | @primusConnectStub.restore() 20 | -------------------------------------------------------------------------------- /test/helpers/stub_user_media.coffee: -------------------------------------------------------------------------------- 1 | CineIOPeer = require('../../src/main') 2 | FakeMediaStream = require('./fake_media_stream') 3 | stubCreateObjectUrl = require("./stub_create_object_url") 4 | 5 | module.exports = (success=true)-> 6 | stubCreateObjectUrl() 7 | 8 | beforeEach -> 9 | sinon.stub CineIOPeer, '_unsafeGetUserMedia', (streamOptions, callback)-> 10 | if success 11 | callback(null, new FakeMediaStream) 12 | else 13 | callback('could not fetch media') 14 | 15 | afterEach -> 16 | CineIOPeer._unsafeGetUserMedia.restore() 17 | -------------------------------------------------------------------------------- /test/main_test.coffee: -------------------------------------------------------------------------------- 1 | setupAndTeardown = require('./helpers/setup_and_teardown') 2 | CineIOPeer = require('../src/main') 3 | CallObject = require('../src/call') 4 | stubPrimus = require('./helpers/stub_primus') 5 | stubUserMedia = require('./helpers/stub_user_media') 6 | inPhantom = typeof window.URL == 'undefined' 7 | 8 | describe 'CineIOPeer', -> 9 | setupAndTeardown() 10 | stubPrimus() 11 | 12 | describe '.version', -> 13 | it 'has a version', -> 14 | expect(CineIOPeer.version).to.equal('0.0.9') 15 | 16 | describe '.reset', -> 17 | it 'resets the config', -> 18 | CineIOPeer.config = {some: 'random', config: 'setting'} 19 | CineIOPeer.reset() 20 | expect(CineIOPeer.config).to.deep.equal(rooms: [], videoElements: {}) 21 | 22 | describe '.init', -> 23 | 24 | setupDataTrigger = (cb)-> 25 | @dataTrigger = cb 26 | CineIOPeer.on 'info', @dataTrigger 27 | CineIOPeer.on 'error', @dataTrigger 28 | 29 | afterEach -> 30 | CineIOPeer.off 'info', @dataTrigger 31 | CineIOPeer.off 'error', @dataTrigger 32 | 33 | it 'initializes the config', (done)-> 34 | setupDataTrigger.call this, -> 35 | done() 36 | CineIOPeer.init('my-public-key') 37 | expect(CineIOPeer.config).to.deep.equal(publicKey: 'my-public-key', rooms: [], videoElements: {}) 38 | 39 | it 'checks for support', (done)-> 40 | setupDataTrigger.call this, (data)-> 41 | expect(data).to.deep.equal(support: !inPhantom) 42 | done() 43 | CineIOPeer.init('my-public-key') 44 | 45 | describe 'after initialized', -> 46 | 47 | beforeEach (done)-> 48 | @dataTrigger = (data)-> 49 | done() 50 | CineIOPeer.on 'info', @dataTrigger 51 | CineIOPeer.on 'error', @dataTrigger 52 | CineIOPeer.init('the-public-key') 53 | 54 | afterEach -> 55 | CineIOPeer.off 'info', @dataTrigger 56 | CineIOPeer.off 'error', @dataTrigger 57 | 58 | describe '.identify', -> 59 | it 'sets an identity', -> 60 | CineIOPeer.identify('Minerva McGonagall', 'timely-timestamp', 'secure-signature') 61 | expect(CineIOPeer.config.identity.identity).to.equal('Minerva McGonagall') 62 | 63 | it 'writes to the signaling connection', -> 64 | CineIOPeer.identify('Minerva McGonagall', 'timely-timestamp', 'secure-signature') 65 | expect(@primusStub.write.calledOnce).to.be.true 66 | args = @primusStub.write.firstCall.args 67 | expect(args).to.have.length(1) 68 | expect(args[0].action).to.equal('identify') 69 | expect(args[0].identity).to.equal('Minerva McGonagall') 70 | expect(args[0].timestamp).to.equal('timely-timestamp') 71 | expect(args[0].signature).to.equal('secure-signature') 72 | expect(args[0].publicKey).to.equal('the-public-key') 73 | 74 | describe '.call', -> 75 | stubUserMedia() 76 | 77 | beforeEach -> 78 | CineIOPeer.identify('Minerva McGonagall') 79 | 80 | it 'writes to the signaling connection', (done)-> 81 | CineIOPeer.call "Albus Dumbledore", (err, data)=> 82 | expect(err).to.be.null 83 | expect(@primusStub.write.calledTwice).to.be.true 84 | args = @primusStub.write.secondCall.args 85 | expect(args).to.have.length(1) 86 | expect(args[0].action).to.equal('call') 87 | expect(args[0].otheridentity).to.equal('Albus Dumbledore') 88 | expect(args[0].identity).to.equal('Minerva McGonagall') 89 | expect(args[0].publicKey).to.equal('the-public-key') 90 | done() 91 | callPlaced = 92 | action: 'ack' 93 | source: 'call' 94 | room: 'some-room-returned-by-the-server' 95 | otheridentity: 'Albus Dumbledore' 96 | CineIOPeer._signalConnection.primus.trigger 'data', callPlaced 97 | 98 | it 'returns a call object', (done)-> 99 | CineIOPeer.call "Albus Dumbledore", (err, data)-> 100 | expect(err).to.be.null 101 | expect(data.call.room).to.equal('some-room-returned-by-the-server') 102 | expect(data.call instanceof CallObject) 103 | done() 104 | callPlaced = 105 | action: 'ack' 106 | source: 'call' 107 | room: 'some-room-returned-by-the-server' 108 | otheridentity: 'Albus Dumbledore' 109 | CineIOPeer._signalConnection.primus.trigger 'data', callPlaced 110 | 111 | it 'takes a room', (done)-> 112 | CineIOPeer.call "Albus Dumbledore", 'some-room', (err)=> 113 | expect(err).to.be.null 114 | expect(@primusStub.write.calledTwice).to.be.true 115 | args = @primusStub.write.secondCall.args 116 | expect(args).to.have.length(1) 117 | expect(args[0].action).to.equal('call') 118 | expect(args[0].otheridentity).to.equal('Albus Dumbledore') 119 | expect(args[0].identity).to.equal('Minerva McGonagall') 120 | expect(args[0].publicKey).to.equal('the-public-key') 121 | done() 122 | callPlaced = 123 | action: 'ack' 124 | source: 'call' 125 | room: 'some-room' 126 | otheridentity: 'Albus Dumbledore' 127 | CineIOPeer._signalConnection.primus.trigger 'data', callPlaced 128 | 129 | 130 | describe '.join', -> 131 | stubUserMedia() 132 | 133 | it 'adds the room to the list of rooms', (done)-> 134 | CineIOPeer.join "Gryffindor Common Room", (err)-> 135 | expect(err).to.be.undefined 136 | expect(CineIOPeer.config.rooms).to.deep.equal(['Gryffindor Common Room']) 137 | done() 138 | 139 | it 'writes to the signaling connection', (done)-> 140 | CineIOPeer.join "Gryffindor Common Room", (err)=> 141 | expect(err).to.be.undefined 142 | expect(@primusStub.write.calledOnce).to.be.true 143 | args = @primusStub.write.firstCall.args 144 | expect(args).to.have.length(1) 145 | expect(args[0].action).to.equal('room-join') 146 | expect(args[0].room).to.equal('Gryffindor Common Room') 147 | expect(args[0].publicKey).to.equal('the-public-key') 148 | done() 149 | 150 | describe '.leave', -> 151 | stubUserMedia() 152 | 153 | it 'requires the user have previously joined the room', (done)-> 154 | errorHandler = (data)-> 155 | expect(data).to.deep.equal(msg: "not connected to room", room: "Gryffindor Common Room") 156 | CineIOPeer.off 'error', errorHandler 157 | done() 158 | CineIOPeer.on 'error', errorHandler 159 | CineIOPeer.leave "Gryffindor Common Room" 160 | 161 | it 'removes the room to the list of rooms', (done)-> 162 | CineIOPeer.join "Gryffindor Common Room", (err)-> 163 | expect(err).to.be.undefined 164 | expect(CineIOPeer.config.rooms).to.contain("Gryffindor Common Room") 165 | CineIOPeer.leave("Gryffindor Common Room") 166 | expect(CineIOPeer.config.rooms).not.to.contain("Gryffindor Common Room") 167 | done() 168 | 169 | it 'writes to the signaling connection', (done)-> 170 | CineIOPeer.join "Gryffindor Common Room", (err)=> 171 | expect(err).to.be.undefined 172 | CineIOPeer.leave("Gryffindor Common Room") 173 | expect(@primusStub.write.calledTwice).to.be.true 174 | args = @primusStub.write.secondCall.args 175 | expect(args).to.have.length(1) 176 | expect(args[0].action).to.equal('room-leave') 177 | expect(args[0].room).to.equal('Gryffindor Common Room') 178 | expect(args[0].publicKey).to.equal('the-public-key') 179 | done() 180 | 181 | describe '.startCameraAndMicrophone', -> 182 | describe 'success', -> 183 | stubUserMedia() 184 | 185 | it 'fetches media', (done)-> 186 | CineIOPeer.startCameraAndMicrophone (err)-> 187 | expect(err).to.be.undefined 188 | expect(CineIOPeer._unsafeGetUserMedia.calledOnce).to.be.true 189 | args = CineIOPeer._unsafeGetUserMedia.firstCall.args 190 | expect(args).to.have.length(2) 191 | expect(args[0]).to.deep.equal(audio: true, video: true) 192 | expect(args[1]).to.be.a('function') 193 | done() 194 | 195 | it 'will not fetch twice', (done)-> 196 | CineIOPeer.startCameraAndMicrophone (err)-> 197 | expect(err).to.be.undefined 198 | CineIOPeer.startCameraAndMicrophone (err)-> 199 | expect(err).to.be.undefined 200 | expect(CineIOPeer._unsafeGetUserMedia.calledOnce).to.be.true 201 | args = CineIOPeer._unsafeGetUserMedia.firstCall.args 202 | expect(args).to.have.length(2) 203 | expect(args[0]).to.deep.equal(audio: true, video: true) 204 | expect(args[1]).to.be.a('function') 205 | done() 206 | 207 | it 'triggers media with the stream and media true', (done)-> 208 | mediaResponse = (data)-> 209 | expect(data.local).to.be.true 210 | expect(data.videoElement.tagName).to.equal('VIDEO') 211 | expect(data.videoElement.src).to.equal("blob:http%3A//#{window.location.host}/identifier") 212 | expect(data.stream.id).to.equal('stream-id') 213 | CineIOPeer.off 'media-added', mediaResponse 214 | done() 215 | CineIOPeer.on 'media-added', mediaResponse 216 | CineIOPeer.startCameraAndMicrophone() 217 | 218 | describe 'failure', -> 219 | stubUserMedia(false) 220 | 221 | it 'returns with the error', (done)-> 222 | mediaResponse = (data)-> 223 | CineIOPeer.off 'media-rejected', mediaResponse 224 | done() 225 | CineIOPeer.on 'media-rejected', mediaResponse 226 | CineIOPeer.startCameraAndMicrophone (err)-> 227 | expect(err).to.equal('could not fetch media') 228 | 229 | it 'triggers media with the stream and media false', (done)-> 230 | mediaResponse = (data)-> 231 | expect(data.local).to.be.true 232 | expect(data.videoElement).to.be.undefined 233 | expect(data.stream).to.be.undefined 234 | 235 | CineIOPeer.off 'media-rejected', mediaResponse 236 | done() 237 | CineIOPeer.on 'media-rejected', mediaResponse 238 | CineIOPeer.startCameraAndMicrophone() 239 | -------------------------------------------------------------------------------- /test/nearest_server_test.coffee: -------------------------------------------------------------------------------- 1 | nearestServer = require('../src/nearest_server') 2 | 3 | describe 'nearestServer', -> 4 | beforeEach -> 5 | @jsonpStub = sinon.stub nearestServer, '_makeJsonpCall' 6 | @jsonpStub.callsArgWith 1, null, {some: 'data'} 7 | 8 | afterEach -> 9 | @jsonpStub.restore() 10 | nearestServer._reset() 11 | 12 | it 'fetches the nearest server', (done)-> 13 | nearestServer (err, ns)-> 14 | expect(err).to.be.null 15 | expect(ns).to.deep.equal(some: 'data') 16 | done() 17 | 18 | it 'will not duplicate fetch when the response has not been returned', (done)-> 19 | called = false 20 | callback = (err, ns)=> 21 | expect(ns).to.deep.equal(some: 'data') 22 | if called 23 | expect(@jsonpStub.calledOnce).to.be.true 24 | done() 25 | called = true 26 | 27 | nearestServer callback 28 | nearestServer callback 29 | 30 | it 'does not fetch twice', (done)-> 31 | nearestServer (err, ns)=> 32 | expect(err).to.be.null 33 | expect(ns).to.deep.equal(some: 'data') 34 | expect(@jsonpStub.calledOnce).to.be.true 35 | nearestServer (err, ns)=> 36 | expect(err).to.be.null 37 | expect(ns).to.deep.equal(some: 'data') 38 | expect(@jsonpStub.calledOnce).to.be.true 39 | done() 40 | -------------------------------------------------------------------------------- /test/peer_connection_factory_test.coffee: -------------------------------------------------------------------------------- 1 | PeerConnectionFactory = require('../src/peer_connection_factory') 2 | CineIOPeer = require('../src/main') 3 | FakePeerConnection = require('./helpers/fake_peer_connection') 4 | setupAndTeardown = require('./helpers/setup_and_teardown') 5 | debug = require('../src/debug')('cine:peer:peer_connection_factory_test') 6 | 7 | describe 'PeerConnectionFactory', -> 8 | setupAndTeardown() 9 | 10 | beforeEach -> 11 | sinon.stub PeerConnectionFactory, '_actuallyCreatePeerConnection', (options)=> 12 | if @fakeConnection 13 | debug("ugh fakeConnection") 14 | throw new Error("Two connections made!!!") 15 | @fakeConnection = new FakePeerConnection(options) 16 | 17 | afterEach -> 18 | PeerConnectionFactory._actuallyCreatePeerConnection.restore() 19 | PeerConnectionFactory._reset() 20 | 21 | describe 'create', -> 22 | it 'requires the ice servers', -> 23 | expect(PeerConnectionFactory.create()).to.be.null 24 | describe 'after getting ice servers', -> 25 | beforeEach -> 26 | CineIOPeer.trigger('gotIceServers', some: 'ice data') 27 | it 'creates a connection', -> 28 | connection = PeerConnectionFactory.create() 29 | expect(connection).to.be.instanceof(FakePeerConnection) 30 | expect(connection.options).to.deep.equal(iceServers: {some: 'ice data'}) 31 | -------------------------------------------------------------------------------- /test/runner.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |