├── .gitignore ├── .npmignore ├── img ├── full-mesh.png └── full-mesh-formula.png ├── .airtap.yml ├── test ├── z-cleanup.js ├── common.js ├── stream.js ├── binary.js ├── negotiation.js ├── object-mode.js ├── trickle.js ├── multiplex.js ├── basic.js ├── multistream.js └── datachannel.js ├── perf ├── server.js ├── send.js └── receive.js ├── .travis.yml ├── LICENSE ├── package.json ├── datachannel.js ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | node_modules 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .airtap.yml 2 | .nyc_output 3 | .travis.yml 4 | img/ 5 | perf/ 6 | test/ 7 | -------------------------------------------------------------------------------- /img/full-mesh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hr/simple-peer/master/img/full-mesh.png -------------------------------------------------------------------------------- /img/full-mesh-formula.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hr/simple-peer/master/img/full-mesh-formula.png -------------------------------------------------------------------------------- /.airtap.yml: -------------------------------------------------------------------------------- 1 | sauce_connect: true 2 | loopback: airtap.local 3 | browsers: 4 | - name: firefox 5 | version: latest 6 | - name: chrome 7 | version: latest 8 | - name: safari 9 | version: latest 10 | - name: iphone 11 | version: latest 12 | -------------------------------------------------------------------------------- /test/z-cleanup.js: -------------------------------------------------------------------------------- 1 | // This test file runs after all the others. This is where we can run the cleanup 2 | // code that is required 3 | 4 | var test = require('tape') 5 | 6 | test('cleanup', function (t) { 7 | // Shut down the process and any daemons 8 | t.end() 9 | if (process && process.exit) { 10 | process.exit(0) 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /perf/server.js: -------------------------------------------------------------------------------- 1 | // run in a terminal, to do signaling for peers 2 | 3 | var ws = require('ws') 4 | 5 | var server = new ws.Server({ 6 | port: 8080 7 | }) 8 | 9 | var sockets = [] 10 | 11 | server.on('connection', function (socket) { 12 | sockets.push(socket) 13 | socket.on('message', onMessage) 14 | socket.on('close', function () { 15 | sockets.splice(sockets.indexOf(socket), 1) 16 | }) 17 | 18 | function onMessage (message) { 19 | sockets 20 | .filter(s => s !== socket) 21 | .forEach(socket => socket.send(message)) 22 | } 23 | 24 | if (sockets.length === 2) { 25 | sockets.forEach(socket => socket.send('ready')) 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - lts/* 5 | addons: 6 | apt: 7 | sources: 8 | - ubuntu-toolchain-r-test 9 | packages: 10 | - g++-4.8 11 | sauce_connect: true 12 | hosts: 13 | - airtap.local 14 | env: 15 | global: 16 | - CXX=g++-4.8 17 | - secure: LqHyGcEyVXqymrZ77atyvoTthiE9ehXaxHjqLWFpIGNBdGBlASJNG42lhTR8YsM2MiIbzhslxLudyy2r4IljwbRV3/AB1mEf8J3lu1S3v5L/P1a6OeJOQWR3Nl50Z518rRFUermFlSjsdDEZp/i/o+KRz6VNAgwS31j6fvV8tdw= 18 | - secure: jEGOmkQelLxLeBQnHxpJN9G/TmGLbUNY+qyzVSV2ImC+EJ1QXRv03ynHO6WxZt0RPbHcOgbY3pA8gpPKA0uQagkVHNkBneJeeqJBD9kSywJpdfhzvD42ZsEWkUAQF3T8DOuSOUS0PR2/h4QpMmFmz9wTgb4Y1QLd47oMSglnIs8= 19 | after_success: npm run coverage -------------------------------------------------------------------------------- /perf/send.js: -------------------------------------------------------------------------------- 1 | // run in a browser, with: 2 | // beefy perf/send.js 3 | 4 | var Peer = require('simple-peer') 5 | var stream = require('readable-stream') 6 | 7 | var buf = Buffer.alloc(10000) 8 | 9 | var endless = new stream.Readable({ 10 | read: function () { 11 | this.push(buf) 12 | } 13 | }) 14 | 15 | var peer 16 | 17 | var socket = new window.WebSocket('ws://localhost:8080') 18 | 19 | socket.addEventListener('message', onMessage) 20 | 21 | function onMessage (event) { 22 | var message = event.data 23 | if (message === 'ready') { 24 | if (peer) return 25 | peer = new Peer({ initiator: true }) 26 | peer.on('signal', function (signal) { 27 | socket.send(JSON.stringify(signal)) 28 | }) 29 | peer.on('connect', function () { 30 | endless.pipe(peer) 31 | }) 32 | } else { 33 | peer.signal(JSON.parse(message)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /perf/receive.js: -------------------------------------------------------------------------------- 1 | // run in a browser and look at console for speed 2 | // beefy perf/receive.js 3 | 4 | // 7.6MB 5 | 6 | var prettierBytes = require('prettier-bytes') 7 | var speedometer = require('speedometer') 8 | var Peer = require('simple-peer') 9 | 10 | var speed = speedometer() 11 | 12 | var peer 13 | 14 | var socket = new window.WebSocket('ws://localhost:8080') 15 | 16 | socket.addEventListener('message', onMessage) 17 | 18 | function onMessage (event) { 19 | var message = event.data 20 | if (message === 'ready') { 21 | if (peer) return 22 | peer = new Peer() 23 | peer.on('signal', function (signal) { 24 | socket.send(JSON.stringify(signal)) 25 | }) 26 | peer.on('data', function (message) { 27 | speed(message.length) 28 | }) 29 | } else { 30 | peer.signal(JSON.parse(message)) 31 | } 32 | } 33 | 34 | setInterval(function () { 35 | console.log(prettierBytes(speed())) 36 | }, 1000) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Feross Aboukhadijeh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | var get = require('simple-get') 2 | var thunky = require('thunky') 3 | 4 | exports.getConfig = thunky(function (cb) { 5 | // Includes TURN -- needed for tests to pass on Sauce Labs 6 | // https://github.com/feross/simple-peer/issues/41 7 | // WARNING: This is *NOT* a public endpoint. Do not depend on it in your app. 8 | get.concat('https://instant.io/__rtcConfig__', function (err, res, data) { 9 | if (err) return cb(err) 10 | data = data.toString() 11 | try { 12 | data = JSON.parse(data) 13 | } catch (err) { 14 | cb(err) 15 | return 16 | } 17 | cb(null, data) 18 | }) 19 | }) 20 | 21 | // For testing on node, we must provide a WebRTC implementation 22 | if (process.env.WRTC === 'wrtc') { 23 | exports.wrtc = require('wrtc') 24 | } 25 | 26 | // create a test MediaStream with two tracks 27 | var canvas 28 | exports.getMediaStream = function () { 29 | if (exports.wrtc) { 30 | const source = new exports.wrtc.nonstandard.RTCVideoSource() 31 | const tracks = [source.createTrack(), source.createTrack()] 32 | return new exports.wrtc.MediaStream(tracks) 33 | } else { 34 | if (!canvas) { 35 | canvas = document.createElement('canvas') 36 | canvas.width = canvas.height = 100 37 | canvas.getContext('2d') // initialize canvas 38 | } 39 | const stream = canvas.captureStream(30) 40 | stream.addTrack(stream.getTracks()[0].clone()) // should have 2 tracks 41 | return stream 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-peer", 3 | "description": "Simple one-to-one WebRTC video/voice and data channels", 4 | "version": "9.3.0", 5 | "author": { 6 | "name": "Feross Aboukhadijeh", 7 | "email": "feross@feross.org", 8 | "url": "http://feross.org/" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/feross/simple-peer/issues" 12 | }, 13 | "dependencies": { 14 | "debug": "^4.1.1", 15 | "get-browser-rtc": "^1.0.2", 16 | "inherits": "^2.0.4", 17 | "randombytes": "^2.1.0", 18 | "readable-stream": "^2.3.6" 19 | }, 20 | "devDependencies": { 21 | "airtap": "1.0.0", 22 | "babel-minify": "^0.5.0", 23 | "bowser": "^1.9.2", 24 | "browserify": "^16.1.0", 25 | "nyc": "^13.1.0", 26 | "prettier-bytes": "^1.0.3", 27 | "simple-get": "^3.0.1", 28 | "speedometer": "^1.0.0", 29 | "standard": "*", 30 | "string-to-stream": "^1.0.0", 31 | "tape": "^4.0.0", 32 | "through2": "^3.0.0", 33 | "thunky": "^1.0.1", 34 | "wrtc": "^0.3.6", 35 | "ws": "^6.0.0" 36 | }, 37 | "keywords": [ 38 | "data", 39 | "data channel", 40 | "data channel stream", 41 | "data channels", 42 | "p2p", 43 | "peer", 44 | "peer", 45 | "peer-to-peer", 46 | "stream", 47 | "video", 48 | "voice", 49 | "webrtc", 50 | "webrtc stream" 51 | ], 52 | "license": "MIT", 53 | "main": "index.js", 54 | "repository": { 55 | "type": "git", 56 | "url": "git://github.com/feross/simple-peer.git" 57 | }, 58 | "scripts": { 59 | "build": "browserify -s SimplePeer -r ./ | minify > simplepeer.min.js", 60 | "size": "npm run build && cat simplepeer.min.js | gzip | wc -c", 61 | "test": "standard && npm run test-node && npm run test-browser", 62 | "test-browser": "airtap --coverage -- test/*.js", 63 | "test-browser-local": "airtap --coverage --local -- test/*.js", 64 | "test-node": "WRTC=wrtc tape test/*.js", 65 | "test-coverage": "nyc report --reporter=html" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/stream.js: -------------------------------------------------------------------------------- 1 | var common = require('./common') 2 | var Peer = require('../') 3 | var str = require('string-to-stream') 4 | var test = require('tape') 5 | 6 | var config 7 | test('get config', function (t) { 8 | common.getConfig(function (err, _config) { 9 | if (err) return t.fail(err) 10 | config = _config 11 | t.end() 12 | }) 13 | }) 14 | 15 | test('duplex stream: send data before "connect" event', function (t) { 16 | t.plan(9) 17 | t.timeoutAfter(20000) 18 | 19 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 20 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 21 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 22 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 23 | 24 | str('abc').pipe(peer1) 25 | 26 | peer1.on('data', function () { 27 | t.fail('peer1 should not get data') 28 | }) 29 | peer1.on('finish', function () { 30 | t.pass('got peer1 "finish"') 31 | t.ok(peer1._writableState.finished) 32 | }) 33 | peer1.on('end', function () { 34 | t.pass('got peer1 "end"') 35 | t.ok(peer1._readableState.ended) 36 | }) 37 | 38 | peer2.on('data', function (chunk) { 39 | t.equal(chunk.toString(), 'abc', 'got correct message') 40 | }) 41 | peer2.on('finish', function () { 42 | t.pass('got peer2 "finish"') 43 | t.ok(peer2._writableState.finished) 44 | }) 45 | peer2.on('end', function () { 46 | t.pass('got peer2 "end"') 47 | t.ok(peer2._readableState.ended) 48 | }) 49 | }) 50 | 51 | test('duplex stream: send data one-way', function (t) { 52 | t.plan(9) 53 | t.timeoutAfter(20000) 54 | 55 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 56 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 57 | peer1.on('signal', function (data) { peer2.signal(data) }) 58 | peer2.on('signal', function (data) { peer1.signal(data) }) 59 | peer1.on('connect', tryTest) 60 | peer2.on('connect', tryTest) 61 | 62 | function tryTest () { 63 | if (!peer1.connected || !peer2.connected) return 64 | 65 | peer1.on('data', function () { 66 | t.fail('peer1 should not get data') 67 | }) 68 | peer1.on('finish', function () { 69 | t.pass('got peer1 "finish"') 70 | t.ok(peer1._writableState.finished) 71 | }) 72 | peer1.on('end', function () { 73 | t.pass('got peer1 "end"') 74 | t.ok(peer1._readableState.ended) 75 | }) 76 | 77 | peer2.on('data', function (chunk) { 78 | t.equal(chunk.toString(), 'abc', 'got correct message') 79 | }) 80 | peer2.on('finish', function () { 81 | t.pass('got peer2 "finish"') 82 | t.ok(peer2._writableState.finished) 83 | }) 84 | peer2.on('end', function () { 85 | t.pass('got peer2 "end"') 86 | t.ok(peer2._readableState.ended) 87 | }) 88 | 89 | str('abc').pipe(peer1) 90 | } 91 | }) 92 | -------------------------------------------------------------------------------- /test/binary.js: -------------------------------------------------------------------------------- 1 | var common = require('./common') 2 | var Peer = require('../') 3 | var test = require('tape') 4 | 5 | var config 6 | test('get config', function (t) { 7 | common.getConfig(function (err, _config) { 8 | if (err) return t.fail(err) 9 | config = _config 10 | t.end() 11 | }) 12 | }) 13 | 14 | test('data send/receive Buffer', function (t) { 15 | t.plan(6) 16 | 17 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 18 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 19 | peer1.on('signal', function (data) { 20 | peer2.signal(data) 21 | }) 22 | peer2.on('signal', function (data) { 23 | peer1.signal(data) 24 | }) 25 | peer1.on('connect', tryTest) 26 | peer2.on('connect', tryTest) 27 | 28 | function tryTest () { 29 | if (!peer1.connected || !peer2.connected) return 30 | 31 | peer1.send(Buffer.from([0, 1, 2])) 32 | peer2.on('data', function (data) { 33 | t.ok(Buffer.isBuffer(data), 'data is Buffer') 34 | t.deepEqual(data, Buffer.from([0, 1, 2]), 'got correct message') 35 | 36 | peer2.send(Buffer.from([0, 2, 4])) 37 | peer1.on('data', function (data) { 38 | t.ok(Buffer.isBuffer(data), 'data is Buffer') 39 | t.deepEqual(data, Buffer.from([0, 2, 4]), 'got correct message') 40 | 41 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 42 | peer1.destroy() 43 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 44 | peer2.destroy() 45 | }) 46 | }) 47 | } 48 | }) 49 | 50 | test('data send/receive Uint8Array', function (t) { 51 | t.plan(6) 52 | 53 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 54 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 55 | peer1.on('signal', function (data) { 56 | peer2.signal(data) 57 | }) 58 | peer2.on('signal', function (data) { 59 | peer1.signal(data) 60 | }) 61 | peer1.on('connect', tryTest) 62 | peer2.on('connect', tryTest) 63 | 64 | function tryTest () { 65 | if (!peer1.connected || !peer2.connected) return 66 | 67 | peer1.send(new Uint8Array([0, 1, 2])) 68 | peer2.on('data', function (data) { 69 | // binary types always get converted to Buffer 70 | // See: https://github.com/feross/simple-peer/issues/138#issuecomment-278240571 71 | t.ok(Buffer.isBuffer(data), 'data is Buffer') 72 | t.deepEqual(data, Buffer.from([0, 1, 2]), 'got correct message') 73 | 74 | peer2.send(new Uint8Array([0, 2, 4])) 75 | peer1.on('data', function (data) { 76 | t.ok(Buffer.isBuffer(data), 'data is Buffer') 77 | t.deepEqual(data, Buffer.from([0, 2, 4]), 'got correct message') 78 | 79 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 80 | peer1.destroy() 81 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 82 | peer2.destroy() 83 | }) 84 | }) 85 | } 86 | }) 87 | 88 | test('data send/receive ArrayBuffer', function (t) { 89 | t.plan(6) 90 | 91 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 92 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 93 | peer1.on('signal', function (data) { 94 | peer2.signal(data) 95 | }) 96 | peer2.on('signal', function (data) { 97 | peer1.signal(data) 98 | }) 99 | peer1.on('connect', tryTest) 100 | peer2.on('connect', tryTest) 101 | 102 | function tryTest () { 103 | if (!peer1.connected || !peer2.connected) return 104 | 105 | peer1.send(new Uint8Array([0, 1, 2]).buffer) 106 | peer2.on('data', function (data) { 107 | t.ok(Buffer.isBuffer(data), 'data is Buffer') 108 | t.deepEqual(data, Buffer.from([0, 1, 2]), 'got correct message') 109 | 110 | peer2.send(new Uint8Array([0, 2, 4]).buffer) 111 | peer1.on('data', function (data) { 112 | t.ok(Buffer.isBuffer(data), 'data is Buffer') 113 | t.deepEqual(data, Buffer.from([0, 2, 4]), 'got correct message') 114 | 115 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 116 | peer1.destroy() 117 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 118 | peer2.destroy() 119 | }) 120 | }) 121 | } 122 | }) 123 | -------------------------------------------------------------------------------- /test/negotiation.js: -------------------------------------------------------------------------------- 1 | var common = require('./common') 2 | var Peer = require('../') 3 | var test = require('tape') 4 | 5 | var config 6 | test('get config', function (t) { 7 | common.getConfig(function (err, _config) { 8 | if (err) return t.fail(err) 9 | config = _config 10 | t.end() 11 | }) 12 | }) 13 | 14 | test('single negotiation', function (t) { 15 | t.plan(10) 16 | 17 | var peer1 = new Peer({ config: config, initiator: true, stream: common.getMediaStream(), wrtc: common.wrtc }) 18 | var peer2 = new Peer({ config: config, stream: common.getMediaStream(), wrtc: common.wrtc }) 19 | 20 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 21 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 22 | 23 | peer1.on('connect', function () { 24 | t.pass('peer1 connected') 25 | }) 26 | peer2.on('connect', function () { 27 | t.pass('peer2 connected') 28 | }) 29 | 30 | peer1.on('stream', function (stream) { 31 | t.pass('peer1 got stream') 32 | }) 33 | peer2.on('stream', function (stream) { 34 | t.pass('peer2 got stream') 35 | }) 36 | 37 | var trackCount1 = 0 38 | peer1.on('track', function (track) { 39 | t.pass('peer1 got track') 40 | trackCount1++ 41 | if (trackCount1 >= 2) { 42 | t.pass('got correct number of tracks') 43 | } 44 | }) 45 | var trackCount2 = 0 46 | peer2.on('track', function (track) { 47 | t.pass('peer2 got track') 48 | trackCount2++ 49 | if (trackCount2 >= 2) { 50 | t.pass('got correct number of tracks') 51 | } 52 | }) 53 | }) 54 | 55 | test('manual renegotiation', function (t) { 56 | t.plan(2) 57 | 58 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 59 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 60 | 61 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 62 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 63 | 64 | peer1.on('connect', function () { 65 | peer1.negotiate() 66 | 67 | peer1.on('negotiate', function () { 68 | t.pass('peer1 negotiated') 69 | }) 70 | peer2.on('negotiate', function () { 71 | t.pass('peer2 negotiated') 72 | }) 73 | }) 74 | }) 75 | 76 | test('repeated manual renegotiation', function (t) { 77 | t.plan(6) 78 | 79 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 80 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 81 | 82 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 83 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 84 | 85 | peer1.once('connect', function () { 86 | peer1.negotiate() 87 | }) 88 | peer1.once('negotiate', function () { 89 | t.pass('peer1 negotiated') 90 | peer1.negotiate() 91 | peer1.once('negotiate', function () { 92 | t.pass('peer1 negotiated again') 93 | peer1.negotiate() 94 | peer1.once('negotiate', function () { 95 | t.pass('peer1 negotiated again') 96 | }) 97 | }) 98 | }) 99 | peer2.once('negotiate', function () { 100 | t.pass('peer2 negotiated') 101 | peer2.negotiate() 102 | peer2.once('negotiate', function () { 103 | t.pass('peer2 negotiated again') 104 | peer1.negotiate() 105 | peer1.once('negotiate', function () { 106 | t.pass('peer1 negotiated again') 107 | }) 108 | }) 109 | }) 110 | }) 111 | 112 | test('renegotiation after addStream', function (t) { 113 | t.plan(4) 114 | 115 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 116 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 117 | 118 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 119 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 120 | 121 | peer1.on('connect', function () { 122 | t.pass('peer1 connect') 123 | peer1.addStream(common.getMediaStream()) 124 | }) 125 | peer2.on('connect', function () { 126 | t.pass('peer2 connect') 127 | peer2.addStream(common.getMediaStream()) 128 | }) 129 | peer1.on('stream', function () { 130 | t.pass('peer1 got stream') 131 | }) 132 | peer2.on('stream', function () { 133 | t.pass('peer2 got stream') 134 | }) 135 | }) 136 | 137 | test('add stream on non-initiator only', function (t) { 138 | t.plan(3) 139 | 140 | var peer1 = new Peer({ 141 | config: config, 142 | initiator: true, 143 | wrtc: common.wrtc 144 | }) 145 | var peer2 = new Peer({ 146 | config: config, 147 | wrtc: common.wrtc, 148 | stream: common.getMediaStream() 149 | }) 150 | 151 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 152 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 153 | 154 | peer1.on('connect', function () { 155 | t.pass('peer1 connect') 156 | }) 157 | peer2.on('connect', function () { 158 | t.pass('peer2 connect') 159 | }) 160 | peer1.on('stream', function () { 161 | t.pass('peer1 got stream') 162 | }) 163 | }) 164 | -------------------------------------------------------------------------------- /test/object-mode.js: -------------------------------------------------------------------------------- 1 | var common = require('./common') 2 | var Peer = require('../') 3 | var test = require('tape') 4 | 5 | var config 6 | test('get config', function (t) { 7 | common.getConfig(function (err, _config) { 8 | if (err) return t.fail(err) 9 | config = _config 10 | t.end() 11 | }) 12 | }) 13 | 14 | test('data send/receive string {objectMode: true}', function (t) { 15 | t.plan(6) 16 | 17 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc, objectMode: true }) 18 | var peer2 = new Peer({ config: config, wrtc: common.wrtc, objectMode: true }) 19 | peer1.on('signal', function (data) { 20 | peer2.signal(data) 21 | }) 22 | peer2.on('signal', function (data) { 23 | peer1.signal(data) 24 | }) 25 | peer1.on('connect', tryTest) 26 | peer2.on('connect', tryTest) 27 | 28 | function tryTest () { 29 | if (!peer1.connected || !peer2.connected) return 30 | 31 | peer1.send('this is a string') 32 | peer2.on('data', function (data) { 33 | t.equal(typeof data, 'string', 'data is a string') 34 | t.equal(data, 'this is a string', 'got correct message') 35 | 36 | peer2.send('this is another string') 37 | peer1.on('data', function (data) { 38 | t.equal(typeof data, 'string', 'data is a string') 39 | t.equal(data, 'this is another string', 'got correct message') 40 | 41 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 42 | peer1.destroy() 43 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 44 | peer2.destroy() 45 | }) 46 | }) 47 | } 48 | }) 49 | 50 | test('data send/receive Buffer {objectMode: true}', function (t) { 51 | t.plan(6) 52 | 53 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc, objectMode: true }) 54 | var peer2 = new Peer({ config: config, wrtc: common.wrtc, objectMode: true }) 55 | peer1.on('signal', function (data) { 56 | peer2.signal(data) 57 | }) 58 | peer2.on('signal', function (data) { 59 | peer1.signal(data) 60 | }) 61 | peer1.on('connect', tryTest) 62 | peer2.on('connect', tryTest) 63 | 64 | function tryTest () { 65 | if (!peer1.connected || !peer2.connected) return 66 | 67 | peer1.send(Buffer.from('this is a Buffer')) 68 | peer2.on('data', function (data) { 69 | t.ok(Buffer.isBuffer(data), 'data is a Buffer') 70 | t.deepEqual(data, Buffer.from('this is a Buffer'), 'got correct message') 71 | 72 | peer2.send(Buffer.from('this is another Buffer')) 73 | peer1.on('data', function (data) { 74 | t.ok(Buffer.isBuffer(data), 'data is a Buffer') 75 | t.deepEqual(data, Buffer.from('this is another Buffer'), 'got correct message') 76 | 77 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 78 | peer1.destroy() 79 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 80 | peer2.destroy() 81 | }) 82 | }) 83 | } 84 | }) 85 | 86 | test('data send/receive Uint8Array {objectMode: true}', function (t) { 87 | t.plan(6) 88 | 89 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc, objectMode: true }) 90 | var peer2 = new Peer({ config: config, wrtc: common.wrtc, objectMode: true }) 91 | peer1.on('signal', function (data) { 92 | peer2.signal(data) 93 | }) 94 | peer2.on('signal', function (data) { 95 | peer1.signal(data) 96 | }) 97 | peer1.on('connect', tryTest) 98 | peer2.on('connect', tryTest) 99 | 100 | function tryTest () { 101 | if (!peer1.connected || !peer2.connected) return 102 | 103 | peer1.send(new Uint8Array([0, 1, 2])) 104 | peer2.on('data', function (data) { 105 | // binary types always get converted to Buffer 106 | // See: https://github.com/feross/simple-peer/issues/138#issuecomment-278240571 107 | t.ok(Buffer.isBuffer(data), 'data is a Buffer') 108 | t.deepEqual(data, Buffer.from([0, 1, 2]), 'got correct message') 109 | 110 | peer2.send(new Uint8Array([1, 2, 3])) 111 | peer1.on('data', function (data) { 112 | t.ok(Buffer.isBuffer(data), 'data is a Buffer') 113 | t.deepEqual(data, Buffer.from([1, 2, 3]), 'got correct message') 114 | 115 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 116 | peer1.destroy() 117 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 118 | peer2.destroy() 119 | }) 120 | }) 121 | } 122 | }) 123 | 124 | test('data send/receive ArrayBuffer {objectMode: true}', function (t) { 125 | t.plan(6) 126 | 127 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc, objectMode: true }) 128 | var peer2 = new Peer({ config: config, wrtc: common.wrtc, objectMode: true }) 129 | peer1.on('signal', function (data) { 130 | peer2.signal(data) 131 | }) 132 | peer2.on('signal', function (data) { 133 | peer1.signal(data) 134 | }) 135 | peer1.on('connect', tryTest) 136 | peer2.on('connect', tryTest) 137 | 138 | function tryTest () { 139 | if (!peer1.connected || !peer2.connected) return 140 | 141 | peer1.send(new Uint8Array([0, 1, 2]).buffer) 142 | peer2.on('data', function (data) { 143 | t.ok(Buffer.isBuffer(data), 'data is a Buffer') 144 | t.deepEqual(data, Buffer.from([0, 1, 2]), 'got correct message') 145 | 146 | peer2.send(new Uint8Array([1, 2, 3]).buffer) 147 | peer1.on('data', function (data) { 148 | t.ok(Buffer.isBuffer(data), 'data is a Buffer') 149 | t.deepEqual(data, Buffer.from([1, 2, 3]), 'got correct message') 150 | 151 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 152 | peer1.destroy() 153 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 154 | peer2.destroy() 155 | }) 156 | }) 157 | } 158 | }) 159 | -------------------------------------------------------------------------------- /test/trickle.js: -------------------------------------------------------------------------------- 1 | var common = require('./common') 2 | var Peer = require('../') 3 | var test = require('tape') 4 | 5 | var config 6 | test('get config', function (t) { 7 | common.getConfig(function (err, _config) { 8 | if (err) return t.fail(err) 9 | config = _config 10 | t.end() 11 | }) 12 | }) 13 | 14 | test('disable trickle', function (t) { 15 | t.plan(8) 16 | 17 | var peer1 = new Peer({ config: config, initiator: true, trickle: false, wrtc: common.wrtc }) 18 | var peer2 = new Peer({ config: config, trickle: false, wrtc: common.wrtc }) 19 | 20 | var numSignal1 = 0 21 | peer1.on('signal', function (data) { 22 | numSignal1 += 1 23 | peer2.signal(data) 24 | }) 25 | 26 | var numSignal2 = 0 27 | peer2.on('signal', function (data) { 28 | numSignal2 += 1 29 | peer1.signal(data) 30 | }) 31 | 32 | peer1.on('connect', tryTest) 33 | peer2.on('connect', tryTest) 34 | 35 | function tryTest () { 36 | if (!peer1.connected || !peer2.connected) return 37 | 38 | t.equal(numSignal1, 1, 'only one `signal` event') 39 | t.equal(numSignal2, 1, 'only one `signal` event') 40 | t.equal(peer1.initiator, true, 'peer1 is initiator') 41 | t.equal(peer2.initiator, false, 'peer2 is not initiator') 42 | 43 | peer1.send('sup peer2') 44 | peer2.on('data', function (data) { 45 | t.equal(data.toString(), 'sup peer2', 'got correct message') 46 | 47 | peer2.send('sup peer1') 48 | peer1.on('data', function (data) { 49 | t.equal(data.toString(), 'sup peer1', 'got correct message') 50 | 51 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 52 | peer1.destroy() 53 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 54 | peer2.destroy() 55 | }) 56 | }) 57 | } 58 | }) 59 | 60 | test('disable trickle (only initiator)', function (t) { 61 | t.plan(8) 62 | 63 | var peer1 = new Peer({ config: config, initiator: true, trickle: false, wrtc: common.wrtc }) 64 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 65 | 66 | var numSignal1 = 0 67 | peer1.on('signal', function (data) { 68 | numSignal1 += 1 69 | peer2.signal(data) 70 | }) 71 | 72 | var numSignal2 = 0 73 | peer2.on('signal', function (data) { 74 | numSignal2 += 1 75 | peer1.signal(data) 76 | }) 77 | 78 | peer1.on('connect', tryTest) 79 | peer2.on('connect', tryTest) 80 | 81 | function tryTest () { 82 | if (!peer1.connected || !peer2.connected) return 83 | 84 | t.equal(numSignal1, 1, 'only one `signal` event for initiator') 85 | t.ok(numSignal2 >= 1, 'at least one `signal` event for receiver') 86 | t.equal(peer1.initiator, true, 'peer1 is initiator') 87 | t.equal(peer2.initiator, false, 'peer2 is not initiator') 88 | 89 | peer1.send('sup peer2') 90 | peer2.on('data', function (data) { 91 | t.equal(data.toString(), 'sup peer2', 'got correct message') 92 | 93 | peer2.send('sup peer1') 94 | peer1.on('data', function (data) { 95 | t.equal(data.toString(), 'sup peer1', 'got correct message') 96 | 97 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 98 | peer1.destroy() 99 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 100 | peer2.destroy() 101 | }) 102 | }) 103 | } 104 | }) 105 | 106 | test('disable trickle (only receiver)', function (t) { 107 | t.plan(8) 108 | 109 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 110 | var peer2 = new Peer({ config: config, trickle: false, wrtc: common.wrtc }) 111 | 112 | var numSignal1 = 0 113 | peer1.on('signal', function (data) { 114 | numSignal1 += 1 115 | peer2.signal(data) 116 | }) 117 | 118 | var numSignal2 = 0 119 | peer2.on('signal', function (data) { 120 | numSignal2 += 1 121 | peer1.signal(data) 122 | }) 123 | 124 | peer1.on('connect', tryTest) 125 | peer2.on('connect', tryTest) 126 | 127 | function tryTest () { 128 | if (!peer1.connected || !peer2.connected) return 129 | 130 | t.ok(numSignal1 >= 1, 'at least one `signal` event for initiator') 131 | t.equal(numSignal2, 1, 'only one `signal` event for receiver') 132 | t.equal(peer1.initiator, true, 'peer1 is initiator') 133 | t.equal(peer2.initiator, false, 'peer2 is not initiator') 134 | 135 | peer1.send('sup peer2') 136 | peer2.on('data', function (data) { 137 | t.equal(data.toString(), 'sup peer2', 'got correct message') 138 | 139 | peer2.send('sup peer1') 140 | peer1.on('data', function (data) { 141 | t.equal(data.toString(), 'sup peer1', 'got correct message') 142 | 143 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 144 | peer1.destroy() 145 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 146 | peer2.destroy() 147 | }) 148 | }) 149 | } 150 | }) 151 | 152 | test('ice candidates received before description', function (t) { 153 | t.plan(3) 154 | 155 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 156 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 157 | 158 | var signalQueue1 = [] 159 | peer1.on('signal', function (data) { 160 | signalQueue1.push(data) 161 | if (data.candidate) { 162 | while (signalQueue1[0]) peer2.signal(signalQueue1.pop()) 163 | } 164 | }) 165 | 166 | var signalQueue2 = [] 167 | peer2.on('signal', function (data) { 168 | signalQueue2.push(data) 169 | if (data.candidate) { 170 | while (signalQueue2[0]) peer1.signal(signalQueue2.pop()) 171 | } 172 | }) 173 | 174 | peer1.on('connect', function () { 175 | t.pass('peers connected') 176 | 177 | peer2.on('connect', function () { 178 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 179 | peer1.destroy() 180 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 181 | peer2.destroy() 182 | }) 183 | }) 184 | }) 185 | -------------------------------------------------------------------------------- /test/multiplex.js: -------------------------------------------------------------------------------- 1 | // multiplexing tests adapted from https://github.com/maxogden/multiplex/blob/master/test.js 2 | 3 | var test = require('tape') 4 | var common = require('./common') 5 | var concat = require('concat-stream') 6 | var through = require('through2') 7 | var Peer = require('../') 8 | var Buffer = require('safe-buffer').Buffer 9 | 10 | var config 11 | test('get config', function (t) { 12 | common.getConfig(function (err, _config) { 13 | if (err) return t.fail(err) 14 | config = _config 15 | t.end() 16 | }) 17 | }) 18 | 19 | test('one way piping work with 2 sub-streams', function (t) { 20 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 21 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 22 | 23 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 24 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 25 | 26 | var stream1 = peer1.createDataChannel() 27 | var stream2 = peer1.createDataChannel() 28 | 29 | peer2.on('datachannel', function onStream (stream) { 30 | stream.pipe(collect()) 31 | }) 32 | 33 | stream1.write(Buffer.from('hello')) 34 | stream2.write(Buffer.from('world')) 35 | stream1.end() 36 | stream2.end() 37 | 38 | var pending = 2 39 | var results = [] 40 | 41 | function collect () { 42 | return concat(function (data) { 43 | results.push(data.toString()) 44 | if (--pending === 0) { 45 | results.sort() 46 | t.equal(results[0].toString(), 'hello') 47 | t.equal(results[1].toString(), 'world') 48 | t.end() 49 | 50 | peer1.destroy() 51 | peer2.destroy() 52 | } 53 | }) 54 | } 55 | }) 56 | 57 | test('two way piping works with 2 sub-streams', function (t) { 58 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 59 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 60 | 61 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 62 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 63 | 64 | peer2.on('datachannel', function onStream (stream) { 65 | var uppercaser = through(function (chunk, _, done) { 66 | this.push(Buffer.from(chunk.toString().toUpperCase())) 67 | this.end() 68 | done() 69 | }) 70 | stream.pipe(uppercaser).pipe(stream) 71 | }) 72 | 73 | var stream1 = peer1.createDataChannel() 74 | var stream2 = peer1.createDataChannel() 75 | 76 | stream1.pipe(collect()) 77 | stream2.pipe(collect()) 78 | 79 | stream1.write(Buffer.from('hello')) 80 | stream2.write(Buffer.from('world')) 81 | 82 | var pending = 2 83 | var results = [] 84 | 85 | function collect () { 86 | return concat(function (data) { 87 | results.push(data.toString()) 88 | if (--pending === 0) { 89 | results.sort() 90 | t.equal(results[0].toString(), 'HELLO') 91 | t.equal(results[1].toString(), 'WORLD') 92 | t.end() 93 | 94 | peer1.destroy() 95 | peer2.destroy() 96 | } 97 | }) 98 | } 99 | }) 100 | 101 | test('channelName should be exposed as channel.channelName', function (t) { 102 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 103 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 104 | 105 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 106 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 107 | 108 | var stream1 = peer1.createDataChannel('5') 109 | t.equal(stream1.channelName, '5') 110 | 111 | peer2.on('datachannel', function onStream (stream, channelName) { 112 | t.equal(stream.channelName, '5') 113 | t.equal(channelName, '5') 114 | t.end() 115 | 116 | peer1.destroy() 117 | peer2.destroy() 118 | }) 119 | 120 | stream1.write(Buffer.from('hello')) 121 | stream1.end() 122 | }) 123 | 124 | test('channelName can be a long string', function (t) { 125 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 126 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 127 | 128 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 129 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 130 | 131 | var stream1 = peer1.createDataChannel('hello-yes-this-is-dog') 132 | t.equal(stream1.channelName, 'hello-yes-this-is-dog') 133 | 134 | peer2.on('datachannel', function onStream (stream, id) { 135 | t.equal(stream.channelName, 'hello-yes-this-is-dog') 136 | t.equal(id, 'hello-yes-this-is-dog') 137 | t.end() 138 | 139 | peer1.destroy() 140 | peer2.destroy() 141 | }) 142 | 143 | stream1.write(Buffer.from('hello')) 144 | stream1.end() 145 | }) 146 | 147 | test('destroy', function (t) { 148 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 149 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 150 | 151 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 152 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 153 | 154 | var stream1 = peer1.createDataChannel('1') 155 | 156 | // we listen on the local stream instead of the remote stream here: 157 | // there's no way to propagate error messages across datachannels like multiplex does 158 | stream1.on('error', function (err) { 159 | t.equal(err.message, '0 had an error') 160 | t.end() 161 | 162 | peer1.destroy() 163 | peer2.destroy() 164 | }) 165 | 166 | stream1.write(Buffer.from('hello')) 167 | stream1.destroy(new Error('0 had an error')) 168 | }) 169 | 170 | test('testing invalid data error', function (t) { 171 | t.pass('skipping test, simple-peer does not have similar data restrictions') 172 | t.end() 173 | }) 174 | 175 | test('overflow', function (t) { 176 | t.pass('skipping test, simple-peer does not have similar data restrictions') 177 | t.end() 178 | }) 179 | 180 | test('2 buffers packed into 1 chunk', function (t) { 181 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 182 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 183 | 184 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 185 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 186 | 187 | peer2.on('datachannel', function onStream (b) { 188 | b.pipe(concat(function (body) { 189 | t.equal(body.toString('utf8'), 'abc\n123\n') 190 | t.end() 191 | 192 | peer1.destroy() 193 | peer2.destroy() 194 | })) 195 | }) 196 | var a = peer1.createDataChannel('1337') 197 | a.write('abc\n') 198 | a.write('123\n') 199 | a.end() 200 | }) 201 | 202 | test('chunks', function (t) { 203 | t.pass('skipping test, we cannot access the multiplexed SRTP stream to chunk it') 204 | t.end() 205 | }) 206 | 207 | test('prefinish + corking', function (t) { 208 | t.pass('skipping test, simple-peer does not support corking or prefinish event') 209 | t.end() 210 | }) 211 | 212 | test('quick message', function (t) { 213 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 214 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 215 | 216 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 217 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 218 | 219 | peer1.on('datachannel', function onStream (stream) { 220 | stream.write('hello world') 221 | }) 222 | 223 | setTimeout(function () { 224 | var stream = peer2.createDataChannel() 225 | stream.on('data', function (data) { 226 | t.same(data, Buffer.from('hello world')) 227 | t.end() 228 | 229 | peer1.destroy() 230 | peer2.destroy() 231 | }) 232 | }, 100) 233 | }) 234 | 235 | test('if onstream is not passed, stream is emitted', function (t) { 236 | t.pass('skipping test, simple-peer does not use same onStream callback API') 237 | t.end() 238 | }) 239 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | var common = require('./common') 2 | var Peer = require('../') 3 | var bowser = require('bowser') 4 | var test = require('tape') 5 | 6 | var config 7 | test('get config', function (t) { 8 | common.getConfig(function (err, _config) { 9 | if (err) return t.fail(err) 10 | config = _config 11 | t.end() 12 | }) 13 | }) 14 | 15 | test('detect WebRTC support', function (t) { 16 | t.equal(Peer.WEBRTC_SUPPORT, typeof window !== 'undefined', 'builtin webrtc support') 17 | t.end() 18 | }) 19 | 20 | test('create peer without options', function (t) { 21 | t.plan(1) 22 | 23 | if (process.browser) { 24 | var peer 25 | t.doesNotThrow(function () { 26 | peer = new Peer() 27 | }) 28 | peer.destroy() 29 | } else { 30 | t.pass('Skip no-option test in Node.js, since the wrtc option is required') 31 | } 32 | }) 33 | 34 | test('signal event gets emitted', function (t) { 35 | t.plan(2) 36 | 37 | var peer = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 38 | peer.once('signal', function () { 39 | t.pass('got signal event') 40 | peer.on('close', function () { t.pass('peer destroyed') }) 41 | peer.destroy() 42 | }) 43 | }) 44 | 45 | test('signal event does not get emitted by non-initiator', function (t) { 46 | var peer = new Peer({ config: config, initiator: false, wrtc: common.wrtc }) 47 | peer.once('signal', function () { 48 | t.fail('got signal event') 49 | peer.on('close', function () { t.pass('peer destroyed') }) 50 | peer.destroy() 51 | }) 52 | 53 | setTimeout(() => { 54 | t.pass('did not get signal after 1000ms') 55 | t.end() 56 | }, 1000) 57 | }) 58 | 59 | test('signal event does not get emitted by non-initiator with stream', function (t) { 60 | var peer = new Peer({ 61 | config: config, 62 | stream: common.getMediaStream(), 63 | initiator: false, 64 | wrtc: common.wrtc 65 | }) 66 | peer.once('signal', function () { 67 | t.fail('got signal event') 68 | peer.on('close', function () { t.pass('peer destroyed') }) 69 | peer.destroy() 70 | }) 71 | 72 | setTimeout(() => { 73 | t.pass('did not get signal after 1000ms') 74 | t.end() 75 | }, 1000) 76 | }) 77 | 78 | test('data send/receive text', function (t) { 79 | t.plan(10) 80 | 81 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 82 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 83 | 84 | var numSignal1 = 0 85 | peer1.on('signal', function (data) { 86 | numSignal1 += 1 87 | peer2.signal(data) 88 | }) 89 | 90 | var numSignal2 = 0 91 | peer2.on('signal', function (data) { 92 | numSignal2 += 1 93 | peer1.signal(data) 94 | }) 95 | 96 | peer1.on('connect', tryTest) 97 | peer2.on('connect', tryTest) 98 | 99 | function tryTest () { 100 | if (!peer1.connected || !peer2.connected) return 101 | 102 | t.ok(numSignal1 >= 1) 103 | t.ok(numSignal2 >= 1) 104 | t.equal(peer1.initiator, true, 'peer1 is initiator') 105 | t.equal(peer2.initiator, false, 'peer2 is not initiator') 106 | 107 | peer1.send('sup peer2') 108 | peer2.on('data', function (data) { 109 | t.ok(Buffer.isBuffer(data), 'data is Buffer') 110 | t.equal(data.toString(), 'sup peer2', 'got correct message') 111 | 112 | peer2.send('sup peer1') 113 | peer1.on('data', function (data) { 114 | t.ok(Buffer.isBuffer(data), 'data is Buffer') 115 | t.equal(data.toString(), 'sup peer1', 'got correct message') 116 | 117 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 118 | peer1.destroy() 119 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 120 | peer2.destroy() 121 | }) 122 | }) 123 | } 124 | }) 125 | 126 | test('sdpTransform function is called', function (t) { 127 | t.plan(3) 128 | 129 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 130 | var peer2 = new Peer({ config: config, sdpTransform: sdpTransform, wrtc: common.wrtc }) 131 | 132 | function sdpTransform (sdp) { 133 | t.equal(typeof sdp, 'string', 'got a string as SDP') 134 | setTimeout(function () { 135 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 136 | peer1.destroy() 137 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 138 | peer2.destroy() 139 | }, 0) 140 | return sdp 141 | } 142 | 143 | peer1.on('signal', function (data) { 144 | peer2.signal(data) 145 | }) 146 | 147 | peer2.on('signal', function (data) { 148 | peer1.signal(data) 149 | }) 150 | }) 151 | 152 | test('old constraint formats are used', function (t) { 153 | t.plan(3) 154 | 155 | var constraints = { 156 | mandatory: { 157 | OfferToReceiveAudio: true, 158 | OfferToReceiveVideo: true 159 | } 160 | } 161 | 162 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc, constraints: constraints }) 163 | var peer2 = new Peer({ config: config, wrtc: common.wrtc, constraints: constraints }) 164 | 165 | peer1.on('signal', function (data) { 166 | peer2.signal(data) 167 | }) 168 | 169 | peer2.on('signal', function (data) { 170 | peer1.signal(data) 171 | }) 172 | 173 | peer1.on('connect', function () { 174 | t.pass('peers connected') 175 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 176 | peer1.destroy() 177 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 178 | peer2.destroy() 179 | }) 180 | }) 181 | 182 | test('new constraint formats are used', function (t) { 183 | t.plan(3) 184 | 185 | var constraints = { 186 | offerToReceiveAudio: true, 187 | offerToReceiveVideo: true 188 | } 189 | 190 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc, constraints: constraints }) 191 | var peer2 = new Peer({ config: config, wrtc: common.wrtc, constraints: constraints }) 192 | 193 | peer1.on('signal', function (data) { 194 | peer2.signal(data) 195 | }) 196 | 197 | peer2.on('signal', function (data) { 198 | peer1.signal(data) 199 | }) 200 | 201 | peer1.on('connect', function () { 202 | t.pass('peers connected') 203 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 204 | peer1.destroy() 205 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 206 | peer2.destroy() 207 | }) 208 | }) 209 | 210 | test('ensure remote address and port are available right after connection', function (t) { 211 | if (bowser.safari || bowser.ios) { 212 | t.pass('Skip on Safari and iOS which do not support modern getStats() calls') 213 | t.end() 214 | return 215 | } 216 | 217 | t.plan(7) 218 | 219 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 220 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 221 | 222 | peer1.on('signal', function (data) { 223 | peer2.signal(data) 224 | }) 225 | 226 | peer2.on('signal', function (data) { 227 | peer1.signal(data) 228 | }) 229 | 230 | peer1.on('connect', function () { 231 | t.pass('peers connected') 232 | 233 | t.ok(peer1.remoteAddress, 'peer1 remote address is present') 234 | t.ok(peer1.remotePort, 'peer1 remote port is present') 235 | 236 | peer2.on('connect', function () { 237 | t.ok(peer2.remoteAddress, 'peer2 remote address is present') 238 | t.ok(peer2.remotePort, 'peer2 remote port is present') 239 | 240 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 241 | peer1.destroy() 242 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 243 | peer2.destroy() 244 | }) 245 | }) 246 | }) 247 | 248 | test('pre-negotiated default channel', function (t) { 249 | t.plan(6) 250 | 251 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc, channelConfig: { negotiated: true, id: 123 } }) 252 | var peer2 = new Peer({ config: config, wrtc: common.wrtc, channelConfig: { negotiated: true, id: 123 } }) 253 | 254 | peer1.on('signal', function (data) { 255 | peer2.signal(data) 256 | }) 257 | 258 | peer2.on('signal', function (data) { 259 | peer1.signal(data) 260 | }) 261 | 262 | peer1.on('connect', function () { 263 | peer1.send('sup peer2') 264 | peer2.on('data', function (data) { 265 | t.ok(Buffer.isBuffer(data), 'data is Buffer') 266 | t.equal(data.toString(), 'sup peer2', 'got correct message') 267 | 268 | peer2.send('sup peer1') 269 | peer1.on('data', function (data) { 270 | t.ok(Buffer.isBuffer(data), 'data is Buffer') 271 | t.equal(data.toString(), 'sup peer1', 'got correct message') 272 | 273 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 274 | peer1.destroy() 275 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 276 | peer2.destroy() 277 | }) 278 | }) 279 | }) 280 | }) 281 | -------------------------------------------------------------------------------- /datachannel.js: -------------------------------------------------------------------------------- 1 | module.exports = DataChannel 2 | 3 | var debug = require('debug')('simple-peer') 4 | var inherits = require('inherits') 5 | var stream = require('readable-stream') 6 | 7 | var MAX_BUFFERED_AMOUNT = 64 * 1024 8 | var CHANNEL_CLOSING_TIMEOUT = 5 * 1000 9 | var CHANNEL_CLOSE_DELAY = 3 * 1000 10 | 11 | inherits(DataChannel, stream.Duplex) 12 | 13 | function DataChannel (opts) { 14 | var self = this 15 | 16 | opts = Object.assign( 17 | { 18 | allowHalfOpen: false 19 | }, 20 | opts 21 | ) 22 | 23 | stream.Duplex.call(self, opts) 24 | 25 | self._chunk = null 26 | self._cb = null 27 | self._interval = null 28 | self._channel = null 29 | self._fresh = true 30 | 31 | self.channelName = null 32 | 33 | // HACK: Chrome will sometimes get stuck in readyState "closing", let's check for this condition 34 | var isClosing = false 35 | self._closingInterval = setInterval(function () { 36 | // No "onclosing" event 37 | if (self._channel && self._channel.readyState === 'closing') { 38 | if (isClosing) self._onChannelClose() // Equivalent to onclose firing. 39 | isClosing = true 40 | } else { 41 | isClosing = false 42 | } 43 | }, CHANNEL_CLOSING_TIMEOUT) 44 | } 45 | 46 | DataChannel.prototype._setDataChannel = function (channel) { 47 | var self = this 48 | 49 | self._channel = channel 50 | self._channel.binaryType = 'arraybuffer' 51 | 52 | if (typeof self._channel.bufferedAmountLowThreshold === 'number') { 53 | self._channel.bufferedAmountLowThreshold = MAX_BUFFERED_AMOUNT 54 | } 55 | 56 | self.channelName = self._channel.label.split('@')[0] 57 | 58 | self._channel.onmessage = function (event) { 59 | self._onChannelMessage(event) 60 | } 61 | self._channel.onbufferedamountlow = function () { 62 | self._onChannelBufferedAmountLow() 63 | } 64 | self._channel.onopen = function () { 65 | self._onChannelOpen() 66 | } 67 | self._channel.onclose = function () { 68 | self._onChannelClose() 69 | } 70 | self._channel.onerror = function (err) { 71 | self.destroy(makeError(err, 'ERR_DATA_CHANNEL')) 72 | } 73 | 74 | self._onFinishBound = function () { 75 | self._onFinish() 76 | } 77 | self.once('finish', self._onFinishBound) 78 | } 79 | 80 | DataChannel.prototype._read = function () {} 81 | 82 | DataChannel.prototype._write = function (chunk, encoding, cb) { 83 | var self = this 84 | if (self.destroyed) 85 | return cb( 86 | makeError('cannot write after channel is destroyed', 'ERR_DATA_CHANNEL') 87 | ) 88 | 89 | if (self._channel && self._channel.readyState === 'open') { 90 | try { 91 | self.send(chunk) 92 | } catch (err) { 93 | return self.destroy(makeError(err, 'ERR_DATA_CHANNEL')) 94 | } 95 | if (self._channel.bufferedAmount > MAX_BUFFERED_AMOUNT) { 96 | self._debug( 97 | 'start backpressure: bufferedAmount %d', 98 | self._channel.bufferedAmount 99 | ) 100 | self._cb = cb 101 | } else { 102 | cb(null) 103 | } 104 | } else { 105 | self._debug('write before connect') 106 | self._chunk = chunk 107 | self._cb = cb 108 | } 109 | } 110 | 111 | // When stream finishes writing, close socket. Half open connections are not 112 | // supported. 113 | DataChannel.prototype._onFinish = function () { 114 | var self = this 115 | if (self.destroyed) return 116 | 117 | if (!self._channel || self._channel.readyState === 'open') { 118 | destroySoon() 119 | } else { 120 | self.once('connect', destroySoon) 121 | } 122 | 123 | // Wait a bit before destroying so the socket flushes. 124 | // TODO: is there a more reliable way to accomplish this? 125 | function destroySoon () { 126 | setTimeout(function () { 127 | self.destroy() 128 | }, 1000) 129 | } 130 | } 131 | 132 | DataChannel.prototype._onInterval = function () { 133 | var self = this 134 | if ( 135 | !self._cb || 136 | !self._channel || 137 | self._channel.bufferedAmount > MAX_BUFFERED_AMOUNT 138 | ) { 139 | return 140 | } 141 | self._onChannelBufferedAmountLow() 142 | } 143 | 144 | DataChannel.prototype._onChannelMessage = function (event) { 145 | var self = this 146 | if (self.destroyed) return 147 | var data = event.data 148 | if (data instanceof ArrayBuffer) data = Buffer.from(data) 149 | self.push(data) 150 | } 151 | 152 | DataChannel.prototype._onChannelBufferedAmountLow = function () { 153 | var self = this 154 | if (self.destroyed || !self._cb) return 155 | self._debug( 156 | 'ending backpressure: bufferedAmount %d', 157 | self._channel.bufferedAmount 158 | ) 159 | var cb = self._cb 160 | self._cb = null 161 | cb(null) 162 | } 163 | 164 | DataChannel.prototype._onChannelOpen = function () { 165 | var self = this 166 | self._debug('on channel open', self.channelName) 167 | self.emit('open') 168 | self._sendChunk() 169 | 170 | setTimeout(function () { 171 | self._fresh = false 172 | }, CHANNEL_CLOSE_DELAY) 173 | } 174 | 175 | DataChannel.prototype._onChannelClose = function () { 176 | var self = this 177 | self._debug('on channel close') 178 | self.destroy() 179 | } 180 | 181 | DataChannel.prototype._sendChunk = function () { 182 | // called when peer connects or self._channel set 183 | var self = this 184 | if (self.destroyed) return 185 | 186 | if (self._chunk) { 187 | try { 188 | self.send(self._chunk) 189 | } catch (err) { 190 | return self.destroy(makeError(err, 'ERR_DATA_CHANNEL')) 191 | } 192 | self._chunk = null 193 | self._debug('sent chunk from "write before connect"') 194 | 195 | var cb = self._cb 196 | self._cb = null 197 | cb(null) 198 | } 199 | 200 | // If `bufferedAmountLowThreshold` and 'onbufferedamountlow' are unsupported, 201 | // fallback to using setInterval to implement backpressure. 202 | if ( 203 | !self._interval && 204 | typeof self._channel.bufferedAmountLowThreshold !== 'number' 205 | ) { 206 | self._interval = setInterval(function () { 207 | self._onInterval() 208 | }, 150) 209 | if (self._interval.unref) self._interval.unref() 210 | } 211 | } 212 | 213 | Object.defineProperty(DataChannel.prototype, 'bufferSize', { 214 | get: function () { 215 | var self = this 216 | return (self._channel && self._channel.bufferedAmount) || 0 217 | } 218 | }) 219 | 220 | /** 221 | * Send text/binary data to the remote peer. 222 | * @param {ArrayBufferView|ArrayBuffer|Buffer|string|Blob} chunk 223 | */ 224 | DataChannel.prototype.send = function (chunk) { 225 | var self = this 226 | if (!self._channel) { 227 | if (self.destroyed) 228 | return self.destroy( 229 | makeError('cannot send after channel is destroyed', 'ERR_DATA_CHANNEL') 230 | ) 231 | else 232 | return self.destroy( 233 | makeError( 234 | 'cannot send before channel is created - use write() to buffer', 235 | 'ERR_DATA_CHANNEL' 236 | ) 237 | ) 238 | } 239 | self._channel.send(chunk) 240 | } 241 | 242 | // TODO: Delete this method once readable-stream is updated to contain a default 243 | // implementation of destroy() that automatically calls _destroy() 244 | // See: https://github.com/nodejs/readable-stream/issues/283 245 | DataChannel.prototype.destroy = function (err) { 246 | var self = this 247 | self._destroy(err, function () {}) 248 | } 249 | 250 | function closeChannel (channel) { 251 | try { 252 | channel.close() 253 | } catch (err) {} 254 | } 255 | 256 | DataChannel.prototype._destroy = function (err, cb) { 257 | var self = this 258 | if (self.destroyed) return 259 | 260 | if (self._channel) { 261 | if (self._fresh) { 262 | // HACK: Safari sometimes cannot close channels immediately after opening them 263 | setTimeout(closeChannel.bind(this, self._channel), CHANNEL_CLOSE_DELAY) 264 | } else { 265 | closeChannel(self._channel) 266 | } 267 | 268 | self._channel.onmessage = null 269 | self._channel.onopen = null 270 | self._channel.onclose = null 271 | self._channel.onerror = null 272 | self._channel = null 273 | } 274 | 275 | self.readable = self.writable = false 276 | 277 | if (!self._readableState.ended) self.push(null) 278 | if (!self._writableState.finished) self.end() 279 | 280 | self.destroyed = true 281 | 282 | clearInterval(self._closingInterval) 283 | self._closingInterval = null 284 | 285 | clearInterval(self._interval) 286 | self._interval = null 287 | self._chunk = null 288 | self._cb = null 289 | 290 | self.channelName = null 291 | 292 | if (self._onFinishBound) self.removeListener('finish', self._onFinishBound) 293 | self._onFinishBound = null 294 | 295 | if (err) self.emit('error', err) 296 | self.emit('close') 297 | cb() 298 | } 299 | 300 | DataChannel.prototype._debug = function () { 301 | var self = this 302 | var args = [].slice.call(arguments) 303 | args[0] = '[' + self._id + '] ' + args[0] 304 | debug.apply(null, args) 305 | } 306 | 307 | function makeError (message, code) { 308 | var err = new Error(message) 309 | err.code = code 310 | return err 311 | } 312 | -------------------------------------------------------------------------------- /test/multistream.js: -------------------------------------------------------------------------------- 1 | var common = require('./common') 2 | var Peer = require('../') 3 | var test = require('tape') 4 | 5 | var config 6 | test('get config', function (t) { 7 | common.getConfig(function (err, _config) { 8 | if (err) return t.fail(err) 9 | config = _config 10 | t.end() 11 | }) 12 | }) 13 | 14 | test('multistream', function (t) { 15 | t.plan(20) 16 | 17 | var peer1 = new Peer({ 18 | config: config, 19 | initiator: true, 20 | wrtc: common.wrtc, 21 | streams: (new Array(10)).fill(null).map(function () { return common.getMediaStream() }) 22 | }) 23 | var peer2 = new Peer({ 24 | config: config, 25 | wrtc: common.wrtc, 26 | streams: (new Array(10)).fill(null).map(function () { return common.getMediaStream() }) 27 | }) 28 | 29 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 30 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 31 | 32 | var receivedIds = {} 33 | 34 | peer1.on('stream', function (stream) { 35 | t.pass('peer1 got stream') 36 | if (receivedIds[stream.id]) { 37 | t.fail('received one unique stream per event') 38 | } else { 39 | receivedIds[stream.id] = true 40 | } 41 | }) 42 | peer2.on('stream', function (stream) { 43 | t.pass('peer2 got stream') 44 | if (receivedIds[stream.id]) { 45 | t.fail('received one unique stream per event') 46 | } else { 47 | receivedIds[stream.id] = true 48 | } 49 | }) 50 | 51 | t.on('end', () => { 52 | peer1.destroy() 53 | peer2.destroy() 54 | }) 55 | }) 56 | 57 | test('multistream on non-initiator only', function (t) { 58 | t.plan(10) 59 | 60 | var peer1 = new Peer({ 61 | config: config, 62 | initiator: true, 63 | wrtc: common.wrtc, 64 | streams: [] 65 | }) 66 | var peer2 = new Peer({ 67 | config: config, 68 | wrtc: common.wrtc, 69 | streams: (new Array(10)).fill(null).map(function () { return common.getMediaStream() }) 70 | }) 71 | 72 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 73 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 74 | 75 | var receivedIds = {} 76 | 77 | peer1.on('stream', function (stream) { 78 | t.pass('peer1 got stream') 79 | if (receivedIds[stream.id]) { 80 | t.fail('received one unique stream per event') 81 | } else { 82 | receivedIds[stream.id] = true 83 | } 84 | }) 85 | 86 | t.on('end', () => { 87 | peer1.destroy() 88 | peer2.destroy() 89 | }) 90 | }) 91 | 92 | test('delayed stream on non-initiator', function (t) { 93 | t.timeoutAfter(15000) 94 | t.plan(1) 95 | 96 | var peer1 = new Peer({ 97 | config: config, 98 | trickle: true, 99 | initiator: true, 100 | wrtc: common.wrtc, 101 | streams: [common.getMediaStream()] 102 | }) 103 | var peer2 = new Peer({ 104 | config: config, 105 | trickle: true, 106 | wrtc: common.wrtc, 107 | streams: [] 108 | }) 109 | 110 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 111 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 112 | 113 | setTimeout(() => { 114 | peer2.addStream(common.getMediaStream()) 115 | }, 10000) 116 | peer1.on('stream', function () { 117 | t.pass('peer1 got stream') 118 | }) 119 | 120 | t.on('end', () => { 121 | peer1.destroy() 122 | peer2.destroy() 123 | }) 124 | }) 125 | 126 | test('multistream on non-initiator only', function (t) { 127 | t.plan(10) 128 | 129 | var peer1 = new Peer({ 130 | config: config, 131 | initiator: true, 132 | wrtc: common.wrtc, 133 | streams: [] 134 | }) 135 | var peer2 = new Peer({ 136 | config: config, 137 | wrtc: common.wrtc, 138 | streams: (new Array(10)).fill(null).map(function () { return common.getMediaStream() }) 139 | }) 140 | 141 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 142 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 143 | 144 | var receivedIds = {} 145 | 146 | peer1.on('stream', function (stream) { 147 | t.pass('peer1 got stream') 148 | if (receivedIds[stream.id]) { 149 | t.fail('received one unique stream per event') 150 | } else { 151 | receivedIds[stream.id] = true 152 | } 153 | }) 154 | 155 | t.on('end', () => { 156 | peer1.destroy() 157 | peer2.destroy() 158 | }) 159 | }) 160 | 161 | test('delayed stream on non-initiator', function (t) { 162 | t.timeoutAfter(15000) 163 | t.plan(1) 164 | 165 | var peer1 = new Peer({ 166 | config: config, 167 | trickle: true, 168 | initiator: true, 169 | wrtc: common.wrtc, 170 | streams: [common.getMediaStream()] 171 | }) 172 | var peer2 = new Peer({ 173 | config: config, 174 | trickle: true, 175 | wrtc: common.wrtc, 176 | streams: [] 177 | }) 178 | 179 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 180 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 181 | 182 | setTimeout(() => { 183 | peer2.addStream(common.getMediaStream()) 184 | }, 10000) 185 | peer1.on('stream', function () { 186 | t.pass('peer1 got stream') 187 | }) 188 | 189 | t.on('end', () => { 190 | peer1.destroy() 191 | peer2.destroy() 192 | }) 193 | }) 194 | 195 | test('incremental multistream', function (t) { 196 | t.plan(12) 197 | 198 | var peer1 = new Peer({ 199 | config: config, 200 | initiator: true, 201 | wrtc: common.wrtc, 202 | streams: [] 203 | }) 204 | var peer2 = new Peer({ 205 | config: config, 206 | wrtc: common.wrtc, 207 | streams: [] 208 | }) 209 | 210 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 211 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 212 | 213 | peer1.on('connect', function () { 214 | t.pass('peer1 connected') 215 | peer1.addStream(common.getMediaStream()) 216 | }) 217 | peer2.on('connect', function () { 218 | t.pass('peer2 connected') 219 | peer2.addStream(common.getMediaStream()) 220 | }) 221 | 222 | var receivedIds = {} 223 | 224 | var count1 = 0 225 | peer1.on('stream', function (stream) { 226 | t.pass('peer1 got stream') 227 | if (receivedIds[stream.id]) { 228 | t.fail('received one unique stream per event') 229 | } else { 230 | receivedIds[stream.id] = true 231 | } 232 | count1++ 233 | if (count1 < 5) { 234 | peer1.addStream(common.getMediaStream()) 235 | } 236 | }) 237 | 238 | var count2 = 0 239 | peer2.on('stream', function (stream) { 240 | t.pass('peer2 got stream') 241 | if (receivedIds[stream.id]) { 242 | t.fail('received one unique stream per event') 243 | } else { 244 | receivedIds[stream.id] = true 245 | } 246 | count2++ 247 | if (count2 < 5) { 248 | peer2.addStream(common.getMediaStream()) 249 | } 250 | }) 251 | 252 | t.on('end', () => { 253 | peer1.destroy() 254 | peer2.destroy() 255 | }) 256 | }) 257 | 258 | test('incremental multistream on non-initiator only', function (t) { 259 | t.plan(7) 260 | 261 | var peer1 = new Peer({ 262 | config: config, 263 | initiator: true, 264 | wrtc: common.wrtc, 265 | streams: [] 266 | }) 267 | var peer2 = new Peer({ 268 | config: config, 269 | wrtc: common.wrtc, 270 | streams: [] 271 | }) 272 | 273 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 274 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 275 | 276 | peer1.on('connect', function () { 277 | t.pass('peer1 connected') 278 | }) 279 | peer2.on('connect', function () { 280 | t.pass('peer2 connected') 281 | peer2.addStream(common.getMediaStream()) 282 | }) 283 | 284 | var receivedIds = {} 285 | 286 | var count = 0 287 | peer1.on('stream', function (stream) { 288 | t.pass('peer1 got stream') 289 | if (receivedIds[stream.id]) { 290 | t.fail('received one unique stream per event') 291 | } else { 292 | receivedIds[stream.id] = true 293 | } 294 | count++ 295 | if (count < 5) { 296 | peer2.addStream(common.getMediaStream()) 297 | } 298 | }) 299 | 300 | t.on('end', () => { 301 | peer1.destroy() 302 | peer2.destroy() 303 | }) 304 | }) 305 | 306 | test('addStream after removeStream', function (t) { 307 | t.plan(2) 308 | 309 | var stream1 = common.getMediaStream() 310 | var stream2 = common.getMediaStream() 311 | 312 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 313 | var peer2 = new Peer({ config: config, wrtc: common.wrtc, streams: [stream1] }) 314 | 315 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 316 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 317 | 318 | peer1.once('stream', () => { 319 | t.pass('peer1 got first stream') 320 | peer2.removeStream(stream1) 321 | setTimeout(() => { 322 | peer1.once('stream', () => { 323 | t.pass('peer1 got second stream') 324 | }) 325 | peer2.addStream(stream2) 326 | }, 1000) 327 | }) 328 | 329 | t.on('end', () => { 330 | peer1.destroy() 331 | peer2.destroy() 332 | }) 333 | }) 334 | 335 | test('removeTrack immediately', function (t) { 336 | t.plan(2) 337 | 338 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 339 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 340 | 341 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 342 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 343 | 344 | var stream1 = common.getMediaStream() 345 | var stream2 = common.getMediaStream() 346 | 347 | peer1.addTrack(stream1.getTracks()[0], stream1) 348 | peer2.addTrack(stream2.getTracks()[0], stream2) 349 | 350 | peer1.removeTrack(stream1.getTracks()[0], stream1) 351 | peer2.removeTrack(stream2.getTracks()[0], stream2) 352 | 353 | peer1.on('track', function (track, stream) { 354 | t.fail('peer1 did not get track event') 355 | }) 356 | peer2.on('track', function (track, stream) { 357 | t.fail('peer2 did not get track event') 358 | }) 359 | 360 | peer1.on('connect', function () { 361 | t.pass('peer1 connected') 362 | }) 363 | peer2.on('connect', function () { 364 | t.pass('peer2 connected') 365 | }) 366 | 367 | t.on('end', () => { 368 | peer1.destroy() 369 | peer2.destroy() 370 | }) 371 | }) 372 | 373 | test('replaceTrack', function (t) { 374 | t.plan(4) 375 | 376 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 377 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 378 | 379 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 380 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 381 | 382 | var stream1 = common.getMediaStream() 383 | var stream2 = common.getMediaStream() 384 | 385 | peer1.addTrack(stream1.getTracks()[0], stream1) 386 | peer2.addTrack(stream2.getTracks()[0], stream2) 387 | 388 | peer1.replaceTrack(stream1.getTracks()[0], stream2.getTracks()[0], stream1) 389 | peer2.replaceTrack(stream2.getTracks()[0], stream1.getTracks()[0], stream2) 390 | 391 | peer1.on('track', function (track, stream) { 392 | t.pass('peer1 got track event') 393 | peer2.replaceTrack(stream2.getTracks()[0], null, stream2) 394 | }) 395 | peer2.on('track', function (track, stream) { 396 | t.pass('peer2 got track event') 397 | peer1.replaceTrack(stream1.getTracks()[0], null, stream1) 398 | }) 399 | 400 | peer1.on('connect', function () { 401 | t.pass('peer1 connected') 402 | }) 403 | peer2.on('connect', function () { 404 | t.pass('peer2 connected') 405 | }) 406 | 407 | t.on('end', () => { 408 | peer1.destroy() 409 | peer2.destroy() 410 | }) 411 | }) 412 | -------------------------------------------------------------------------------- /test/datachannel.js: -------------------------------------------------------------------------------- 1 | var common = require('./common') 2 | var Peer = require('../') 3 | var DataChannel = require('../datachannel') 4 | var str = require('string-to-stream') 5 | var test = require('tape') 6 | var bowser = require('bowser') 7 | 8 | var config 9 | test('get config', function (t) { 10 | common.getConfig(function (err, _config) { 11 | if (err) return t.fail(err) 12 | config = _config 13 | t.end() 14 | }) 15 | }) 16 | 17 | test('create multiple DataChannels', function (t) { 18 | t.plan(7) 19 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 20 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 21 | 22 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 23 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 24 | 25 | var dc1 = peer1.createDataChannel('1', {}, {}) 26 | var dc2 = peer1.createDataChannel('2', {}) 27 | var dc3 = peer1.createDataChannel('3') 28 | 29 | t.assert(peer1 instanceof DataChannel) 30 | t.assert(peer2 instanceof DataChannel) 31 | t.assert(dc1 instanceof DataChannel) 32 | t.assert(dc2 instanceof DataChannel) 33 | t.assert(dc3 instanceof DataChannel) 34 | 35 | t.equals(peer1._channels.length, 4, 'peer1 has correct number of datachannels') 36 | 37 | var count = 0 38 | peer2.on('datachannel', function () { 39 | count++ 40 | if (count >= 3) { 41 | t.equals(peer2._channels.length, 4, 'peer2 has correct number of datachannels') 42 | } 43 | }) 44 | }) 45 | 46 | test('datachannel event', function (t) { 47 | t.plan(8) 48 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 49 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 50 | 51 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 52 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 53 | 54 | peer1.createDataChannel('1') 55 | peer2.createDataChannel('2') 56 | 57 | peer2.on('connect', function () { 58 | t.equals(peer2.channelName, 'default', 'default channelName is correct') 59 | t.equals(peer1.channelName, 'default', 'default channelName is correct') 60 | t.assert(peer2 instanceof DataChannel, 'peer1 is instance of DataChannel') 61 | t.assert(peer2 instanceof DataChannel, 'peer2 is instance of DataChannel') 62 | }) 63 | 64 | peer2.on('datachannel', function (dc) { 65 | t.equals(dc.channelName, '1', 'channelName 1 is correct') 66 | t.assert(dc instanceof DataChannel, 'dc is instance of DataChannel') 67 | }) 68 | 69 | peer1.on('datachannel', function (dc) { 70 | t.equals(dc.channelName, '2', 'channelName 2 is correct') 71 | t.assert(dc instanceof DataChannel, 'dc is instance of DataChannel') 72 | }) 73 | }) 74 | 75 | test('data sends on seperate channels', function (t) { 76 | t.plan(32) 77 | 78 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 79 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 80 | 81 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 82 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 83 | 84 | var dc1 = peer1.createDataChannel('1') 85 | var dc2 = peer2.createDataChannel('2') 86 | 87 | var ended = 0 88 | function assertChannel (channel, label, testData) { 89 | str(testData).pipe(channel) 90 | 91 | channel.on('data', function (chunk) { 92 | t.equal(chunk.toString(), testData, label + ' got correct message') 93 | }) 94 | channel.on('finish', function () { 95 | t.pass(label + ' got "finish"') 96 | t.ok(channel._writableState.finished) 97 | }) 98 | channel.on('end', function () { 99 | t.pass(label + ' got "end"') 100 | t.ok(channel._readableState.ended) 101 | ended++ 102 | if (ended === 6) { 103 | peer1.destroy() 104 | peer2.destroy() 105 | t.end() 106 | } 107 | }) 108 | } 109 | 110 | assertChannel(dc1, 'channel 1, creator side', '123') 111 | peer2.on('datachannel', function (dc1) { 112 | t.pass('got "datachannel" event on peer2') 113 | assertChannel(dc1, 'channel 1, receiver side', '123') 114 | }) 115 | 116 | assertChannel(dc2, 'channel 2, creator side', '456') 117 | peer1.on('datachannel', function (dc2) { 118 | t.pass('got "datachannel" event on peer1') 119 | assertChannel(dc2, 'channel 2, receiver side', '456') 120 | }) 121 | 122 | assertChannel(peer1, 'default, initiator', 'abc') 123 | assertChannel(peer2, 'default, non-initiator', 'abc') 124 | }) 125 | 126 | test('data sends on seperate channels, async creation', function (t) { 127 | t.plan(32) 128 | 129 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 130 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 131 | 132 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 133 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 134 | 135 | setTimeout(function () { 136 | var dc1 = peer1.createDataChannel('1') 137 | var dc2 = peer2.createDataChannel('2') 138 | 139 | var ended = 0 140 | function assertChannel (channel, label, testData) { 141 | str(testData).pipe(channel) 142 | 143 | channel.on('data', function (chunk) { 144 | t.equal(chunk.toString(), testData, label + ' got correct message') 145 | }) 146 | channel.on('finish', function () { 147 | t.pass(label + ' got "finish"') 148 | t.ok(channel._writableState.finished) 149 | }) 150 | channel.on('end', function () { 151 | t.pass(label + ' got "end"') 152 | t.ok(channel._readableState.ended) 153 | ended++ 154 | if (ended === 6) { 155 | peer1.destroy() 156 | peer2.destroy() 157 | t.end() 158 | } 159 | }) 160 | } 161 | 162 | assertChannel(dc1, 'channel 1, creator side', '123') 163 | peer2.on('datachannel', function (dc1) { 164 | t.pass('got "datachannel" event on peer2') 165 | assertChannel(dc1, 'channel 1, receiver side', '123') 166 | }) 167 | 168 | assertChannel(dc2, 'channel 2, creator side', '456') 169 | peer1.on('datachannel', function (dc2) { 170 | t.pass('got "datachannel" event on peer1') 171 | assertChannel(dc2, 'channel 2, receiver side', '456') 172 | }) 173 | 174 | assertChannel(peer1, 'default, initiator', 'abc') 175 | assertChannel(peer2, 'default, non-initiator', 'abc') 176 | }, 2000) 177 | }) 178 | 179 | test('closing channels from creator side', function (t) { 180 | t.plan(4) 181 | 182 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 183 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 184 | 185 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 186 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 187 | 188 | var dc1 = peer1.createDataChannel('1') 189 | var dc2 = peer2.createDataChannel('2') 190 | 191 | peer1.on('datachannel', function (dc) { 192 | t.pass('peer1 got datachannel event') 193 | dc.on('open', function () { 194 | dc2.destroy() 195 | try { 196 | dc.send('123') 197 | } catch (err) {} 198 | }) 199 | dc.on('close', function () { 200 | t.pass('dc2 closed') 201 | }) 202 | }) 203 | 204 | peer2.on('datachannel', function (dc) { 205 | t.pass('peer2 got datachannel event') 206 | dc.on('open', function () { 207 | dc1.destroy() 208 | try { 209 | dc.send('abc') 210 | } catch (err) {} 211 | }) 212 | dc.on('close', function () { 213 | t.pass('dc1 closed') 214 | }) 215 | }) 216 | 217 | dc1.on('data', function () { 218 | t.fail('received data after destruction') 219 | }) 220 | dc2.on('data', function () { 221 | t.fail('received data after destruction') 222 | }) 223 | }) 224 | 225 | test('closing channels from non-creator side', function (t) { 226 | t.plan(2) 227 | 228 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 229 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 230 | 231 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 232 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 233 | 234 | var dc1 = peer1.createDataChannel('1') 235 | var dc2 = peer2.createDataChannel('2') 236 | 237 | peer1.on('datachannel', function (dc) { 238 | dc.on('data', function () { 239 | t.fail('received data after destruction') 240 | }) 241 | dc.on('open', function () { 242 | dc.destroy() 243 | try { 244 | dc2.send('123') 245 | } catch (err) {} 246 | }) 247 | }) 248 | peer2.on('datachannel', function (dc) { 249 | dc.on('data', function () { 250 | t.fail('received data after destruction') 251 | }) 252 | dc.on('open', function () { 253 | dc.destroy() 254 | try { 255 | dc1.send('abc') 256 | } catch (err) {} 257 | }) 258 | }) 259 | 260 | dc1.on('close', function () { 261 | t.pass('dc1 closed') 262 | }) 263 | dc2.on('close', function () { 264 | t.pass('dc2 closed') 265 | }) 266 | }) 267 | 268 | test('open new channel after closing one', function (t) { 269 | if (bowser.firefox) { 270 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1513107 271 | t.pass('Skip on Firefox which does not support this reliably') 272 | t.end() 273 | return 274 | } 275 | t.plan(10) 276 | 277 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 278 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 279 | 280 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 281 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 282 | 283 | var count = 0 284 | 285 | const dc1 = peer1.createDataChannel('1') 286 | dc1.on('close', function () { 287 | t.pass('created channel #3') 288 | const dc12 = peer1.createDataChannel('3') 289 | str('456').pipe(dc12) 290 | }) 291 | var dc2 = peer2.createDataChannel('2') 292 | dc2.on('close', function () { 293 | t.pass('created channel #4') 294 | const dc22 = peer2.createDataChannel('4') 295 | str('123').pipe(dc22) 296 | }) 297 | 298 | peer1.once('datachannel', function (dc) { 299 | t.pass('got #2 datachannel') 300 | 301 | dc.on('close', function () { 302 | t.pass('#2 channel instance closed') 303 | }) 304 | dc.on('data', function () { 305 | t.fail('received data on closed #2 channel') 306 | }) 307 | dc.on('open', function () { 308 | dc.destroy() 309 | }) 310 | 311 | peer1.once('datachannel', function (dc) { 312 | t.equals(dc.channelName, '4', '#4 channel has correct name') 313 | dc.on('data', function (data) { 314 | t.equal(data.toString(), '123', 'received correct message on #4 channel') 315 | count++ 316 | if (count === 2) { 317 | peer1.destroy() 318 | peer2.destroy() 319 | t.end() 320 | } 321 | }) 322 | }) 323 | }) 324 | 325 | peer2.once('datachannel', function (dc) { 326 | t.pass('got #1 datachannel') 327 | 328 | dc.on('close', function () { 329 | t.pass('#1 channel instance closed') 330 | }) 331 | dc.on('data', function () { 332 | t.fail('received data on #1 closed channel') 333 | }) 334 | dc.on('open', function () { 335 | dc.destroy() 336 | }) 337 | 338 | peer2.once('datachannel', function (dc) { 339 | t.equals(dc.channelName, '3', '#3 channel has same channelName') 340 | dc.on('data', function (data) { 341 | t.equal(data.toString(), '456', 'received correct message on #3 channel') 342 | count++ 343 | if (count === 2) { 344 | peer1.destroy() 345 | peer2.destroy() 346 | t.end() 347 | } 348 | }) 349 | }) 350 | }) 351 | }) 352 | 353 | test('reusing channelNames of closed channels', function (t) { 354 | if (bowser.firefox) { 355 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1513107 356 | t.pass('Skip on Firefox which does not support this reliably') 357 | t.end() 358 | return 359 | } 360 | t.plan(10) 361 | 362 | var peer1 = new Peer({ config: config, initiator: true, wrtc: common.wrtc }) 363 | var peer2 = new Peer({ config: config, wrtc: common.wrtc }) 364 | 365 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 366 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 367 | 368 | var count = 0 369 | 370 | const dc1 = peer1.createDataChannel('1') 371 | dc1.on('close', function () { 372 | t.pass('dc1 closed') 373 | const dc12 = peer1.createDataChannel('1') 374 | dc12.write('456') 375 | }) 376 | const dc2 = peer2.createDataChannel('2') 377 | dc2.on('close', function () { 378 | t.pass('dc2 closed') 379 | const dc22 = peer2.createDataChannel('2') 380 | dc22.write('123') 381 | }) 382 | 383 | peer1.once('datachannel', function (dc) { 384 | dc.on('open', function () { 385 | t.pass('first channel instance closed #1') 386 | dc.destroy() 387 | }) 388 | dc.on('close', function () { 389 | t.pass('first channel instance closed #1') 390 | }) 391 | dc.on('data', function () { 392 | t.fail('received data on closed channel #1') 393 | }) 394 | 395 | peer1.once('datachannel', function (dc) { 396 | t.equals(dc.channelName, '2', 'second channel has same channelName #1') 397 | dc.on('data', function (data) { 398 | t.equal(data.toString(), '123', 'received correct message on channel #1') 399 | count++ 400 | if (count === 2) { 401 | peer1.destroy() 402 | peer2.destroy() 403 | t.end() 404 | } 405 | }) 406 | }) 407 | }) 408 | 409 | peer2.once('datachannel', function (dc) { 410 | dc.on('open', function () { 411 | t.pass('first channel instance closed #2') 412 | dc.destroy() 413 | }) 414 | dc.on('close', function () { 415 | t.pass('first channel instance closed #2') 416 | }) 417 | dc.on('data', function () { 418 | t.fail('received data on closed channel #2') 419 | }) 420 | 421 | peer2.once('datachannel', function (dc) { 422 | t.equals(dc.channelName, '1', 'second channel has same channelName #2') 423 | dc.on('data', function (data) { 424 | t.equal(data.toString(), '456', 'received correct message on second channel #2') 425 | count++ 426 | if (count === 2) { 427 | peer1.destroy() 428 | peer2.destroy() 429 | t.end() 430 | } 431 | }) 432 | }) 433 | }) 434 | }) 435 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simple-peer [![travis][travis-image]][travis-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url] 2 | 3 | [travis-image]: https://img.shields.io/travis/feross/simple-peer/master.svg 4 | [travis-url]: https://travis-ci.org/feross/simple-peer 5 | [npm-image]: https://img.shields.io/npm/v/simple-peer.svg 6 | [npm-url]: https://npmjs.org/package/simple-peer 7 | [downloads-image]: https://img.shields.io/npm/dm/simple-peer.svg 8 | [downloads-url]: https://npmjs.org/package/simple-peer 9 | [standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg 10 | [standard-url]: https://standardjs.com 11 | 12 | #### Simple WebRTC video/voice and data channels. 13 | 14 | [![Sauce Test Status](https://saucelabs.com/browser-matrix/feross-simple-peer.svg)](https://saucelabs.com/u/feross-simple-peer) 15 | 16 | ## features 17 | 18 | - concise, **node.js style** API for [WebRTC](https://en.wikipedia.org/wiki/WebRTC) 19 | - **works in node and the browser!** 20 | - supports **video/voice streams** 21 | - supports **data channel** 22 | - text and binary data 23 | - node.js [duplex stream](http://nodejs.org/api/stream.html) interface 24 | - supports advanced options like: 25 | - enable/disable [trickle ICE candidates](http://webrtchacks.com/trickle-ice/) 26 | - manually set config options 27 | - transceivers and renegotiation 28 | 29 | This module works in the browser with [browserify](http://browserify.org/). 30 | 31 | **Note:** If you're **NOT** using browserify, then use the included standalone file 32 | `simplepeer.min.js`. This exports a `SimplePeer` constructor on `window`. Wherever 33 | you see `Peer` in the examples below, substitute that with `SimplePeer`. 34 | 35 | ## install 36 | 37 | ``` 38 | npm install simple-peer 39 | ``` 40 | 41 | ## usage 42 | 43 | Let's create an html page that lets you manually connect two peers: 44 | 45 | ```html 46 | 47 | 48 | 55 |
56 | 57 | 58 |
59 |

 60 |     
 61 |   
 62 | 
 63 | ```
 64 | 
 65 | ```js
 66 | var Peer = require('simple-peer')
 67 | var p = new Peer({ initiator: location.hash === '#1', trickle: false })
 68 | 
 69 | p.on('error', function (err) { console.log('error', err) })
 70 | 
 71 | p.on('signal', function (data) {
 72 |   console.log('SIGNAL', JSON.stringify(data))
 73 |   document.querySelector('#outgoing').textContent = JSON.stringify(data)
 74 | })
 75 | 
 76 | document.querySelector('form').addEventListener('submit', function (ev) {
 77 |   ev.preventDefault()
 78 |   p.signal(JSON.parse(document.querySelector('#incoming').value))
 79 | })
 80 | 
 81 | p.on('connect', function () {
 82 |   console.log('CONNECT')
 83 |   p.send('whatever' + Math.random())
 84 | })
 85 | 
 86 | p.on('data', function (data) {
 87 |   console.log('data: ' + data)
 88 | })
 89 | ```
 90 | 
 91 | Visit `index.html#1` from one browser (the initiator) and `index.html` from another
 92 | browser (the receiver).
 93 | 
 94 | An "offer" will be generated by the initiator. Paste this into the receiver's form and
 95 | hit submit. The receiver generates an "answer". Paste this into the initiator's form and
 96 | hit submit.
 97 | 
 98 | Now you have a direct P2P connection between two browsers!
 99 | 
100 | ### A simpler example
101 | 
102 | This example create two peers **in the same web page**.
103 | 
104 | In a real-world application, *you would never do this*. The sender and receiver `Peer`
105 | instances would exist in separate browsers. A "signaling server" (usually implemented with
106 | websockets) would be used to exchange signaling data between the two browsers until a
107 | peer-to-peer connection is established.
108 | 
109 | ### data channels
110 | 
111 | ```js
112 | var Peer = require('simple-peer')
113 | 
114 | var peer1 = new Peer({ initiator: true })
115 | var peer2 = new Peer()
116 | 
117 | peer1.on('signal', function (data) {
118 |   // when peer1 has signaling data, give it to peer2 somehow
119 |   peer2.signal(data)
120 | })
121 | 
122 | peer2.on('signal', function (data) {
123 |   // when peer2 has signaling data, give it to peer1 somehow
124 |   peer1.signal(data)
125 | })
126 | 
127 | peer1.on('connect', function () {
128 |   // wait for 'connect' event before using the data channel
129 |   peer1.send('hey peer2, how is it going?')
130 | })
131 | 
132 | peer2.on('data', function (data) {
133 |   // got a data channel message
134 |   console.log('got a message from peer1: ' + data)
135 | })
136 | ```
137 | 
138 | ### video/voice
139 | 
140 | Video/voice is also super simple! In this example, peer1 sends video to peer2.
141 | 
142 | ```js
143 | var Peer = require('simple-peer')
144 | 
145 | // get video/voice stream
146 | navigator.getUserMedia({ video: true, audio: true }, gotMedia, function () {})
147 | 
148 | function gotMedia (stream) {
149 |   var peer1 = new Peer({ initiator: true, stream: stream })
150 |   var peer2 = new Peer()
151 | 
152 |   peer1.on('signal', function (data) {
153 |     peer2.signal(data)
154 |   })
155 | 
156 |   peer2.on('signal', function (data) {
157 |     peer1.signal(data)
158 |   })
159 | 
160 |   peer2.on('stream', function (stream) {
161 |     // got remote video stream, now let's show it in a video tag
162 |     var video = document.querySelector('video')
163 |     video.src = window.URL.createObjectURL(stream)
164 |     video.play()
165 |   })
166 | }
167 | ```
168 | 
169 | For two-way video, simply pass a `stream` option into both `Peer` constructors. Simple!
170 | 
171 | ### in node
172 | 
173 | To use this library in node, pass in `opts.wrtc` as a parameter:
174 | 
175 | ```js
176 | var Peer = require('simple-peer')
177 | var wrtc = require('wrtc')
178 | 
179 | var peer1 = new Peer({ initiator: true, wrtc: wrtc })
180 | var peer2 = new Peer({ wrtc: wrtc })
181 | ```
182 | 
183 | ## Who is using `simple-peer`?
184 | 
185 | - [WebTorrent](http://webtorrent.io) - Streaming torrent client in the browser
186 | - [Instant.io](https://instant.io) - Secure, anonymous, streaming file transfer
187 | - [Zencastr](https://zencastr.com) - Easily record your remote podcast interviews in studio quality.
188 | - [Friends](https://github.com/moose-team/friends) - Peer-to-peer chat powered by the web
189 | - [Socket.io-p2p](https://github.com/socketio/socket.io-p2p) - Official Socket.io P2P communication library
190 | - [ScreenCat](https://maxogden.github.io/screencat/) - Screen sharing + remote collaboration app
191 | - [WebCat](https://www.npmjs.com/package/webcat) - P2P pipe across the web using Github private/public key for auth
192 | - [RTCCat](https://www.npmjs.com/package/rtcat) - WebRTC netcat
193 | - [PeerNet](https://www.npmjs.com/package/peernet) - Peer-to-peer gossip network using randomized algorithms
194 | - [PusherTC](http://pushertc.herokuapp.com) - Video chat with using Pusher. See [guide](http://blog.carbonfive.com/2014/10/16/webrtc-made-simple/).
195 | - [lxjs-chat](https://github.com/feross/lxjs-chat) - Omegle-like video chat site
196 | - [Whiteboard](https://github.com/feross/whiteboard) - P2P Whiteboard powered by WebRTC and WebTorrent
197 | - [Peer Calls](https://peercalls.com) - WebRTC group video calling. Create a room. Share the link.
198 | - [Netsix](https://mmorainville.github.io/netsix-gh-pages/) - Send videos to your friends using WebRTC so that they can watch them right away.
199 | - [Stealthy](https://www.stealthy.im) - Stealthy is a decentralized, end-to-end encrypted, p2p chat application.
200 | - [oorja.io](https://github.com/akshayKMR/oorja) - Effortless video-voice chat with realtime collaborative features. Extensible using react components 🙌
201 | - [TalktoMe](https://talktome.space) - Skype alternative for audio/video conferencing based on WebRTC, but without the loss of packets.
202 | - [CDNBye](https://github.com/cdnbye/hlsjs-p2p-engine) - CDNBye implements WebRTC datachannel to scale live/vod video streaming by peer-to-peer network using bittorrent-like protocol
203 | - [Detox](https://github.com/Detox) - Overlay network for distributed anonymous P2P communications entirely in the browser
204 | - [Metastream](https://github.com/samuelmaddock/metastream) - Watch streaming media with friends.
205 | - [firepeer](https://github.com/natzcam/firepeer) - secure signalling and authentication using firebase realtime database
206 | - *Your app here! - send a PR!*
207 | 
208 | ## api
209 | 
210 | ### `peer = new Peer([opts])`
211 | 
212 | Create a new WebRTC peer connection.
213 | 
214 | A "data channel" for text/binary communication is always established, because it's cheap and often useful. For video/voice communication, pass the `stream` option.
215 | 
216 | If `opts` is specified, then the default options (shown below) will be overridden.
217 | 
218 | ```
219 | {
220 |   initiator: false,
221 |   channelConfig: {},
222 |   channelName: 'default',
223 |   config: { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:global.stun.twilio.com:3478?transport=udp' }] },
224 |   offerOptions: {},
225 |   answerOptions: {},
226 |   sdpTransform: function (sdp) { return sdp },
227 |   stream: false,
228 |   streams: [],
229 |   trickle: true,
230 |   allowHalfTrickle: false,
231 |   wrtc: {}, // RTCPeerConnection/RTCSessionDescription/RTCIceCandidate
232 |   objectMode: false
233 | }
234 | ```
235 | 
236 | The options do the following:
237 | 
238 | - `initiator` - set to `true` if this is the initiating peer
239 | - `channelConfig` - custom webrtc data channel configuration (used by `createDataChannel`)
240 | - `channelName` - custom webrtc data channel name
241 | - `config` - custom webrtc configuration (used by `RTCPeerConnection` constructor)
242 | - `offerOptions` - custom offer options (used by `createOffer` method)
243 | - `answerOptions` - custom answer options (used by `createAnswer` method)
244 | - `sdpTransform` - function to transform the generated SDP signaling data (for advanced users)
245 | - `stream` - if video/voice is desired, pass stream returned from `getUserMedia`
246 | - `streams` - an array of MediaStreams returned from `getUserMedia`
247 | - `trickle` - set to `false` to disable [trickle ICE](http://webrtchacks.com/trickle-ice/) and get a single 'signal' event (slower)
248 | - `wrtc` - custom webrtc implementation, mainly useful in node to specify in the [wrtc](https://npmjs.com/package/wrtc) package
249 | - `objectMode` - set to `true` to create the stream in [Object Mode](https://nodejs.org/api/stream.html#stream_object_mode). In this mode, incoming string data is not automatically converted to `Buffer` objects.
250 | 
251 | ### `peer.signal(data)`
252 | 
253 | Call this method whenever the remote peer emits a `peer.on('signal')` event.
254 | 
255 | The `data` will encapsulate a webrtc offer, answer, or ice candidate. These messages help
256 | the peers to eventually establish a direct connection to each other. The contents of these
257 | strings are an implementation detail that can be ignored by the user of this module;
258 | simply pass the data from 'signal' events to the remote peer and call `peer.signal(data)`
259 | to get connected.
260 | 
261 | ### `peer.send(data)`
262 | 
263 | Send text/binary data to the remote peer. `data` can be any of several types: `String`,
264 | `Buffer` (see [buffer](https://github.com/feross/buffer)), `ArrayBufferView` (`Uint8Array`,
265 | etc.), `ArrayBuffer`, or `Blob` (in browsers that support it).
266 | 
267 | Note: If this method is called before the `peer.on('connect')` event has fired, then data
268 | will be buffered.
269 | 
270 | ### `peer.addStream(stream)`
271 | 
272 | Add a `MediaStream` to the connection.
273 | 
274 | ### `peer.removeStream(stream)`
275 | 
276 | Remove a `MediaStream` from the connection.
277 | 
278 | ### `peer.addTrack(track, stream)`
279 | 
280 | Add a `MediaStreamTrack` to the connection. Must also pass the `MediaStream` you want to attach it to.
281 | 
282 | ### `peer.removeTrack(track, stream)`
283 | 
284 | Remove a `MediaStreamTrack` from the connection. Must also pass the `MediaStream` that it was attached to.
285 | 
286 | ### `peer.addTransceiver(kind, init)`
287 | 
288 | Add a `RTCRtpTransceiver` to the connection. Can be used to add transceivers before adding tracks. Automatically called as neccesary by `addTrack`.
289 | 
290 | ### `datachannel = peer.createDataChannel(channelName, channelConfig)`
291 | 
292 | Used to create additional DataChannel objects. DataChannels are instances of `stream.Duplex`.
293 | 
294 | ### `peer.destroy([err])`
295 | 
296 | Destroy and cleanup this peer connection.
297 | 
298 | If the optional `err` parameter is passed, then it will be emitted as an `'error'`
299 | event on the stream.
300 | 
301 | ### `Peer.WEBRTC_SUPPORT`
302 | 
303 | Detect native WebRTC support in the javascript environment.
304 | 
305 | ```js
306 | var Peer = require('simple-peer')
307 | 
308 | if (Peer.WEBRTC_SUPPORT) {
309 |   // webrtc support!
310 | } else {
311 |   // fallback
312 | }
313 | ```
314 | 
315 | ### duplex stream
316 | 
317 | `Peer` objects are instances of `stream.Duplex`. They behave very similarly to a
318 | `net.Socket` from the node core `net` module. The duplex stream reads/writes to the data
319 | channel.
320 | 
321 | ```js
322 | var peer = new Peer(opts)
323 | // ... signaling ...
324 | peer.write(new Buffer('hey'))
325 | peer.on('data', function (chunk) {
326 |   console.log('got a chunk', chunk)
327 | })
328 | ```
329 | 
330 | ## events
331 | 
332 | 
333 | ### `peer.on('signal', function (data) {})`
334 | 
335 | Fired when the peer wants to send signaling data to the remote peer.
336 | 
337 | **It is the responsibility of the application developer (that's you!) to get this data to
338 | the other peer.** This usually entails using a websocket signaling server. This data is an
339 | `Object`, so  remember to call `JSON.stringify(data)` to serialize it first. Then, simply
340 | call `peer.signal(data)` on the remote peer.
341 | 
342 | (Be sure to listen to this event immediately to avoid missing it. For `initiator: true`
343 | peers, it fires right away. For `initatior: false` peers, it fires when the remote
344 | offer is received.)
345 | 
346 | ### `peer.on('connect', function () {})`
347 | 
348 | Fired when the peer connection and data channel are ready to use.
349 | 
350 | ### `peer.on('data', function (data) {})`
351 | 
352 | Received a message from the remote peer (via the data channel).
353 | 
354 | `data` will be either a `String` or a `Buffer/Uint8Array` (see [buffer](https://github.com/feross/buffer)).
355 | 
356 | ### `peer.on('stream', function (stream) {})`
357 | 
358 | Received a remote video stream, which can be displayed in a video tag:
359 | 
360 | ```js
361 | peer.on('stream', function (stream) {
362 |   var video = document.createElement('video')
363 |   video.src = window.URL.createObjectURL(stream)
364 |   document.body.appendChild(video)
365 |   video.play()
366 | })
367 | ```
368 | 
369 | ### `peer.on('track', function (track, stream) {})`
370 | 
371 | Received a remote audio/video track. Streams may contain multiple tracks.
372 | 
373 | ### `peer.on('datachannel', function (datachannel, channelName) {})`
374 | 
375 | Received an additional DataChannel. This fires after the remote peer calls `peer.createDataChannel()`.
376 | 
377 | ### `peer.on('close', function () {})`
378 | 
379 | Called when the peer connection has closed.
380 | 
381 | ### `peer.on('error', function (err) {})`
382 | 
383 | Fired when a fatal error occurs. Usually, this means bad signaling data was received from the remote peer.
384 | 
385 | `err` is an `Error` object.
386 | 
387 | ## error codes
388 | 
389 | Errors returned by the `error` event have an `err.code` property that will indicate the origin of the failure.
390 | 
391 | Possible error codes:
392 | - `ERR_WEBRTC_SUPPORT`
393 | - `ERR_CREATE_OFFER`
394 | - `ERR_CREATE_ANSWER`
395 | - `ERR_SET_LOCAL_DESCRIPTION`
396 | - `ERR_SET_REMOTE_DESCRIPTION`
397 | - `ERR_ADD_ICE_CANDIDATE`
398 | - `ERR_ICE_CONNECTION_FAILURE`
399 | - `ERR_SIGNALING`
400 | - `ERR_DATA_CHANNEL`
401 | 
402 | 
403 | ## connecting more than 2 peers?
404 | 
405 | The simplest way to do that is to create a full-mesh topology. That means that every peer
406 | opens a connection to every other peer. To illustrate:
407 | 
408 | ![full mesh topology](img/full-mesh.png)
409 | 
410 | To broadcast a message, just iterate over all the peers and call `peer.send`.
411 | 
412 | So, say you have 3 peers. Then, when a peer wants to send some data it must send it 2
413 | times, once to each of the other peers. So you're going to want to be a bit careful about
414 | the size of the data you send.
415 | 
416 | Full mesh topologies don't scale well when the number of peers is very large. The total
417 | number of edges in the network will be ![full mesh formula](img/full-mesh-formula.png)
418 | where `n` is the number of peers.
419 | 
420 | For clarity, here is the code to connect 3 peers together:
421 | 
422 | #### Peer 1
423 | 
424 | ```js
425 | // These are peer1's connections to peer2 and peer3
426 | var peer2 = new Peer({ initiator: true })
427 | var peer3 = new Peer({ initiator: true })
428 | 
429 | peer2.on('signal', function (data) {
430 |   // send this signaling data to peer2 somehow
431 | })
432 | 
433 | peer2.on('connect', function () {
434 |   peer2.send('hi peer2, this is peer1')
435 | })
436 | 
437 | peer2.on('data', function (data) {
438 |   console.log('got a message from peer2: ' + data)
439 | })
440 | 
441 | peer3.on('signal', function (data) {
442 |   // send this signaling data to peer3 somehow
443 | })
444 | 
445 | peer3.on('connect', function () {
446 |   peer3.send('hi peer3, this is peer1')
447 | })
448 | 
449 | peer3.on('data', function (data) {
450 |   console.log('got a message from peer3: ' + data)
451 | })
452 | ```
453 | 
454 | #### Peer 2
455 | 
456 | ```js
457 | // These are peer2's connections to peer1 and peer3
458 | var peer1 = new Peer()
459 | var peer3 = new Peer({ initiator: true })
460 | 
461 | peer1.on('signal', function (data) {
462 |   // send this signaling data to peer1 somehow
463 | })
464 | 
465 | peer1.on('connect', function () {
466 |   peer1.send('hi peer1, this is peer2')
467 | })
468 | 
469 | peer1.on('data', function (data) {
470 |   console.log('got a message from peer1: ' + data)
471 | })
472 | 
473 | peer3.on('signal', function (data) {
474 |   // send this signaling data to peer3 somehow
475 | })
476 | 
477 | peer3.on('connect', function () {
478 |   peer3.send('hi peer3, this is peer2')
479 | })
480 | 
481 | peer3.on('data', function (data) {
482 |   console.log('got a message from peer3: ' + data)
483 | })
484 | ```
485 | 
486 | #### Peer 3
487 | 
488 | ```js
489 | // These are peer3's connections to peer1 and peer2
490 | var peer1 = new Peer()
491 | var peer2 = new Peer()
492 | 
493 | peer1.on('signal', function (data) {
494 |   // send this signaling data to peer1 somehow
495 | })
496 | 
497 | peer1.on('connect', function () {
498 |   peer1.send('hi peer1, this is peer3')
499 | })
500 | 
501 | peer1.on('data', function (data) {
502 |   console.log('got a message from peer1: ' + data)
503 | })
504 | 
505 | peer2.on('signal', function (data) {
506 |   // send this signaling data to peer2 somehow
507 | })
508 | 
509 | peer2.on('connect', function () {
510 |   peer2.send('hi peer2, this is peer3')
511 | })
512 | 
513 | peer2.on('data', function (data) {
514 |   console.log('got a message from peer2: ' + data)
515 | })
516 | ```
517 | 
518 | ## memory usage
519 | 
520 | If you call `peer.send(buf)`, `simple-peer` is not keeping a reference to `buf`
521 | and sending the buffer at some later point in time. We immediately call
522 | `channel.send()` on the data channel. So it should be fine to mutate the buffer
523 | right afterward.
524 | 
525 | However, beware that `peer.write(buf)` (a writable stream method) does not have
526 | the same contract. It will potentially buffer the data and call
527 | `channel.send()` at a future point in time, so definitely don't assume it's
528 | safe to mutate the buffer.
529 | 
530 | 
531 | ## connection does not work on some networks?
532 | 
533 | If a direct connection fails, in particular, because of NAT traversal and/or firewalls,
534 | WebRTC ICE uses an intermediary (relay) TURN server. In other words, ICE will first use
535 | STUN with UDP to directly connect peers and, if that fails, will fall back to a TURN relay
536 | server.
537 | 
538 | In order to use a TURN server, you must specify the `config` option to the `Peer`
539 | constructor. See the API docs above.
540 | 
541 | [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard)
542 | 
543 | ## license
544 | 
545 | MIT. Copyright (c) [Feross Aboukhadijeh](http://feross.org).
546 | 


--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
  1 | module.exports = Peer
  2 | 
  3 | var debug = require('debug')('simple-peer')
  4 | var getBrowserRTC = require('get-browser-rtc')
  5 | var inherits = require('inherits')
  6 | var randombytes = require('randombytes')
  7 | var DataChannel = require('./datachannel')
  8 | 
  9 | var ICECOMPLETE_TIMEOUT = 5 * 1000
 10 | 
 11 | inherits(Peer, DataChannel)
 12 | 
 13 | /**
 14 |  * WebRTC peer connection. Same API as node core `net.Socket`, plus a few extra methods.
 15 |  * Duplex stream.
 16 |  * @param {Object} opts
 17 |  */
 18 | function Peer (opts) {
 19 |   var self = this
 20 |   if (!(self instanceof Peer)) return new Peer(opts)
 21 | 
 22 |   opts = opts || {}
 23 | 
 24 |   self._id = randombytes(4).toString('hex').slice(0, 7)
 25 |   self._debug('new peer %o', opts)
 26 | 
 27 |   DataChannel.call(self, opts) // the Peer is a DataChannel
 28 | 
 29 |   self.initiator = opts.initiator || false
 30 |   self.channelName = opts.channelName
 31 |   self.channelConfig = opts.channelConfig || Peer.channelConfig
 32 |   self.config = Object.assign({}, Peer.config, opts.config)
 33 |   self.offerOptions = opts.offerOptions || {}
 34 |   self.answerOptions = opts.answerOptions || {}
 35 |   self.sdpTransform = opts.sdpTransform || function (sdp) { return sdp }
 36 |   self.streams = opts.streams || (opts.stream ? [opts.stream] : []) // support old "stream" option
 37 |   self.trickle = opts.trickle !== undefined ? opts.trickle : true
 38 |   self.allowHalfTrickle = opts.allowHalfTrickle !== undefined ? opts.allowHalfTrickle : false
 39 |   self.iceCompleteTimeout = opts.iceCompleteTimeout || ICECOMPLETE_TIMEOUT
 40 | 
 41 |   self.destroyed = false
 42 |   self.connected = false
 43 | 
 44 |   self.remoteAddress = undefined
 45 |   self.remoteFamily = undefined
 46 |   self.remotePort = undefined
 47 |   self.localAddress = undefined
 48 |   self.localFamily = undefined
 49 |   self.localPort = undefined
 50 | 
 51 |   self._wrtc = (opts.wrtc && typeof opts.wrtc === 'object')
 52 |     ? opts.wrtc
 53 |     : getBrowserRTC()
 54 | 
 55 |   if (!self._wrtc) {
 56 |     if (typeof window === 'undefined') {
 57 |       throw makeError('No WebRTC support: Specify `opts.wrtc` option in this environment', 'ERR_WEBRTC_SUPPORT')
 58 |     } else {
 59 |       throw makeError('No WebRTC support: Not a supported browser', 'ERR_WEBRTC_SUPPORT')
 60 |     }
 61 |   }
 62 | 
 63 |   self._pcReady = false
 64 |   self._channelReady = false
 65 |   self._iceComplete = false // ice candidate trickle done (got null candidate)
 66 |   self._iceCompleteTimer = null // send an offer/answer anyway after some timeout
 67 |   self._pendingCandidates = []
 68 | 
 69 |   self._isNegotiating = !self.initiator // is this peer waiting for negotiation to complete?
 70 |   self._batchedNegotiation = false // batch synchronous negotiations
 71 |   self._queuedNegotiation = false // is there a queued negotiation request?
 72 |   self._sendersAwaitingStable = []
 73 |   self._senderMap = new Map()
 74 |   self._firstStable = true
 75 | 
 76 |   self._remoteTracks = []
 77 |   self._remoteStreams = []
 78 | 
 79 |   self._channels = []
 80 |   self._channelNameCounter = 0
 81 | 
 82 |   try {
 83 |     self._pc = new (self._wrtc.RTCPeerConnection)(self.config)
 84 |   } catch (err) {
 85 |     self.destroy(err)
 86 |   }
 87 | 
 88 |   // We prefer feature detection whenever possible, but sometimes that's not
 89 |   // possible for certain implementations.
 90 |   self._isReactNativeWebrtc = typeof self._pc._peerConnectionId === 'number'
 91 | 
 92 |   self._pc.oniceconnectionstatechange = function () {
 93 |     self._onIceStateChange()
 94 |   }
 95 |   self._pc.onicegatheringstatechange = function () {
 96 |     self._onIceStateChange()
 97 |   }
 98 |   self._pc.onsignalingstatechange = function () {
 99 |     self._onSignalingStateChange()
100 |   }
101 |   self._pc.onicecandidate = function (event) {
102 |     self._onIceCandidate(event)
103 |   }
104 | 
105 |   // Other spec events, unused by this implementation:
106 |   // - onconnectionstatechange
107 |   // - onicecandidateerror
108 |   // - onfingerprintfailure
109 |   // - onnegotiationneeded
110 | 
111 |   if (self.initiator || self.channelConfig.negotiated) {
112 |     var channelName = self._makeUniqueChannelName(self.channelName || 'default')
113 |     var channel = self._pc.createDataChannel(channelName, self.channelConfig) // use label 'default' for datachannel correlation
114 |     self._setDataChannel(channel)
115 |   }
116 |   self._pc.ondatachannel = function (event) {
117 |     self._debug('ondatachannel', event.channel.label)
118 | 
119 |     if (!self._channels[0]._channel) {
120 |       self._setDataChannel(event.channel)
121 |     } else {
122 |       var channel = new DataChannel(opts)
123 |       channel._setDataChannel(event.channel)
124 |       self._channels.push(channel)
125 |       self.emit('datachannel', channel, channel.channelName)
126 |     }
127 |   }
128 |   self._channels.push(self)
129 | 
130 |   if (self.streams) {
131 |     self.streams.forEach(function (stream) {
132 |       self.addStream(stream)
133 |     })
134 |   }
135 |   self._pc.ontrack = function (event) {
136 |     self._onTrack(event)
137 |   }
138 | 
139 |   self.on('open', function () {
140 |     self._channelReady = true
141 |     self._maybeReady()
142 |   })
143 | 
144 |   if (self.initiator) {
145 |     self._needsNegotiation()
146 |   }
147 | }
148 | 
149 | Peer.WEBRTC_SUPPORT = !!getBrowserRTC()
150 | 
151 | /**
152 |  * Expose peer and data channel config for overriding all Peer
153 |  * instances. Otherwise, just set opts.config or opts.channelConfig
154 |  * when constructing a Peer.
155 |  */
156 | Peer.config = {
157 |   iceServers: [
158 |     {
159 |       urls: 'stun:stun.l.google.com:19302'
160 |     },
161 |     {
162 |       urls: 'stun:global.stun.twilio.com:3478?transport=udp'
163 |     }
164 |   ],
165 |   sdpSemantics: 'unified-plan'
166 | }
167 | Peer.channelConfig = {}
168 | 
169 | Peer.prototype.address = function () {
170 |   var self = this
171 |   return { port: self.localPort, family: self.localFamily, address: self.localAddress }
172 | }
173 | 
174 | Peer.prototype.signal = function (data) {
175 |   var self = this
176 |   if (self.destroyed) throw makeError('cannot signal after peer is destroyed', 'ERR_SIGNALING')
177 |   if (typeof data === 'string') {
178 |     try {
179 |       data = JSON.parse(data)
180 |     } catch (err) {
181 |       data = {}
182 |     }
183 |   }
184 |   self._debug('signal()')
185 | 
186 |   if (data.renegotiate && self.initiator) {
187 |     self._debug('got request to renegotiate')
188 |     self._needsNegotiation()
189 |   }
190 |   if (data.transceiverRequest && self.initiator) {
191 |     self._debug('got request for transceiver')
192 |     self.addTransceiver(data.transceiverRequest.kind, data.transceiverRequest.init)
193 |   }
194 |   if (data.candidate) {
195 |     if (self._pc.localDescription && self._pc.localDescription.type && self._pc.remoteDescription && self._pc.remoteDescription.type) {
196 |       self._addIceCandidate(data.candidate)
197 |     } else {
198 |       self._pendingCandidates.push(data.candidate)
199 |     }
200 |   }
201 |   if (data.sdp) {
202 |     self._pc.setRemoteDescription(new (self._wrtc.RTCSessionDescription)(data)).then(function () {
203 |       if (self.destroyed) return
204 | 
205 |       self._pendingCandidates.forEach(function (candidate) {
206 |         self._addIceCandidate(candidate)
207 |       })
208 |       self._pendingCandidates = []
209 | 
210 |       if (self._pc.remoteDescription.type === 'offer') self._createAnswer()
211 |     }).catch(function (err) { self.destroy(makeError(err, 'ERR_SET_REMOTE_DESCRIPTION')) })
212 |   }
213 |   if (!data.sdp && !data.candidate && !data.renegotiate && !data.transceiverRequest) {
214 |     self.destroy(makeError('signal() called with invalid signal data', 'ERR_SIGNALING'))
215 |   }
216 | }
217 | 
218 | Peer.prototype._addIceCandidate = function (candidate) {
219 |   var self = this
220 |   self._pc.addIceCandidate(new self._wrtc.RTCIceCandidate(candidate)).catch(function (err) {
221 |     // HACK: node-webrtc throws an incorrect error https://github.com/node-webrtc/node-webrtc/issues/498
222 |     if (self._pc.signalingState !== 'closed' && err.message === 'Failed to set ICE candidate; RTCPeerConnection is closed.') {
223 |       return self._debug('ignoring incorrect wrtc error')
224 |     }
225 |     self.destroy(makeError(err, 'ERR_ADD_ICE_CANDIDATE'))
226 |   })
227 | }
228 | 
229 | Peer.prototype.createDataChannel = function (channelName, channelConfig, opts) {
230 |   var self = this
231 |   var channel = new DataChannel(opts)
232 |   channelName = self._makeUniqueChannelName(channelName)
233 |   channel._setDataChannel(self._pc.createDataChannel(channelName, channelConfig))
234 |   self._channels.push(channel)
235 |   return channel
236 | }
237 | 
238 | /**
239 |  * Add a Transceiver to the connection.
240 |  * @param {String} kind
241 |  * @param {Object} init
242 |  */
243 | Peer.prototype.addTransceiver = function (kind, init) {
244 |   var self = this
245 | 
246 |   self._debug('addTransceiver()')
247 | 
248 |   if (self.initiator) {
249 |     try {
250 |       self._pc.addTransceiver(kind, init)
251 |       self._needsNegotiation()
252 |     } catch (err) {
253 |       self.destroy(err)
254 |     }
255 |   } else {
256 |     self.emit('signal', { // request initiator to renegotiate
257 |       transceiverRequest: { kind, init }
258 |     })
259 |   }
260 | }
261 | 
262 | /**
263 |  * Add a Transceiver to the connection.
264 |  * @param {String} kind
265 |  * @param {Object} init
266 |  */
267 | Peer.prototype.addTransceiver = function (kind, init) {
268 |   var self = this
269 | 
270 |   self._debug('addTransceiver()')
271 | 
272 |   if (self.initiator) {
273 |     try {
274 |       self._pc.addTransceiver(kind, init)
275 |       self._needsNegotiation()
276 |     } catch (err) {
277 |       self.destroy(err)
278 |     }
279 |   } else {
280 |     self.emit('signal', { // request initiator to renegotiate
281 |       transceiverRequest: { kind, init }
282 |     })
283 |   }
284 | }
285 | 
286 | /**
287 |  * Add a MediaStream to the connection.
288 |  * @param {MediaStream} stream
289 |  */
290 | Peer.prototype.addStream = function (stream) {
291 |   var self = this
292 | 
293 |   self._debug('addStream()')
294 | 
295 |   stream.getTracks().forEach(function (track) {
296 |     self.addTrack(track, stream)
297 |   })
298 | }
299 | 
300 | /**
301 |  * Add a MediaStreamTrack to the connection.
302 |  * @param {MediaStreamTrack} track
303 |  * @param {MediaStream} stream
304 |  */
305 | Peer.prototype.addTrack = function (track, stream) {
306 |   var self = this
307 | 
308 |   self._debug('addTrack()')
309 | 
310 |   var submap = self._senderMap.get(track) || new Map() // nested Maps map [track, stream] to sender
311 |   var sender = submap.get(stream)
312 |   if (!sender) {
313 |     sender = self._pc.addTrack(track, stream)
314 |     submap.set(stream, sender)
315 |     self._senderMap.set(track, submap)
316 |     self._needsNegotiation()
317 |   } else if (sender.removed) {
318 |     self.destroy(makeError('Track has been removed. You should enable/disable tracks that you want to re-add.'), 'ERR_SENDER_REMOVED')
319 |   } else {
320 |     self.destroy(makeError('Track has already been added to that stream.'), 'ERR_SENDER_ALREADY_ADDED')
321 |   }
322 | }
323 | 
324 | /**
325 |  * Replace a MediaStreamTrack by another in the connection.
326 |  * @param {MediaStreamTrack} oldTrack
327 |  * @param {MediaStreamTrack} newTrack
328 |  * @param {MediaStream} stream
329 |  */
330 | Peer.prototype.replaceTrack = function (oldTrack, newTrack, stream) {
331 |   var self = this
332 | 
333 |   self._debug('replaceTrack()')
334 | 
335 |   var submap = self._senderMap.get(oldTrack)
336 |   var sender = submap ? submap.get(stream) : null
337 |   if (!sender) {
338 |     self.destroy(makeError('Cannot replace track that was never added.'), 'ERR_TRACK_NOT_ADDED')
339 |   }
340 |   if (newTrack) self._senderMap.set(newTrack, submap)
341 | 
342 |   if (sender.replaceTrack != null) {
343 |     sender.replaceTrack(newTrack)
344 |   } else {
345 |     self.destroy(makeError('replaceTrack is not supported in this browser', 'ERR_UNSUPPORTED_REPLACETRACK'))
346 |   }
347 | }
348 | 
349 | /**
350 |  * Remove a MediaStreamTrack from the connection.
351 |  * @param {MediaStreamTrack} track
352 |  * @param {MediaStream} stream
353 |  */
354 | Peer.prototype.removeTrack = function (track, stream) {
355 |   var self = this
356 | 
357 |   self._debug('removeSender()')
358 | 
359 |   var submap = self._senderMap.get(track)
360 |   var sender = submap ? submap.get(stream) : null
361 |   if (!sender) {
362 |     self.destroy(makeError('Cannot remove track that was never added.', 'ERR_TRACK_NOT_ADDED'))
363 |   }
364 |   try {
365 |     sender.removed = true
366 |     self._pc.removeTrack(sender)
367 |   } catch (err) {
368 |     if (err.name === 'NS_ERROR_UNEXPECTED') {
369 |       self._sendersAwaitingStable.push(sender) // HACK: Firefox must wait until (signalingState === stable) https://bugzilla.mozilla.org/show_bug.cgi?id=1133874
370 |     } else {
371 |       self.destroy(err)
372 |     }
373 |   }
374 |   self._needsNegotiation()
375 | }
376 | 
377 | /**
378 |  * Remove a MediaStream from the connection.
379 |  * @param {MediaStream} stream
380 |  */
381 | Peer.prototype.removeStream = function (stream) {
382 |   var self = this
383 | 
384 |   self._debug('removeSenders()')
385 | 
386 |   stream.getTracks().forEach(function (track) {
387 |     self.removeTrack(track, stream)
388 |   })
389 | }
390 | 
391 | Peer.prototype._needsNegotiation = function () {
392 |   var self = this
393 | 
394 |   self._debug('_needsNegotiation')
395 |   if (self._batchedNegotiation) return // batch synchronous renegotiations
396 |   self._batchedNegotiation = true
397 |   setTimeout(function () {
398 |     self._batchedNegotiation = false
399 |     self._debug('starting batched negotiation')
400 |     self.negotiate()
401 |   }, 0)
402 | }
403 | 
404 | Peer.prototype.negotiate = function () {
405 |   var self = this
406 | 
407 |   if (self.initiator) {
408 |     if (self._isNegotiating) {
409 |       self._queuedNegotiation = true
410 |       self._debug('already negotiating, queueing')
411 |     } else {
412 |       self._debug('start negotiation')
413 |       setTimeout(() => { // HACK: Chrome crashes if we immediately call createOffer
414 |         self._createOffer()
415 |       }, 0)
416 |     }
417 |   } else {
418 |     if (!self._isNegotiating) {
419 |       self._debug('requesting negotiation from initiator')
420 |       self.emit('signal', { // request initiator to renegotiate
421 |         renegotiate: true
422 |       })
423 |     }
424 |   }
425 |   self._isNegotiating = true
426 | }
427 | 
428 | // TODO: Delete this method once readable-stream is updated to contain a default
429 | // implementation of destroy() that automatically calls _destroy()
430 | // See: https://github.com/nodejs/readable-stream/issues/283
431 | Peer.prototype.destroy = function (err) {
432 |   var self = this
433 |   if (self.destroyed) return
434 | 
435 |   self._debug('destroy (error: %s)', err && (err.message || err))
436 | 
437 |   self._channels.forEach(function (channel) {
438 |     DataChannel.prototype.destroy.apply(channel, err)
439 |   })
440 |   self._channels = null
441 |   self._channelNameCounter = null
442 | 
443 |   self.destroyed = true
444 |   self.connected = false
445 |   self._pcReady = false
446 |   self._remoteTracks = null
447 |   self._remoteStreams = null
448 |   self._senderMap = null
449 | 
450 |   if (self._pc) {
451 |     try {
452 |       self._pc.close()
453 |     } catch (err) {}
454 | 
455 |     self._pc.oniceconnectionstatechange = null
456 |     self._pc.onicegatheringstatechange = null
457 |     self._pc.onsignalingstatechange = null
458 |     self._pc.onicecandidate = null
459 |     self._pc.ontrack = null
460 |     self._pc.ondatachannel = null
461 |   }
462 |   self._pc = null
463 | }
464 | 
465 | Peer.prototype._startIceCompleteTimeout = function () {
466 |   var self = this
467 |   if (self.destroyed) return
468 |   if (self._iceCompleteTimer) return
469 |   self._debug('started iceComplete timeout')
470 |   self._iceCompleteTimer = setTimeout(function () {
471 |     if (!self._iceComplete) {
472 |       self._iceComplete = true
473 |       self._debug('iceComplete timeout completed')
474 |       self.emit('iceTimeout')
475 |       self.emit('_iceComplete')
476 |     }
477 |   }, self.iceCompleteTimeout)
478 | }
479 | 
480 | Peer.prototype._createOffer = function () {
481 |   var self = this
482 |   if (self.destroyed) return
483 | 
484 |   self._pc.createOffer(self.offerOptions).then(function (offer) {
485 |     if (self.destroyed) return
486 |     if (!self.trickle && !self.allowHalfTrickle) offer.sdp = filterTrickle(offer.sdp)
487 |     offer.sdp = self.sdpTransform(offer.sdp)
488 |     self._pc.setLocalDescription(offer).then(onSuccess).catch(onError)
489 | 
490 |     function onSuccess () {
491 |       self._debug('createOffer success')
492 |       if (self.destroyed) return
493 |       if (self.trickle || self._iceComplete) sendOffer()
494 |       else self.once('_iceComplete', sendOffer) // wait for candidates
495 |     }
496 | 
497 |     function onError (err) {
498 |       self.destroy(makeError(err, 'ERR_SET_LOCAL_DESCRIPTION'))
499 |     }
500 | 
501 |     function sendOffer () {
502 |       if (self.destroyed) return
503 |       var signal = self._pc.localDescription || offer
504 |       self._debug('signal')
505 |       self.emit('signal', {
506 |         type: signal.type,
507 |         sdp: signal.sdp
508 |       })
509 |     }
510 |   }).catch(function (err) { self.destroy(makeError(err, 'ERR_CREATE_OFFER')) })
511 | }
512 | 
513 | Peer.prototype._requestMissingTransceivers = function () {
514 |   var self = this
515 | 
516 |   if (self._pc.getTransceivers) {
517 |     self._pc.getTransceivers().forEach(transceiver => {
518 |       if (!transceiver.mid && transceiver.sender.track) {
519 |         self.addTransceiver(transceiver.sender.track.kind)
520 |       }
521 |     })
522 |   }
523 | }
524 | 
525 | Peer.prototype._createAnswer = function () {
526 |   var self = this
527 |   if (self.destroyed) return
528 | 
529 |   self._pc.createAnswer(self.answerOptions).then(function (answer) {
530 |     if (self.destroyed) return
531 |     if (!self.trickle && !self.allowHalfTrickle) answer.sdp = filterTrickle(answer.sdp)
532 |     answer.sdp = self.sdpTransform(answer.sdp)
533 |     self._pc.setLocalDescription(answer).then(onSuccess).catch(onError)
534 | 
535 |     function onSuccess () {
536 |       if (self.destroyed) return
537 |       if (self.trickle || self._iceComplete) sendAnswer()
538 |       else self.once('_iceComplete', sendAnswer)
539 |     }
540 | 
541 |     function onError (err) {
542 |       self.destroy(makeError(err, 'ERR_SET_LOCAL_DESCRIPTION'))
543 |     }
544 | 
545 |     function sendAnswer () {
546 |       if (self.destroyed) return
547 |       var signal = self._pc.localDescription || answer
548 |       self._debug('signal')
549 |       self.emit('signal', {
550 |         type: signal.type,
551 |         sdp: signal.sdp
552 |       })
553 |       if (!self.initiator) self._requestMissingTransceivers()
554 |     }
555 |   }).catch(function (err) { self.destroy(makeError(err, 'ERR_CREATE_ANSWER')) })
556 | }
557 | 
558 | Peer.prototype._onIceStateChange = function () {
559 |   var self = this
560 |   if (self.destroyed) return
561 |   var iceConnectionState = self._pc.iceConnectionState
562 |   var iceGatheringState = self._pc.iceGatheringState
563 | 
564 |   self._debug(
565 |     'iceStateChange (connection: %s) (gathering: %s)',
566 |     iceConnectionState,
567 |     iceGatheringState
568 |   )
569 |   self.emit('iceStateChange', iceConnectionState, iceGatheringState)
570 | 
571 |   if (iceConnectionState === 'connected' || iceConnectionState === 'completed') {
572 |     self._pcReady = true
573 |     self._maybeReady()
574 |   }
575 |   if (iceConnectionState === 'failed') {
576 |     self.destroy(makeError('Ice connection failed.', 'ERR_ICE_CONNECTION_FAILURE'))
577 |   }
578 |   if (iceConnectionState === 'closed') {
579 |     self.destroy(makeError('Ice connection closed.', 'ERR_ICE_CONNECTION_CLOSED'))
580 |   }
581 | }
582 | 
583 | Peer.prototype.getStats = function (cb) {
584 |   var self = this
585 | 
586 |   // Promise-based getStats() (standard)
587 |   if (self._pc.getStats.length === 0) {
588 |     self._pc.getStats().then(function (res) {
589 |       var reports = []
590 |       res.forEach(function (report) {
591 |         reports.push(flattenValues(report))
592 |       })
593 |       cb(null, reports)
594 |     }, function (err) { cb(err) })
595 | 
596 |   // Two-parameter callback-based getStats() (deprecated, former standard)
597 |   } else if (self._isReactNativeWebrtc) {
598 |     self._pc.getStats(null, function (res) {
599 |       var reports = []
600 |       res.forEach(function (report) {
601 |         reports.push(flattenValues(report))
602 |       })
603 |       cb(null, reports)
604 |     }, function (err) { cb(err) })
605 | 
606 |   // Single-parameter callback-based getStats() (non-standard)
607 |   } else if (self._pc.getStats.length > 0) {
608 |     self._pc.getStats(function (res) {
609 |       // If we destroy connection in `connect` callback this code might happen to run when actual connection is already closed
610 |       if (self.destroyed) return
611 | 
612 |       var reports = []
613 |       res.result().forEach(function (result) {
614 |         var report = {}
615 |         result.names().forEach(function (name) {
616 |           report[name] = result.stat(name)
617 |         })
618 |         report.id = result.id
619 |         report.type = result.type
620 |         report.timestamp = result.timestamp
621 |         reports.push(flattenValues(report))
622 |       })
623 |       cb(null, reports)
624 |     }, function (err) { cb(err) })
625 | 
626 |   // Unknown browser, skip getStats() since it's anyone's guess which style of
627 |   // getStats() they implement.
628 |   } else {
629 |     cb(null, [])
630 |   }
631 | 
632 |   // statreports can come with a value array instead of properties
633 |   function flattenValues (report) {
634 |     if (Object.prototype.toString.call(report.values) === '[object Array]') {
635 |       report.values.forEach(function (value) {
636 |         Object.assign(report, value)
637 |       })
638 |     }
639 |     return report
640 |   }
641 | }
642 | 
643 | Peer.prototype._maybeReady = function () {
644 |   var self = this
645 |   self._debug('maybeReady pc %s channel %s', self._pcReady, self._channelReady)
646 |   if (self.connected || self._connecting || !self._pcReady || !self._channelReady) return
647 | 
648 |   self._connecting = true
649 | 
650 |   // HACK: We can't rely on order here, for details see https://github.com/js-platform/node-webrtc/issues/339
651 |   function findCandidatePair () {
652 |     if (self.destroyed) return
653 | 
654 |     self.getStats(function (err, items) {
655 |       if (self.destroyed) return
656 | 
657 |       // Treat getStats error as non-fatal. It's not essential.
658 |       if (err) items = []
659 | 
660 |       var remoteCandidates = {}
661 |       var localCandidates = {}
662 |       var candidatePairs = {}
663 |       var foundSelectedCandidatePair = false
664 | 
665 |       items.forEach(function (item) {
666 |         // TODO: Once all browsers support the hyphenated stats report types, remove
667 |         // the non-hypenated ones
668 |         if (item.type === 'remotecandidate' || item.type === 'remote-candidate') {
669 |           remoteCandidates[item.id] = item
670 |         }
671 |         if (item.type === 'localcandidate' || item.type === 'local-candidate') {
672 |           localCandidates[item.id] = item
673 |         }
674 |         if (item.type === 'candidatepair' || item.type === 'candidate-pair') {
675 |           candidatePairs[item.id] = item
676 |         }
677 |       })
678 | 
679 |       items.forEach(function (item) {
680 |         // Spec-compliant
681 |         if (item.type === 'transport' && item.selectedCandidatePairId) {
682 |           setSelectedCandidatePair(candidatePairs[item.selectedCandidatePairId])
683 |         }
684 | 
685 |         // Old implementations
686 |         if (
687 |           (item.type === 'googCandidatePair' && item.googActiveConnection === 'true') ||
688 |           ((item.type === 'candidatepair' || item.type === 'candidate-pair') && item.selected)
689 |         ) {
690 |           setSelectedCandidatePair(item)
691 |         }
692 |       })
693 | 
694 |       function setSelectedCandidatePair (selectedCandidatePair) {
695 |         foundSelectedCandidatePair = true
696 | 
697 |         var local = localCandidates[selectedCandidatePair.localCandidateId]
698 | 
699 |         if (local && (local.ip || local.address)) {
700 |           // Spec
701 |           self.localAddress = local.ip || local.address
702 |           self.localPort = Number(local.port)
703 |         } else if (local && local.ipAddress) {
704 |           // Firefox
705 |           self.localAddress = local.ipAddress
706 |           self.localPort = Number(local.portNumber)
707 |         } else if (typeof selectedCandidatePair.googLocalAddress === 'string') {
708 |           // TODO: remove this once Chrome 58 is released
709 |           local = selectedCandidatePair.googLocalAddress.split(':')
710 |           self.localAddress = local[0]
711 |           self.localPort = Number(local[1])
712 |         }
713 |         if (self.localAddress) {
714 |           self.localFamily = self.localAddress.includes(':') ? 'IPv6' : 'IPv4'
715 |         }
716 | 
717 |         var remote = remoteCandidates[selectedCandidatePair.remoteCandidateId]
718 | 
719 |         if (remote && (remote.ip || remote.address)) {
720 |           // Spec
721 |           self.remoteAddress = remote.ip || remote.address
722 |           self.remotePort = Number(remote.port)
723 |         } else if (remote && remote.ipAddress) {
724 |           // Firefox
725 |           self.remoteAddress = remote.ipAddress
726 |           self.remotePort = Number(remote.portNumber)
727 |         } else if (typeof selectedCandidatePair.googRemoteAddress === 'string') {
728 |           // TODO: remove this once Chrome 58 is released
729 |           remote = selectedCandidatePair.googRemoteAddress.split(':')
730 |           self.remoteAddress = remote[0]
731 |           self.remotePort = Number(remote[1])
732 |         }
733 |         if (self.remoteAddress) {
734 |           self.remoteFamily = self.remoteAddress.includes(':') ? 'IPv6' : 'IPv4'
735 |         }
736 | 
737 |         self._debug(
738 |           'connect local: %s:%s remote: %s:%s',
739 |           self.localAddress, self.localPort, self.remoteAddress, self.remotePort
740 |         )
741 |       }
742 | 
743 |       // Ignore candidate pair selection in browsers like Safari 11 that do not have any local or remote candidates
744 |       // But wait until at least 1 candidate pair is available
745 |       if (!foundSelectedCandidatePair && (!Object.keys(candidatePairs).length || Object.keys(localCandidates).length)) {
746 |         setTimeout(findCandidatePair, 100)
747 |         return
748 |       } else {
749 |         self._connecting = false
750 |         self.connected = true
751 |       }
752 | 
753 |       self._debug('connect')
754 |       self.emit('connect')
755 |     })
756 |   }
757 |   findCandidatePair()
758 | }
759 | 
760 | Peer.prototype._onSignalingStateChange = function () {
761 |   var self = this
762 |   if (self.destroyed) return
763 | 
764 |   if (self._pc.signalingState === 'stable' && !self._firstStable) {
765 |     self._isNegotiating = false
766 | 
767 |     // HACK: Firefox doesn't yet support removing tracks when signalingState !== 'stable'
768 |     self._debug('flushing sender queue', self._sendersAwaitingStable)
769 |     self._sendersAwaitingStable.forEach(function (sender) {
770 |       self._pc.removeTrack(sender)
771 |       self._queuedNegotiation = true
772 |     })
773 |     self._sendersAwaitingStable = []
774 | 
775 |     if (self._queuedNegotiation) {
776 |       self._debug('flushing negotiation queue')
777 |       self._queuedNegotiation = false
778 |       self._needsNegotiation() // negotiate again
779 |     }
780 | 
781 |     self._debug('negotiate')
782 |     self.emit('negotiate')
783 |   }
784 |   self._firstStable = false
785 | 
786 |   self._debug('signalingStateChange %s', self._pc.signalingState)
787 |   self.emit('signalingStateChange', self._pc.signalingState)
788 | }
789 | 
790 | Peer.prototype._onIceCandidate = function (event) {
791 |   var self = this
792 |   if (self.destroyed) return
793 |   if (event.candidate && self.trickle) {
794 |     self._debug('iceCandidate')
795 |     self.emit('signal', {
796 |       candidate: {
797 |         candidate: event.candidate.candidate,
798 |         sdpMLineIndex: event.candidate.sdpMLineIndex,
799 |         sdpMid: event.candidate.sdpMid
800 |       }
801 |     })
802 |   } else if (!event.candidate && !self._iceComplete) {
803 |     self._iceComplete = true
804 |     self.emit('_iceComplete')
805 |   }
806 |   // as soon as we've received one valid candidate start timeout
807 |   if (event.candidate) {
808 |     self._startIceCompleteTimeout()
809 |   }
810 | }
811 | 
812 | Peer.prototype._onTrack = function (event) {
813 |   var self = this
814 |   if (self.destroyed) return
815 | 
816 |   event.streams.forEach(function (eventStream) {
817 |     self._debug('on track')
818 |     self.emit('track', event.track, eventStream)
819 | 
820 |     self._remoteTracks.push({
821 |       track: event.track,
822 |       stream: eventStream
823 |     })
824 | 
825 |     if (self._remoteStreams.some(function (remoteStream) {
826 |       return remoteStream.id === eventStream.id
827 |     })) return // Only fire one 'stream' event, even though there may be multiple tracks per stream
828 | 
829 |     self._remoteStreams.push(eventStream)
830 |     setTimeout(function () {
831 |       self.emit('stream', eventStream) // ensure all tracks have been added
832 |     }, 0)
833 |   })
834 | }
835 | 
836 | Peer.prototype._debug = function () {
837 |   var self = this
838 |   var args = [].slice.call(arguments)
839 |   args[0] = '[' + self._id + '] ' + args[0]
840 |   debug.apply(null, args)
841 | }
842 | 
843 | // HACK: We cannot reuse channel names, so we use the peer ID and a counter
844 | Peer.prototype._makeUniqueChannelName = function (channelName) {
845 |   var self = this
846 |   channelName = channelName || ''
847 |   if (channelName.indexOf('@') !== -1) {
848 |     return self.destroy(makeError('channelName cannot include "@" character', 'INVALID_CHANNEL_NAME'))
849 |   }
850 |   return channelName + '@' + self._id + (self._channelNameCounter++)
851 | }
852 | 
853 | // HACK: Filter trickle lines when trickle is disabled #354
854 | function filterTrickle (sdp) {
855 |   return sdp.replace(/a=ice-options:trickle\s\n/g, '')
856 | }
857 | 
858 | function makeError (message, code) {
859 |   var err = new Error(message)
860 |   err.code = code
861 |   return err
862 | }
863 | 


--------------------------------------------------------------------------------