├── .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 | [](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 | 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 |  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  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 | [](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 | --------------------------------------------------------------------------------