├── .bithoundrc ├── .gitignore ├── .travis.yml ├── .zuul.yml ├── AUTHORS ├── LICENSE ├── README.md ├── cleanup.js ├── couple.js ├── detect.js ├── docs.json ├── docs └── coupling-events.md ├── examples └── getting-started.js ├── generators.js ├── index.js ├── monitor.js ├── package.json └── test ├── all.js └── server.js /.bithoundrc: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | "**/node_modules/**", 4 | "**/dist/**", 5 | "**/examples/**", 6 | "**/test/**", 7 | "**/Gruntfile.js", 8 | "**/gulpfile.js" 9 | ] 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | browsers/ 2 | .DS_Store 3 | node_modules 4 | npm-debug.log 5 | .travis 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "6" 5 | - "lts/*" 6 | 7 | env: 8 | matrix: 9 | - BROWSER=chrome BVER=stable 10 | - BROWSER=chrome BVER=beta 11 | - BROWSER=chrome BVER=unstable 12 | - BROWSER=firefox BVER=stable 13 | - BROWSER=firefox BVER=beta 14 | - BROWSER=firefox BVER=unstable 15 | 16 | matrix: 17 | fast_finish: true 18 | 19 | allow_failures: 20 | - env: BROWSER=chrome BVER=unstable 21 | - env: BROWSER=firefox BVER=unstable 22 | 23 | before_script: 24 | - ./node_modules/travis-multirunner/setup.sh 25 | - export DISPLAY=:99.0 26 | - sh -e /etc/init.d/xvfb start 27 | 28 | after_failure: 29 | - for file in *.log; do echo $file; echo "======================"; cat $file; done || true 30 | 31 | notifications: 32 | email: 33 | - damon.oehlman@nicta.com.au -------------------------------------------------------------------------------- /.zuul.yml: -------------------------------------------------------------------------------- 1 | ui: tape 2 | browsers: 3 | - name: chrome 4 | version: [35,36,beta] 5 | 6 | - name: firefox 7 | version: 30..latest 8 | 9 | server: ./test/server.js 10 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Damon Oehlman (https://github.com/DamonOehlman) 2 | Silvia Pfeiffer (https://github.com/silviapfeiffer) 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2013 National ICT Australia Limited (NICTA) 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rtc-tools 2 | 3 | The `rtc-tools` module does most of the heavy lifting within the 4 | [rtc.io](http://rtc.io) suite. Primarily it handles the logic of coupling 5 | a local `RTCPeerConnection` with it's remote counterpart via an 6 | [rtc-signaller](https://github.com/rtc-io/rtc-signaller) signalling 7 | channel. 8 | 9 | 10 | [![NPM](https://nodei.co/npm/rtc-tools.png)](https://nodei.co/npm/rtc-tools/) 11 | 12 | [![Build Status](https://img.shields.io/travis/rtc-io/rtc-tools.svg?branch=master)](https://travis-ci.org/rtc-io/rtc-tools) [![unstable](https://img.shields.io/badge/stability-unstable-yellowgreen.svg)](https://github.com/dominictarr/stability#unstable) [![bitHound Score](https://www.bithound.io/github/rtc-io/rtc-tools/badges/score.svg)](https://www.bithound.io/github/rtc-io/rtc-tools) 13 | [![Gitter chat](https://badges.gitter.im/rtc-io/discuss.png)](https://gitter.im/rtc-io/discuss) 14 | 15 | 16 | 17 | ## Getting Started 18 | 19 | If you decide that the `rtc-tools` module is a better fit for you than either 20 | [rtc-quickconnect](https://github.com/rtc-io/rtc-quickconnect) or 21 | [rtc](https://github.com/rtc-io/rtc) then the code snippet below 22 | will provide you a guide on how to get started using it in conjunction with 23 | the [rtc-signaller](https://github.com/rtc-io/rtc-signaller) (version 5.0 and above) 24 | and [rtc-media](https://github.com/rtc-io/rtc-media) modules: 25 | 26 | ```js 27 | var messenger = require('rtc-switchboard-messenger'); 28 | var signaller = require('rtc-signaller')(messenger('https://switchboard.rtc.io/')); 29 | var rtc = require('rtc-tools'); 30 | var getUserMedia = require('getusermedia'); 31 | var attachMedia = require('attachmediastream'); 32 | 33 | // capture local media first as firefox 34 | // will want a local stream and doesn't support onnegotiationneeded event 35 | getUserMedia({ video: true, audio: true }, function(err, localStream) { 36 | if (err) { 37 | return console.error('could not capture media: ', err); 38 | } 39 | 40 | document.body.appendChild(attachMedia(localStream)); 41 | 42 | // look for friends 43 | signaller.on('peer:announce', function(data) { 44 | var pc = rtc.createConnection(); 45 | var monitor = rtc.couple(pc, data.id, signaller); 46 | 47 | // add the stream to the connection 48 | pc.addStream(localStream); 49 | 50 | // once the connection is active, log a console message 51 | monitor.once('connected', function() { 52 | console.log('connection active to: ' + data.id); 53 | 54 | pc.getRemoteStreams().forEach(function(stream) { 55 | document.body.appendChild(attachMedia(stream)); 56 | }); 57 | }); 58 | 59 | 60 | monitor.createOffer(); 61 | }); 62 | 63 | // announce ourself in the rtc-getting-started room 64 | signaller.announce({ room: 'rtc-getting-started' }); 65 | }); 66 | 67 | ``` 68 | 69 | This code definitely doesn't cover all the cases that you need to consider 70 | (i.e. peers leaving, etc) but it should demonstrate how to: 71 | 72 | 1. Capture video and add it to a peer connection 73 | 2. Couple a local peer connection with a remote peer connection 74 | 3. Deal with the remote steam being discovered and how to render 75 | that to the local interface. 76 | 77 | ## Reference 78 | 79 | ### createConnection 80 | 81 | ``` 82 | createConnection(opts?, constraints?) => RTCPeerConnection 83 | ``` 84 | 85 | Create a new `RTCPeerConnection` auto generating default opts as required. 86 | 87 | ```js 88 | var conn; 89 | 90 | // this is ok 91 | conn = rtc.createConnection(); 92 | 93 | // and so is this 94 | conn = rtc.createConnection({ 95 | iceServers: [] 96 | }); 97 | ``` 98 | 99 | ### rtc-tools/cleanup 100 | 101 | ``` 102 | cleanup(pc) 103 | ``` 104 | 105 | The `cleanup` function is used to ensure that a peer connection is properly 106 | closed and ready to be cleaned up by the browser. 107 | 108 | ### rtc-tools/couple 109 | 110 | #### couple(pc, targetId, signaller, opts?) 111 | 112 | Couple a WebRTC connection with another webrtc connection identified by 113 | `targetId` via the signaller. 114 | 115 | The following options can be provided in the `opts` argument: 116 | 117 | - `sdpfilter` (default: null) 118 | 119 | A simple function for filtering SDP as part of the peer 120 | connection handshake (see the Using Filters details below). 121 | 122 | ##### Example Usage 123 | 124 | ```js 125 | var couple = require('rtc/couple'); 126 | 127 | couple(pc, '54879965-ce43-426e-a8ef-09ac1e39a16d', signaller); 128 | ``` 129 | 130 | ##### Using Filters 131 | 132 | In certain instances you may wish to modify the raw SDP that is provided 133 | by the `createOffer` and `createAnswer` calls. This can be done by passing 134 | a `sdpfilter` function (or array) in the options. For example: 135 | 136 | ```js 137 | // run the sdp from through a local tweakSdp function. 138 | couple(pc, '54879965-ce43-426e-a8ef-09ac1e39a16d', signaller, { 139 | sdpfilter: tweakSdp 140 | }); 141 | ``` 142 | 143 | ### rtc-tools/detect 144 | 145 | Provide the [rtc-core/detect](https://github.com/rtc-io/rtc-core#detect) 146 | functionality. 147 | 148 | ### rtc-tools/generators 149 | 150 | The generators package provides some utility methods for generating 151 | constraint objects and similar constructs. 152 | 153 | ```js 154 | var generators = require('rtc/generators'); 155 | ``` 156 | 157 | #### generators.config(config) 158 | 159 | Generate a configuration object suitable for passing into an W3C 160 | RTCPeerConnection constructor first argument, based on our custom config. 161 | 162 | In the event that you use short term authentication for TURN, and you want 163 | to generate new `iceServers` regularly, you can specify an iceServerGenerator 164 | that will be used prior to coupling. This generator should return a fully 165 | compliant W3C (RTCIceServer dictionary)[http://www.w3.org/TR/webrtc/#idl-def-RTCIceServer]. 166 | 167 | If you pass in both a generator and iceServers, the iceServers _will be 168 | ignored and the generator used instead. 169 | 170 | #### generators.connectionConstraints(flags, constraints) 171 | 172 | This is a helper function that will generate appropriate connection 173 | constraints for a new `RTCPeerConnection` object which is constructed 174 | in the following way: 175 | 176 | ```js 177 | var conn = new RTCPeerConnection(flags, constraints); 178 | ``` 179 | 180 | In most cases the constraints object can be left empty, but when creating 181 | data channels some additional options are required. This function 182 | can generate those additional options and intelligently combine any 183 | user defined constraints (in `constraints`) with shorthand flags that 184 | might be passed while using the `rtc.createConnection` helper. 185 | 186 | ### rtc-tools/monitor 187 | 188 | ``` 189 | monitor(pc, targetId, signaller, parentBus) => mbus 190 | ``` 191 | 192 | The monitor is a useful tool for determining the state of `pc` (an 193 | `RTCPeerConnection`) instance in the context of your application. The 194 | monitor uses both the `iceConnectionState` information of the peer 195 | connection and also the various 196 | [signaller events](https://github.com/rtc-io/rtc-signaller#signaller-events) 197 | to determine when the connection has been `connected` and when it has 198 | been `disconnected`. 199 | 200 | A monitor created `mbus` is returned as the result of a 201 | [couple](https://github.com/rtc-io/rtc#rtccouple) between a local peer 202 | connection and it's remote counterpart. 203 | 204 | ## License(s) 205 | 206 | ### Apache 2.0 207 | 208 | Copyright 2015 National ICT Australia Limited (NICTA) 209 | 210 | Licensed under the Apache License, Version 2.0 (the "License"); 211 | you may not use this file except in compliance with the License. 212 | You may obtain a copy of the License at 213 | 214 | http://www.apache.org/licenses/LICENSE-2.0 215 | 216 | Unless required by applicable law or agreed to in writing, software 217 | distributed under the License is distributed on an "AS IS" BASIS, 218 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 219 | See the License for the specific language governing permissions and 220 | limitations under the License. 221 | -------------------------------------------------------------------------------- /cleanup.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var debug = require('cog/logger')('rtc/cleanup'); 5 | 6 | var CANNOT_CLOSE_STATES = [ 7 | 'closed' 8 | ]; 9 | 10 | var EVENTS_DECOUPLE_BC = [ 11 | 'addstream', 12 | 'datachannel', 13 | 'icecandidate', 14 | 'negotiationneeded', 15 | 'removestream', 16 | 'signalingstatechange' 17 | ]; 18 | 19 | var EVENTS_DECOUPLE_AC = [ 20 | 'iceconnectionstatechange' 21 | ]; 22 | 23 | /** 24 | ### rtc-tools/cleanup 25 | 26 | ``` 27 | cleanup(pc) 28 | ``` 29 | 30 | The `cleanup` function is used to ensure that a peer connection is properly 31 | closed and ready to be cleaned up by the browser. 32 | 33 | **/ 34 | module.exports = function(pc) { 35 | if (!pc) return; 36 | 37 | // see if we can close the connection 38 | var currentState = pc.iceConnectionState; 39 | var currentSignaling = pc.signalingState; 40 | var canClose = CANNOT_CLOSE_STATES.indexOf(currentState) < 0 && CANNOT_CLOSE_STATES.indexOf(currentSignaling) < 0; 41 | 42 | function decouple(events) { 43 | events.forEach(function(evtName) { 44 | if (pc['on' + evtName]) { 45 | pc['on' + evtName] = null; 46 | } 47 | }); 48 | } 49 | 50 | // decouple "before close" events 51 | decouple(EVENTS_DECOUPLE_BC); 52 | 53 | if (canClose) { 54 | debug('attempting connection close, current state: '+ pc.iceConnectionState); 55 | try { 56 | pc.close(); 57 | } catch (e) { 58 | console.warn('Could not close connection', e); 59 | } 60 | } 61 | 62 | // remove the event listeners 63 | // after a short delay giving the connection time to trigger 64 | // close and iceconnectionstatechange events 65 | setTimeout(function() { 66 | decouple(EVENTS_DECOUPLE_AC); 67 | }, 100); 68 | }; 69 | -------------------------------------------------------------------------------- /couple.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var mbus = require('mbus'); 5 | var queue = require('rtc-taskqueue'); 6 | var cleanup = require('./cleanup'); 7 | var monitor = require('./monitor'); 8 | var throttle = require('cog/throttle'); 9 | var pluck = require('whisk/pluck'); 10 | var pluckCandidate = pluck('candidate', 'sdpMid', 'sdpMLineIndex'); 11 | var CLOSED_STATES = [ 'closed', 'failed' ]; 12 | var CHECKING_STATES = [ 'checking' ]; 13 | 14 | /** 15 | ### rtc-tools/couple 16 | 17 | #### couple(pc, targetId, signaller, opts?) 18 | 19 | Couple a WebRTC connection with another webrtc connection identified by 20 | `targetId` via the signaller. 21 | 22 | The following options can be provided in the `opts` argument: 23 | 24 | - `sdpfilter` (default: null) 25 | 26 | A simple function for filtering SDP as part of the peer 27 | connection handshake (see the Using Filters details below). 28 | 29 | ##### Example Usage 30 | 31 | ```js 32 | var couple = require('rtc/couple'); 33 | 34 | couple(pc, '54879965-ce43-426e-a8ef-09ac1e39a16d', signaller); 35 | ``` 36 | 37 | ##### Using Filters 38 | 39 | In certain instances you may wish to modify the raw SDP that is provided 40 | by the `createOffer` and `createAnswer` calls. This can be done by passing 41 | a `sdpfilter` function (or array) in the options. For example: 42 | 43 | ```js 44 | // run the sdp from through a local tweakSdp function. 45 | couple(pc, '54879965-ce43-426e-a8ef-09ac1e39a16d', signaller, { 46 | sdpfilter: tweakSdp 47 | }); 48 | ``` 49 | 50 | **/ 51 | function couple(pc, targetId, signaller, opts) { 52 | var debugLabel = (opts || {}).debugLabel || 'rtc'; 53 | var debug = require('cog/logger')(debugLabel + '/couple'); 54 | 55 | // create a monitor for the connection 56 | var mon = monitor(pc, targetId, signaller, (opts || {}).logger); 57 | var emit = mbus('', mon); 58 | var reactive = (opts || {}).reactive; 59 | var endOfCandidates = true; 60 | 61 | // configure the time to wait between receiving a 'disconnect' 62 | // iceConnectionState and determining that we are closed 63 | var disconnectTimeout = (opts || {}).disconnectTimeout || 10000; 64 | var disconnectTimer; 65 | 66 | // Target ready indicates that the target peer has indicated it is 67 | // ready to begin coupling 68 | var targetReady = false; 69 | var targetInfo = undefined; 70 | var readyInterval = (opts || {}).readyInterval || 100; 71 | var readyTimer; 72 | 73 | // Failure timeout 74 | var failTimeout = (opts || {}).failTimeout || 30000; 75 | var failTimer; 76 | 77 | // Request offer timer 78 | var requestOfferTimer; 79 | 80 | // Interoperability flags 81 | var allowReactiveInterop = (opts || {}).allowReactiveInterop; 82 | 83 | // initilaise the negotiation helpers 84 | var isMaster = signaller.isMaster(targetId); 85 | 86 | // initialise the processing queue (one at a time please) 87 | var q = queue(pc, opts); 88 | var coupling = false; 89 | var negotiationRequired = false; 90 | var renegotiateRequired = false; 91 | var creatingOffer = false; 92 | var awaitingAnswer = false; 93 | var interoperating = false; 94 | 95 | /** 96 | Indicates whether this peer connection is in a state where it is able to have new offers created 97 | **/ 98 | function isReadyForOffer() { 99 | return !coupling && pc.signalingState === 'stable'; 100 | } 101 | 102 | function createOffer() { 103 | // If coupling is already in progress, return 104 | if (!isReadyForOffer()) return; 105 | 106 | debug('[' + signaller.id + '] ' + 'Creating new offer for connection to ' + targetId); 107 | // Otherwise, create the offer 108 | coupling = true; 109 | creatingOffer = true; 110 | awaitingAnswer = true; 111 | negotiationRequired = false; 112 | q.createOffer().then(function() { 113 | creatingOffer = false; 114 | }).catch(function() { 115 | creatingOffer = false; 116 | awaitingAnswer = true; 117 | }); 118 | } 119 | 120 | var createOrRequestOffer = throttle(function() { 121 | if (awaitingAnswer) { 122 | debug('[' + signaller.id + '] awaiting answer from ' + targetId + ' before sending new offer'); 123 | return; 124 | } 125 | 126 | if (!targetReady) { 127 | debug('[' + signaller.id + '] ' + targetId + ' not yet ready for offer'); 128 | return emit.once('target.ready', createOrRequestOffer); 129 | } 130 | 131 | // If this is not the master, always send the negotiate request 132 | // Redundant requests are eliminated on the master side 133 | if (! isMaster) { 134 | debug('[' + signaller.id + '] ' + 'Requesting negotiation from ' + targetId + ' (requesting offerer? ' + renegotiateRequired + ')'); 135 | // Due to https://bugs.chromium.org/p/webrtc/issues/detail?id=2782 which involves incompatibilities between 136 | // Chrome and Firefox created offers by default client offers are disabled to ensure that all offers are coming 137 | // from the same source. By passing `allowReactiveInterop` you can reallow this, then use the `filtersdp` option 138 | // to provide a munged SDP that might be able to work 139 | return signaller.to(targetId).send('/negotiate', { 140 | requestOfferer: (allowReactiveInterop || !interoperating) && renegotiateRequired 141 | }); 142 | } 143 | 144 | debug('[' + signaller.id + '] Creating new offer for ' + targetId); 145 | return createOffer(); 146 | }, 100, { leading: false }); 147 | 148 | function decouple() { 149 | debug('decoupling ' + signaller.id + ' from ' + targetId); 150 | 151 | // Reset values 152 | coupling = false; 153 | creatingOffer = false; 154 | awaitingAnswer = false; 155 | 156 | // Clear any outstanding timers 157 | clearTimeout(readyTimer); 158 | clearTimeout(disconnectTimer); 159 | clearTimeout(requestOfferTimer); 160 | clearTimeout(failTimer); 161 | 162 | // stop the monitor 163 | // mon.removeAllListeners(); 164 | mon.close(); 165 | 166 | // cleanup the peerconnection 167 | cleanup(pc); 168 | 169 | // remove listeners 170 | signaller.removeListener('sdp', handleSdp); 171 | signaller.removeListener('candidate', handleCandidate); 172 | signaller.removeListener('endofcandidates', handleLastCandidate); 173 | signaller.removeListener('negotiate', handleNegotiateRequest); 174 | signaller.removeListener('ready', handleReady); 175 | signaller.removeListener('requestoffer', handleRequestOffer); 176 | 177 | // remove listeners (version >= 5) 178 | signaller.removeListener('message:sdp', handleSdp); 179 | signaller.removeListener('message:candidate', handleCandidate); 180 | signaller.removeListener('message:endofcandidates', handleLastCandidate); 181 | signaller.removeListener('message:negotiate', handleNegotiateRequest); 182 | signaller.removeListener('message:ready', handleReady); 183 | signaller.removeListener('message:requestoffer', handleRequestOffer); 184 | 185 | } 186 | 187 | function handleCandidate(data, src) { 188 | // if the source is unknown or not a match, then don't process 189 | if ((! src) || (src.id !== targetId)) { 190 | return; 191 | } 192 | 193 | q.addIceCandidate(data); 194 | } 195 | 196 | // No op 197 | function handleLastCandidate() { 198 | } 199 | 200 | function handleSdp(sdp, src) { 201 | // if the source is unknown or not a match, then don't process 202 | if ((! src) || (src.id !== targetId)) { 203 | return; 204 | } 205 | 206 | emit('sdp.remote', sdp); 207 | 208 | // To speed up things on the renegotiation side of things, determine whether we have 209 | // finished the coupling (offer -> answer) cycle, and whether it is safe to start 210 | // renegotiating prior to the iceConnectionState "completed" state 211 | q.setRemoteDescription(sdp).then(function() { 212 | 213 | // If this is the peer that is coupling, and we have received the answer so we can 214 | // and assume that coupling (offer -> answer) process is complete, so we can clear the coupling flag 215 | if (coupling && sdp.type === 'answer') { 216 | awaitingAnswer = false; 217 | 218 | // Check if the coupling is complete 219 | checkIfCouplingComplete(); 220 | 221 | debug('coupling complete, can now trigger any pending renegotiations'); 222 | if (negotiationRequired) createOrRequestOffer(); 223 | } 224 | }); 225 | } 226 | 227 | function handleReady(src) { 228 | if (targetReady || !src || src.id !== targetId) { 229 | return; 230 | } 231 | debug('[' + signaller.id + '] ' + targetId + ' is ready for coupling'); 232 | targetReady = true; 233 | targetInfo = src.data; 234 | interoperating = (targetInfo.browser !== signaller.attributes.browser); 235 | emit('target.ready'); 236 | } 237 | 238 | function handleConnectionClose() { 239 | debug('captured pc close, iceConnectionState = ' + pc.iceConnectionState); 240 | decouple(); 241 | } 242 | 243 | function handleDisconnect() { 244 | debug('captured pc disconnect, monitoring connection status'); 245 | 246 | // start the disconnect timer 247 | disconnectTimer = setTimeout(function() { 248 | debug('manually closing connection after disconnect timeout'); 249 | mon('failed'); 250 | cleanup(pc); 251 | }, disconnectTimeout); 252 | 253 | mon.on('statechange', handleDisconnectAbort); 254 | mon('failing'); 255 | } 256 | 257 | function handleDisconnectAbort() { 258 | debug('connection state changed to: ' + pc.iceConnectionState); 259 | 260 | // if the state is checking, then do not reset the disconnect timer as 261 | // we are doing our own checking 262 | if (CHECKING_STATES.indexOf(pc.iceConnectionState) >= 0) { 263 | return; 264 | } 265 | 266 | resetDisconnectTimer(); 267 | 268 | // if we have a closed or failed status, then close the connection 269 | if (CLOSED_STATES.indexOf(pc.iceConnectionState) >= 0) { 270 | return mon('closed'); 271 | } 272 | 273 | mon.once('disconnect', handleDisconnect); 274 | } 275 | 276 | function handleLocalCandidate(evt) { 277 | var data = evt.candidate && pluckCandidate(evt.candidate); 278 | 279 | if (evt.candidate) { 280 | resetDisconnectTimer(); 281 | emit('ice.local', data); 282 | signaller.to(targetId).send('/candidate', data); 283 | endOfCandidates = false; 284 | } 285 | else if (! endOfCandidates) { 286 | endOfCandidates = true; 287 | emit('ice.gathercomplete'); 288 | signaller.to(targetId).send('/endofcandidates', {}); 289 | } 290 | } 291 | 292 | function requestNegotiation() { 293 | // This is a redundant request if not reactive 294 | if (coupling && !reactive) return; 295 | 296 | // If no coupling is occurring, regardless of reactive, start the offer process 297 | if (!coupling) return createOrRequestOffer(); 298 | 299 | // If we are already coupling, we are reactive and renegotiation has not been indicated 300 | // defer a negotiation request 301 | if (coupling && reactive && !negotiationRequired) { 302 | debug('renegotiation is required, but deferring until existing connection is established'); 303 | negotiationRequired = true; 304 | 305 | // NOTE: This is commented out, as the functionality after the setRemoteDescription 306 | // should adequately take care of this. But should it not, re-enable this 307 | // mon.once('connectionstate:completed', function() { 308 | // createOrRequestOffer(); 309 | // }); 310 | } 311 | } 312 | 313 | 314 | /** 315 | This allows the master to request the client to send an offer 316 | **/ 317 | function requestOfferFromClient() { 318 | if (requestOfferTimer) clearTimeout(requestOfferTimer); 319 | if (pc.signalingState === 'closed') return; 320 | 321 | // Check if we are ready for a new offer, otherwise delay 322 | if (!isReadyForOffer()) { 323 | debug('[' + signaller.id + '] negotiation request denied, not in a state to accept new offers [coupling = ' + coupling + ', creatingOffer = ' + creatingOffer + ', awaitingAnswer = ' + awaitingAnswer + ', ' + pc.signalingState + ']'); 324 | // Do a check to see if the coupling is complete 325 | checkIfCouplingComplete(); 326 | // Do a recheck of the request 327 | requestOfferTimer = setTimeout(requestOfferFromClient, 500); 328 | } else { 329 | // Flag as coupling and request the client send the offer 330 | debug('[' + signaller.id + '] ' + targetId + ' has requested the ability to create the offer'); 331 | coupling = true; 332 | return signaller.to(targetId).send('/requestoffer'); 333 | } 334 | } 335 | 336 | function handleNegotiateRequest(data, src) { 337 | debug('[' + signaller.id + '] ' + src.id + ' has requested a negotiation'); 338 | 339 | // Sanity check that this is for the target 340 | if (!src || src.id !== targetId) return; 341 | emit('negotiate.request', src.id, data); 342 | 343 | // Check if the client is requesting the ability to create the offer themselves 344 | if (data && data.requestOfferer) { 345 | return requestOfferFromClient(); 346 | } 347 | 348 | // Otherwise, begin the traditional master driven negotiation process 349 | requestNegotiation(); 350 | } 351 | 352 | function handleRenegotiateRequest() { 353 | if (!reactive) return; 354 | emit('negotiate.renegotiate'); 355 | renegotiateRequired = true; 356 | requestNegotiation(); 357 | } 358 | 359 | function resetDisconnectTimer() { 360 | var recovered = !!disconnectTimer && CLOSED_STATES.indexOf(pc.iceConnectionState) === -1; 361 | mon.off('statechange', handleDisconnectAbort); 362 | 363 | // clear the disconnect timer 364 | debug('reset disconnect timer, state: ' + pc.iceConnectionState); 365 | clearTimeout(disconnectTimer); 366 | disconnectTimer = undefined; 367 | 368 | // Trigger the recovered event if this is a recovery 369 | if (recovered) { 370 | mon('recovered'); 371 | } 372 | } 373 | 374 | /** 375 | Allow clients to send offers 376 | **/ 377 | function handleRequestOffer(src) { 378 | if (!src || src.id !== targetId) return; 379 | debug('[' + signaller.id + '] ' + targetId + ' has requested that the offer be sent [' + src.id + ']'); 380 | return createOffer(); 381 | } 382 | 383 | function checkIfCouplingComplete() { 384 | // Check if the coupling process is over 385 | // The coupling process should be check whenever the signaling state is stable 386 | // or when a remote answer has been received 387 | // A coupling is considered over when the offer is no longer being created, and 388 | // there is no answer being waited for 389 | if (!coupling || creatingOffer || awaitingAnswer) return; 390 | debug('[' + signaller.id + '] coupling completed to ' + targetId); 391 | coupling = false; 392 | } 393 | 394 | // when regotiation is needed look for the peer 395 | if (reactive) { 396 | pc.onnegotiationneeded = handleRenegotiateRequest; 397 | } 398 | 399 | pc.onicecandidate = handleLocalCandidate; 400 | 401 | // when the task queue tells us we have sdp available, send that over the wire 402 | q.on('sdp.local', function(desc) { 403 | signaller.to(targetId).send('/sdp', desc); 404 | }); 405 | 406 | // when we receive sdp, then 407 | signaller.on('sdp', handleSdp); 408 | signaller.on('candidate', handleCandidate); 409 | signaller.on('endofcandidates', handleLastCandidate); 410 | signaller.on('ready', handleReady); 411 | 412 | // listeners (signaller >= 5) 413 | signaller.on('message:sdp', handleSdp); 414 | signaller.on('message:candidate', handleCandidate); 415 | signaller.on('message:endofcandidates', handleLastCandidate); 416 | signaller.on('message:ready', handleReady); 417 | 418 | // if this is a master connection, listen for negotiate events 419 | if (isMaster) { 420 | signaller.on('negotiate', handleNegotiateRequest); 421 | signaller.on('message:negotiate', handleNegotiateRequest); // signaller >= 5 422 | } else { 423 | signaller.on('requestoffer', handleRequestOffer); 424 | signaller.on('message:requestoffer', handleRequestOffer); 425 | } 426 | 427 | // when the connection closes, remove event handlers 428 | mon.once('closed', handleConnectionClose); 429 | mon.once('disconnected', handleDisconnect); 430 | 431 | // patch in the create offer functions 432 | mon.createOffer = createOrRequestOffer; 433 | 434 | // A heavy handed approach to ensuring readiness across the coupling 435 | // peers. Will periodically send the `ready` message to the target peer 436 | // until the target peer has acknowledged that it also is ready - at which 437 | // point the offer can be sent 438 | function checkReady() { 439 | clearTimeout(readyTimer); 440 | signaller.to(targetId).send('/ready'); 441 | 442 | // If we are ready, they've told us they are ready, and we've told 443 | // them we're ready, then exit 444 | if (targetReady) return; 445 | 446 | // Otherwise, keep telling them we're ready 447 | readyTimer = setTimeout(checkReady, readyInterval); 448 | } 449 | checkReady(); 450 | debug('[' + signaller.id + '] ready for coupling to ' + targetId); 451 | 452 | // If we fail to connect within the given timeframe, trigger a failure 453 | failTimer = setTimeout(function() { 454 | debug('[' + signaller.id + '] failed to connect to ' + targetId + ' within allocated time'); 455 | mon('failed'); 456 | decouple(); 457 | }, failTimeout); 458 | 459 | mon.once('connected', function() { 460 | clearTimeout(failTimer); 461 | }); 462 | 463 | mon.on('signalingchange', function(pc, state) { 464 | debug('[' + signaller.id + '] signaling state ' + state + ' to ' + targetId); 465 | }); 466 | 467 | mon.on('signaling:stable', function() { 468 | // Check if the coupling if complete 469 | checkIfCouplingComplete(); 470 | 471 | // Check if we have any pending negotiations 472 | if (negotiationRequired) { 473 | debug('signalling stable and a negotiation is required, so creating one'); 474 | createOrRequestOffer(); 475 | } 476 | }); 477 | 478 | mon.stop = decouple; 479 | 480 | /** 481 | Allows for manually triggering a renegotiation 482 | */ 483 | mon.requestRenegotiation = handleRenegotiateRequest; 484 | 485 | /** 486 | Aborts the coupling process 487 | **/ 488 | mon.abort = function() { 489 | if (failTimer) { 490 | clearTimeout(failTimer); 491 | } 492 | decouple(); 493 | mon('aborted'); 494 | }; 495 | 496 | // Override destroy to clear the task queue as well 497 | mon.destroy = function() { 498 | mon.clear(); 499 | q.clear(); 500 | }; 501 | 502 | return mon; 503 | } 504 | 505 | module.exports = couple; -------------------------------------------------------------------------------- /detect.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | /** 5 | ### rtc-tools/detect 6 | 7 | Provide the [rtc-core/detect](https://github.com/rtc-io/rtc-core#detect) 8 | functionality. 9 | **/ 10 | module.exports = require('rtc-core/detect'); 11 | -------------------------------------------------------------------------------- /docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "badges": { 3 | "nodeico": true, 4 | "travis": true, 5 | "stability": "unstable", 6 | "bithound": true, 7 | "gitter": "rtc-io/discuss" 8 | }, 9 | 10 | "license": { 11 | "holder": "National ICT Australia Limited (NICTA)" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docs/coupling-events.md: -------------------------------------------------------------------------------- 1 | ## Events Communicated through Connection Coupling 2 | 3 | The following is a list of events that are emitted by the connection monitor to assist with debugging what is occuring during p2p connection negotiation. 4 | 5 | - `negotiate:abort` => `f(stage, sdp?)` 6 | 7 | The `negotiate:abort` event is triggered if the connection negotiation fails for any particular reason. The `stage` will either be `createOffer` or `createAnswer` depending on the role a peer is playing in the connection negotiation. 8 | 9 | - `negotiate:createOffer` => `f()` 10 | 11 | Triggered immediately prior to the `createOffer` method being called on a `RTCPeerConnection` object. 12 | 13 | - `negotiate:createAnswer` => `f()` 14 | 15 | Triggered immediately prior to the `createAnswer` method being called on an `RTCPeerConnection` object. 16 | 17 | - `negotiate:createOffer:created` / `negotiate:createAnswer:created` => `f(sdp)` 18 | 19 | Triggered when SDP has successfully created for either a `createOffer` or `createAnswer` request. 20 | 21 | - `negotiate:setlocaldescription` => `f(desc, err?)` 22 | 23 | Triggered after the local description of the peer connection has been set. If the `err` argument is present, then the operation has failed and `err` contains the error details. 24 | 25 | - `negotiate:request` => `f(peerId)` 26 | 27 | Triggered when a peer has requested a renegotiation of the `RTCPeerConnection`. To simplify our connection flow one peer in a pairing always starts the connection handshake (i.e. initiates the `createOffer` call). An event when the remote party has made changes to the inputs to the peer connection (media streams, data channels, etc) and would like to renegotiate the connection. 28 | 29 | - `negotiate:renegotiate` => `f()` 30 | 31 | Triggered when a local peer connection has triggered the `onnegotiationneeded` event which indicates that something has changed in the connection and the connection should be re-established with the peer. 32 | 33 | - `sdp:received` => `f(data)` 34 | 35 | Triggered when the remote peer's SDP has been received via the signalling channel. 36 | 37 | - `icecandidate:local` => `f(candidate)` 38 | 39 | Triggered when an ice candidate has been gathered by a connection. These are candidate details that will be passed to the other negotiating peer (via the signalling channel) to assist with establishing a P2P connection. 40 | 41 | - `icecandidate:remote` => `f(data)` 42 | 43 | Triggered when a remote ice candidate has been sent via the signalling channel. 44 | 45 | - `icecandidate:gathered` => `f()` 46 | 47 | Triggered when all the local ice candidates have been gathered by the local `RTCPeerConnection`. 48 | 49 | - `ice:candidate:added` => `f(data, err?)` 50 | 51 | Triggered after a remote candidate has successfully been added to the local peer connection. If the `err` argument is supplied to the function then the operation has failed (i.e. the candidate was not added to the connection) and the `err` argument contains the exception details. 52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/getting-started.js: -------------------------------------------------------------------------------- 1 | var messenger = require('rtc-switchboard-messenger'); 2 | var signaller = require('rtc-signaller')(messenger('https://switchboard.rtc.io/')); 3 | var rtc = require('..'); 4 | var getUserMedia = require('getusermedia'); 5 | var attachMedia = require('attachmediastream'); 6 | 7 | // capture local media first as firefox 8 | // will want a local stream and doesn't support onnegotiationneeded event 9 | getUserMedia({ video: true, audio: true }, function(err, localStream) { 10 | if (err) { 11 | return console.error('could not capture media: ', err); 12 | } 13 | 14 | document.body.appendChild(attachMedia(localStream)); 15 | 16 | // look for friends 17 | signaller.on('peer:announce', function(data) { 18 | var pc = rtc.createConnection(); 19 | var monitor = rtc.couple(pc, data.id, signaller); 20 | 21 | // add the stream to the connection 22 | pc.addStream(localStream); 23 | 24 | // once the connection is active, log a console message 25 | monitor.once('connected', function() { 26 | console.log('connection active to: ' + data.id); 27 | 28 | pc.getRemoteStreams().forEach(function(stream) { 29 | document.body.appendChild(attachMedia(stream)); 30 | }); 31 | }); 32 | 33 | 34 | monitor.createOffer(); 35 | }); 36 | 37 | // announce ourself in the rtc-getting-started room 38 | signaller.announce({ room: 'rtc-getting-started' }); 39 | }); 40 | -------------------------------------------------------------------------------- /generators.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var debug = require('cog/logger')('generators'); 5 | var detect = require('./detect'); 6 | var defaults = require('cog/defaults'); 7 | 8 | var mappings = { 9 | create: { 10 | dtls: function(c) { 11 | if (! detect.moz) { 12 | c.optional = (c.optional || []).concat({ DtlsSrtpKeyAgreement: true }); 13 | } 14 | } 15 | } 16 | }; 17 | 18 | /** 19 | ### rtc-tools/generators 20 | 21 | The generators package provides some utility methods for generating 22 | constraint objects and similar constructs. 23 | 24 | ```js 25 | var generators = require('rtc/generators'); 26 | ``` 27 | 28 | **/ 29 | 30 | /** 31 | #### generators.config(config) 32 | 33 | Generate a configuration object suitable for passing into an W3C 34 | RTCPeerConnection constructor first argument, based on our custom config. 35 | 36 | In the event that you use short term authentication for TURN, and you want 37 | to generate new `iceServers` regularly, you can specify an iceServerGenerator 38 | that will be used prior to coupling. This generator should return a fully 39 | compliant W3C (RTCIceServer dictionary)[http://www.w3.org/TR/webrtc/#idl-def-RTCIceServer]. 40 | 41 | If you pass in both a generator and iceServers, the iceServers _will be 42 | ignored and the generator used instead. 43 | **/ 44 | 45 | exports.config = function(config) { 46 | var iceServerGenerator = (config || {}).iceServerGenerator; 47 | 48 | return defaults({}, config, { 49 | iceServers: typeof iceServerGenerator == 'function' ? iceServerGenerator() : [] 50 | }); 51 | }; 52 | 53 | /** 54 | #### generators.connectionConstraints(flags, constraints) 55 | 56 | This is a helper function that will generate appropriate connection 57 | constraints for a new `RTCPeerConnection` object which is constructed 58 | in the following way: 59 | 60 | ```js 61 | var conn = new RTCPeerConnection(flags, constraints); 62 | ``` 63 | 64 | In most cases the constraints object can be left empty, but when creating 65 | data channels some additional options are required. This function 66 | can generate those additional options and intelligently combine any 67 | user defined constraints (in `constraints`) with shorthand flags that 68 | might be passed while using the `rtc.createConnection` helper. 69 | **/ 70 | exports.connectionConstraints = function(flags, constraints) { 71 | var generated = {}; 72 | var m = mappings.create; 73 | var out; 74 | 75 | // iterate through the flags and apply the create mappings 76 | Object.keys(flags || {}).forEach(function(key) { 77 | if (m[key]) { 78 | m[key](generated); 79 | } 80 | }); 81 | 82 | // generate the connection constraints 83 | out = defaults({}, constraints, generated); 84 | debug('generated connection constraints: ', out); 85 | 86 | return out; 87 | }; 88 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | 'use strict'; 4 | 5 | /** 6 | # rtc-tools 7 | 8 | The `rtc-tools` module does most of the heavy lifting within the 9 | [rtc.io](http://rtc.io) suite. Primarily it handles the logic of coupling 10 | a local `RTCPeerConnection` with it's remote counterpart via an 11 | [rtc-signaller](https://github.com/rtc-io/rtc-signaller) signalling 12 | channel. 13 | 14 | ## Getting Started 15 | 16 | If you decide that the `rtc-tools` module is a better fit for you than either 17 | [rtc-quickconnect](https://github.com/rtc-io/rtc-quickconnect) or 18 | [rtc](https://github.com/rtc-io/rtc) then the code snippet below 19 | will provide you a guide on how to get started using it in conjunction with 20 | the [rtc-signaller](https://github.com/rtc-io/rtc-signaller) (version 5.0 and above) 21 | and [rtc-media](https://github.com/rtc-io/rtc-media) modules: 22 | 23 | <<< examples/getting-started.js 24 | 25 | This code definitely doesn't cover all the cases that you need to consider 26 | (i.e. peers leaving, etc) but it should demonstrate how to: 27 | 28 | 1. Capture video and add it to a peer connection 29 | 2. Couple a local peer connection with a remote peer connection 30 | 3. Deal with the remote steam being discovered and how to render 31 | that to the local interface. 32 | 33 | ## Reference 34 | 35 | **/ 36 | 37 | var gen = require('./generators'); 38 | 39 | // export detect 40 | var detect = exports.detect = require('./detect'); 41 | var findPlugin = require('rtc-core/plugin'); 42 | 43 | // export cog logger for convenience 44 | exports.logger = require('cog/logger'); 45 | 46 | // export peer connection 47 | var RTCPeerConnection = 48 | exports.RTCPeerConnection = detect('RTCPeerConnection'); 49 | 50 | // add the couple utility 51 | exports.couple = require('./couple'); 52 | 53 | /** 54 | ### createConnection 55 | 56 | ``` 57 | createConnection(opts?, constraints?) => RTCPeerConnection 58 | ``` 59 | 60 | Create a new `RTCPeerConnection` auto generating default opts as required. 61 | 62 | ```js 63 | var conn; 64 | 65 | // this is ok 66 | conn = rtc.createConnection(); 67 | 68 | // and so is this 69 | conn = rtc.createConnection({ 70 | iceServers: [] 71 | }); 72 | ``` 73 | **/ 74 | exports.createConnection = function(opts, constraints) { 75 | var plugin = findPlugin((opts || {}).plugins); 76 | var PeerConnection = (opts || {}).RTCPeerConnection || RTCPeerConnection; 77 | 78 | // generate the config based on options provided 79 | var config = gen.config(opts); 80 | 81 | // generate appropriate connection constraints 82 | constraints = gen.connectionConstraints(opts, constraints); 83 | 84 | if (plugin && typeof plugin.createConnection == 'function') { 85 | return plugin.createConnection(config, constraints); 86 | } 87 | 88 | return new PeerConnection(config, constraints); 89 | }; 90 | -------------------------------------------------------------------------------- /monitor.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var mbus = require('mbus'); 5 | 6 | // define some state mappings to simplify the events we generate 7 | var stateMappings = { 8 | completed: 'connected' 9 | }; 10 | 11 | // define the events that we need to watch for peer connection 12 | // state changes 13 | var peerStateEvents = [ 14 | 'signalingstatechange', 15 | 'iceconnectionstatechange', 16 | ]; 17 | 18 | /** 19 | ### rtc-tools/monitor 20 | 21 | ``` 22 | monitor(pc, targetId, signaller, parentBus) => mbus 23 | ``` 24 | 25 | The monitor is a useful tool for determining the state of `pc` (an 26 | `RTCPeerConnection`) instance in the context of your application. The 27 | monitor uses both the `iceConnectionState` information of the peer 28 | connection and also the various 29 | [signaller events](https://github.com/rtc-io/rtc-signaller#signaller-events) 30 | to determine when the connection has been `connected` and when it has 31 | been `disconnected`. 32 | 33 | A monitor created `mbus` is returned as the result of a 34 | [couple](https://github.com/rtc-io/rtc#rtccouple) between a local peer 35 | connection and it's remote counterpart. 36 | 37 | **/ 38 | module.exports = function(pc, targetId, signaller, parentBus) { 39 | var monitor = mbus('', parentBus); 40 | var state; 41 | var connectionState; 42 | var signalingState; 43 | var isClosed = false; 44 | 45 | function checkState() { 46 | var newConnectionState = pc.iceConnectionState; 47 | var newState = getMappedState(newConnectionState); 48 | var newSignalingState = pc.signalingState; 49 | 50 | // flag the we had a state change 51 | monitor('statechange', pc, newState); 52 | monitor('connectionstatechange', pc, newConnectionState); 53 | 54 | // if the active state has changed, then send the appopriate message 55 | if (state !== newState) { 56 | monitor(newState); 57 | state = newState; 58 | } 59 | 60 | if (connectionState !== newConnectionState) { 61 | monitor('connectionstate:' + newConnectionState); 62 | connectionState = newConnectionState; 63 | } 64 | 65 | // As Firefox does not always support `onclose`, if the state is closed 66 | // and we haven't already handled the close, do so now 67 | if (newState === 'closed' && !isClosed) { 68 | handleClose(); 69 | } 70 | 71 | // Check the signalling state to see if it has also changed 72 | if (signalingState !== newSignalingState) { 73 | monitor('signalingchange', pc, newSignalingState, signalingState); 74 | monitor('signaling:' + newSignalingState, pc, newSignalingState, signalingState); 75 | signalingState = newSignalingState; 76 | } 77 | } 78 | 79 | function handleClose() { 80 | isClosed = true; 81 | monitor('closed'); 82 | } 83 | 84 | pc.onclose = handleClose; 85 | peerStateEvents.forEach(function(evtName) { 86 | pc['on' + evtName] = checkState; 87 | }); 88 | 89 | monitor.close = function() { 90 | pc.onclose = null; 91 | peerStateEvents.forEach(function(evtName) { 92 | pc['on' + evtName] = null; 93 | }); 94 | }; 95 | 96 | monitor.checkState = checkState; 97 | 98 | monitor.destroy = function() { 99 | monitor.clear(); 100 | }; 101 | 102 | // if we haven't been provided a valid peer connection, abort 103 | if (! pc) { 104 | return monitor; 105 | } 106 | 107 | // determine the initial is active state 108 | state = getMappedState(pc.iceConnectionState); 109 | 110 | return monitor; 111 | }; 112 | 113 | /* internal helpers */ 114 | 115 | function getMappedState(state) { 116 | return stateMappings[state] || state; 117 | } 118 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rtc-tools", 3 | "description": "Cross-browser WebRTC helpers", 4 | "author": "Damon Oehlman ", 5 | "version": "5.6.0", 6 | "dependencies": { 7 | "cog": "^1.0.0", 8 | "mbus": "^2.0.0", 9 | "rtc-core": "^4.0.0", 10 | "rtc-taskqueue": "^2.9.0", 11 | "whisk": "^1.1.0" 12 | }, 13 | "devDependencies": { 14 | "attachmediastream": "^1.0.1", 15 | "broth": "^2.1.1", 16 | "browserify": "^11.0.0", 17 | "freeice": "^2.1.1", 18 | "getusermedia": "^1.1.0", 19 | "messenger-memory": "^1.2.0", 20 | "peerpair": "^1.0.0", 21 | "rtc-signaller": "^6.2.1", 22 | "rtc-switchboard": "^3.0.0", 23 | "rtc-switchboard-messenger": "^1.0.0", 24 | "rtc-tools-test": "^2.1.3", 25 | "tap-spec": "^4.0.2", 26 | "travis-multirunner": "^4.3.1", 27 | "uuid": "^2.0.1" 28 | }, 29 | "scripts": { 30 | "hint": "jshint *.js", 31 | "test": "browserify test/all.js | broth start | tap-spec", 32 | "gendocs": "gendocs > README.md" 33 | }, 34 | "main": "index.js", 35 | "directories": { 36 | "test": "test" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/rtc-io/rtc-tools.git" 41 | }, 42 | "keywords": [ 43 | "webrtc", 44 | "rtc.io" 45 | ], 46 | "license": "Apache 2.0", 47 | "bugs": { 48 | "url": "https://github.com/rtc-io/rtc-tools/issues" 49 | }, 50 | "testling": { 51 | "files": "test/all.js", 52 | "server": "test/server.js" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/all.js: -------------------------------------------------------------------------------- 1 | var signaller = require('rtc-signaller'); 2 | var messenger = require('rtc-switchboard-messenger'); 3 | var extend = require('cog/extend'); 4 | 5 | function createSignaller(opts) { 6 | return signaller(messenger(location.origin), opts); 7 | } 8 | 9 | require('rtc-tools-test/')( 10 | require('..'), 11 | createSignaller 12 | ); 13 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | module.exports = require('rtc-tools-test/test/server'); 2 | --------------------------------------------------------------------------------