├── .gitignore ├── LICENSE ├── README.md ├── collaborators.md ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mathias Buus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webrtc-swarm 2 | 3 | > Create a swarm of p2p connections using webrtc and a 4 | [signalhub](https://github.com/mafintosh/signalhub). 5 | 6 | ``` 7 | npm install webrtc-swarm 8 | ``` 9 | 10 | ## Usage 11 | 12 | ``` js 13 | var swarm = require('webrtc-swarm') 14 | var signalhub = require('signalhub') 15 | 16 | var hub = signalhub('swarm-example', ['http://yourdomain.com']) 17 | 18 | var sw = swarm(hub, { 19 | wrtc: require('wrtc') // don't need this if used in the browser 20 | }) 21 | 22 | sw.on('peer', function (peer, id) { 23 | console.log('connected to a new peer:', id) 24 | console.log('total peers:', sw.peers.length) 25 | }) 26 | 27 | sw.on('disconnect', function (peer, id) { 28 | console.log('disconnected from a peer:', id) 29 | console.log('total peers:', sw.peers.length) 30 | }) 31 | ``` 32 | 33 | ## API 34 | 35 | ```js 36 | var swarm = require('webrtc-swarm') 37 | ``` 38 | 39 | ### var sw = swarm(hub, opts) 40 | 41 | Creates a new webrtc swarm using 42 | [signalhub](https://github.com/mafintosh/signalhub) `hub` for discovery and 43 | connection brokering. 44 | 45 | Valid keys for `opts` include: 46 | 47 | - `wrtc` - (optional) a reference to the `wrtc` library, if using Node. 48 | - `uuid` - (optional) a unique identifier for this peer. One is generated for 49 | you if not supplied. 50 | - `maxPeers` - (optional) the maximum number of peers you wish to connect to. 51 | Defaults to unlimited. 52 | - `wrap` - (optional) a function that can modify the WebRTC signaling data 53 | before it gets send out. It's called with `wrap(outgoingSignalingData, 54 | destinationSignalhubChannel)` and must return the wrapped signaling data. 55 | - `unwrap` - (optional) a function that can modify the WebRTC signaling data 56 | before it gets processed. It's called with `unwrap(incomingData, 57 | sourceSignalhubChannel)` and must return the raw signaling data. 58 | 59 | Additional optional keys can be passed through to the underlying 60 | [simple-peer](https://www.npmjs.com/package/simple-peer) instances: 61 | 62 | - `channelConfig` - custom webrtc data channel configuration (used by 63 | `createDataChannel`) 64 | - `config` - custom webrtc configuration (used by `RTCPeerConnection` 65 | constructor) 66 | - `stream` - if video/voice is desired, pass stream returned from 67 | `getUserMedia` 68 | 69 | 70 | ### sw.close() 71 | 72 | Disconnect from swarm 73 | 74 | ### sw.on('peer|connect', peer, id) 75 | 76 | `peer` and `connect` are interchangeable. Fires when a connection has been 77 | established to a new peer `peer`, with unique id `id`. 78 | 79 | ### sw.on('disconnect', peer, id) 80 | 81 | Fires when an existing peer connection is lost. 82 | 83 | `peer` is a [simple-peer](https://www.npmjs.com/package/simple-peer) instance. 84 | 85 | ### sw.on('close') 86 | 87 | Fires when all peer and signalhub connections are closed 88 | 89 | ### sw.peers 90 | 91 | A list of peers that `sw` is currently connected to. 92 | 93 | ### swarm.WEBRTC_SUPPORT 94 | 95 | Detect native WebRTC support in the javascript environment. 96 | 97 | ```js 98 | var swarm = require('webrtc-swarm') 99 | 100 | if (swarm.WEBRTC_SUPPORT) { 101 | // webrtc support! 102 | } else { 103 | // fallback 104 | } 105 | ``` 106 | 107 | ## License 108 | 109 | MIT 110 | -------------------------------------------------------------------------------- /collaborators.md: -------------------------------------------------------------------------------- 1 | ## Collaborators 2 | 3 | webrtc-swarm is only possible due to the excellent work of the following collaborators: 4 | 5 | 6 | 7 |
maxogdenGitHub/maxogden
mafintoshGitHub/mafintosh
8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var SimplePeer = require('simple-peer') 2 | var inherits = require('inherits') 3 | var events = require('events') 4 | var through = require('through2') 5 | var cuid = require('cuid') 6 | var once = require('once') 7 | var debug = require('debug')('webrtc-swarm') 8 | 9 | module.exports = WebRTCSwarm 10 | 11 | function WebRTCSwarm (hub, opts) { 12 | if (!(this instanceof WebRTCSwarm)) return new WebRTCSwarm(hub, opts) 13 | if (!hub) throw new Error('SignalHub instance required') 14 | if (!opts) opts = {} 15 | 16 | events.EventEmitter.call(this) 17 | this.setMaxListeners(0) 18 | 19 | this.hub = hub 20 | this.wrtc = opts.wrtc 21 | this.channelConfig = opts.channelConfig 22 | this.config = opts.config 23 | this.stream = opts.stream 24 | this.wrap = opts.wrap || function (data) { return data } 25 | this.unwrap = opts.unwrap || function (data) { return data } 26 | this.offerConstraints = opts.offerConstraints || {} 27 | this.maxPeers = opts.maxPeers || Infinity 28 | this.me = opts.uuid || cuid() 29 | debug('my uuid:', this.me) 30 | 31 | this.remotes = {} 32 | this.peers = [] 33 | this.closed = false 34 | 35 | subscribe(this, hub) 36 | } 37 | 38 | inherits(WebRTCSwarm, events.EventEmitter) 39 | 40 | WebRTCSwarm.WEBRTC_SUPPORT = SimplePeer.WEBRTC_SUPPORT 41 | 42 | WebRTCSwarm.prototype.close = function (cb) { 43 | if (this.closed) return 44 | this.closed = true 45 | 46 | if (cb) this.once('close', cb) 47 | 48 | var self = this 49 | this.hub.close(function () { 50 | var len = self.peers.length 51 | if (len > 0) { 52 | var closed = 0 53 | self.peers.forEach(function (peer) { 54 | peer.once('close', function () { 55 | if (++closed === len) { 56 | self.emit('close') 57 | } 58 | }) 59 | process.nextTick(function () { 60 | peer.destroy() 61 | }) 62 | }) 63 | } else { 64 | self.emit('close') 65 | } 66 | }) 67 | } 68 | 69 | function setup (swarm, peer, id) { 70 | peer.on('connect', function () { 71 | debug('connected to peer', id) 72 | swarm.peers.push(peer) 73 | swarm.emit('peer', peer, id) 74 | swarm.emit('connect', peer, id) 75 | }) 76 | 77 | var onclose = once(function (err) { 78 | debug('disconnected from peer', id, err) 79 | if (swarm.remotes[id] === peer) delete swarm.remotes[id] 80 | var i = swarm.peers.indexOf(peer) 81 | if (i > -1) swarm.peers.splice(i, 1) 82 | swarm.emit('disconnect', peer, id) 83 | }) 84 | 85 | var signals = [] 86 | var sending = false 87 | 88 | function kick () { 89 | if (swarm.closed || sending || !signals.length) return 90 | sending = true 91 | var data = {from: swarm.me, signal: signals.shift()} 92 | data = swarm.wrap(data, id) 93 | swarm.hub.broadcast(id, data, function () { 94 | sending = false 95 | kick() 96 | }) 97 | } 98 | 99 | peer.on('signal', function (sig) { 100 | signals.push(sig) 101 | kick() 102 | }) 103 | 104 | peer.on('error', onclose) 105 | peer.once('close', onclose) 106 | } 107 | 108 | function subscribe (swarm, hub) { 109 | hub.subscribe('all').pipe(through.obj(function (data, enc, cb) { 110 | data = swarm.unwrap(data, 'all') 111 | if (swarm.closed || !data) return cb() 112 | 113 | debug('/all', data) 114 | if (data.from === swarm.me) { 115 | debug('skipping self', data.from) 116 | return cb() 117 | } 118 | 119 | if (data.type === 'connect') { 120 | if (swarm.peers.length >= swarm.maxPeers) { 121 | debug('skipping because maxPeers is met', data.from) 122 | return cb() 123 | } 124 | if (swarm.remotes[data.from]) { 125 | debug('skipping existing remote', data.from) 126 | return cb() 127 | } 128 | 129 | debug('connecting to new peer (as initiator)', data.from) 130 | var peer = new SimplePeer({ 131 | wrtc: swarm.wrtc, 132 | initiator: true, 133 | channelConfig: swarm.channelConfig, 134 | config: swarm.config, 135 | stream: swarm.stream, 136 | offerConstraints: swarm.offerConstraints 137 | }) 138 | 139 | setup(swarm, peer, data.from) 140 | swarm.remotes[data.from] = peer 141 | } 142 | 143 | cb() 144 | })) 145 | 146 | hub.subscribe(swarm.me).once('open', connect.bind(null, swarm, hub)).pipe(through.obj(function (data, enc, cb) { 147 | data = swarm.unwrap(data, swarm.me) 148 | if (swarm.closed || !data) return cb() 149 | 150 | var peer = swarm.remotes[data.from] 151 | if (!peer) { 152 | if (!data.signal || data.signal.type !== 'offer') { 153 | debug('skipping non-offer', data) 154 | return cb() 155 | } 156 | 157 | debug('connecting to new peer (as not initiator)', data.from) 158 | peer = swarm.remotes[data.from] = new SimplePeer({ 159 | wrtc: swarm.wrtc, 160 | channelConfig: swarm.channelConfig, 161 | config: swarm.config, 162 | stream: swarm.stream, 163 | offerConstraints: swarm.offerConstraints 164 | }) 165 | 166 | setup(swarm, peer, data.from) 167 | } 168 | 169 | debug('signalling', data.from, data.signal) 170 | peer.signal(data.signal) 171 | cb() 172 | })) 173 | } 174 | 175 | function connect (swarm, hub) { 176 | if (swarm.closed || swarm.peers.length >= swarm.maxPeers) return 177 | var data = {type: 'connect', from: swarm.me} 178 | data = swarm.wrap(data, 'all') 179 | hub.broadcast('all', data, function () { 180 | setTimeout(connect.bind(null, swarm, hub), Math.floor(Math.random() * 2000) + (swarm.peers.length ? 13000 : 3000)) 181 | }) 182 | } 183 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webrtc-swarm", 3 | "version": "2.9.0", 4 | "description": "Create a swarm of p2p connections using webrtc and a signalhub", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard && tape test.js" 8 | }, 9 | "dependencies": { 10 | "cuid": "^1.2.4", 11 | "debug": "^2.2.0", 12 | "inherits": "^2.0.1", 13 | "once": "^1.3.1", 14 | "simple-peer": "^6.0.1", 15 | "through2": "^2.0.0" 16 | }, 17 | "devDependencies": { 18 | "electron-webrtc": "^0.2.6", 19 | "signalhub": "^4.7.1", 20 | "standard": "^7.1.2", 21 | "tape": "^4.6.0" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/mafintosh/webrtc-swarm.git" 26 | }, 27 | "author": "Mathias Buus (@mafintosh)", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/mafintosh/webrtc-swarm/issues" 31 | }, 32 | "homepage": "https://github.com/mafintosh/webrtc-swarm" 33 | } 34 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var server = require('signalhub/server')() 2 | var signalhub = require('signalhub') 3 | var swarm = require('./') 4 | var test = require('tape') 5 | var wrtc = require('electron-webrtc')() 6 | 7 | test.onFinish(function () { 8 | server.close() 9 | wrtc.close() 10 | }) 11 | 12 | server.listen(9000, function () { 13 | test('greet and close', function (t) { 14 | t.plan(8) 15 | 16 | var hub1 = signalhub('app', 'localhost:9000') 17 | var hub2 = signalhub('app', 'localhost:9000') 18 | 19 | var sw1 = swarm(hub1, {wrtc}) 20 | var sw2 = swarm(hub2, {wrtc}) 21 | 22 | var hello = 'hello' 23 | var goodbye = 'goodbye' 24 | 25 | var peerIds = {} 26 | 27 | sw1.on('peer', function (peer, id) { 28 | t.pass('connected to peer from sw2') 29 | peerIds.sw2 = id 30 | peer.send(hello) 31 | peer.on('data', function (data) { 32 | t.equal(data.toString(), goodbye, 'goodbye received') 33 | sw1.close(function () { 34 | t.pass('swarm sw1 closed') 35 | }) 36 | }) 37 | }) 38 | 39 | sw2.on('peer', function (peer, id) { 40 | t.pass('connected to peer from sw1') 41 | peerIds.sw1 = id 42 | peer.on('data', function (data) { 43 | t.equal(data.toString(), hello, 'hello received') 44 | peer.send(goodbye) 45 | sw2.close(function () { 46 | t.pass('swarm sw2 closed') 47 | }) 48 | }) 49 | }) 50 | 51 | sw1.on('disconnect', function (peer, id) { 52 | if (id === peerIds.sw2) { 53 | t.pass('connection to peer from sw2 lost') 54 | } 55 | }) 56 | 57 | sw2.on('disconnect', function (peer, id) { 58 | if (id === peerIds.sw1) { 59 | t.pass('connection to peer from sw1 lost') 60 | } 61 | }) 62 | }) 63 | }) 64 | --------------------------------------------------------------------------------