├── .airtap.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── img ├── dfinity-sponsor.png ├── full-mesh-formula.png └── full-mesh.png ├── index.js ├── package.json ├── perf ├── receive.js ├── send.js └── server.js ├── simplepeer.min.js └── test ├── basic.js ├── binary.js ├── common.js ├── multistream.js ├── negotiation.js ├── object-mode.js ├── stream.js ├── trickle.js └── z-cleanup.js /.airtap.yml: -------------------------------------------------------------------------------- 1 | sauce_connect: true 2 | browsers: 3 | - name: firefox 4 | version: latest 5 | - name: chrome 6 | version: latest 7 | - name: safari 8 | version: latest 9 | - name: edge 10 | version: latest 11 | - name: and_chr 12 | version: latest 13 | - name: ios_saf 14 | version: latest 15 | providers: 16 | - airtap-sauce 17 | presets: 18 | local: 19 | providers: airtap-manual 20 | browsers: 21 | - name: manual 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🐞 Bug report" 3 | about: Report an issue with this software 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | **What version of this package are you using?** 13 | 14 | **What operating system, Node.js, and npm version?** 15 | 16 | **What happened?** 17 | 18 | 19 | 20 | **What did you expect to happen?** 21 | 22 | **Are you willing to submit a pull request to fix this bug?** 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: ❓ Ask a question 4 | url: https://discord.gg/CNxFAzdEmr 5 | about: Ask questions about this software 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "⭐️ Feature request" 3 | about: Request a new feature to be added 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | **What version of this package are you using?** 13 | 14 | **What problem do you want to solve?** 15 | 16 | **What do you think is the correct solution to this problem?** 17 | 18 | **Are you willing to submit a pull request to implement this change?** 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **What is the purpose of this pull request? (put an "X" next to item)** 2 | 3 | [ ] Documentation update 4 | [ ] Bug fix 5 | [ ] New feature 6 | [ ] Other, please explain: 7 | 8 | **What changes did you make? (Give an overview)** 9 | 10 | **Which issue (if any) does this pull request address?** 11 | 12 | **Is there anything you'd like reviewers to focus on?** 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: / 5 | schedule: 6 | interval: daily 7 | labels: 8 | - dependency 9 | versioning-strategy: increase 10 | - package-ecosystem: github-actions 11 | directory: / 12 | schedule: 13 | interval: daily 14 | labels: 15 | - dependency 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 'on': 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node ${{ matrix.node }} / ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | environment: ci 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | os: 14 | - ubuntu-latest 15 | node: 16 | - '14' 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: actions/setup-node@v2 20 | with: 21 | node-version: ${{ matrix.node }} 22 | - run: npm install 23 | - run: npm run build --if-present 24 | - run: echo "127.0.0.1 airtap.local" | sudo tee -a /etc/hosts 25 | - run: npm test 26 | env: 27 | SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} 28 | SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | node_modules 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .airtap.yml 2 | .nyc_output 3 | .github/ 4 | img/ 5 | perf/ 6 | test/ 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simple-peer [![ci][ci-image]][ci-url] [![coveralls][coveralls-image]][coveralls-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url] [![javascript style guide][sauce-image]][sauce-url] 2 | 3 | [ci-image]: https://img.shields.io/github/workflow/status/feross/simple-peer/ci/master 4 | [ci-url]: https://github.com/feross/simple-peer/actions 5 | [coveralls-image]: https://coveralls.io/repos/github/feross/simple-peer/badge.svg?branch=master 6 | [coveralls-url]: https://coveralls.io/github/feross/simple-peer?branch=master 7 | [npm-image]: https://img.shields.io/npm/v/simple-peer.svg 8 | [npm-url]: https://npmjs.org/package/simple-peer 9 | [downloads-image]: https://img.shields.io/npm/dm/simple-peer.svg 10 | [downloads-url]: https://npmjs.org/package/simple-peer 11 | [standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg 12 | [standard-url]: https://standardjs.com 13 | [sauce-image]: https://saucelabs.com/buildstatus/simple-peer 14 | [sauce-url]: https://saucelabs.com/u/simple-peer 15 | 16 | #### Simple WebRTC video, voice, and data channels 17 | 18 |
19 | Sponsored by    DFINITY 20 |
21 | 22 | > We are hiring a peer-to-peer WebRTC mobile Web application expert. 23 | > 24 | > [DFINITY](http://dfinity.org/) is building an exciting peer-to-peer WebRTC-based mobile Web app to help improve democracy on the Internet Computer blockchain. The mobile web app connects groups of up to four people in a peer-to-peer WebRTC audio and video call so that they can mutually prove unique personhood. 25 | > 26 | > We are looking for a software engineer or consultant who can help us solve (platform-dependent) reliability issues of our implementation. We are interested in applicants with substantial WebRTC experience for mobile Web apps, experience with different communication patterns (e.g., peer-to-peer, server relay), and substantial problem-solving skills. Having experience in automated testing of this type of applications is a plus. Pay is extremely competitive for the right expertise. For details, please see the [full job description](https://boards.greenhouse.io/dfinity/jobs/5910101002?gh_src=c28327ae2us). 27 | 28 | ## features 29 | 30 | - concise, **node.js style** API for [WebRTC](https://en.wikipedia.org/wiki/WebRTC) 31 | - **works in node and the browser!** 32 | - supports **video/voice streams** 33 | - supports **data channel** 34 | - text and binary data 35 | - node.js [duplex stream](http://nodejs.org/api/stream.html) interface 36 | - supports advanced options like: 37 | - enable/disable [trickle ICE candidates](http://webrtchacks.com/trickle-ice/) 38 | - manually set config options 39 | - transceivers and renegotiation 40 | 41 | This package is used by [WebTorrent](https://webtorrent.io) and [many others](#who-is-using-simple-peer). 42 | 43 | - [install](#install) 44 | - [examples](#usage) 45 | * [A simpler example](#a-simpler-example) 46 | * [data channels](#data-channels) 47 | * [video/voice](#videovoice) 48 | * [dynamic video/voice](#dynamic-videovoice) 49 | * [in node](#in-node) 50 | - [api](#api) 51 | - [events](#events) 52 | - [error codes](#error-codes) 53 | - [connecting more than 2 peers?](#connecting-more-than-2-peers) 54 | - [memory usage](#memory-usage) 55 | - [connection does not work on some networks?](#connection-does-not-work-on-some-networks) 56 | - [Who is using `simple-peer`?](#who-is-using-simple-peer) 57 | - [license](#license) 58 | 59 | ## install 60 | 61 | ``` 62 | npm install simple-peer 63 | ``` 64 | 65 | This package works in the browser with [browserify](https://browserify.org). If 66 | you do not use a bundler, you can use the `simplepeer.min.js` standalone script 67 | directly in a ` 91 | 118 | 119 | 120 | ``` 121 | 122 | Visit `index.html#1` from one browser (the initiator) and `index.html` from another 123 | browser (the receiver). 124 | 125 | An "offer" will be generated by the initiator. Paste this into the receiver's form and 126 | hit submit. The receiver generates an "answer". Paste this into the initiator's form and 127 | hit submit. 128 | 129 | Now you have a direct P2P connection between two browsers! 130 | 131 | ### A simpler example 132 | 133 | This example create two peers **in the same web page**. 134 | 135 | In a real-world application, *you would never do this*. The sender and receiver `Peer` 136 | instances would exist in separate browsers. A "signaling server" (usually implemented with 137 | websockets) would be used to exchange signaling data between the two browsers until a 138 | peer-to-peer connection is established. 139 | 140 | ### data channels 141 | 142 | ```js 143 | var Peer = require('simple-peer') 144 | 145 | var peer1 = new Peer({ initiator: true }) 146 | var peer2 = new Peer() 147 | 148 | peer1.on('signal', data => { 149 | // when peer1 has signaling data, give it to peer2 somehow 150 | peer2.signal(data) 151 | }) 152 | 153 | peer2.on('signal', data => { 154 | // when peer2 has signaling data, give it to peer1 somehow 155 | peer1.signal(data) 156 | }) 157 | 158 | peer1.on('connect', () => { 159 | // wait for 'connect' event before using the data channel 160 | peer1.send('hey peer2, how is it going?') 161 | }) 162 | 163 | peer2.on('data', data => { 164 | // got a data channel message 165 | console.log('got a message from peer1: ' + data) 166 | }) 167 | ``` 168 | 169 | ### video/voice 170 | 171 | Video/voice is also super simple! In this example, peer1 sends video to peer2. 172 | 173 | ```js 174 | var Peer = require('simple-peer') 175 | 176 | // get video/voice stream 177 | navigator.mediaDevices.getUserMedia({ 178 | video: true, 179 | audio: true 180 | }).then(gotMedia).catch(() => {}) 181 | 182 | function gotMedia (stream) { 183 | var peer1 = new Peer({ initiator: true, stream: stream }) 184 | var peer2 = new Peer() 185 | 186 | peer1.on('signal', data => { 187 | peer2.signal(data) 188 | }) 189 | 190 | peer2.on('signal', data => { 191 | peer1.signal(data) 192 | }) 193 | 194 | peer2.on('stream', stream => { 195 | // got remote video stream, now let's show it in a video tag 196 | var video = document.querySelector('video') 197 | 198 | if ('srcObject' in video) { 199 | video.srcObject = stream 200 | } else { 201 | video.src = window.URL.createObjectURL(stream) // for older browsers 202 | } 203 | 204 | video.play() 205 | }) 206 | } 207 | ``` 208 | 209 | For two-way video, simply pass a `stream` option into both `Peer` constructors. Simple! 210 | 211 | Please notice that `getUserMedia` only works in [pages loaded via **https**](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#Encryption_based_security). 212 | 213 | ### dynamic video/voice 214 | 215 | It is also possible to establish a data-only connection at first, and later add 216 | a video/voice stream, if desired. 217 | 218 | ```js 219 | var Peer = require('simple-peer') // create peer without waiting for media 220 | 221 | var peer1 = new Peer({ initiator: true }) // you don't need streams here 222 | var peer2 = new Peer() 223 | 224 | peer1.on('signal', data => { 225 | peer2.signal(data) 226 | }) 227 | 228 | peer2.on('signal', data => { 229 | peer1.signal(data) 230 | }) 231 | 232 | peer2.on('stream', stream => { 233 | // got remote video stream, now let's show it in a video tag 234 | var video = document.querySelector('video') 235 | 236 | if ('srcObject' in video) { 237 | video.srcObject = stream 238 | } else { 239 | video.src = window.URL.createObjectURL(stream) // for older browsers 240 | } 241 | 242 | video.play() 243 | }) 244 | 245 | function addMedia (stream) { 246 | peer1.addStream(stream) // <- add streams to peer dynamically 247 | } 248 | 249 | // then, anytime later... 250 | navigator.mediaDevices.getUserMedia({ 251 | video: true, 252 | audio: true 253 | }).then(addMedia).catch(() => {}) 254 | ``` 255 | 256 | ### in node 257 | 258 | To use this library in node, pass in `opts.wrtc` as a parameter (see [the constructor options](#peer--new-peeropts)): 259 | 260 | ```js 261 | var Peer = require('simple-peer') 262 | var wrtc = require('wrtc') 263 | 264 | var peer1 = new Peer({ initiator: true, wrtc: wrtc }) 265 | var peer2 = new Peer({ wrtc: wrtc }) 266 | ``` 267 | 268 | ## api 269 | 270 | ### `peer = new Peer([opts])` 271 | 272 | Create a new WebRTC peer connection. 273 | 274 | 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. 275 | 276 | If `opts` is specified, then the default options (shown below) will be overridden. 277 | 278 | ``` 279 | { 280 | initiator: false, 281 | channelConfig: {}, 282 | channelName: '', 283 | config: { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:global.stun.twilio.com:3478?transport=udp' }] }, 284 | offerOptions: {}, 285 | answerOptions: {}, 286 | sdpTransform: function (sdp) { return sdp }, 287 | stream: false, 288 | streams: [], 289 | trickle: true, 290 | allowHalfTrickle: false, 291 | wrtc: {}, // RTCPeerConnection/RTCSessionDescription/RTCIceCandidate 292 | objectMode: false 293 | } 294 | ``` 295 | 296 | The options do the following: 297 | 298 | - `initiator` - set to `true` if this is the initiating peer 299 | - `channelConfig` - custom webrtc data channel configuration (used by [`createDataChannel`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createDataChannel)) 300 | - `channelName` - custom webrtc data channel name 301 | - `config` - custom webrtc configuration (used by [`RTCPeerConnection`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection) constructor) 302 | - `offerOptions` - custom offer options (used by [`createOffer`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer) method) 303 | - `answerOptions` - custom answer options (used by [`createAnswer`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createAnswer) method) 304 | - `sdpTransform` - function to transform the generated SDP signaling data (for advanced users) 305 | - `stream` - if video/voice is desired, pass stream returned from [`getUserMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) 306 | - `streams` - an array of MediaStreams returned from [`getUserMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) 307 | - `trickle` - set to `false` to disable [trickle ICE](http://webrtchacks.com/trickle-ice/) and get a single 'signal' event (slower) 308 | - `wrtc` - custom webrtc implementation, mainly useful in node to specify in the [wrtc](https://npmjs.com/package/wrtc) package. Contains an object with the properties: 309 | - [`RTCPeerConnection`](https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection) 310 | - [`RTCSessionDescription`](https://www.w3.org/TR/webrtc/#dom-rtcsessiondescription) 311 | - [`RTCIceCandidate`](https://www.w3.org/TR/webrtc/#dom-rtcicecandidate) 312 | 313 | - `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. 314 | 315 | ### `peer.signal(data)` 316 | 317 | Call this method whenever the remote peer emits a `peer.on('signal')` event. 318 | 319 | The `data` will encapsulate a webrtc offer, answer, or ice candidate. These messages help 320 | the peers to eventually establish a direct connection to each other. The contents of these 321 | strings are an implementation detail that can be ignored by the user of this module; 322 | simply pass the data from 'signal' events to the remote peer and call `peer.signal(data)` 323 | to get connected. 324 | 325 | ### `peer.send(data)` 326 | 327 | Send text/binary data to the remote peer. `data` can be any of several types: `String`, 328 | `Buffer` (see [buffer](https://github.com/feross/buffer)), `ArrayBufferView` (`Uint8Array`, 329 | etc.), `ArrayBuffer`, or `Blob` (in browsers that support it). 330 | 331 | Note: If this method is called before the `peer.on('connect')` event has fired, then an exception will be thrown. Use `peer.write(data)` (which is inherited from the node.js [duplex stream](http://nodejs.org/api/stream.html) interface) if you want this data to be buffered instead. 332 | 333 | ### `peer.addStream(stream)` 334 | 335 | Add a `MediaStream` to the connection. 336 | 337 | ### `peer.removeStream(stream)` 338 | 339 | Remove a `MediaStream` from the connection. 340 | 341 | ### `peer.addTrack(track, stream)` 342 | 343 | Add a `MediaStreamTrack` to the connection. Must also pass the `MediaStream` you want to attach it to. 344 | 345 | ### `peer.removeTrack(track, stream)` 346 | 347 | Remove a `MediaStreamTrack` from the connection. Must also pass the `MediaStream` that it was attached to. 348 | 349 | ### `peer.replaceTrack(oldTrack, newTrack, stream)` 350 | 351 | Replace a `MediaStreamTrack` with another track. Must also pass the `MediaStream` that the old track was attached to. 352 | 353 | ### `peer.addTransceiver(kind, init)` 354 | 355 | Add a `RTCRtpTransceiver` to the connection. Can be used to add transceivers before adding tracks. Automatically called as neccesary by `addTrack`. 356 | 357 | ### `peer.destroy([err])` 358 | 359 | Destroy and cleanup this peer connection. 360 | 361 | If the optional `err` parameter is passed, then it will be emitted as an `'error'` 362 | event on the stream. 363 | 364 | ### `Peer.WEBRTC_SUPPORT` 365 | 366 | Detect native WebRTC support in the javascript environment. 367 | 368 | ```js 369 | var Peer = require('simple-peer') 370 | 371 | if (Peer.WEBRTC_SUPPORT) { 372 | // webrtc support! 373 | } else { 374 | // fallback 375 | } 376 | ``` 377 | 378 | ### duplex stream 379 | 380 | `Peer` objects are instances of `stream.Duplex`. They behave very similarly to a 381 | `net.Socket` from the node core `net` module. The duplex stream reads/writes to the data 382 | channel. 383 | 384 | ```js 385 | var peer = new Peer(opts) 386 | // ... signaling ... 387 | peer.write(new Buffer('hey')) 388 | peer.on('data', function (chunk) { 389 | console.log('got a chunk', chunk) 390 | }) 391 | ``` 392 | 393 | ## events 394 | 395 | `Peer` objects are instance of `EventEmitter`. Take a look at the [nodejs events documentation](https://nodejs.org/api/events.html) for more information. 396 | 397 | Example of removing all registered **close**-event listeners: 398 | ```js 399 | peer.removeAllListeners('close') 400 | ``` 401 | 402 | ### `peer.on('signal', data => {})` 403 | 404 | Fired when the peer wants to send signaling data to the remote peer. 405 | 406 | **It is the responsibility of the application developer (that's you!) to get this data to 407 | the other peer.** This usually entails using a websocket signaling server. This data is an 408 | `Object`, so remember to call `JSON.stringify(data)` to serialize it first. Then, simply 409 | call `peer.signal(data)` on the remote peer. 410 | 411 | (Be sure to listen to this event immediately to avoid missing it. For `initiator: true` 412 | peers, it fires right away. For `initatior: false` peers, it fires when the remote 413 | offer is received.) 414 | 415 | ### `peer.on('connect', () => {})` 416 | 417 | Fired when the peer connection and data channel are ready to use. 418 | 419 | ### `peer.on('data', data => {})` 420 | 421 | Received a message from the remote peer (via the data channel). 422 | 423 | `data` will be either a `String` or a `Buffer/Uint8Array` (see [buffer](https://github.com/feross/buffer)). 424 | 425 | ### `peer.on('stream', stream => {})` 426 | 427 | Received a remote video stream, which can be displayed in a video tag: 428 | 429 | ```js 430 | peer.on('stream', stream => { 431 | var video = document.querySelector('video') 432 | if ('srcObject' in video) { 433 | video.srcObject = stream 434 | } else { 435 | video.src = window.URL.createObjectURL(stream) 436 | } 437 | video.play() 438 | }) 439 | ``` 440 | 441 | ### `peer.on('track', (track, stream) => {})` 442 | 443 | Received a remote audio/video track. Streams may contain multiple tracks. 444 | 445 | ### `peer.on('close', () => {})` 446 | 447 | Called when the peer connection has closed. 448 | 449 | ### `peer.on('error', (err) => {})` 450 | 451 | Fired when a fatal error occurs. Usually, this means bad signaling data was received from the remote peer. 452 | 453 | `err` is an `Error` object. 454 | 455 | ## error codes 456 | 457 | Errors returned by the `error` event have an `err.code` property that will indicate the origin of the failure. 458 | 459 | Possible error codes: 460 | - `ERR_WEBRTC_SUPPORT` 461 | - `ERR_CREATE_OFFER` 462 | - `ERR_CREATE_ANSWER` 463 | - `ERR_SET_LOCAL_DESCRIPTION` 464 | - `ERR_SET_REMOTE_DESCRIPTION` 465 | - `ERR_ADD_ICE_CANDIDATE` 466 | - `ERR_ICE_CONNECTION_FAILURE` 467 | - `ERR_SIGNALING` 468 | - `ERR_DATA_CHANNEL` 469 | - `ERR_CONNECTION_FAILURE` 470 | 471 | 472 | ## connecting more than 2 peers? 473 | 474 | The simplest way to do that is to create a full-mesh topology. That means that every peer 475 | opens a connection to every other peer. To illustrate: 476 | 477 | ![full mesh topology](img/full-mesh.png) 478 | 479 | To broadcast a message, just iterate over all the peers and call `peer.send`. 480 | 481 | So, say you have 3 peers. Then, when a peer wants to send some data it must send it 2 482 | times, once to each of the other peers. So you're going to want to be a bit careful about 483 | the size of the data you send. 484 | 485 | Full mesh topologies don't scale well when the number of peers is very large. The total 486 | number of edges in the network will be ![full mesh formula](img/full-mesh-formula.png) 487 | where `n` is the number of peers. 488 | 489 | For clarity, here is the code to connect 3 peers together: 490 | 491 | #### Peer 1 492 | 493 | ```js 494 | // These are peer1's connections to peer2 and peer3 495 | var peer2 = new Peer({ initiator: true }) 496 | var peer3 = new Peer({ initiator: true }) 497 | 498 | peer2.on('signal', data => { 499 | // send this signaling data to peer2 somehow 500 | }) 501 | 502 | peer2.on('connect', () => { 503 | peer2.send('hi peer2, this is peer1') 504 | }) 505 | 506 | peer2.on('data', data => { 507 | console.log('got a message from peer2: ' + data) 508 | }) 509 | 510 | peer3.on('signal', data => { 511 | // send this signaling data to peer3 somehow 512 | }) 513 | 514 | peer3.on('connect', () => { 515 | peer3.send('hi peer3, this is peer1') 516 | }) 517 | 518 | peer3.on('data', data => { 519 | console.log('got a message from peer3: ' + data) 520 | }) 521 | ``` 522 | 523 | #### Peer 2 524 | 525 | ```js 526 | // These are peer2's connections to peer1 and peer3 527 | var peer1 = new Peer() 528 | var peer3 = new Peer({ initiator: true }) 529 | 530 | peer1.on('signal', data => { 531 | // send this signaling data to peer1 somehow 532 | }) 533 | 534 | peer1.on('connect', () => { 535 | peer1.send('hi peer1, this is peer2') 536 | }) 537 | 538 | peer1.on('data', data => { 539 | console.log('got a message from peer1: ' + data) 540 | }) 541 | 542 | peer3.on('signal', data => { 543 | // send this signaling data to peer3 somehow 544 | }) 545 | 546 | peer3.on('connect', () => { 547 | peer3.send('hi peer3, this is peer2') 548 | }) 549 | 550 | peer3.on('data', data => { 551 | console.log('got a message from peer3: ' + data) 552 | }) 553 | ``` 554 | 555 | #### Peer 3 556 | 557 | ```js 558 | // These are peer3's connections to peer1 and peer2 559 | var peer1 = new Peer() 560 | var peer2 = new Peer() 561 | 562 | peer1.on('signal', data => { 563 | // send this signaling data to peer1 somehow 564 | }) 565 | 566 | peer1.on('connect', () => { 567 | peer1.send('hi peer1, this is peer3') 568 | }) 569 | 570 | peer1.on('data', data => { 571 | console.log('got a message from peer1: ' + data) 572 | }) 573 | 574 | peer2.on('signal', data => { 575 | // send this signaling data to peer2 somehow 576 | }) 577 | 578 | peer2.on('connect', () => { 579 | peer2.send('hi peer2, this is peer3') 580 | }) 581 | 582 | peer2.on('data', data => { 583 | console.log('got a message from peer2: ' + data) 584 | }) 585 | ``` 586 | 587 | ## memory usage 588 | 589 | If you call `peer.send(buf)`, `simple-peer` is not keeping a reference to `buf` 590 | and sending the buffer at some later point in time. We immediately call 591 | `channel.send()` on the data channel. So it should be fine to mutate the buffer 592 | right afterward. 593 | 594 | However, beware that `peer.write(buf)` (a writable stream method) does not have 595 | the same contract. It will potentially buffer the data and call 596 | `channel.send()` at a future point in time, so definitely don't assume it's 597 | safe to mutate the buffer. 598 | 599 | 600 | ## connection does not work on some networks? 601 | 602 | If a direct connection fails, in particular, because of NAT traversal and/or firewalls, 603 | WebRTC ICE uses an intermediary (relay) TURN server. In other words, ICE will first use 604 | STUN with UDP to directly connect peers and, if that fails, will fall back to a TURN relay 605 | server. 606 | 607 | In order to use a TURN server, you must specify the `config` option to the `Peer` 608 | constructor. See the API docs above. 609 | 610 | [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) 611 | 612 | 613 | ## Who is using `simple-peer`? 614 | 615 | - [WebTorrent](http://webtorrent.io) - Streaming torrent client in the browser 616 | - [Virus Cafe](https://virus.cafe) - Make a friend in 2 minutes 617 | - [Instant.io](https://instant.io) - Secure, anonymous, streaming file transfer 618 | - [Zencastr](https://zencastr.com) - Easily record your remote podcast interviews in studio quality. 619 | - [Friends](https://github.com/moose-team/friends) - Peer-to-peer chat powered by the web 620 | - [Socket.io-p2p](https://github.com/socketio/socket.io-p2p) - Official Socket.io P2P communication library 621 | - [ScreenCat](https://maxogden.github.io/screencat/) - Screen sharing + remote collaboration app 622 | - [WebCat](https://www.npmjs.com/package/webcat) - P2P pipe across the web using Github private/public key for auth 623 | - [RTCCat](https://www.npmjs.com/package/rtcat) - WebRTC netcat 624 | - [PeerNet](https://www.npmjs.com/package/peernet) - Peer-to-peer gossip network using randomized algorithms 625 | - [PusherTC](http://pushertc.herokuapp.com) - Video chat with using Pusher. See [guide](http://blog.carbonfive.com/2014/10/16/webrtc-made-simple/). 626 | - [lxjs-chat](https://github.com/feross/lxjs-chat) - Omegle-like video chat site 627 | - [Whiteboard](https://github.com/feross/whiteboard) - P2P Whiteboard powered by WebRTC and WebTorrent 628 | - [Peer Calls](https://peercalls.com) - WebRTC group video calling. Create a room. Share the link. 629 | - [Netsix](https://mmorainville.github.io/netsix-gh-pages/) - Send videos to your friends using WebRTC so that they can watch them right away. 630 | - [Stealthy](https://www.stealthy.im) - Stealthy is a decentralized, end-to-end encrypted, p2p chat application. 631 | - [oorja.io](https://github.com/akshayKMR/oorja) - Effortless video-voice chat with realtime collaborative features. Extensible using react components 🙌 632 | - [TalktoMe](https://talktome.universal-apps.xyz) - Skype alternative for audio/video conferencing based on WebRTC, but without the loss of packets. 633 | - [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 634 | - [Detox](https://github.com/Detox) - Overlay network for distributed anonymous P2P communications entirely in the browser 635 | - [Metastream](https://github.com/samuelmaddock/metastream) - Watch streaming media with friends. 636 | - [firepeer](https://github.com/natzcam/firepeer) - secure signalling and authentication using firebase realtime database 637 | - [Genet](https://github.com/elavoie/webrtc-tree-overlay) - Fat-tree overlay to scale the number of concurrent WebRTC connections to a single source ([paper](https://arxiv.org/abs/1904.11402)). 638 | - [WebRTC Connection Testing](https://github.com/elavoie/webrtc-connection-testing) - Quickly test direct connectivity between all pairs of participants ([demo](https://webrtc-connection-testing.herokuapp.com/)). 639 | - [Firstdate.co](https://firstdate.co) - Online video dating for actually meeting people and not just messaging them 640 | - [TensorChat](https://github.com/EhsaanIqbal/tensorchat) - It's simple - Create. Share. Chat. 641 | - [On/Office](https://onoffice.app) - View your desktop in a WebVR-powered environment 642 | - [Cyph](https://www.cyph.com) - Cryptographically secure messaging and social networking service, providing an extreme level of privacy combined with best-in-class ease of use 643 | - [Ciphora](https://github.com/HR/ciphora) - A peer-to-peer end-to-end encrypted messaging chat app. 644 | - [Whisthub](https://www.whisthub.com) - Online card game Color Whist with the possibility to start a video chat while playing. 645 | - [Brie.fi/ng](https://brie.fi/ng) - Secure anonymous video chat 646 | - [Peer.School](https://github.com/holtwick/peer2school) - Simple virtual classroom starting from the 1st class including video chat and real time whiteboard 647 | - [FileFire](https://filefire.ca) - Transfer large files and folders at high speed without size limits. 648 | - [safeShare](https://github.com/vj-abishek/airdrop) - Transfer files easily with text and voice communication. 649 | - [CubeChat](https://cubechat.io) - Party in 3D 🎉 650 | - [Homely School](https://homelyschool.com) - A virtual schooling system 651 | - [AnyDrop](https://anydrop.io) - Cross-platform AirDrop alternative [with an Android app available at Google Play](https://play.google.com/store/apps/details?id=com.benjijanssens.anydrop) 652 | - [Share-Anywhere](https://share-anywhere.com/) - Cross-platform file transfer 653 | - [QuaranTime.io](https://quarantime.io/) - The Activity board-game in video! 654 | - [Trango](https://web.trango.io) - Cross-platform calling and file sharing solution. 655 | - [P2PT](https://github.com/subins2000/p2pt) - Use WebTorrent trackers as signalling servers for making WebRTC connections 656 | - [Dots](https://github.com/subins2000/vett) - Online multiplayer Dots & Boxes game. [Play Here!](https://vett.space) 657 | - [simple-peer-files](https://github.com/subins2000/simple-peer-files) - A simple library to easily transfer files over WebRTC. Has a feature to resume file transfer after uploader interruption. 658 | - [WebDrop.Space](https://WebDrop.Space) - Share files and messages across devices. Cross-platform, no installation alternative to AirDrop, Xender. [Source Code](https://github.com/subins2000/WebDrop) 659 | - [Speakrandom](https://speakrandom.com) - Voice-chat social network using simple-peer to create audio conferences! 660 | - [Deskreen](https://deskreen.com) - A desktop app that helps you to turn any device into a secondary screen for your computer. It uses simple-peer for sharing entire computer screen to any device with a web browser. 661 | 662 | 663 | 664 | - *Your app here! - send a PR!* 665 | 666 | ## license 667 | 668 | MIT. Copyright (c) [Feross Aboukhadijeh](http://feross.org). 669 | -------------------------------------------------------------------------------- /img/dfinity-sponsor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feross/simple-peer/f1a492d1999ce727fa87193ebdea20ac89c1fc6d/img/dfinity-sponsor.png -------------------------------------------------------------------------------- /img/full-mesh-formula.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feross/simple-peer/f1a492d1999ce727fa87193ebdea20ac89c1fc6d/img/full-mesh-formula.png -------------------------------------------------------------------------------- /img/full-mesh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feross/simple-peer/f1a492d1999ce727fa87193ebdea20ac89c1fc6d/img/full-mesh.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! simple-peer. MIT License. Feross Aboukhadijeh */ 2 | const debug = require('debug')('simple-peer') 3 | const getBrowserRTC = require('get-browser-rtc') 4 | const randombytes = require('randombytes') 5 | const stream = require('readable-stream') 6 | const queueMicrotask = require('queue-microtask') // TODO: remove when Node 10 is not supported 7 | const errCode = require('err-code') 8 | const { Buffer } = require('buffer') 9 | 10 | const MAX_BUFFERED_AMOUNT = 64 * 1024 11 | const ICECOMPLETE_TIMEOUT = 5 * 1000 12 | const CHANNEL_CLOSING_TIMEOUT = 5 * 1000 13 | 14 | // HACK: Filter trickle lines when trickle is disabled #354 15 | function filterTrickle (sdp) { 16 | return sdp.replace(/a=ice-options:trickle\s\n/g, '') 17 | } 18 | 19 | function warn (message) { 20 | console.warn(message) 21 | } 22 | 23 | /** 24 | * WebRTC peer connection. Same API as node core `net.Socket`, plus a few extra methods. 25 | * Duplex stream. 26 | * @param {Object} opts 27 | */ 28 | class Peer extends stream.Duplex { 29 | constructor (opts) { 30 | opts = Object.assign({ 31 | allowHalfOpen: false 32 | }, opts) 33 | 34 | super(opts) 35 | 36 | this._id = randombytes(4).toString('hex').slice(0, 7) 37 | this._debug('new peer %o', opts) 38 | 39 | this.channelName = opts.initiator 40 | ? opts.channelName || randombytes(20).toString('hex') 41 | : null 42 | 43 | this.initiator = opts.initiator || false 44 | this.channelConfig = opts.channelConfig || Peer.channelConfig 45 | this.channelNegotiated = this.channelConfig.negotiated 46 | this.config = Object.assign({}, Peer.config, opts.config) 47 | this.offerOptions = opts.offerOptions || {} 48 | this.answerOptions = opts.answerOptions || {} 49 | this.sdpTransform = opts.sdpTransform || (sdp => sdp) 50 | this.streams = opts.streams || (opts.stream ? [opts.stream] : []) // support old "stream" option 51 | this.trickle = opts.trickle !== undefined ? opts.trickle : true 52 | this.allowHalfTrickle = opts.allowHalfTrickle !== undefined ? opts.allowHalfTrickle : false 53 | this.iceCompleteTimeout = opts.iceCompleteTimeout || ICECOMPLETE_TIMEOUT 54 | 55 | this.destroyed = false 56 | this.destroying = false 57 | this._connected = false 58 | 59 | this.remoteAddress = undefined 60 | this.remoteFamily = undefined 61 | this.remotePort = undefined 62 | this.localAddress = undefined 63 | this.localFamily = undefined 64 | this.localPort = undefined 65 | 66 | this._wrtc = (opts.wrtc && typeof opts.wrtc === 'object') 67 | ? opts.wrtc 68 | : getBrowserRTC() 69 | 70 | if (!this._wrtc) { 71 | if (typeof window === 'undefined') { 72 | throw errCode(new Error('No WebRTC support: Specify `opts.wrtc` option in this environment'), 'ERR_WEBRTC_SUPPORT') 73 | } else { 74 | throw errCode(new Error('No WebRTC support: Not a supported browser'), 'ERR_WEBRTC_SUPPORT') 75 | } 76 | } 77 | 78 | this._pcReady = false 79 | this._channelReady = false 80 | this._iceComplete = false // ice candidate trickle done (got null candidate) 81 | this._iceCompleteTimer = null // send an offer/answer anyway after some timeout 82 | this._channel = null 83 | this._pendingCandidates = [] 84 | 85 | this._isNegotiating = false // is this peer waiting for negotiation to complete? 86 | this._firstNegotiation = true 87 | this._batchedNegotiation = false // batch synchronous negotiations 88 | this._queuedNegotiation = false // is there a queued negotiation request? 89 | this._sendersAwaitingStable = [] 90 | this._senderMap = new Map() 91 | this._closingInterval = null 92 | 93 | this._remoteTracks = [] 94 | this._remoteStreams = [] 95 | 96 | this._chunk = null 97 | this._cb = null 98 | this._interval = null 99 | 100 | try { 101 | this._pc = new (this._wrtc.RTCPeerConnection)(this.config) 102 | } catch (err) { 103 | this.destroy(errCode(err, 'ERR_PC_CONSTRUCTOR')) 104 | return 105 | } 106 | 107 | // We prefer feature detection whenever possible, but sometimes that's not 108 | // possible for certain implementations. 109 | this._isReactNativeWebrtc = typeof this._pc._peerConnectionId === 'number' 110 | 111 | this._pc.oniceconnectionstatechange = () => { 112 | this._onIceStateChange() 113 | } 114 | this._pc.onicegatheringstatechange = () => { 115 | this._onIceStateChange() 116 | } 117 | this._pc.onconnectionstatechange = () => { 118 | this._onConnectionStateChange() 119 | } 120 | this._pc.onsignalingstatechange = () => { 121 | this._onSignalingStateChange() 122 | } 123 | this._pc.onicecandidate = event => { 124 | this._onIceCandidate(event) 125 | } 126 | 127 | // HACK: Fix for odd Firefox behavior, see: https://github.com/feross/simple-peer/pull/783 128 | if (typeof this._pc.peerIdentity === 'object') { 129 | this._pc.peerIdentity.catch(err => { 130 | this.destroy(errCode(err, 'ERR_PC_PEER_IDENTITY')) 131 | }) 132 | } 133 | 134 | // Other spec events, unused by this implementation: 135 | // - onconnectionstatechange 136 | // - onicecandidateerror 137 | // - onfingerprintfailure 138 | // - onnegotiationneeded 139 | 140 | if (this.initiator || this.channelNegotiated) { 141 | this._setupData({ 142 | channel: this._pc.createDataChannel(this.channelName, this.channelConfig) 143 | }) 144 | } else { 145 | this._pc.ondatachannel = event => { 146 | this._setupData(event) 147 | } 148 | } 149 | 150 | if (this.streams) { 151 | this.streams.forEach(stream => { 152 | this.addStream(stream) 153 | }) 154 | } 155 | this._pc.ontrack = event => { 156 | this._onTrack(event) 157 | } 158 | 159 | this._debug('initial negotiation') 160 | this._needsNegotiation() 161 | 162 | this._onFinishBound = () => { 163 | this._onFinish() 164 | } 165 | this.once('finish', this._onFinishBound) 166 | } 167 | 168 | get bufferSize () { 169 | return (this._channel && this._channel.bufferedAmount) || 0 170 | } 171 | 172 | // HACK: it's possible channel.readyState is "closing" before peer.destroy() fires 173 | // https://bugs.chromium.org/p/chromium/issues/detail?id=882743 174 | get connected () { 175 | return (this._connected && this._channel.readyState === 'open') 176 | } 177 | 178 | address () { 179 | return { port: this.localPort, family: this.localFamily, address: this.localAddress } 180 | } 181 | 182 | signal (data) { 183 | if (this.destroying) return 184 | if (this.destroyed) throw errCode(new Error('cannot signal after peer is destroyed'), 'ERR_DESTROYED') 185 | if (typeof data === 'string') { 186 | try { 187 | data = JSON.parse(data) 188 | } catch (err) { 189 | data = {} 190 | } 191 | } 192 | this._debug('signal()') 193 | 194 | if (data.renegotiate && this.initiator) { 195 | this._debug('got request to renegotiate') 196 | this._needsNegotiation() 197 | } 198 | if (data.transceiverRequest && this.initiator) { 199 | this._debug('got request for transceiver') 200 | this.addTransceiver(data.transceiverRequest.kind, data.transceiverRequest.init) 201 | } 202 | if (data.candidate) { 203 | if (this._pc.remoteDescription && this._pc.remoteDescription.type) { 204 | this._addIceCandidate(data.candidate) 205 | } else { 206 | this._pendingCandidates.push(data.candidate) 207 | } 208 | } 209 | if (data.sdp) { 210 | this._pc.setRemoteDescription(new (this._wrtc.RTCSessionDescription)(data)) 211 | .then(() => { 212 | if (this.destroyed) return 213 | 214 | this._pendingCandidates.forEach(candidate => { 215 | this._addIceCandidate(candidate) 216 | }) 217 | this._pendingCandidates = [] 218 | 219 | if (this._pc.remoteDescription.type === 'offer') this._createAnswer() 220 | }) 221 | .catch(err => { 222 | this.destroy(errCode(err, 'ERR_SET_REMOTE_DESCRIPTION')) 223 | }) 224 | } 225 | if (!data.sdp && !data.candidate && !data.renegotiate && !data.transceiverRequest) { 226 | this.destroy(errCode(new Error('signal() called with invalid signal data'), 'ERR_SIGNALING')) 227 | } 228 | } 229 | 230 | _addIceCandidate (candidate) { 231 | const iceCandidateObj = new this._wrtc.RTCIceCandidate(candidate) 232 | this._pc.addIceCandidate(iceCandidateObj) 233 | .catch(err => { 234 | if (!iceCandidateObj.address || iceCandidateObj.address.endsWith('.local')) { 235 | warn('Ignoring unsupported ICE candidate.') 236 | } else { 237 | this.destroy(errCode(err, 'ERR_ADD_ICE_CANDIDATE')) 238 | } 239 | }) 240 | } 241 | 242 | /** 243 | * Send text/binary data to the remote peer. 244 | * @param {ArrayBufferView|ArrayBuffer|Buffer|string|Blob} chunk 245 | */ 246 | send (chunk) { 247 | if (this.destroying) return 248 | if (this.destroyed) throw errCode(new Error('cannot send after peer is destroyed'), 'ERR_DESTROYED') 249 | this._channel.send(chunk) 250 | } 251 | 252 | /** 253 | * Add a Transceiver to the connection. 254 | * @param {String} kind 255 | * @param {Object} init 256 | */ 257 | addTransceiver (kind, init) { 258 | if (this.destroying) return 259 | if (this.destroyed) throw errCode(new Error('cannot addTransceiver after peer is destroyed'), 'ERR_DESTROYED') 260 | this._debug('addTransceiver()') 261 | 262 | if (this.initiator) { 263 | try { 264 | this._pc.addTransceiver(kind, init) 265 | this._needsNegotiation() 266 | } catch (err) { 267 | this.destroy(errCode(err, 'ERR_ADD_TRANSCEIVER')) 268 | } 269 | } else { 270 | this.emit('signal', { // request initiator to renegotiate 271 | type: 'transceiverRequest', 272 | transceiverRequest: { kind, init } 273 | }) 274 | } 275 | } 276 | 277 | /** 278 | * Add a MediaStream to the connection. 279 | * @param {MediaStream} stream 280 | */ 281 | addStream (stream) { 282 | if (this.destroying) return 283 | if (this.destroyed) throw errCode(new Error('cannot addStream after peer is destroyed'), 'ERR_DESTROYED') 284 | this._debug('addStream()') 285 | 286 | stream.getTracks().forEach(track => { 287 | this.addTrack(track, stream) 288 | }) 289 | } 290 | 291 | /** 292 | * Add a MediaStreamTrack to the connection. 293 | * @param {MediaStreamTrack} track 294 | * @param {MediaStream} stream 295 | */ 296 | addTrack (track, stream) { 297 | if (this.destroying) return 298 | if (this.destroyed) throw errCode(new Error('cannot addTrack after peer is destroyed'), 'ERR_DESTROYED') 299 | this._debug('addTrack()') 300 | 301 | const submap = this._senderMap.get(track) || new Map() // nested Maps map [track, stream] to sender 302 | let sender = submap.get(stream) 303 | if (!sender) { 304 | sender = this._pc.addTrack(track, stream) 305 | submap.set(stream, sender) 306 | this._senderMap.set(track, submap) 307 | this._needsNegotiation() 308 | } else if (sender.removed) { 309 | throw errCode(new Error('Track has been removed. You should enable/disable tracks that you want to re-add.'), 'ERR_SENDER_REMOVED') 310 | } else { 311 | throw errCode(new Error('Track has already been added to that stream.'), 'ERR_SENDER_ALREADY_ADDED') 312 | } 313 | } 314 | 315 | /** 316 | * Replace a MediaStreamTrack by another in the connection. 317 | * @param {MediaStreamTrack} oldTrack 318 | * @param {MediaStreamTrack} newTrack 319 | * @param {MediaStream} stream 320 | */ 321 | replaceTrack (oldTrack, newTrack, stream) { 322 | if (this.destroying) return 323 | if (this.destroyed) throw errCode(new Error('cannot replaceTrack after peer is destroyed'), 'ERR_DESTROYED') 324 | this._debug('replaceTrack()') 325 | 326 | const submap = this._senderMap.get(oldTrack) 327 | const sender = submap ? submap.get(stream) : null 328 | if (!sender) { 329 | throw errCode(new Error('Cannot replace track that was never added.'), 'ERR_TRACK_NOT_ADDED') 330 | } 331 | if (newTrack) this._senderMap.set(newTrack, submap) 332 | 333 | if (sender.replaceTrack != null) { 334 | sender.replaceTrack(newTrack) 335 | } else { 336 | this.destroy(errCode(new Error('replaceTrack is not supported in this browser'), 'ERR_UNSUPPORTED_REPLACETRACK')) 337 | } 338 | } 339 | 340 | /** 341 | * Remove a MediaStreamTrack from the connection. 342 | * @param {MediaStreamTrack} track 343 | * @param {MediaStream} stream 344 | */ 345 | removeTrack (track, stream) { 346 | if (this.destroying) return 347 | if (this.destroyed) throw errCode(new Error('cannot removeTrack after peer is destroyed'), 'ERR_DESTROYED') 348 | this._debug('removeSender()') 349 | 350 | const submap = this._senderMap.get(track) 351 | const sender = submap ? submap.get(stream) : null 352 | if (!sender) { 353 | throw errCode(new Error('Cannot remove track that was never added.'), 'ERR_TRACK_NOT_ADDED') 354 | } 355 | try { 356 | sender.removed = true 357 | this._pc.removeTrack(sender) 358 | } catch (err) { 359 | if (err.name === 'NS_ERROR_UNEXPECTED') { 360 | this._sendersAwaitingStable.push(sender) // HACK: Firefox must wait until (signalingState === stable) https://bugzilla.mozilla.org/show_bug.cgi?id=1133874 361 | } else { 362 | this.destroy(errCode(err, 'ERR_REMOVE_TRACK')) 363 | } 364 | } 365 | this._needsNegotiation() 366 | } 367 | 368 | /** 369 | * Remove a MediaStream from the connection. 370 | * @param {MediaStream} stream 371 | */ 372 | removeStream (stream) { 373 | if (this.destroying) return 374 | if (this.destroyed) throw errCode(new Error('cannot removeStream after peer is destroyed'), 'ERR_DESTROYED') 375 | this._debug('removeSenders()') 376 | 377 | stream.getTracks().forEach(track => { 378 | this.removeTrack(track, stream) 379 | }) 380 | } 381 | 382 | _needsNegotiation () { 383 | this._debug('_needsNegotiation') 384 | if (this._batchedNegotiation) return // batch synchronous renegotiations 385 | this._batchedNegotiation = true 386 | queueMicrotask(() => { 387 | this._batchedNegotiation = false 388 | if (this.initiator || !this._firstNegotiation) { 389 | this._debug('starting batched negotiation') 390 | this.negotiate() 391 | } else { 392 | this._debug('non-initiator initial negotiation request discarded') 393 | } 394 | this._firstNegotiation = false 395 | }) 396 | } 397 | 398 | negotiate () { 399 | if (this.destroying) return 400 | if (this.destroyed) throw errCode(new Error('cannot negotiate after peer is destroyed'), 'ERR_DESTROYED') 401 | 402 | if (this.initiator) { 403 | if (this._isNegotiating) { 404 | this._queuedNegotiation = true 405 | this._debug('already negotiating, queueing') 406 | } else { 407 | this._debug('start negotiation') 408 | setTimeout(() => { // HACK: Chrome crashes if we immediately call createOffer 409 | this._createOffer() 410 | }, 0) 411 | } 412 | } else { 413 | if (this._isNegotiating) { 414 | this._queuedNegotiation = true 415 | this._debug('already negotiating, queueing') 416 | } else { 417 | this._debug('requesting negotiation from initiator') 418 | this.emit('signal', { // request initiator to renegotiate 419 | type: 'renegotiate', 420 | renegotiate: true 421 | }) 422 | } 423 | } 424 | this._isNegotiating = true 425 | } 426 | 427 | // TODO: Delete this method once readable-stream is updated to contain a default 428 | // implementation of destroy() that automatically calls _destroy() 429 | // See: https://github.com/nodejs/readable-stream/issues/283 430 | destroy (err) { 431 | this._destroy(err, () => {}) 432 | } 433 | 434 | _destroy (err, cb) { 435 | if (this.destroyed || this.destroying) return 436 | this.destroying = true 437 | 438 | this._debug('destroying (error: %s)', err && (err.message || err)) 439 | 440 | queueMicrotask(() => { // allow events concurrent with the call to _destroy() to fire (see #692) 441 | this.destroyed = true 442 | this.destroying = false 443 | 444 | this._debug('destroy (error: %s)', err && (err.message || err)) 445 | 446 | this.readable = this.writable = false 447 | 448 | if (!this._readableState.ended) this.push(null) 449 | if (!this._writableState.finished) this.end() 450 | 451 | this._connected = false 452 | this._pcReady = false 453 | this._channelReady = false 454 | this._remoteTracks = null 455 | this._remoteStreams = null 456 | this._senderMap = null 457 | 458 | clearInterval(this._closingInterval) 459 | this._closingInterval = null 460 | 461 | clearInterval(this._interval) 462 | this._interval = null 463 | this._chunk = null 464 | this._cb = null 465 | 466 | if (this._onFinishBound) this.removeListener('finish', this._onFinishBound) 467 | this._onFinishBound = null 468 | 469 | if (this._channel) { 470 | try { 471 | this._channel.close() 472 | } catch (err) {} 473 | 474 | // allow events concurrent with destruction to be handled 475 | this._channel.onmessage = null 476 | this._channel.onopen = null 477 | this._channel.onclose = null 478 | this._channel.onerror = null 479 | } 480 | if (this._pc) { 481 | try { 482 | this._pc.close() 483 | } catch (err) {} 484 | 485 | // allow events concurrent with destruction to be handled 486 | this._pc.oniceconnectionstatechange = null 487 | this._pc.onicegatheringstatechange = null 488 | this._pc.onsignalingstatechange = null 489 | this._pc.onicecandidate = null 490 | this._pc.ontrack = null 491 | this._pc.ondatachannel = null 492 | } 493 | this._pc = null 494 | this._channel = null 495 | 496 | if (err) this.emit('error', err) 497 | this.emit('close') 498 | cb() 499 | }) 500 | } 501 | 502 | _setupData (event) { 503 | if (!event.channel) { 504 | // In some situations `pc.createDataChannel()` returns `undefined` (in wrtc), 505 | // which is invalid behavior. Handle it gracefully. 506 | // See: https://github.com/feross/simple-peer/issues/163 507 | return this.destroy(errCode(new Error('Data channel event is missing `channel` property'), 'ERR_DATA_CHANNEL')) 508 | } 509 | 510 | this._channel = event.channel 511 | this._channel.binaryType = 'arraybuffer' 512 | 513 | if (typeof this._channel.bufferedAmountLowThreshold === 'number') { 514 | this._channel.bufferedAmountLowThreshold = MAX_BUFFERED_AMOUNT 515 | } 516 | 517 | this.channelName = this._channel.label 518 | 519 | this._channel.onmessage = event => { 520 | this._onChannelMessage(event) 521 | } 522 | this._channel.onbufferedamountlow = () => { 523 | this._onChannelBufferedAmountLow() 524 | } 525 | this._channel.onopen = () => { 526 | this._onChannelOpen() 527 | } 528 | this._channel.onclose = () => { 529 | this._onChannelClose() 530 | } 531 | this._channel.onerror = event => { 532 | const err = event.error instanceof Error 533 | ? event.error 534 | : new Error(`Datachannel error: ${event.message} ${event.filename}:${event.lineno}:${event.colno}`) 535 | this.destroy(errCode(err, 'ERR_DATA_CHANNEL')) 536 | } 537 | 538 | // HACK: Chrome will sometimes get stuck in readyState "closing", let's check for this condition 539 | // https://bugs.chromium.org/p/chromium/issues/detail?id=882743 540 | let isClosing = false 541 | this._closingInterval = setInterval(() => { // No "onclosing" event 542 | if (this._channel && this._channel.readyState === 'closing') { 543 | if (isClosing) this._onChannelClose() // closing timed out: equivalent to onclose firing 544 | isClosing = true 545 | } else { 546 | isClosing = false 547 | } 548 | }, CHANNEL_CLOSING_TIMEOUT) 549 | } 550 | 551 | _read () {} 552 | 553 | _write (chunk, encoding, cb) { 554 | if (this.destroyed) return cb(errCode(new Error('cannot write after peer is destroyed'), 'ERR_DATA_CHANNEL')) 555 | 556 | if (this._connected) { 557 | try { 558 | this.send(chunk) 559 | } catch (err) { 560 | return this.destroy(errCode(err, 'ERR_DATA_CHANNEL')) 561 | } 562 | if (this._channel.bufferedAmount > MAX_BUFFERED_AMOUNT) { 563 | this._debug('start backpressure: bufferedAmount %d', this._channel.bufferedAmount) 564 | this._cb = cb 565 | } else { 566 | cb(null) 567 | } 568 | } else { 569 | this._debug('write before connect') 570 | this._chunk = chunk 571 | this._cb = cb 572 | } 573 | } 574 | 575 | // When stream finishes writing, close socket. Half open connections are not 576 | // supported. 577 | _onFinish () { 578 | if (this.destroyed) return 579 | 580 | // Wait a bit before destroying so the socket flushes. 581 | // TODO: is there a more reliable way to accomplish this? 582 | const destroySoon = () => { 583 | setTimeout(() => this.destroy(), 1000) 584 | } 585 | 586 | if (this._connected) { 587 | destroySoon() 588 | } else { 589 | this.once('connect', destroySoon) 590 | } 591 | } 592 | 593 | _startIceCompleteTimeout () { 594 | if (this.destroyed) return 595 | if (this._iceCompleteTimer) return 596 | this._debug('started iceComplete timeout') 597 | this._iceCompleteTimer = setTimeout(() => { 598 | if (!this._iceComplete) { 599 | this._iceComplete = true 600 | this._debug('iceComplete timeout completed') 601 | this.emit('iceTimeout') 602 | this.emit('_iceComplete') 603 | } 604 | }, this.iceCompleteTimeout) 605 | } 606 | 607 | _createOffer () { 608 | if (this.destroyed) return 609 | 610 | this._pc.createOffer(this.offerOptions) 611 | .then(offer => { 612 | if (this.destroyed) return 613 | if (!this.trickle && !this.allowHalfTrickle) offer.sdp = filterTrickle(offer.sdp) 614 | offer.sdp = this.sdpTransform(offer.sdp) 615 | 616 | const sendOffer = () => { 617 | if (this.destroyed) return 618 | const signal = this._pc.localDescription || offer 619 | this._debug('signal') 620 | this.emit('signal', { 621 | type: signal.type, 622 | sdp: signal.sdp 623 | }) 624 | } 625 | 626 | const onSuccess = () => { 627 | this._debug('createOffer success') 628 | if (this.destroyed) return 629 | if (this.trickle || this._iceComplete) sendOffer() 630 | else this.once('_iceComplete', sendOffer) // wait for candidates 631 | } 632 | 633 | const onError = err => { 634 | this.destroy(errCode(err, 'ERR_SET_LOCAL_DESCRIPTION')) 635 | } 636 | 637 | this._pc.setLocalDescription(offer) 638 | .then(onSuccess) 639 | .catch(onError) 640 | }) 641 | .catch(err => { 642 | this.destroy(errCode(err, 'ERR_CREATE_OFFER')) 643 | }) 644 | } 645 | 646 | _requestMissingTransceivers () { 647 | if (this._pc.getTransceivers) { 648 | this._pc.getTransceivers().forEach(transceiver => { 649 | if (!transceiver.mid && transceiver.sender.track && !transceiver.requested) { 650 | transceiver.requested = true // HACK: Safari returns negotiated transceivers with a null mid 651 | this.addTransceiver(transceiver.sender.track.kind) 652 | } 653 | }) 654 | } 655 | } 656 | 657 | _createAnswer () { 658 | if (this.destroyed) return 659 | 660 | this._pc.createAnswer(this.answerOptions) 661 | .then(answer => { 662 | if (this.destroyed) return 663 | if (!this.trickle && !this.allowHalfTrickle) answer.sdp = filterTrickle(answer.sdp) 664 | answer.sdp = this.sdpTransform(answer.sdp) 665 | 666 | const sendAnswer = () => { 667 | if (this.destroyed) return 668 | const signal = this._pc.localDescription || answer 669 | this._debug('signal') 670 | this.emit('signal', { 671 | type: signal.type, 672 | sdp: signal.sdp 673 | }) 674 | if (!this.initiator) this._requestMissingTransceivers() 675 | } 676 | 677 | const onSuccess = () => { 678 | if (this.destroyed) return 679 | if (this.trickle || this._iceComplete) sendAnswer() 680 | else this.once('_iceComplete', sendAnswer) 681 | } 682 | 683 | const onError = err => { 684 | this.destroy(errCode(err, 'ERR_SET_LOCAL_DESCRIPTION')) 685 | } 686 | 687 | this._pc.setLocalDescription(answer) 688 | .then(onSuccess) 689 | .catch(onError) 690 | }) 691 | .catch(err => { 692 | this.destroy(errCode(err, 'ERR_CREATE_ANSWER')) 693 | }) 694 | } 695 | 696 | _onConnectionStateChange () { 697 | if (this.destroyed) return 698 | if (this._pc.connectionState === 'failed') { 699 | this.destroy(errCode(new Error('Connection failed.'), 'ERR_CONNECTION_FAILURE')) 700 | } 701 | } 702 | 703 | _onIceStateChange () { 704 | if (this.destroyed) return 705 | const iceConnectionState = this._pc.iceConnectionState 706 | const iceGatheringState = this._pc.iceGatheringState 707 | 708 | this._debug( 709 | 'iceStateChange (connection: %s) (gathering: %s)', 710 | iceConnectionState, 711 | iceGatheringState 712 | ) 713 | this.emit('iceStateChange', iceConnectionState, iceGatheringState) 714 | 715 | if (iceConnectionState === 'connected' || iceConnectionState === 'completed') { 716 | this._pcReady = true 717 | this._maybeReady() 718 | } 719 | if (iceConnectionState === 'failed') { 720 | this.destroy(errCode(new Error('Ice connection failed.'), 'ERR_ICE_CONNECTION_FAILURE')) 721 | } 722 | if (iceConnectionState === 'closed') { 723 | this.destroy(errCode(new Error('Ice connection closed.'), 'ERR_ICE_CONNECTION_CLOSED')) 724 | } 725 | } 726 | 727 | getStats (cb) { 728 | // statreports can come with a value array instead of properties 729 | const flattenValues = report => { 730 | if (Object.prototype.toString.call(report.values) === '[object Array]') { 731 | report.values.forEach(value => { 732 | Object.assign(report, value) 733 | }) 734 | } 735 | return report 736 | } 737 | 738 | // Promise-based getStats() (standard) 739 | if (this._pc.getStats.length === 0 || this._isReactNativeWebrtc) { 740 | this._pc.getStats() 741 | .then(res => { 742 | const reports = [] 743 | res.forEach(report => { 744 | reports.push(flattenValues(report)) 745 | }) 746 | cb(null, reports) 747 | }, err => cb(err)) 748 | 749 | // Single-parameter callback-based getStats() (non-standard) 750 | } else if (this._pc.getStats.length > 0) { 751 | this._pc.getStats(res => { 752 | // If we destroy connection in `connect` callback this code might happen to run when actual connection is already closed 753 | if (this.destroyed) return 754 | 755 | const reports = [] 756 | res.result().forEach(result => { 757 | const report = {} 758 | result.names().forEach(name => { 759 | report[name] = result.stat(name) 760 | }) 761 | report.id = result.id 762 | report.type = result.type 763 | report.timestamp = result.timestamp 764 | reports.push(flattenValues(report)) 765 | }) 766 | cb(null, reports) 767 | }, err => cb(err)) 768 | 769 | // Unknown browser, skip getStats() since it's anyone's guess which style of 770 | // getStats() they implement. 771 | } else { 772 | cb(null, []) 773 | } 774 | } 775 | 776 | _maybeReady () { 777 | this._debug('maybeReady pc %s channel %s', this._pcReady, this._channelReady) 778 | if (this._connected || this._connecting || !this._pcReady || !this._channelReady) return 779 | 780 | this._connecting = true 781 | 782 | // HACK: We can't rely on order here, for details see https://github.com/js-platform/node-webrtc/issues/339 783 | const findCandidatePair = () => { 784 | if (this.destroyed) return 785 | 786 | this.getStats((err, items) => { 787 | if (this.destroyed) return 788 | 789 | // Treat getStats error as non-fatal. It's not essential. 790 | if (err) items = [] 791 | 792 | const remoteCandidates = {} 793 | const localCandidates = {} 794 | const candidatePairs = {} 795 | let foundSelectedCandidatePair = false 796 | 797 | items.forEach(item => { 798 | // TODO: Once all browsers support the hyphenated stats report types, remove 799 | // the non-hypenated ones 800 | if (item.type === 'remotecandidate' || item.type === 'remote-candidate') { 801 | remoteCandidates[item.id] = item 802 | } 803 | if (item.type === 'localcandidate' || item.type === 'local-candidate') { 804 | localCandidates[item.id] = item 805 | } 806 | if (item.type === 'candidatepair' || item.type === 'candidate-pair') { 807 | candidatePairs[item.id] = item 808 | } 809 | }) 810 | 811 | const setSelectedCandidatePair = selectedCandidatePair => { 812 | foundSelectedCandidatePair = true 813 | 814 | let local = localCandidates[selectedCandidatePair.localCandidateId] 815 | 816 | if (local && (local.ip || local.address)) { 817 | // Spec 818 | this.localAddress = local.ip || local.address 819 | this.localPort = Number(local.port) 820 | } else if (local && local.ipAddress) { 821 | // Firefox 822 | this.localAddress = local.ipAddress 823 | this.localPort = Number(local.portNumber) 824 | } else if (typeof selectedCandidatePair.googLocalAddress === 'string') { 825 | // TODO: remove this once Chrome 58 is released 826 | local = selectedCandidatePair.googLocalAddress.split(':') 827 | this.localAddress = local[0] 828 | this.localPort = Number(local[1]) 829 | } 830 | if (this.localAddress) { 831 | this.localFamily = this.localAddress.includes(':') ? 'IPv6' : 'IPv4' 832 | } 833 | 834 | let remote = remoteCandidates[selectedCandidatePair.remoteCandidateId] 835 | 836 | if (remote && (remote.ip || remote.address)) { 837 | // Spec 838 | this.remoteAddress = remote.ip || remote.address 839 | this.remotePort = Number(remote.port) 840 | } else if (remote && remote.ipAddress) { 841 | // Firefox 842 | this.remoteAddress = remote.ipAddress 843 | this.remotePort = Number(remote.portNumber) 844 | } else if (typeof selectedCandidatePair.googRemoteAddress === 'string') { 845 | // TODO: remove this once Chrome 58 is released 846 | remote = selectedCandidatePair.googRemoteAddress.split(':') 847 | this.remoteAddress = remote[0] 848 | this.remotePort = Number(remote[1]) 849 | } 850 | if (this.remoteAddress) { 851 | this.remoteFamily = this.remoteAddress.includes(':') ? 'IPv6' : 'IPv4' 852 | } 853 | 854 | this._debug( 855 | 'connect local: %s:%s remote: %s:%s', 856 | this.localAddress, 857 | this.localPort, 858 | this.remoteAddress, 859 | this.remotePort 860 | ) 861 | } 862 | 863 | items.forEach(item => { 864 | // Spec-compliant 865 | if (item.type === 'transport' && item.selectedCandidatePairId) { 866 | setSelectedCandidatePair(candidatePairs[item.selectedCandidatePairId]) 867 | } 868 | 869 | // Old implementations 870 | if ( 871 | (item.type === 'googCandidatePair' && item.googActiveConnection === 'true') || 872 | ((item.type === 'candidatepair' || item.type === 'candidate-pair') && item.selected) 873 | ) { 874 | setSelectedCandidatePair(item) 875 | } 876 | }) 877 | 878 | // Ignore candidate pair selection in browsers like Safari 11 that do not have any local or remote candidates 879 | // But wait until at least 1 candidate pair is available 880 | if (!foundSelectedCandidatePair && (!Object.keys(candidatePairs).length || Object.keys(localCandidates).length)) { 881 | setTimeout(findCandidatePair, 100) 882 | return 883 | } else { 884 | this._connecting = false 885 | this._connected = true 886 | } 887 | 888 | if (this._chunk) { 889 | try { 890 | this.send(this._chunk) 891 | } catch (err) { 892 | return this.destroy(errCode(err, 'ERR_DATA_CHANNEL')) 893 | } 894 | this._chunk = null 895 | this._debug('sent chunk from "write before connect"') 896 | 897 | const cb = this._cb 898 | this._cb = null 899 | cb(null) 900 | } 901 | 902 | // If `bufferedAmountLowThreshold` and 'onbufferedamountlow' are unsupported, 903 | // fallback to using setInterval to implement backpressure. 904 | if (typeof this._channel.bufferedAmountLowThreshold !== 'number') { 905 | this._interval = setInterval(() => this._onInterval(), 150) 906 | if (this._interval.unref) this._interval.unref() 907 | } 908 | 909 | this._debug('connect') 910 | this.emit('connect') 911 | }) 912 | } 913 | findCandidatePair() 914 | } 915 | 916 | _onInterval () { 917 | if (!this._cb || !this._channel || this._channel.bufferedAmount > MAX_BUFFERED_AMOUNT) { 918 | return 919 | } 920 | this._onChannelBufferedAmountLow() 921 | } 922 | 923 | _onSignalingStateChange () { 924 | if (this.destroyed) return 925 | 926 | if (this._pc.signalingState === 'stable') { 927 | this._isNegotiating = false 928 | 929 | // HACK: Firefox doesn't yet support removing tracks when signalingState !== 'stable' 930 | this._debug('flushing sender queue', this._sendersAwaitingStable) 931 | this._sendersAwaitingStable.forEach(sender => { 932 | this._pc.removeTrack(sender) 933 | this._queuedNegotiation = true 934 | }) 935 | this._sendersAwaitingStable = [] 936 | 937 | if (this._queuedNegotiation) { 938 | this._debug('flushing negotiation queue') 939 | this._queuedNegotiation = false 940 | this._needsNegotiation() // negotiate again 941 | } else { 942 | this._debug('negotiated') 943 | this.emit('negotiated') 944 | } 945 | } 946 | 947 | this._debug('signalingStateChange %s', this._pc.signalingState) 948 | this.emit('signalingStateChange', this._pc.signalingState) 949 | } 950 | 951 | _onIceCandidate (event) { 952 | if (this.destroyed) return 953 | if (event.candidate && this.trickle) { 954 | this.emit('signal', { 955 | type: 'candidate', 956 | candidate: { 957 | candidate: event.candidate.candidate, 958 | sdpMLineIndex: event.candidate.sdpMLineIndex, 959 | sdpMid: event.candidate.sdpMid 960 | } 961 | }) 962 | } else if (!event.candidate && !this._iceComplete) { 963 | this._iceComplete = true 964 | this.emit('_iceComplete') 965 | } 966 | // as soon as we've received one valid candidate start timeout 967 | if (event.candidate) { 968 | this._startIceCompleteTimeout() 969 | } 970 | } 971 | 972 | _onChannelMessage (event) { 973 | if (this.destroyed) return 974 | let data = event.data 975 | if (data instanceof ArrayBuffer) data = Buffer.from(data) 976 | this.push(data) 977 | } 978 | 979 | _onChannelBufferedAmountLow () { 980 | if (this.destroyed || !this._cb) return 981 | this._debug('ending backpressure: bufferedAmount %d', this._channel.bufferedAmount) 982 | const cb = this._cb 983 | this._cb = null 984 | cb(null) 985 | } 986 | 987 | _onChannelOpen () { 988 | if (this._connected || this.destroyed) return 989 | this._debug('on channel open') 990 | this._channelReady = true 991 | this._maybeReady() 992 | } 993 | 994 | _onChannelClose () { 995 | if (this.destroyed) return 996 | this._debug('on channel close') 997 | this.destroy() 998 | } 999 | 1000 | _onTrack (event) { 1001 | if (this.destroyed) return 1002 | 1003 | event.streams.forEach(eventStream => { 1004 | this._debug('on track') 1005 | this.emit('track', event.track, eventStream) 1006 | 1007 | this._remoteTracks.push({ 1008 | track: event.track, 1009 | stream: eventStream 1010 | }) 1011 | 1012 | if (this._remoteStreams.some(remoteStream => { 1013 | return remoteStream.id === eventStream.id 1014 | })) return // Only fire one 'stream' event, even though there may be multiple tracks per stream 1015 | 1016 | this._remoteStreams.push(eventStream) 1017 | queueMicrotask(() => { 1018 | this._debug('on stream') 1019 | this.emit('stream', eventStream) // ensure all tracks have been added 1020 | }) 1021 | }) 1022 | } 1023 | 1024 | _debug () { 1025 | const args = [].slice.call(arguments) 1026 | args[0] = '[' + this._id + '] ' + args[0] 1027 | debug.apply(null, args) 1028 | } 1029 | } 1030 | 1031 | Peer.WEBRTC_SUPPORT = !!getBrowserRTC() 1032 | 1033 | /** 1034 | * Expose peer and data channel config for overriding all Peer 1035 | * instances. Otherwise, just set opts.config or opts.channelConfig 1036 | * when constructing a Peer. 1037 | */ 1038 | Peer.config = { 1039 | iceServers: [ 1040 | { 1041 | urls: [ 1042 | 'stun:stun.l.google.com:19302', 1043 | 'stun:global.stun.twilio.com:3478' 1044 | ] 1045 | } 1046 | ], 1047 | sdpSemantics: 'unified-plan' 1048 | } 1049 | 1050 | Peer.channelConfig = {} 1051 | 1052 | module.exports = Peer 1053 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-peer", 3 | "description": "Simple one-to-one WebRTC video/voice and data channels", 4 | "version": "9.11.1", 5 | "author": { 6 | "name": "Feross Aboukhadijeh", 7 | "email": "feross@feross.org", 8 | "url": "https://feross.org" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/feross/simple-peer/issues" 12 | }, 13 | "dependencies": { 14 | "buffer": "^6.0.3", 15 | "debug": "^4.3.2", 16 | "err-code": "^3.0.1", 17 | "get-browser-rtc": "^1.1.0", 18 | "queue-microtask": "^1.2.3", 19 | "randombytes": "^2.1.0", 20 | "readable-stream": "^3.6.0" 21 | }, 22 | "devDependencies": { 23 | "airtap": "^4.0.3", 24 | "airtap-manual": "^1.0.0", 25 | "airtap-sauce": "^1.1.0", 26 | "babel-minify": "^0.5.1", 27 | "bowser": "^2.11.0", 28 | "browserify": "^17.0.0", 29 | "coveralls": "^3.1.1", 30 | "nyc": "^15.1.0", 31 | "prettier-bytes": "^1.0.4", 32 | "simple-get": "^4.0.0", 33 | "speedometer": "^1.1.0", 34 | "standard": "*", 35 | "string-to-stream": "^3.0.1", 36 | "tape": "^5.5.2", 37 | "thunky": "^1.1.0", 38 | "wrtc": "^0.4.7", 39 | "ws": "^7.5.3" 40 | }, 41 | "keywords": [ 42 | "data", 43 | "data channel", 44 | "data channel stream", 45 | "data channels", 46 | "p2p", 47 | "peer", 48 | "peer", 49 | "peer-to-peer", 50 | "stream", 51 | "video", 52 | "voice", 53 | "webrtc", 54 | "webrtc stream" 55 | ], 56 | "license": "MIT", 57 | "main": "index.js", 58 | "repository": { 59 | "type": "git", 60 | "url": "git://github.com/feross/simple-peer.git" 61 | }, 62 | "scripts": { 63 | "build": "browserify -s SimplePeer -r . | minify > simplepeer.min.js", 64 | "size": "npm run build && cat simplepeer.min.js | gzip | wc -c", 65 | "// test": "standard && npm run test-node && npm run test-browser", 66 | "test": "standard && npm run test-browser", 67 | "test-browser": "airtap --coverage --concurrency 1 -- test/*.js", 68 | "test-browser-local": "airtap --coverage --preset local -- test/*.js", 69 | "test-node": "WRTC=wrtc tape test/*.js", 70 | "coverage": "nyc report --reporter=text-lcov | coveralls" 71 | }, 72 | "funding": [ 73 | { 74 | "type": "github", 75 | "url": "https://github.com/sponsors/feross" 76 | }, 77 | { 78 | "type": "patreon", 79 | "url": "https://www.patreon.com/feross" 80 | }, 81 | { 82 | "type": "consulting", 83 | "url": "https://feross.org/support" 84 | } 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /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 | const prettierBytes = require('prettier-bytes') 7 | const speedometer = require('speedometer') 8 | const Peer = require('simple-peer') 9 | 10 | const speed = speedometer() 11 | 12 | let peer 13 | 14 | const socket = new window.WebSocket('ws://localhost:8080') 15 | 16 | socket.addEventListener('message', onMessage) 17 | 18 | function onMessage (event) { 19 | const 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 | -------------------------------------------------------------------------------- /perf/send.js: -------------------------------------------------------------------------------- 1 | // run in a browser, with: 2 | // beefy perf/send.js 3 | 4 | const Peer = require('simple-peer') 5 | const stream = require('readable-stream') 6 | 7 | const buf = Buffer.alloc(10000) 8 | 9 | const endless = new stream.Readable({ 10 | read: function () { 11 | this.push(buf) 12 | } 13 | }) 14 | 15 | let peer 16 | 17 | const socket = new window.WebSocket('ws://localhost:8080') 18 | 19 | socket.addEventListener('message', onMessage) 20 | 21 | function onMessage (event) { 22 | const 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/server.js: -------------------------------------------------------------------------------- 1 | // run in a terminal, to do signaling for peers 2 | 3 | const ws = require('ws') 4 | 5 | const server = new ws.Server({ 6 | port: 8080 7 | }) 8 | 9 | const 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 | -------------------------------------------------------------------------------- /simplepeer.min.js: -------------------------------------------------------------------------------- 1 | (function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"==typeof window?"undefined"==typeof global?"undefined"==typeof self?this:self:global:window,t.SimplePeer=e()}})(function(){var t=Math.floor,n=Math.abs,r=Math.pow;return function(){function d(s,e,n){function t(o,i){if(!e[o]){if(!s[o]){var l="function"==typeof require&&require;if(!i&&l)return l(o,!0);if(r)return r(o,!0);var c=new Error("Cannot find module '"+o+"'");throw c.code="MODULE_NOT_FOUND",c}var a=e[o]={exports:{}};s[o][0].call(a.exports,function(e){var r=s[o][1][e];return t(r||e)},a,a.exports,d,s,e,n)}return e[o].exports}for(var r="function"==typeof require&&require,a=0;a>16,l[c++]=255&t>>8,l[c++]=255&t;return 2===s&&(t=u[e.charCodeAt(n)]<<2|u[e.charCodeAt(n+1)]>>4,l[c++]=255&t),1===s&&(t=u[e.charCodeAt(n)]<<10|u[e.charCodeAt(n+1)]<<4|u[e.charCodeAt(n+2)]>>2,l[c++]=255&t>>8,l[c++]=255&t),l}function d(e){return c[63&e>>18]+c[63&e>>12]+c[63&e>>6]+c[63&e]}function s(e,t,n){for(var r,a=[],o=t;ol?l:d+o));return 1===r?(t=e[n-1],a.push(c[t>>2]+c[63&t<<4]+"==")):2===r&&(t=(e[n-2]<<8)+e[n-1],a.push(c[t>>10]+c[63&t>>4]+c[63&t<<2]+"=")),a.join("")}n.byteLength=function(e){var t=r(e),n=t[0],a=t[1];return 3*(n+a)/4-a},n.toByteArray=o,n.fromByteArray=l;for(var c=[],u=[],p="undefined"==typeof Uint8Array?Array:Uint8Array,f="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",g=0,_=f.length;g<_;++g)c[g]=f[g],u[f.charCodeAt(g)]=g;u[45]=62,u[95]=63},{}],2:[function(){},{}],3:[function(e,t,n){(function(){(function(){/*! 2 | * The buffer module from node.js, for the browser. 3 | * 4 | * @author Feross Aboukhadijeh 5 | * @license MIT 6 | */'use strict';var t=String.fromCharCode,o=Math.min;function d(e){if(2147483647e)throw new RangeError("The value \""+e+"\" is invalid for option \"size\"")}function u(e,t,n){return c(e),0>=e?d(e):void 0===t?d(e):"string"==typeof n?d(e).fill(t,n):d(e).fill(t)}function p(e){return c(e),d(0>e?0:0|m(e))}function f(e,t){if(("string"!=typeof t||""===t)&&(t="utf8"),!s.isEncoding(t))throw new TypeError("Unknown encoding: "+t);var n=0|b(e,t),r=d(n),a=r.write(e,t);return a!==n&&(r=r.slice(0,a)),r}function g(e){for(var t=0>e.length?0:0|m(e.length),n=d(t),r=0;rt||e.byteLength=2147483647)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+2147483647 .toString(16)+" bytes");return 0|e}function b(e,t){if(s.isBuffer(e))return e.length;if(ArrayBuffer.isView(e)||K(e,ArrayBuffer))return e.byteLength;if("string"!=typeof e)throw new TypeError("The \"string\" argument must be one of type string, Buffer, or ArrayBuffer. Received type "+typeof e);var n=e.length,r=2>>1;case"base64":return z(e).length;default:if(a)return r?-1:H(e).length;t=(""+t).toLowerCase(),a=!0;}}function y(e,t,n){var r=!1;if((void 0===t||0>t)&&(t=0),t>this.length)return"";if((void 0===n||n>this.length)&&(n=this.length),0>=n)return"";if(n>>>=0,t>>>=0,n<=t)return"";for(e||(e="utf8");;)switch(e){case"hex":return P(this,t,n);case"utf8":case"utf-8":return x(this,t,n);case"ascii":return D(this,t,n);case"latin1":case"binary":return I(this,t,n);case"base64":return A(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return M(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0;}}function C(e,t,n){var r=e[t];e[t]=e[n],e[n]=r}function R(e,t,n,r,a){if(0===e.length)return-1;if("string"==typeof n?(r=n,n=0):2147483647n&&(n=-2147483648),n=+n,X(n)&&(n=a?0:e.length-1),0>n&&(n=e.length+n),n>=e.length){if(a)return-1;n=e.length-1}else if(0>n)if(a)n=0;else return-1;if("string"==typeof t&&(t=s.from(t,r)),s.isBuffer(t))return 0===t.length?-1:E(e,t,n,r,a);if("number"==typeof t)return t&=255,"function"==typeof Uint8Array.prototype.indexOf?a?Uint8Array.prototype.indexOf.call(e,t,n):Uint8Array.prototype.lastIndexOf.call(e,t,n):E(e,[t],n,r,a);throw new TypeError("val must be string, number or Buffer")}function E(e,t,n,r,a){function o(e,t){return 1===d?e[t]:e.readUInt16BE(t*d)}var d=1,s=e.length,l=t.length;if(void 0!==r&&(r=(r+"").toLowerCase(),"ucs2"===r||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(2>e.length||2>t.length)return-1;d=2,s/=2,l/=2,n/=2}var c;if(a){var u=-1;for(c=n;cs&&(n=s-l),c=n;0<=c;c--){for(var p=!0,f=0;fa&&(r=a)):r=a;var o=t.length;r>o/2&&(r=o/2);for(var d,s=0;sd&&(s=d):2===l?(c=e[a+1],128==(192&c)&&(f=(31&d)<<6|63&c,127f||57343f&&(s=f))):void 0}null===s?(s=65533,l=1):65535>>10),s=56320|1023&s),r.push(s),a+=l}return N(r)}function N(e){var n=e.length;if(n<=4096)return t.apply(String,e);for(var r="",a=0;at)&&(t=0),(!n||0>n||n>r)&&(n=r);for(var a="",o=t;oe)throw new RangeError("offset is not uint");if(e+t>n)throw new RangeError("Trying to access beyond buffer length")}function F(e,t,n,r,a,o){if(!s.isBuffer(e))throw new TypeError("\"buffer\" argument must be a Buffer instance");if(t>a||te.length)throw new RangeError("Index out of range")}function B(e,t,n,r){if(n+r>e.length)throw new RangeError("Index out of range");if(0>n)throw new RangeError("Index out of range")}function U(e,t,n,r,a){return t=+t,n>>>=0,a||B(e,t,n,4,34028234663852886e22,-34028234663852886e22),J.write(e,t,n,r,23,4),n+4}function j(e,t,n,r,a){return t=+t,n>>>=0,a||B(e,t,n,8,17976931348623157e292,-17976931348623157e292),J.write(e,t,n,r,52,8),n+8}function q(e){if(e=e.split("=")[0],e=e.trim().replace(Q,""),2>e.length)return"";for(;0!=e.length%4;)e+="=";return e}function W(e){return 16>e?"0"+e.toString(16):e.toString(16)}function H(e,t){t=t||1/0;for(var n,r=e.length,a=null,o=[],d=0;dn){if(!a){if(56319n){-1<(t-=3)&&o.push(239,191,189),a=n;continue}n=(a-55296<<10|n-56320)+65536}else a&&-1<(t-=3)&&o.push(239,191,189);if(a=null,128>n){if(0>(t-=1))break;o.push(n)}else if(2048>n){if(0>(t-=2))break;o.push(192|n>>6,128|63&n)}else if(65536>n){if(0>(t-=3))break;o.push(224|n>>12,128|63&n>>6,128|63&n)}else if(1114112>n){if(0>(t-=4))break;o.push(240|n>>18,128|63&n>>12,128|63&n>>6,128|63&n)}else throw new Error("Invalid code point")}return o}function Y(e){for(var t=[],n=0;n(t-=2));++d)n=e.charCodeAt(d),r=n>>8,a=n%256,o.push(a),o.push(r);return o}function z(e){return $.toByteArray(q(e))}function G(e,t,n,r){for(var a=0;a=t.length||a>=e.length);++a)t[a+n]=e[a];return a}function K(e,t){return e instanceof t||null!=e&&null!=e.constructor&&null!=e.constructor.name&&e.constructor.name===t.name}function X(e){return e!==e}var $=e("base64-js"),J=e("ieee754");n.Buffer=s,n.SlowBuffer=function(e){return+e!=e&&(e=0),s.alloc(+e)},n.INSPECT_MAX_BYTES=50;n.kMaxLength=2147483647,s.TYPED_ARRAY_SUPPORT=function(){try{var e=new Uint8Array(1);return e.__proto__={__proto__:Uint8Array.prototype,foo:function(){return 42}},42===e.foo()}catch(t){return!1}}(),s.TYPED_ARRAY_SUPPORT||"undefined"==typeof console||"function"!=typeof console.error||console.error("This browser lacks typed array (Uint8Array) support which is required by `buffer` v5.x. Use `buffer` v4.x if you require old browser support."),Object.defineProperty(s.prototype,"parent",{enumerable:!0,get:function(){return s.isBuffer(this)?this.buffer:void 0}}),Object.defineProperty(s.prototype,"offset",{enumerable:!0,get:function(){return s.isBuffer(this)?this.byteOffset:void 0}}),"undefined"!=typeof Symbol&&null!=Symbol.species&&s[Symbol.species]===s&&Object.defineProperty(s,Symbol.species,{value:null,configurable:!0,enumerable:!1,writable:!1}),s.poolSize=8192,s.from=function(e,t,n){return l(e,t,n)},s.prototype.__proto__=Uint8Array.prototype,s.__proto__=Uint8Array,s.alloc=function(e,t,n){return u(e,t,n)},s.allocUnsafe=function(e){return p(e)},s.allocUnsafeSlow=function(e){return p(e)},s.isBuffer=function(e){return null!=e&&!0===e._isBuffer&&e!==s.prototype},s.compare=function(e,t){if(K(e,Uint8Array)&&(e=s.from(e,e.offset,e.byteLength)),K(t,Uint8Array)&&(t=s.from(t,t.offset,t.byteLength)),!s.isBuffer(e)||!s.isBuffer(t))throw new TypeError("The \"buf1\", \"buf2\" arguments must be one of type Buffer or Uint8Array");if(e===t)return 0;for(var n=e.length,r=t.length,d=0,l=o(n,r);dt&&(e+=" ... "),""},s.prototype.compare=function(e,t,n,r,a){if(K(e,Uint8Array)&&(e=s.from(e,e.offset,e.byteLength)),!s.isBuffer(e))throw new TypeError("The \"target\" argument must be one of type Buffer or Uint8Array. Received type "+typeof e);if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===a&&(a=this.length),0>t||n>e.length||0>r||a>this.length)throw new RangeError("out of range index");if(r>=a&&t>=n)return 0;if(r>=a)return-1;if(t>=n)return 1;if(t>>>=0,n>>>=0,r>>>=0,a>>>=0,this===e)return 0;for(var d=a-r,l=n-t,c=o(d,l),u=this.slice(r,a),p=e.slice(t,n),f=0;f>>=0,isFinite(n)?(n>>>=0,void 0===r&&(r="utf8")):(r=n,n=void 0);else throw new Error("Buffer.write(string, encoding, offset[, length]) is no longer supported");var a=this.length-t;if((void 0===n||n>a)&&(n=a),0n||0>t)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var o=!1;;)switch(r){case"hex":return w(this,e,t,n);case"utf8":case"utf-8":return S(this,e,t,n);case"ascii":return T(this,e,t,n);case"latin1":case"binary":return v(this,e,t,n);case"base64":return k(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return L(this,e,t,n);default:if(o)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),o=!0;}},s.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};s.prototype.slice=function(e,t){var n=this.length;e=~~e,t=t===void 0?n:~~t,0>e?(e+=n,0>e&&(e=0)):e>n&&(e=n),0>t?(t+=n,0>t&&(t=0)):t>n&&(t=n),t>>=0,t>>>=0,n||O(e,t,this.length);for(var r=this[e],a=1,o=0;++o>>=0,t>>>=0,n||O(e,t,this.length);for(var r=this[e+--t],a=1;0>>=0,t||O(e,1,this.length),this[e]},s.prototype.readUInt16LE=function(e,t){return e>>>=0,t||O(e,2,this.length),this[e]|this[e+1]<<8},s.prototype.readUInt16BE=function(e,t){return e>>>=0,t||O(e,2,this.length),this[e]<<8|this[e+1]},s.prototype.readUInt32LE=function(e,t){return e>>>=0,t||O(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},s.prototype.readUInt32BE=function(e,t){return e>>>=0,t||O(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},s.prototype.readIntLE=function(e,t,n){e>>>=0,t>>>=0,n||O(e,t,this.length);for(var a=this[e],o=1,d=0;++d=o&&(a-=r(2,8*t)),a},s.prototype.readIntBE=function(e,t,n){e>>>=0,t>>>=0,n||O(e,t,this.length);for(var a=t,o=1,d=this[e+--a];0=o&&(d-=r(2,8*t)),d},s.prototype.readInt8=function(e,t){return e>>>=0,t||O(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},s.prototype.readInt16LE=function(e,t){e>>>=0,t||O(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},s.prototype.readInt16BE=function(e,t){e>>>=0,t||O(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},s.prototype.readInt32LE=function(e,t){return e>>>=0,t||O(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},s.prototype.readInt32BE=function(e,t){return e>>>=0,t||O(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},s.prototype.readFloatLE=function(e,t){return e>>>=0,t||O(e,4,this.length),J.read(this,e,!0,23,4)},s.prototype.readFloatBE=function(e,t){return e>>>=0,t||O(e,4,this.length),J.read(this,e,!1,23,4)},s.prototype.readDoubleLE=function(e,t){return e>>>=0,t||O(e,8,this.length),J.read(this,e,!0,52,8)},s.prototype.readDoubleBE=function(e,t){return e>>>=0,t||O(e,8,this.length),J.read(this,e,!1,52,8)},s.prototype.writeUIntLE=function(e,t,n,a){if(e=+e,t>>>=0,n>>>=0,!a){var o=r(2,8*n)-1;F(this,e,t,n,o,0)}var d=1,s=0;for(this[t]=255&e;++s>>=0,n>>>=0,!a){var o=r(2,8*n)-1;F(this,e,t,n,o,0)}var d=n-1,s=1;for(this[t+d]=255&e;0<=--d&&(s*=256);)this[t+d]=255&e/s;return t+n},s.prototype.writeUInt8=function(e,t,n){return e=+e,t>>>=0,n||F(this,e,t,1,255,0),this[t]=255&e,t+1},s.prototype.writeUInt16LE=function(e,t,n){return e=+e,t>>>=0,n||F(this,e,t,2,65535,0),this[t]=255&e,this[t+1]=e>>>8,t+2},s.prototype.writeUInt16BE=function(e,t,n){return e=+e,t>>>=0,n||F(this,e,t,2,65535,0),this[t]=e>>>8,this[t+1]=255&e,t+2},s.prototype.writeUInt32LE=function(e,t,n){return e=+e,t>>>=0,n||F(this,e,t,4,4294967295,0),this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e,t+4},s.prototype.writeUInt32BE=function(e,t,n){return e=+e,t>>>=0,n||F(this,e,t,4,4294967295,0),this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e,t+4},s.prototype.writeIntLE=function(e,t,n,a){if(e=+e,t>>>=0,!a){var o=r(2,8*n-1);F(this,e,t,n,o-1,-o)}var d=0,s=1,l=0;for(this[t]=255&e;++de&&0===l&&0!==this[t+d-1]&&(l=1),this[t+d]=255&(e/s>>0)-l;return t+n},s.prototype.writeIntBE=function(e,t,n,a){if(e=+e,t>>>=0,!a){var o=r(2,8*n-1);F(this,e,t,n,o-1,-o)}var d=n-1,s=1,l=0;for(this[t+d]=255&e;0<=--d&&(s*=256);)0>e&&0===l&&0!==this[t+d+1]&&(l=1),this[t+d]=255&(e/s>>0)-l;return t+n},s.prototype.writeInt8=function(e,t,n){return e=+e,t>>>=0,n||F(this,e,t,1,127,-128),0>e&&(e=255+e+1),this[t]=255&e,t+1},s.prototype.writeInt16LE=function(e,t,n){return e=+e,t>>>=0,n||F(this,e,t,2,32767,-32768),this[t]=255&e,this[t+1]=e>>>8,t+2},s.prototype.writeInt16BE=function(e,t,n){return e=+e,t>>>=0,n||F(this,e,t,2,32767,-32768),this[t]=e>>>8,this[t+1]=255&e,t+2},s.prototype.writeInt32LE=function(e,t,n){return e=+e,t>>>=0,n||F(this,e,t,4,2147483647,-2147483648),this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24,t+4},s.prototype.writeInt32BE=function(e,t,n){return e=+e,t>>>=0,n||F(this,e,t,4,2147483647,-2147483648),0>e&&(e=4294967295+e+1),this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e,t+4},s.prototype.writeFloatLE=function(e,t,n){return U(this,e,t,!0,n)},s.prototype.writeFloatBE=function(e,t,n){return U(this,e,t,!1,n)},s.prototype.writeDoubleLE=function(e,t,n){return j(this,e,t,!0,n)},s.prototype.writeDoubleBE=function(e,t,n){return j(this,e,t,!1,n)},s.prototype.copy=function(e,t,n,r){if(!s.isBuffer(e))throw new TypeError("argument should be a Buffer");if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),0t)throw new RangeError("targetStart out of bounds");if(0>n||n>=this.length)throw new RangeError("Index out of range");if(0>r)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-ta||"latin1"===r)&&(e=a)}}else"number"==typeof e&&(e&=255);if(0>t||this.length>>=0,n=n===void 0?this.length:n>>>0,e||(e=0);var o;if("number"==typeof e)for(o=t;o{"%%"===e||(r++,"%c"===e&&(a=r))}),e.splice(a,0,n)},n.save=function(e){try{e?n.storage.setItem("debug",e):n.storage.removeItem("debug")}catch(e){}},n.load=r,n.useColors=function(){return!!("undefined"!=typeof window&&window.process&&("renderer"===window.process.type||window.process.__nwjs))||!("undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))&&("undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)&&31<=parseInt(RegExp.$1,10)||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))},n.storage=function(){try{return localStorage}catch(e){}}(),n.destroy=(()=>{let e=!1;return()=>{e||(e=!0,console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."))}})(),n.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],n.log=console.debug||console.log||(()=>{}),t.exports=e("./common")(n);const{formatters:o}=t.exports;o.j=function(e){try{return JSON.stringify(e)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}}}).call(this)}).call(this,e("_process"))},{"./common":5,_process:12}],5:[function(e,t){t.exports=function(t){function r(e){function t(...e){if(!t.enabled)return;const a=t,o=+new Date,i=o-(n||o);a.diff=i,a.prev=n,a.curr=o,n=o,e[0]=r.coerce(e[0]),"string"!=typeof e[0]&&e.unshift("%O");let d=0;e[0]=e[0].replace(/%([a-zA-Z%])/g,(t,n)=>{if("%%"===t)return"%";d++;const o=r.formatters[n];if("function"==typeof o){const n=e[d];t=o.call(a,n),e.splice(d,1),d--}return t}),r.formatArgs.call(a,e);const s=a.log||r.log;s.apply(a,e)}let n,o=null;return t.namespace=e,t.useColors=r.useColors(),t.color=r.selectColor(e),t.extend=a,t.destroy=r.destroy,Object.defineProperty(t,"enabled",{enumerable:!0,configurable:!1,get:()=>null===o?r.enabled(e):o,set:e=>{o=e}}),"function"==typeof r.init&&r.init(t),t}function a(e,t){const n=r(this.namespace+("undefined"==typeof t?":":t)+e);return n.log=this.log,n}function o(e){return e.toString().substring(2,e.toString().length-2).replace(/\.\*\?$/,"*")}return r.debug=r,r.default=r,r.coerce=function(e){return e instanceof Error?e.stack||e.message:e},r.disable=function(){const e=[...r.names.map(o),...r.skips.map(o).map(e=>"-"+e)].join(",");return r.enable(""),e},r.enable=function(e){r.save(e),r.names=[],r.skips=[];let t;const n=("string"==typeof e?e:"").split(/[\s,]+/),a=n.length;for(t=0;t{r[e]=t[e]}),r.names=[],r.skips=[],r.formatters={},r.selectColor=function(e){let t=0;for(let n=0;nd&&!l.warned){l.warned=!0;var c=new Error("Possible EventEmitter memory leak detected. "+l.length+" "+(t+" listeners added. Use emitter.setMaxListeners() to increase limit"));c.name="MaxListenersExceededWarning",c.emitter=e,c.type=t,c.count=l.length,n(c)}return e}function d(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function s(e,t,n){var r={fired:!1,wrapFn:void 0,target:e,type:t,listener:n},a=d.bind(r);return a.listener=n,r.wrapFn=a,a}function l(e,t,n){var r=e._events;if(r===void 0)return[];var a=r[t];return void 0===a?[]:"function"==typeof a?n?[a.listener||a]:[a]:n?f(a):u(a,a.length)}function c(e){var t=this._events;if(t!==void 0){var n=t[e];if("function"==typeof n)return 1;if(void 0!==n)return n.length}return 0}function u(e,t){for(var n=Array(t),r=0;re||y(e))throw new RangeError("The value of \"defaultMaxListeners\" is out of range. It must be a non-negative number. Received "+e+".");C=e}}),r.init=function(){(this._events===void 0||this._events===Object.getPrototypeOf(this)._events)&&(this._events=Object.create(null),this._eventsCount=0),this._maxListeners=this._maxListeners||void 0},r.prototype.setMaxListeners=function(e){if("number"!=typeof e||0>e||y(e))throw new RangeError("The value of \"n\" is out of range. It must be a non-negative number. Received "+e+".");return this._maxListeners=e,this},r.prototype.getMaxListeners=function(){return o(this)},r.prototype.emit=function(e){for(var t=[],n=1;no)return this;0===o?n.shift():p(n,o),1===n.length&&(r[e]=n[0]),void 0!==r.removeListener&&this.emit("removeListener",e,s||t)}return this},r.prototype.off=r.prototype.removeListener,r.prototype.removeAllListeners=function(e){var t,n,r;if(n=this._events,void 0===n)return this;if(void 0===n.removeListener)return 0===arguments.length?(this._events=Object.create(null),this._eventsCount=0):void 0!==n[e]&&(0==--this._eventsCount?this._events=Object.create(null):delete n[e]),this;if(0===arguments.length){var a,o=Object.keys(n);for(r=0;r */o.read=function(t,n,a,o,l){var c,u,p=8*l-o-1,f=(1<>1,_=-7,h=a?l-1:0,b=a?-1:1,d=t[n+h];for(h+=b,c=d&(1<<-_)-1,d>>=-_,_+=p;0<_;c=256*c+t[n+h],h+=b,_-=8);for(u=c&(1<<-_)-1,c>>=-_,_+=o;0<_;u=256*u+t[n+h],h+=b,_-=8);if(0===c)c=1-g;else{if(c===f)return u?NaN:(d?-1:1)*(1/0);u+=r(2,o),c-=g}return(d?-1:1)*u*r(2,c-o)},o.write=function(a,o,l,u,p,f){var h,b,y,g=Math.LN2,_=Math.log,C=8*f-p-1,R=(1<>1,w=23===p?r(2,-24)-r(2,-77):0,S=u?0:f-1,T=u?1:-1,d=0>o||0===o&&0>1/o?1:0;for(o=n(o),isNaN(o)||o===1/0?(b=isNaN(o)?1:0,h=R):(h=t(_(o)/g),1>o*(y=r(2,-h))&&(h--,y*=2),o+=1<=h+E?w/y:w*r(2,1-E),2<=o*y&&(h++,y/=2),h+E>=R?(b=0,h=R):1<=h+E?(b=(o*y-1)*r(2,p),h+=E):(b=o*r(2,E-1)*r(2,p),h=0));8<=p;a[l+S]=255&b,S+=T,b/=256,p-=8);for(h=h<=1.5*a?"s":"")}var l=24*(60*60000);t.exports=function(e,t){t=t||{};var n=typeof e;if("string"==n&&0 */let n;t.exports="function"==typeof queueMicrotask?queueMicrotask.bind("undefined"==typeof window?e:window):e=>(n||(n=Promise.resolve())).then(e).catch(e=>setTimeout(()=>{throw e},0))}).call(this)}).call(this,"undefined"==typeof global?"undefined"==typeof self?"undefined"==typeof window?{}:window:self:global)},{}],14:[function(e,t){(function(n,r){(function(){'use strict';var a=e("safe-buffer").Buffer,o=r.crypto||r.msCrypto;t.exports=o&&o.getRandomValues?function(e,t){if(e>4294967295)throw new RangeError("requested too many random bytes");var r=a.allocUnsafe(e);if(0n?0:+n,t.length)===t}function i(e,t,n){return(void 0===n||n>e.length)&&(n=e.length),e.substring(n-t.length,n)===t}function d(e,t,n){return"number"!=typeof n&&(n=0),!(n+t.length>e.length)&&-1!==e.indexOf(t,n)}var s={};r("ERR_INVALID_OPT_VALUE",function(e,t){return"The value \""+t+"\" is invalid for option \""+e+"\""},TypeError),r("ERR_INVALID_ARG_TYPE",function(e,t,n){var r;"string"==typeof t&&o(t,"not ")?(r="must not be",t=t.replace(/^not /,"")):r="must be";var s;if(i(e," argument"))s="The ".concat(e," ").concat(r," ").concat(a(t,"type"));else{var l=d(e,".")?"property":"argument";s="The \"".concat(e,"\" ").concat(l," ").concat(r," ").concat(a(t,"type"))}return s+=". Received type ".concat(typeof n),s},TypeError),r("ERR_STREAM_PUSH_AFTER_EOF","stream.push() after EOF"),r("ERR_METHOD_NOT_IMPLEMENTED",function(e){return"The "+e+" method is not implemented"}),r("ERR_STREAM_PREMATURE_CLOSE","Premature close"),r("ERR_STREAM_DESTROYED",function(e){return"Cannot call "+e+" after a stream was destroyed"}),r("ERR_MULTIPLE_CALLBACK","Callback called multiple times"),r("ERR_STREAM_CANNOT_PIPE","Cannot pipe, not readable"),r("ERR_STREAM_WRITE_AFTER_END","write after end"),r("ERR_STREAM_NULL_VALUES","May not write null values to stream",TypeError),r("ERR_UNKNOWN_ENCODING",function(e){return"Unknown encoding: "+e},TypeError),r("ERR_STREAM_UNSHIFT_AFTER_END_EVENT","stream.unshift() after end event"),t.exports.codes=s},{}],16:[function(e,t){(function(n){(function(){'use strict';function r(e){return this instanceof r?void(d.call(this,e),s.call(this,e),this.allowHalfOpen=!0,e&&(!1===e.readable&&(this.readable=!1),!1===e.writable&&(this.writable=!1),!1===e.allowHalfOpen&&(this.allowHalfOpen=!1,this.once("end",a)))):new r(e)}function a(){this._writableState.ended||n.nextTick(o,this)}function o(e){e.end()}var i=Object.keys||function(e){var t=[];for(var n in e)t.push(n);return t};t.exports=r;var d=e("./_stream_readable"),s=e("./_stream_writable");e("inherits")(r,d);for(var l,c=i(s.prototype),u=0;u>>1,e|=e>>>2,e|=e>>>4,e|=e>>>8,e|=e>>>16,e++),e}function f(e,t){return 0>=e||0===t.length&&t.ended?0:t.objectMode?1:e===e?(e>t.highWaterMark&&(t.highWaterMark=p(e)),e<=t.length?e:t.ended?t.length:(t.needReadable=!0,0)):t.flowing&&t.length?t.buffer.head.data.length:t.length}function g(e,t){if(x("onEofChunk"),!t.ended){if(t.decoder){var n=t.decoder.end();n&&n.length&&(t.buffer.push(n),t.length+=t.objectMode?1:n.length)}t.ended=!0,t.sync?_(e):(t.needReadable=!1,!t.emittedReadable&&(t.emittedReadable=!0,h(e)))}}function _(e){var t=e._readableState;x("emitReadable",t.needReadable,t.emittedReadable),t.needReadable=!1,t.emittedReadable||(x("emitReadable",t.flowing),t.emittedReadable=!0,n.nextTick(h,e))}function h(e){var t=e._readableState;x("emitReadable_",t.destroyed,t.length,t.ended),!t.destroyed&&(t.length||t.ended)&&(e.emit("readable"),t.emittedReadable=!1),t.needReadable=!t.flowing&&!t.ended&&t.length<=t.highWaterMark,S(e)}function m(e,t){t.readingMore||(t.readingMore=!0,n.nextTick(b,e,t))}function b(e,t){for(;!t.reading&&!t.ended&&(t.length=t.length?(n=t.decoder?t.buffer.join(""):1===t.buffer.length?t.buffer.first():t.buffer.concat(t.length),t.buffer.clear()):n=t.buffer.consume(e,t.decoder),n}function v(e){var t=e._readableState;x("endReadable",t.endEmitted),t.endEmitted||(t.ended=!0,n.nextTick(k,t,e))}function k(e,t){if(x("endReadableNT",e.endEmitted,e.length),!e.endEmitted&&0===e.length&&(e.endEmitted=!0,t.readable=!1,t.emit("end"),e.autoDestroy)){var n=t._writableState;(!n||n.autoDestroy&&n.finished)&&t.destroy()}}function L(e,t){for(var n=0,r=e.length;n=t.highWaterMark)||t.ended))return x("read: emitReadable",t.length,t.ended),0===t.length&&t.ended?v(this):_(this),null;if(e=f(e,t),0===e&&t.ended)return 0===t.length&&v(this),null;var a=t.needReadable;x("need readable",a),(0===t.length||t.length-e>>0),n=this.head,r=0;n;)s(n.data,t,r),r+=n.data.length,n=n.next;return t}},{key:"consume",value:function(e,t){var n;return eo.length?o.length:e;if(a+=i===o.length?o:o.slice(0,e),e-=i,0===e){i===o.length?(++r,this.head=t.next?t.next:this.tail=null):(this.head=t,t.data=o.slice(i));break}++r}return this.length-=r,a}},{key:"_getBuffer",value:function(e){var t=u.allocUnsafe(e),r=this.head,a=1;for(r.data.copy(t),e-=r.data.length;r=r.next;){var o=r.data,i=e>o.length?o.length:e;if(o.copy(t,t.length-e,0,i),e-=i,0===e){i===o.length?(++a,this.head=r.next?r.next:this.tail=null):(this.head=r,r.data=o.slice(i));break}++a}return this.length-=a,t}},{key:g,value:function(e,t){return f(this,r({},t,{depth:0,customInspect:!1}))}}]),e}()},{buffer:3,util:2}],23:[function(e,t){(function(e){(function(){'use strict';function n(e,t){a(e,t),r(e)}function r(e){e._writableState&&!e._writableState.emitClose||e._readableState&&!e._readableState.emitClose||e.emit("close")}function a(e,t){e.emit("error",t)}t.exports={destroy:function(t,o){var i=this,d=this._readableState&&this._readableState.destroyed,s=this._writableState&&this._writableState.destroyed;return d||s?(o?o(t):t&&(this._writableState?!this._writableState.errorEmitted&&(this._writableState.errorEmitted=!0,e.nextTick(a,this,t)):e.nextTick(a,this,t)),this):(this._readableState&&(this._readableState.destroyed=!0),this._writableState&&(this._writableState.destroyed=!0),this._destroy(t||null,function(t){!o&&t?i._writableState?i._writableState.errorEmitted?e.nextTick(r,i):(i._writableState.errorEmitted=!0,e.nextTick(n,i,t)):e.nextTick(n,i,t):o?(e.nextTick(r,i),o(t)):e.nextTick(r,i)}),this)},undestroy:function(){this._readableState&&(this._readableState.destroyed=!1,this._readableState.reading=!1,this._readableState.ended=!1,this._readableState.endEmitted=!1),this._writableState&&(this._writableState.destroyed=!1,this._writableState.ended=!1,this._writableState.ending=!1,this._writableState.finalCalled=!1,this._writableState.prefinished=!1,this._writableState.finished=!1,this._writableState.errorEmitted=!1)},errorOrDestroy:function(e,t){var n=e._readableState,r=e._writableState;n&&n.autoDestroy||r&&r.autoDestroy?e.destroy(t):e.emit("error",t)}}}).call(this)}).call(this,e("_process"))},{_process:12}],24:[function(e,t){'use strict';function n(e){var t=!1;return function(){if(!t){t=!0;for(var n=arguments.length,r=Array(n),a=0;at.length)throw new u("streams");var a,l=t.map(function(e,n){var d=nd){var s=i?o:"highWaterMark";throw new a(s,d)}return t(d)}return e.objectMode?16:16384}}},{"../../../errors":15}],28:[function(e,t){t.exports=e("events").EventEmitter},{events:7}],29:[function(e,t,n){n=t.exports=e("./lib/_stream_readable.js"),n.Stream=n,n.Readable=n,n.Writable=e("./lib/_stream_writable.js"),n.Duplex=e("./lib/_stream_duplex.js"),n.Transform=e("./lib/_stream_transform.js"),n.PassThrough=e("./lib/_stream_passthrough.js"),n.finished=e("./lib/internal/streams/end-of-stream.js"),n.pipeline=e("./lib/internal/streams/pipeline.js")},{"./lib/_stream_duplex.js":16,"./lib/_stream_passthrough.js":17,"./lib/_stream_readable.js":18,"./lib/_stream_transform.js":19,"./lib/_stream_writable.js":20,"./lib/internal/streams/end-of-stream.js":24,"./lib/internal/streams/pipeline.js":26}],30:[function(e,t,n){function r(e,t){for(var n in e)t[n]=e[n]}function a(e,t,n){return i(e,t,n)}/*! safe-buffer. MIT License. Feross Aboukhadijeh */var o=e("buffer"),i=o.Buffer;i.from&&i.alloc&&i.allocUnsafe&&i.allocUnsafeSlow?t.exports=o:(r(o,n),n.Buffer=a),a.prototype=Object.create(i.prototype),r(i,a),a.from=function(e,t,n){if("number"==typeof e)throw new TypeError("Argument must not be a number");return i(e,t,n)},a.alloc=function(e,t,n){if("number"!=typeof e)throw new TypeError("Argument must be a number");var r=i(e);return void 0===t?r.fill(0):"string"==typeof n?r.fill(t,n):r.fill(t),r},a.allocUnsafe=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return i(e)},a.allocUnsafeSlow=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return o.SlowBuffer(e)}},{buffer:3}],31:[function(e,t,n){'use strict';function r(e){if(!e)return"utf8";for(var t;;)switch(e){case"utf8":case"utf-8":return"utf8";case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return"utf16le";case"latin1":case"binary":return"latin1";case"base64":case"ascii":case"hex":return e;default:if(t)return;e=(""+e).toLowerCase(),t=!0;}}function a(e){var t=r(e);if("string"!=typeof t&&(m.isEncoding===b||!b(e)))throw new Error("Unknown encoding: "+e);return t||e}function o(e){this.encoding=a(e);var t;switch(this.encoding){case"utf16le":this.text=u,this.end=p,t=4;break;case"utf8":this.fillLast=c,t=4;break;case"base64":this.text=f,this.end=g,t=3;break;default:return this.write=_,void(this.end=h);}this.lastNeed=0,this.lastTotal=0,this.lastChar=m.allocUnsafe(t)}function d(e){if(127>=e)return 0;return 6==e>>5?2:14==e>>4?3:30==e>>3?4:2==e>>6?-1:-2}function s(e,t,n){var r=t.length-1;if(r=r)return this.lastNeed=2,this.lastTotal=4,this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1],n.slice(0,-1)}return n}return this.lastNeed=1,this.lastTotal=2,this.lastChar[0]=e[e.length-1],e.toString("utf16le",t,e.length-1)}function p(e){var t=e&&e.length?this.write(e):"";if(this.lastNeed){var n=this.lastTotal-this.lastNeed;return t+this.lastChar.toString("utf16le",0,n)}return t}function f(e,t){var r=(e.length-t)%3;return 0==r?e.toString("base64",t):(this.lastNeed=3-r,this.lastTotal=3,1==r?this.lastChar[0]=e[e.length-1]:(this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1]),e.toString("base64",t,e.length-r))}function g(e){var t=e&&e.length?this.write(e):"";return this.lastNeed?t+this.lastChar.toString("base64",0,3-this.lastNeed):t}function _(e){return e.toString(this.encoding)}function h(e){return e&&e.length?this.write(e):""}var m=e("safe-buffer").Buffer,b=m.isEncoding||function(e){switch(e=""+e,e&&e.toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"binary":case"base64":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":case"raw":return!0;default:return!1;}};n.StringDecoder=o,o.prototype.write=function(e){if(0===e.length)return"";var t,n;if(this.lastNeed){if(t=this.fillLast(e),void 0===t)return"";n=this.lastNeed,this.lastNeed=0}else n=0;return n */const a=e("debug")("simple-peer"),o=e("get-browser-rtc"),i=e("randombytes"),d=e("readable-stream"),s=e("queue-microtask"),l=e("err-code"),{Buffer:c}=e("buffer"),u=65536;class p extends d.Duplex{constructor(e){if(e=Object.assign({allowHalfOpen:!1},e),super(e),this._id=i(4).toString("hex").slice(0,7),this._debug("new peer %o",e),this.channelName=e.initiator?e.channelName||i(20).toString("hex"):null,this.initiator=e.initiator||!1,this.channelConfig=e.channelConfig||p.channelConfig,this.channelNegotiated=this.channelConfig.negotiated,this.config=Object.assign({},p.config,e.config),this.offerOptions=e.offerOptions||{},this.answerOptions=e.answerOptions||{},this.sdpTransform=e.sdpTransform||(e=>e),this.streams=e.streams||(e.stream?[e.stream]:[]),this.trickle=void 0===e.trickle||e.trickle,this.allowHalfTrickle=void 0!==e.allowHalfTrickle&&e.allowHalfTrickle,this.iceCompleteTimeout=e.iceCompleteTimeout||5000,this.destroyed=!1,this.destroying=!1,this._connected=!1,this.remoteAddress=void 0,this.remoteFamily=void 0,this.remotePort=void 0,this.localAddress=void 0,this.localFamily=void 0,this.localPort=void 0,this._wrtc=e.wrtc&&"object"==typeof e.wrtc?e.wrtc:o(),!this._wrtc)if("undefined"==typeof window)throw l(new Error("No WebRTC support: Specify `opts.wrtc` option in this environment"),"ERR_WEBRTC_SUPPORT");else throw l(new Error("No WebRTC support: Not a supported browser"),"ERR_WEBRTC_SUPPORT");this._pcReady=!1,this._channelReady=!1,this._iceComplete=!1,this._iceCompleteTimer=null,this._channel=null,this._pendingCandidates=[],this._isNegotiating=!1,this._firstNegotiation=!0,this._batchedNegotiation=!1,this._queuedNegotiation=!1,this._sendersAwaitingStable=[],this._senderMap=new Map,this._closingInterval=null,this._remoteTracks=[],this._remoteStreams=[],this._chunk=null,this._cb=null,this._interval=null;try{this._pc=new this._wrtc.RTCPeerConnection(this.config)}catch(e){return void this.destroy(l(e,"ERR_PC_CONSTRUCTOR"))}this._isReactNativeWebrtc="number"==typeof this._pc._peerConnectionId,this._pc.oniceconnectionstatechange=()=>{this._onIceStateChange()},this._pc.onicegatheringstatechange=()=>{this._onIceStateChange()},this._pc.onconnectionstatechange=()=>{this._onConnectionStateChange()},this._pc.onsignalingstatechange=()=>{this._onSignalingStateChange()},this._pc.onicecandidate=e=>{this._onIceCandidate(e)},"object"==typeof this._pc.peerIdentity&&this._pc.peerIdentity.catch(e=>{this.destroy(l(e,"ERR_PC_PEER_IDENTITY"))}),this.initiator||this.channelNegotiated?this._setupData({channel:this._pc.createDataChannel(this.channelName,this.channelConfig)}):this._pc.ondatachannel=e=>{this._setupData(e)},this.streams&&this.streams.forEach(e=>{this.addStream(e)}),this._pc.ontrack=e=>{this._onTrack(e)},this._debug("initial negotiation"),this._needsNegotiation(),this._onFinishBound=()=>{this._onFinish()},this.once("finish",this._onFinishBound)}get bufferSize(){return this._channel&&this._channel.bufferedAmount||0}get connected(){return this._connected&&"open"===this._channel.readyState}address(){return{port:this.localPort,family:this.localFamily,address:this.localAddress}}signal(e){if(!this.destroying){if(this.destroyed)throw l(new Error("cannot signal after peer is destroyed"),"ERR_DESTROYED");if("string"==typeof e)try{e=JSON.parse(e)}catch(t){e={}}this._debug("signal()"),e.renegotiate&&this.initiator&&(this._debug("got request to renegotiate"),this._needsNegotiation()),e.transceiverRequest&&this.initiator&&(this._debug("got request for transceiver"),this.addTransceiver(e.transceiverRequest.kind,e.transceiverRequest.init)),e.candidate&&(this._pc.remoteDescription&&this._pc.remoteDescription.type?this._addIceCandidate(e.candidate):this._pendingCandidates.push(e.candidate)),e.sdp&&this._pc.setRemoteDescription(new this._wrtc.RTCSessionDescription(e)).then(()=>{this.destroyed||(this._pendingCandidates.forEach(e=>{this._addIceCandidate(e)}),this._pendingCandidates=[],"offer"===this._pc.remoteDescription.type&&this._createAnswer())}).catch(e=>{this.destroy(l(e,"ERR_SET_REMOTE_DESCRIPTION"))}),e.sdp||e.candidate||e.renegotiate||e.transceiverRequest||this.destroy(l(new Error("signal() called with invalid signal data"),"ERR_SIGNALING"))}}_addIceCandidate(e){const t=new this._wrtc.RTCIceCandidate(e);this._pc.addIceCandidate(t).catch(e=>{!t.address||t.address.endsWith(".local")?r("Ignoring unsupported ICE candidate."):this.destroy(l(e,"ERR_ADD_ICE_CANDIDATE"))})}send(e){if(!this.destroying){if(this.destroyed)throw l(new Error("cannot send after peer is destroyed"),"ERR_DESTROYED");this._channel.send(e)}}addTransceiver(e,t){if(!this.destroying){if(this.destroyed)throw l(new Error("cannot addTransceiver after peer is destroyed"),"ERR_DESTROYED");if(this._debug("addTransceiver()"),this.initiator)try{this._pc.addTransceiver(e,t),this._needsNegotiation()}catch(e){this.destroy(l(e,"ERR_ADD_TRANSCEIVER"))}else this.emit("signal",{type:"transceiverRequest",transceiverRequest:{kind:e,init:t}})}}addStream(e){if(!this.destroying){if(this.destroyed)throw l(new Error("cannot addStream after peer is destroyed"),"ERR_DESTROYED");this._debug("addStream()"),e.getTracks().forEach(t=>{this.addTrack(t,e)})}}addTrack(e,t){if(this.destroying)return;if(this.destroyed)throw l(new Error("cannot addTrack after peer is destroyed"),"ERR_DESTROYED");this._debug("addTrack()");const n=this._senderMap.get(e)||new Map;let r=n.get(t);if(!r)r=this._pc.addTrack(e,t),n.set(t,r),this._senderMap.set(e,n),this._needsNegotiation();else if(r.removed)throw l(new Error("Track has been removed. You should enable/disable tracks that you want to re-add."),"ERR_SENDER_REMOVED");else throw l(new Error("Track has already been added to that stream."),"ERR_SENDER_ALREADY_ADDED")}replaceTrack(e,t,n){if(this.destroying)return;if(this.destroyed)throw l(new Error("cannot replaceTrack after peer is destroyed"),"ERR_DESTROYED");this._debug("replaceTrack()");const r=this._senderMap.get(e),a=r?r.get(n):null;if(!a)throw l(new Error("Cannot replace track that was never added."),"ERR_TRACK_NOT_ADDED");t&&this._senderMap.set(t,r),null==a.replaceTrack?this.destroy(l(new Error("replaceTrack is not supported in this browser"),"ERR_UNSUPPORTED_REPLACETRACK")):a.replaceTrack(t)}removeTrack(e,t){if(this.destroying)return;if(this.destroyed)throw l(new Error("cannot removeTrack after peer is destroyed"),"ERR_DESTROYED");this._debug("removeSender()");const n=this._senderMap.get(e),r=n?n.get(t):null;if(!r)throw l(new Error("Cannot remove track that was never added."),"ERR_TRACK_NOT_ADDED");try{r.removed=!0,this._pc.removeTrack(r)}catch(e){"NS_ERROR_UNEXPECTED"===e.name?this._sendersAwaitingStable.push(r):this.destroy(l(e,"ERR_REMOVE_TRACK"))}this._needsNegotiation()}removeStream(e){if(!this.destroying){if(this.destroyed)throw l(new Error("cannot removeStream after peer is destroyed"),"ERR_DESTROYED");this._debug("removeSenders()"),e.getTracks().forEach(t=>{this.removeTrack(t,e)})}}_needsNegotiation(){this._debug("_needsNegotiation"),this._batchedNegotiation||(this._batchedNegotiation=!0,s(()=>{this._batchedNegotiation=!1,this.initiator||!this._firstNegotiation?(this._debug("starting batched negotiation"),this.negotiate()):this._debug("non-initiator initial negotiation request discarded"),this._firstNegotiation=!1}))}negotiate(){if(!this.destroying){if(this.destroyed)throw l(new Error("cannot negotiate after peer is destroyed"),"ERR_DESTROYED");this.initiator?this._isNegotiating?(this._queuedNegotiation=!0,this._debug("already negotiating, queueing")):(this._debug("start negotiation"),setTimeout(()=>{this._createOffer()},0)):this._isNegotiating?(this._queuedNegotiation=!0,this._debug("already negotiating, queueing")):(this._debug("requesting negotiation from initiator"),this.emit("signal",{type:"renegotiate",renegotiate:!0})),this._isNegotiating=!0}}destroy(e){this._destroy(e,()=>{})}_destroy(e,t){this.destroyed||this.destroying||(this.destroying=!0,this._debug("destroying (error: %s)",e&&(e.message||e)),s(()=>{if(this.destroyed=!0,this.destroying=!1,this._debug("destroy (error: %s)",e&&(e.message||e)),this.readable=this.writable=!1,this._readableState.ended||this.push(null),this._writableState.finished||this.end(),this._connected=!1,this._pcReady=!1,this._channelReady=!1,this._remoteTracks=null,this._remoteStreams=null,this._senderMap=null,clearInterval(this._closingInterval),this._closingInterval=null,clearInterval(this._interval),this._interval=null,this._chunk=null,this._cb=null,this._onFinishBound&&this.removeListener("finish",this._onFinishBound),this._onFinishBound=null,this._channel){try{this._channel.close()}catch(e){}this._channel.onmessage=null,this._channel.onopen=null,this._channel.onclose=null,this._channel.onerror=null}if(this._pc){try{this._pc.close()}catch(e){}this._pc.oniceconnectionstatechange=null,this._pc.onicegatheringstatechange=null,this._pc.onsignalingstatechange=null,this._pc.onicecandidate=null,this._pc.ontrack=null,this._pc.ondatachannel=null}this._pc=null,this._channel=null,e&&this.emit("error",e),this.emit("close"),t()}))}_setupData(e){if(!e.channel)return this.destroy(l(new Error("Data channel event is missing `channel` property"),"ERR_DATA_CHANNEL"));this._channel=e.channel,this._channel.binaryType="arraybuffer","number"==typeof this._channel.bufferedAmountLowThreshold&&(this._channel.bufferedAmountLowThreshold=u),this.channelName=this._channel.label,this._channel.onmessage=e=>{this._onChannelMessage(e)},this._channel.onbufferedamountlow=()=>{this._onChannelBufferedAmountLow()},this._channel.onopen=()=>{this._onChannelOpen()},this._channel.onclose=()=>{this._onChannelClose()},this._channel.onerror=e=>{const t=e.error instanceof Error?e.error:new Error(`Datachannel error: ${e.message} ${e.filename}:${e.lineno}:${e.colno}`);this.destroy(l(t,"ERR_DATA_CHANNEL"))};let t=!1;this._closingInterval=setInterval(()=>{this._channel&&"closing"===this._channel.readyState?(t&&this._onChannelClose(),t=!0):t=!1},5000)}_read(){}_write(e,t,n){if(this.destroyed)return n(l(new Error("cannot write after peer is destroyed"),"ERR_DATA_CHANNEL"));if(this._connected){try{this.send(e)}catch(e){return this.destroy(l(e,"ERR_DATA_CHANNEL"))}this._channel.bufferedAmount>u?(this._debug("start backpressure: bufferedAmount %d",this._channel.bufferedAmount),this._cb=n):n(null)}else this._debug("write before connect"),this._chunk=e,this._cb=n}_onFinish(){if(!this.destroyed){const e=()=>{setTimeout(()=>this.destroy(),1e3)};this._connected?e():this.once("connect",e)}}_startIceCompleteTimeout(){this.destroyed||this._iceCompleteTimer||(this._debug("started iceComplete timeout"),this._iceCompleteTimer=setTimeout(()=>{this._iceComplete||(this._iceComplete=!0,this._debug("iceComplete timeout completed"),this.emit("iceTimeout"),this.emit("_iceComplete"))},this.iceCompleteTimeout))}_createOffer(){this.destroyed||this._pc.createOffer(this.offerOptions).then(e=>{if(this.destroyed)return;this.trickle||this.allowHalfTrickle||(e.sdp=n(e.sdp)),e.sdp=this.sdpTransform(e.sdp);const t=()=>{if(!this.destroyed){const t=this._pc.localDescription||e;this._debug("signal"),this.emit("signal",{type:t.type,sdp:t.sdp})}};this._pc.setLocalDescription(e).then(()=>{this._debug("createOffer success"),this.destroyed||(this.trickle||this._iceComplete?t():this.once("_iceComplete",t))}).catch(e=>{this.destroy(l(e,"ERR_SET_LOCAL_DESCRIPTION"))})}).catch(e=>{this.destroy(l(e,"ERR_CREATE_OFFER"))})}_requestMissingTransceivers(){this._pc.getTransceivers&&this._pc.getTransceivers().forEach(e=>{e.mid||!e.sender.track||e.requested||(e.requested=!0,this.addTransceiver(e.sender.track.kind))})}_createAnswer(){this.destroyed||this._pc.createAnswer(this.answerOptions).then(e=>{if(this.destroyed)return;this.trickle||this.allowHalfTrickle||(e.sdp=n(e.sdp)),e.sdp=this.sdpTransform(e.sdp);const t=()=>{if(!this.destroyed){const t=this._pc.localDescription||e;this._debug("signal"),this.emit("signal",{type:t.type,sdp:t.sdp}),this.initiator||this._requestMissingTransceivers()}};this._pc.setLocalDescription(e).then(()=>{this.destroyed||(this.trickle||this._iceComplete?t():this.once("_iceComplete",t))}).catch(e=>{this.destroy(l(e,"ERR_SET_LOCAL_DESCRIPTION"))})}).catch(e=>{this.destroy(l(e,"ERR_CREATE_ANSWER"))})}_onConnectionStateChange(){this.destroyed||"failed"===this._pc.connectionState&&this.destroy(l(new Error("Connection failed."),"ERR_CONNECTION_FAILURE"))}_onIceStateChange(){if(this.destroyed)return;const e=this._pc.iceConnectionState,t=this._pc.iceGatheringState;this._debug("iceStateChange (connection: %s) (gathering: %s)",e,t),this.emit("iceStateChange",e,t),("connected"===e||"completed"===e)&&(this._pcReady=!0,this._maybeReady()),"failed"===e&&this.destroy(l(new Error("Ice connection failed."),"ERR_ICE_CONNECTION_FAILURE")),"closed"===e&&this.destroy(l(new Error("Ice connection closed."),"ERR_ICE_CONNECTION_CLOSED"))}getStats(e){const t=e=>("[object Array]"===Object.prototype.toString.call(e.values)&&e.values.forEach(t=>{Object.assign(e,t)}),e);0===this._pc.getStats.length||this._isReactNativeWebrtc?this._pc.getStats().then(n=>{const r=[];n.forEach(e=>{r.push(t(e))}),e(null,r)},t=>e(t)):0{if(this.destroyed)return;const r=[];n.result().forEach(e=>{const n={};e.names().forEach(t=>{n[t]=e.stat(t)}),n.id=e.id,n.type=e.type,n.timestamp=e.timestamp,r.push(t(n))}),e(null,r)},t=>e(t)):e(null,[])}_maybeReady(){if(this._debug("maybeReady pc %s channel %s",this._pcReady,this._channelReady),this._connected||this._connecting||!this._pcReady||!this._channelReady)return;this._connecting=!0;const e=()=>{this.destroyed||this.getStats((t,n)=>{if(this.destroyed)return;t&&(n=[]);const r={},a={},o={};let i=!1;n.forEach(e=>{("remotecandidate"===e.type||"remote-candidate"===e.type)&&(r[e.id]=e),("localcandidate"===e.type||"local-candidate"===e.type)&&(a[e.id]=e),("candidatepair"===e.type||"candidate-pair"===e.type)&&(o[e.id]=e)});const d=e=>{i=!0;let t=a[e.localCandidateId];t&&(t.ip||t.address)?(this.localAddress=t.ip||t.address,this.localPort=+t.port):t&&t.ipAddress?(this.localAddress=t.ipAddress,this.localPort=+t.portNumber):"string"==typeof e.googLocalAddress&&(t=e.googLocalAddress.split(":"),this.localAddress=t[0],this.localPort=+t[1]),this.localAddress&&(this.localFamily=this.localAddress.includes(":")?"IPv6":"IPv4");let n=r[e.remoteCandidateId];n&&(n.ip||n.address)?(this.remoteAddress=n.ip||n.address,this.remotePort=+n.port):n&&n.ipAddress?(this.remoteAddress=n.ipAddress,this.remotePort=+n.portNumber):"string"==typeof e.googRemoteAddress&&(n=e.googRemoteAddress.split(":"),this.remoteAddress=n[0],this.remotePort=+n[1]),this.remoteAddress&&(this.remoteFamily=this.remoteAddress.includes(":")?"IPv6":"IPv4"),this._debug("connect local: %s:%s remote: %s:%s",this.localAddress,this.localPort,this.remoteAddress,this.remotePort)};if(n.forEach(e=>{"transport"===e.type&&e.selectedCandidatePairId&&d(o[e.selectedCandidatePairId]),("googCandidatePair"===e.type&&"true"===e.googActiveConnection||("candidatepair"===e.type||"candidate-pair"===e.type)&&e.selected)&&d(e)}),!i&&(!Object.keys(o).length||Object.keys(a).length))return void setTimeout(e,100);if(this._connecting=!1,this._connected=!0,this._chunk){try{this.send(this._chunk)}catch(e){return this.destroy(l(e,"ERR_DATA_CHANNEL"))}this._chunk=null,this._debug("sent chunk from \"write before connect\"");const e=this._cb;this._cb=null,e(null)}"number"!=typeof this._channel.bufferedAmountLowThreshold&&(this._interval=setInterval(()=>this._onInterval(),150),this._interval.unref&&this._interval.unref()),this._debug("connect"),this.emit("connect")})};e()}_onInterval(){this._cb&&this._channel&&!(this._channel.bufferedAmount>u)&&this._onChannelBufferedAmountLow()}_onSignalingStateChange(){this.destroyed||("stable"===this._pc.signalingState&&(this._isNegotiating=!1,this._debug("flushing sender queue",this._sendersAwaitingStable),this._sendersAwaitingStable.forEach(e=>{this._pc.removeTrack(e),this._queuedNegotiation=!0}),this._sendersAwaitingStable=[],this._queuedNegotiation?(this._debug("flushing negotiation queue"),this._queuedNegotiation=!1,this._needsNegotiation()):(this._debug("negotiated"),this.emit("negotiated"))),this._debug("signalingStateChange %s",this._pc.signalingState),this.emit("signalingStateChange",this._pc.signalingState))}_onIceCandidate(e){this.destroyed||(e.candidate&&this.trickle?this.emit("signal",{type:"candidate",candidate:{candidate:e.candidate.candidate,sdpMLineIndex:e.candidate.sdpMLineIndex,sdpMid:e.candidate.sdpMid}}):!e.candidate&&!this._iceComplete&&(this._iceComplete=!0,this.emit("_iceComplete")),e.candidate&&this._startIceCompleteTimeout())}_onChannelMessage(e){if(this.destroyed)return;let t=e.data;t instanceof ArrayBuffer&&(t=c.from(t)),this.push(t)}_onChannelBufferedAmountLow(){if(!this.destroyed&&this._cb){this._debug("ending backpressure: bufferedAmount %d",this._channel.bufferedAmount);const e=this._cb;this._cb=null,e(null)}}_onChannelOpen(){this._connected||this.destroyed||(this._debug("on channel open"),this._channelReady=!0,this._maybeReady())}_onChannelClose(){this.destroyed||(this._debug("on channel close"),this.destroy())}_onTrack(e){this.destroyed||e.streams.forEach(t=>{this._debug("on track"),this.emit("track",e.track,t),this._remoteTracks.push({track:e.track,stream:t}),this._remoteStreams.some(e=>e.id===t.id)||(this._remoteStreams.push(t),s(()=>{this._debug("on stream"),this.emit("stream",t)}))})}_debug(){const e=[].slice.call(arguments);e[0]="["+this._id+"] "+e[0],a.apply(null,e)}}p.WEBRTC_SUPPORT=!!o(),p.config={iceServers:[{urls:["stun:stun.l.google.com:19302","stun:global.stun.twilio.com:3478"]}],sdpSemantics:"unified-plan"},p.channelConfig={},t.exports=p},{buffer:3,debug:4,"err-code":6,"get-browser-rtc":8,"queue-microtask":13,randombytes:14,"readable-stream":29}]},{},[])("/")}); -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | const common = require('./common') 2 | const Peer = require('../') 3 | const test = require('tape') 4 | 5 | let 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('detect WebRTC support', function (t) { 15 | t.equal(Peer.WEBRTC_SUPPORT, typeof window !== 'undefined', 'builtin webrtc support') 16 | t.end() 17 | }) 18 | 19 | test('create peer without options', function (t) { 20 | t.plan(1) 21 | 22 | if (process.browser) { 23 | let peer 24 | t.doesNotThrow(function () { 25 | peer = new Peer() 26 | }) 27 | peer.destroy() 28 | } else { 29 | t.pass('Skip no-option test in Node.js, since the wrtc option is required') 30 | } 31 | }) 32 | 33 | test('can detect error when RTCPeerConstructor throws', function (t) { 34 | t.plan(1) 35 | 36 | const peer = new Peer({ wrtc: { RTCPeerConnection: null } }) 37 | peer.once('error', function () { 38 | t.pass('got error event') 39 | peer.destroy() 40 | }) 41 | }) 42 | 43 | test('signal event gets emitted', function (t) { 44 | t.plan(2) 45 | 46 | const peer = new Peer({ config, initiator: true, wrtc: common.wrtc }) 47 | peer.once('signal', function () { 48 | t.pass('got signal event') 49 | peer.on('close', function () { t.pass('peer destroyed') }) 50 | peer.destroy() 51 | }) 52 | }) 53 | 54 | test('signal event does not get emitted by non-initiator', function (t) { 55 | const peer = new Peer({ config, initiator: false, wrtc: common.wrtc }) 56 | peer.once('signal', function () { 57 | t.fail('got signal event') 58 | peer.on('close', function () { t.pass('peer destroyed') }) 59 | peer.destroy() 60 | }) 61 | 62 | setTimeout(() => { 63 | t.pass('did not get signal after 1000ms') 64 | t.end() 65 | }, 1000) 66 | }) 67 | 68 | test('signal event does not get emitted by non-initiator with stream', function (t) { 69 | const peer = new Peer({ 70 | config, 71 | stream: common.getMediaStream(), 72 | initiator: false, 73 | wrtc: common.wrtc 74 | }) 75 | peer.once('signal', function () { 76 | t.fail('got signal event') 77 | peer.on('close', function () { t.pass('peer destroyed') }) 78 | peer.destroy() 79 | }) 80 | 81 | setTimeout(() => { 82 | t.pass('did not get signal after 1000ms') 83 | t.end() 84 | }, 1000) 85 | }) 86 | 87 | test('data send/receive text', function (t) { 88 | t.plan(10) 89 | 90 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc }) 91 | const peer2 = new Peer({ config, wrtc: common.wrtc }) 92 | 93 | let numSignal1 = 0 94 | peer1.on('signal', function (data) { 95 | numSignal1 += 1 96 | peer2.signal(data) 97 | }) 98 | 99 | let numSignal2 = 0 100 | peer2.on('signal', function (data) { 101 | numSignal2 += 1 102 | peer1.signal(data) 103 | }) 104 | 105 | peer1.on('connect', tryTest) 106 | peer2.on('connect', tryTest) 107 | 108 | function tryTest () { 109 | if (!peer1.connected || !peer2.connected) return 110 | 111 | t.ok(numSignal1 >= 1) 112 | t.ok(numSignal2 >= 1) 113 | t.equal(peer1.initiator, true, 'peer1 is initiator') 114 | t.equal(peer2.initiator, false, 'peer2 is not initiator') 115 | 116 | peer1.send('sup peer2') 117 | peer2.on('data', function (data) { 118 | t.ok(Buffer.isBuffer(data), 'data is Buffer') 119 | t.equal(data.toString(), 'sup peer2', 'got correct message') 120 | 121 | peer2.send('sup peer1') 122 | peer1.on('data', function (data) { 123 | t.ok(Buffer.isBuffer(data), 'data is Buffer') 124 | t.equal(data.toString(), 'sup peer1', 'got correct message') 125 | 126 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 127 | peer1.destroy() 128 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 129 | peer2.destroy() 130 | }) 131 | }) 132 | } 133 | }) 134 | 135 | test('sdpTransform function is called', function (t) { 136 | t.plan(3) 137 | 138 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc }) 139 | const peer2 = new Peer({ config, sdpTransform, wrtc: common.wrtc }) 140 | 141 | function sdpTransform (sdp) { 142 | t.equal(typeof sdp, 'string', 'got a string as SDP') 143 | setTimeout(function () { 144 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 145 | peer1.destroy() 146 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 147 | peer2.destroy() 148 | }, 0) 149 | return sdp 150 | } 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 | 161 | test('old constraint formats are used', function (t) { 162 | t.plan(3) 163 | 164 | const constraints = { 165 | mandatory: { 166 | OfferToReceiveAudio: true, 167 | OfferToReceiveVideo: true 168 | } 169 | } 170 | 171 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc, constraints }) 172 | const peer2 = new Peer({ config, wrtc: common.wrtc, constraints }) 173 | 174 | peer1.on('signal', function (data) { 175 | peer2.signal(data) 176 | }) 177 | 178 | peer2.on('signal', function (data) { 179 | peer1.signal(data) 180 | }) 181 | 182 | peer1.on('connect', function () { 183 | t.pass('peers connected') 184 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 185 | peer1.destroy() 186 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 187 | peer2.destroy() 188 | }) 189 | }) 190 | 191 | test('new constraint formats are used', function (t) { 192 | t.plan(3) 193 | 194 | const constraints = { 195 | offerToReceiveAudio: true, 196 | offerToReceiveVideo: true 197 | } 198 | 199 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc, constraints }) 200 | const peer2 = new Peer({ config, wrtc: common.wrtc, constraints }) 201 | 202 | peer1.on('signal', function (data) { 203 | peer2.signal(data) 204 | }) 205 | 206 | peer2.on('signal', function (data) { 207 | peer1.signal(data) 208 | }) 209 | 210 | peer1.on('connect', function () { 211 | t.pass('peers connected') 212 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 213 | peer1.destroy() 214 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 215 | peer2.destroy() 216 | }) 217 | }) 218 | 219 | test('ensure remote address and port are available right after connection', function (t) { 220 | if (common.isBrowser('safari') || common.isBrowser('ios')) { 221 | t.pass('Skip on Safari and iOS which do not support modern getStats() calls') 222 | t.end() 223 | return 224 | } 225 | if (common.isBrowser('chrome') || common.isBrowser('edge')) { 226 | t.pass('Skip on Chrome and Edge which hide local IPs with mDNS') 227 | t.end() 228 | return 229 | } 230 | 231 | t.plan(7) 232 | 233 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc }) 234 | const peer2 = new Peer({ config, wrtc: common.wrtc }) 235 | 236 | peer1.on('signal', function (data) { 237 | peer2.signal(data) 238 | }) 239 | 240 | peer2.on('signal', function (data) { 241 | peer1.signal(data) 242 | }) 243 | 244 | peer1.on('connect', function () { 245 | t.pass('peers connected') 246 | 247 | t.ok(peer1.remoteAddress, 'peer1 remote address is present') 248 | t.ok(peer1.remotePort, 'peer1 remote port is present') 249 | 250 | peer2.on('connect', function () { 251 | t.ok(peer2.remoteAddress, 'peer2 remote address is present') 252 | t.ok(peer2.remotePort, 'peer2 remote port is present') 253 | 254 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 255 | peer1.destroy() 256 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 257 | peer2.destroy() 258 | }) 259 | }) 260 | }) 261 | 262 | test('ensure iceStateChange fires when connection failed', (t) => { 263 | t.plan(1) 264 | const peer = new Peer({ config, initiator: true, wrtc: common.wrtc }) 265 | 266 | peer.on('iceStateChange', (connectionState, gatheringState) => { 267 | t.pass('got iceStateChange') 268 | t.end() 269 | }) 270 | 271 | // simulate concurrent iceConnectionStateChange and destroy() 272 | peer.destroy() 273 | peer._pc.oniceconnectionstatechange() 274 | }) 275 | -------------------------------------------------------------------------------- /test/binary.js: -------------------------------------------------------------------------------- 1 | const common = require('./common') 2 | const Peer = require('../') 3 | const test = require('tape') 4 | 5 | let 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 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc }) 18 | const peer2 = new Peer({ 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 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc }) 54 | const peer2 = new Peer({ 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 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc }) 92 | const peer2 = new Peer({ 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/common.js: -------------------------------------------------------------------------------- 1 | const get = require('simple-get') 2 | const thunky = require('thunky') 3 | const bowser = require('bowser') 4 | 5 | exports.getConfig = thunky(function (cb) { 6 | // Includes TURN -- needed for tests to pass on Sauce Labs 7 | // https://github.com/feross/simple-peer/issues/41 8 | // WARNING: This is *NOT* a public endpoint. Do not depend on it in your app. 9 | get.concat('https://instant.io/__rtcConfig__', function (err, res, data) { 10 | if (err) return cb(err) 11 | data = data.toString() 12 | try { 13 | data = JSON.parse(data) 14 | } catch (err) { 15 | cb(err) 16 | return 17 | } 18 | cb(null, data) 19 | }) 20 | }) 21 | 22 | // For testing on node, we must provide a WebRTC implementation 23 | if (process.env.WRTC === 'wrtc') { 24 | exports.wrtc = require('wrtc') 25 | } 26 | 27 | // create a test MediaStream with two tracks 28 | let canvas 29 | exports.getMediaStream = function () { 30 | if (exports.wrtc) { 31 | const source = new exports.wrtc.nonstandard.RTCVideoSource() 32 | const tracks = [source.createTrack(), source.createTrack()] 33 | return new exports.wrtc.MediaStream(tracks) 34 | } else { 35 | if (!canvas) { 36 | canvas = document.createElement('canvas') 37 | canvas.width = canvas.height = 100 38 | canvas.getContext('2d') // initialize canvas 39 | } 40 | const stream = canvas.captureStream(30) 41 | stream.addTrack(stream.getTracks()[0].clone()) // should have 2 tracks 42 | return stream 43 | } 44 | } 45 | 46 | exports.isBrowser = function (name) { 47 | if (typeof (window) === 'undefined') return false 48 | const satifyObject = {} 49 | if (name === 'ios') { // bowser can't directly name iOS Safari 50 | satifyObject.mobile = { safari: '>=0' } 51 | } else { 52 | satifyObject[name] = '>=0' 53 | } 54 | return bowser.getParser(window.navigator.userAgent).satisfies(satifyObject) 55 | } 56 | -------------------------------------------------------------------------------- /test/multistream.js: -------------------------------------------------------------------------------- 1 | const common = require('./common') 2 | const Peer = require('../') 3 | const test = require('tape') 4 | 5 | let 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 | if (common.isBrowser('ios')) { 16 | t.pass('Skip on iOS emulator which does not support this reliably') // iOS emulator issue #486 17 | t.end() 18 | return 19 | } 20 | t.plan(20) 21 | 22 | const peer1 = new Peer({ 23 | config, 24 | initiator: true, 25 | wrtc: common.wrtc, 26 | streams: (new Array(10)).fill(null).map(function () { return common.getMediaStream() }) 27 | }) 28 | const peer2 = new Peer({ 29 | config, 30 | wrtc: common.wrtc, 31 | streams: (new Array(10)).fill(null).map(function () { return common.getMediaStream() }) 32 | }) 33 | 34 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 35 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 36 | 37 | const receivedIds = {} 38 | 39 | peer1.on('stream', function (stream) { 40 | t.pass('peer1 got stream') 41 | if (receivedIds[stream.id]) { 42 | t.fail('received one unique stream per event') 43 | } else { 44 | receivedIds[stream.id] = true 45 | } 46 | }) 47 | peer2.on('stream', function (stream) { 48 | t.pass('peer2 got stream') 49 | if (receivedIds[stream.id]) { 50 | t.fail('received one unique stream per event') 51 | } else { 52 | receivedIds[stream.id] = true 53 | } 54 | }) 55 | 56 | t.on('end', () => { 57 | peer1.destroy() 58 | peer2.destroy() 59 | }) 60 | }) 61 | 62 | test('multistream (track event)', function (t) { 63 | t.plan(20) 64 | 65 | const peer1 = new Peer({ 66 | config, 67 | initiator: true, 68 | wrtc: common.wrtc, 69 | streams: (new Array(5)).fill(null).map(function () { return common.getMediaStream() }) 70 | }) 71 | const peer2 = new Peer({ 72 | config, 73 | wrtc: common.wrtc, 74 | streams: (new Array(5)).fill(null).map(function () { return common.getMediaStream() }) 75 | }) 76 | 77 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 78 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 79 | 80 | const receivedIds = {} 81 | 82 | peer1.on('track', function (track) { 83 | t.pass('peer1 got track') 84 | if (receivedIds[track.id]) { 85 | t.fail('received one unique track per event') 86 | } else { 87 | receivedIds[track.id] = true 88 | } 89 | }) 90 | peer2.on('track', function (track) { 91 | t.pass('peer2 got track') 92 | if (receivedIds[track.id]) { 93 | t.fail('received one unique track per event') 94 | } else { 95 | receivedIds[track.id] = true 96 | } 97 | }) 98 | 99 | t.on('end', () => { 100 | peer1.destroy() 101 | peer2.destroy() 102 | }) 103 | }) 104 | 105 | test('multistream on non-initiator only', function (t) { 106 | t.plan(30) 107 | 108 | const peer1 = new Peer({ 109 | config, 110 | initiator: true, 111 | wrtc: common.wrtc, 112 | streams: [] 113 | }) 114 | const peer2 = new Peer({ 115 | config, 116 | wrtc: common.wrtc, 117 | streams: (new Array(10)).fill(null).map(function () { return common.getMediaStream() }) 118 | }) 119 | 120 | peer1.on('signal', function (data) { 121 | if (data.transceiverRequest) t.pass('got transceiverRequest') 122 | if (!peer2.destroyed) peer2.signal(data) 123 | }) 124 | peer2.on('signal', function (data) { 125 | if (data.transceiverRequest) t.pass('got transceiverRequest') 126 | if (!peer1.destroyed) peer1.signal(data) 127 | }) 128 | 129 | const receivedIds = {} 130 | 131 | peer1.on('stream', function (stream) { 132 | t.pass('peer1 got stream') 133 | if (receivedIds[stream.id]) { 134 | t.fail('received one unique stream per event') 135 | } else { 136 | receivedIds[stream.id] = true 137 | } 138 | }) 139 | 140 | t.on('end', () => { 141 | peer1.destroy() 142 | peer2.destroy() 143 | }) 144 | }) 145 | 146 | test('delayed stream on non-initiator', function (t) { 147 | if (common.isBrowser('ios')) { 148 | t.pass('Skip on iOS which does not support this reliably') 149 | t.end() 150 | return 151 | } 152 | t.timeoutAfter(15000) 153 | t.plan(1) 154 | 155 | const peer1 = new Peer({ 156 | config, 157 | trickle: true, 158 | initiator: true, 159 | wrtc: common.wrtc, 160 | streams: [common.getMediaStream()] 161 | }) 162 | const peer2 = new Peer({ 163 | config, 164 | trickle: true, 165 | wrtc: common.wrtc, 166 | streams: [] 167 | }) 168 | 169 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 170 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 171 | 172 | setTimeout(() => { 173 | peer2.addStream(common.getMediaStream()) 174 | }, 10000) 175 | peer1.on('stream', function () { 176 | t.pass('peer1 got stream') 177 | }) 178 | 179 | t.on('end', () => { 180 | peer1.destroy() 181 | peer2.destroy() 182 | }) 183 | }) 184 | 185 | test('incremental multistream', function (t) { 186 | if (common.isBrowser('ios')) { 187 | t.pass('Skip on iOS emulator which does not support this reliably') // iOS emulator issue #486 188 | t.end() 189 | return 190 | } 191 | t.plan(12) 192 | 193 | const peer1 = new Peer({ 194 | config, 195 | initiator: true, 196 | wrtc: common.wrtc, 197 | streams: [] 198 | }) 199 | const peer2 = new Peer({ 200 | config, 201 | wrtc: common.wrtc, 202 | streams: [] 203 | }) 204 | 205 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 206 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 207 | 208 | peer1.on('connect', function () { 209 | t.pass('peer1 connected') 210 | peer1.addStream(common.getMediaStream()) 211 | }) 212 | peer2.on('connect', function () { 213 | t.pass('peer2 connected') 214 | peer2.addStream(common.getMediaStream()) 215 | }) 216 | 217 | const receivedIds = {} 218 | 219 | let count1 = 0 220 | peer1.on('stream', function (stream) { 221 | t.pass('peer1 got stream') 222 | if (receivedIds[stream.id]) { 223 | t.fail('received one unique stream per event') 224 | } else { 225 | receivedIds[stream.id] = true 226 | } 227 | count1++ 228 | if (count1 < 5) { 229 | peer1.addStream(common.getMediaStream()) 230 | } 231 | }) 232 | 233 | let count2 = 0 234 | peer2.on('stream', function (stream) { 235 | t.pass('peer2 got stream') 236 | if (receivedIds[stream.id]) { 237 | t.fail('received one unique stream per event') 238 | } else { 239 | receivedIds[stream.id] = true 240 | } 241 | count2++ 242 | if (count2 < 5) { 243 | peer2.addStream(common.getMediaStream()) 244 | } 245 | }) 246 | 247 | t.on('end', () => { 248 | peer1.destroy() 249 | peer2.destroy() 250 | }) 251 | }) 252 | 253 | test('incremental multistream (track event)', function (t) { 254 | t.plan(22) 255 | 256 | const peer1 = new Peer({ 257 | config, 258 | initiator: true, 259 | wrtc: common.wrtc, 260 | streams: [] 261 | }) 262 | const peer2 = new Peer({ 263 | config, 264 | wrtc: common.wrtc, 265 | streams: [] 266 | }) 267 | 268 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 269 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 270 | 271 | peer1.on('connect', function () { 272 | t.pass('peer1 connected') 273 | peer1.addStream(common.getMediaStream()) 274 | }) 275 | peer2.on('connect', function () { 276 | t.pass('peer2 connected') 277 | peer2.addStream(common.getMediaStream()) 278 | }) 279 | 280 | const receivedIds = {} 281 | 282 | let count1 = 0 283 | peer1.on('track', function (track) { 284 | t.pass('peer1 got track') 285 | if (receivedIds[track.id]) { 286 | t.fail('received one unique track per event') 287 | } else { 288 | receivedIds[track.id] = true 289 | } 290 | count1++ 291 | if (count1 % 2 === 0 && count1 < 10) { 292 | peer1.addStream(common.getMediaStream()) 293 | } 294 | }) 295 | 296 | let count2 = 0 297 | peer2.on('track', function (track) { 298 | t.pass('peer2 got track') 299 | if (receivedIds[track.id]) { 300 | t.fail('received one unique track per event') 301 | } else { 302 | receivedIds[track.id] = true 303 | } 304 | count2++ 305 | if (count2 % 2 === 0 && count2 < 10) { 306 | peer2.addStream(common.getMediaStream()) 307 | } 308 | }) 309 | 310 | t.on('end', () => { 311 | peer1.destroy() 312 | peer2.destroy() 313 | }) 314 | }) 315 | 316 | test('incremental multistream on non-initiator only', function (t) { 317 | if (common.isBrowser('ios')) { 318 | t.pass('Skip on iOS emulator which does not support this reliably') // iOS emulator issue #486 319 | t.end() 320 | return 321 | } 322 | t.plan(7) 323 | 324 | const peer1 = new Peer({ 325 | config, 326 | initiator: true, 327 | wrtc: common.wrtc, 328 | streams: [] 329 | }) 330 | const peer2 = new Peer({ 331 | config, 332 | wrtc: common.wrtc, 333 | streams: [] 334 | }) 335 | 336 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 337 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 338 | 339 | peer1.on('connect', function () { 340 | t.pass('peer1 connected') 341 | }) 342 | peer2.on('connect', function () { 343 | t.pass('peer2 connected') 344 | peer2.addStream(common.getMediaStream()) 345 | }) 346 | 347 | const receivedIds = {} 348 | 349 | let count = 0 350 | peer1.on('stream', function (stream) { 351 | t.pass('peer1 got stream') 352 | if (receivedIds[stream.id]) { 353 | t.fail('received one unique stream per event') 354 | } else { 355 | receivedIds[stream.id] = true 356 | } 357 | count++ 358 | if (count < 5) { 359 | peer2.addStream(common.getMediaStream()) 360 | } 361 | }) 362 | 363 | t.on('end', () => { 364 | peer1.destroy() 365 | peer2.destroy() 366 | }) 367 | }) 368 | 369 | test('incremental multistream on non-initiator only (track event)', function (t) { 370 | t.plan(12) 371 | 372 | const peer1 = new Peer({ 373 | config, 374 | initiator: true, 375 | wrtc: common.wrtc, 376 | streams: [] 377 | }) 378 | const peer2 = new Peer({ 379 | config, 380 | wrtc: common.wrtc, 381 | streams: [] 382 | }) 383 | 384 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 385 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 386 | 387 | peer1.on('connect', function () { 388 | t.pass('peer1 connected') 389 | }) 390 | peer2.on('connect', function () { 391 | t.pass('peer2 connected') 392 | peer2.addStream(common.getMediaStream()) 393 | }) 394 | 395 | const receivedIds = {} 396 | 397 | let count = 0 398 | peer1.on('track', function (track) { 399 | t.pass('peer1 got track') 400 | if (receivedIds[track.id]) { 401 | t.fail('received one unique track per event') 402 | } else { 403 | receivedIds[track.id] = true 404 | } 405 | count++ 406 | if (count % 2 === 0 && count < 10) { 407 | peer2.addStream(common.getMediaStream()) 408 | } 409 | }) 410 | 411 | t.on('end', () => { 412 | peer1.destroy() 413 | peer2.destroy() 414 | }) 415 | }) 416 | 417 | test('addStream after removeStream', function (t) { 418 | if (common.isBrowser('ios')) { 419 | t.pass('Skip on iOS which does not support this reliably') 420 | t.end() 421 | return 422 | } 423 | t.plan(2) 424 | 425 | const stream1 = common.getMediaStream() 426 | const stream2 = common.getMediaStream() 427 | 428 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc }) 429 | const peer2 = new Peer({ config, wrtc: common.wrtc, streams: [stream1] }) 430 | 431 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 432 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 433 | 434 | peer1.once('stream', () => { 435 | t.pass('peer1 got first stream') 436 | peer2.removeStream(stream1) 437 | setTimeout(() => { 438 | peer1.once('stream', () => { 439 | t.pass('peer1 got second stream') 440 | }) 441 | peer2.addStream(stream2) 442 | }, 1000) 443 | }) 444 | 445 | t.on('end', () => { 446 | peer1.destroy() 447 | peer2.destroy() 448 | }) 449 | }) 450 | 451 | test('removeTrack immediately', function (t) { 452 | t.plan(2) 453 | 454 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc }) 455 | const peer2 = new Peer({ config, wrtc: common.wrtc }) 456 | 457 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 458 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 459 | 460 | const stream1 = common.getMediaStream() 461 | const stream2 = common.getMediaStream() 462 | 463 | peer1.addTrack(stream1.getTracks()[0], stream1) 464 | peer2.addTrack(stream2.getTracks()[0], stream2) 465 | 466 | peer1.removeTrack(stream1.getTracks()[0], stream1) 467 | peer2.removeTrack(stream2.getTracks()[0], stream2) 468 | 469 | peer1.on('track', function (track, stream) { 470 | t.fail('peer1 did not get track event') 471 | }) 472 | peer2.on('track', function (track, stream) { 473 | t.fail('peer2 did not get track event') 474 | }) 475 | 476 | peer1.on('connect', function () { 477 | t.pass('peer1 connected') 478 | }) 479 | peer2.on('connect', function () { 480 | t.pass('peer2 connected') 481 | }) 482 | 483 | t.on('end', () => { 484 | peer1.destroy() 485 | peer2.destroy() 486 | }) 487 | }) 488 | 489 | test('replaceTrack', function (t) { 490 | t.plan(4) 491 | 492 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc }) 493 | const peer2 = new Peer({ config, wrtc: common.wrtc }) 494 | 495 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 496 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 497 | 498 | const stream1 = common.getMediaStream() 499 | const stream2 = common.getMediaStream() 500 | 501 | peer1.addTrack(stream1.getTracks()[0], stream1) 502 | peer2.addTrack(stream2.getTracks()[0], stream2) 503 | 504 | peer1.replaceTrack(stream1.getTracks()[0], stream2.getTracks()[0], stream1) 505 | peer2.replaceTrack(stream2.getTracks()[0], stream1.getTracks()[0], stream2) 506 | 507 | peer1.on('track', function (track, stream) { 508 | t.pass('peer1 got track event') 509 | peer2.replaceTrack(stream2.getTracks()[0], null, stream2) 510 | }) 511 | peer2.on('track', function (track, stream) { 512 | t.pass('peer2 got track event') 513 | peer1.replaceTrack(stream1.getTracks()[0], null, stream1) 514 | }) 515 | 516 | peer1.on('connect', function () { 517 | t.pass('peer1 connected') 518 | }) 519 | peer2.on('connect', function () { 520 | t.pass('peer2 connected') 521 | }) 522 | 523 | t.on('end', () => { 524 | peer1.destroy() 525 | peer2.destroy() 526 | }) 527 | }) 528 | -------------------------------------------------------------------------------- /test/negotiation.js: -------------------------------------------------------------------------------- 1 | const common = require('./common') 2 | const Peer = require('../') 3 | const test = require('tape') 4 | 5 | let 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 | const peer1 = new Peer({ config, initiator: true, stream: common.getMediaStream(), wrtc: common.wrtc }) 18 | const peer2 = new Peer({ config, stream: common.getMediaStream(), wrtc: common.wrtc }) 19 | 20 | peer1.on('signal', function (data) { 21 | if (data.renegotiate) t.fail('got unexpected request to renegotiate') 22 | if (!peer2.destroyed) peer2.signal(data) 23 | }) 24 | peer2.on('signal', function (data) { 25 | if (data.renegotiate) t.fail('got unexpected request to renegotiate') 26 | if (!peer1.destroyed) peer1.signal(data) 27 | }) 28 | 29 | peer1.on('connect', function () { 30 | t.pass('peer1 connected') 31 | }) 32 | peer2.on('connect', function () { 33 | t.pass('peer2 connected') 34 | }) 35 | 36 | peer1.on('stream', function (stream) { 37 | t.pass('peer1 got stream') 38 | }) 39 | peer2.on('stream', function (stream) { 40 | t.pass('peer2 got stream') 41 | }) 42 | 43 | let trackCount1 = 0 44 | peer1.on('track', function (track) { 45 | t.pass('peer1 got track') 46 | trackCount1++ 47 | if (trackCount1 >= 2) { 48 | t.pass('got correct number of tracks') 49 | } 50 | }) 51 | let trackCount2 = 0 52 | peer2.on('track', function (track) { 53 | t.pass('peer2 got track') 54 | trackCount2++ 55 | if (trackCount2 >= 2) { 56 | t.pass('got correct number of tracks') 57 | } 58 | }) 59 | }) 60 | 61 | test('manual renegotiation', function (t) { 62 | t.plan(2) 63 | 64 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc }) 65 | const peer2 = new Peer({ config, wrtc: common.wrtc }) 66 | 67 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 68 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 69 | 70 | peer1.on('connect', function () { 71 | peer1.negotiate() 72 | 73 | peer1.on('negotiated', function () { 74 | t.pass('peer1 negotiated') 75 | }) 76 | peer2.on('negotiated', function () { 77 | t.pass('peer2 negotiated') 78 | }) 79 | }) 80 | }) 81 | 82 | test('repeated manual renegotiation', function (t) { 83 | t.plan(6) 84 | 85 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc }) 86 | const peer2 = new Peer({ config, wrtc: common.wrtc }) 87 | 88 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 89 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 90 | 91 | peer1.once('connect', function () { 92 | peer1.negotiate() 93 | }) 94 | peer1.once('negotiated', function () { 95 | t.pass('peer1 negotiated') 96 | peer1.negotiate() 97 | peer1.once('negotiated', function () { 98 | t.pass('peer1 negotiated again') 99 | peer1.negotiate() 100 | peer1.once('negotiated', function () { 101 | t.pass('peer1 negotiated again') 102 | }) 103 | }) 104 | }) 105 | peer2.once('negotiated', function () { 106 | t.pass('peer2 negotiated') 107 | peer2.negotiate() 108 | peer2.once('negotiated', function () { 109 | t.pass('peer2 negotiated again') 110 | peer1.negotiate() 111 | peer1.once('negotiated', function () { 112 | t.pass('peer1 negotiated again') 113 | }) 114 | }) 115 | }) 116 | }) 117 | 118 | test('renegotiation after addStream', function (t) { 119 | if (common.isBrowser('ios')) { 120 | t.pass('Skip on iOS which does not support this reliably') 121 | t.end() 122 | return 123 | } 124 | t.plan(4) 125 | 126 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc }) 127 | const peer2 = new Peer({ config, wrtc: common.wrtc }) 128 | 129 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 130 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 131 | 132 | peer1.on('connect', function () { 133 | t.pass('peer1 connect') 134 | peer1.addStream(common.getMediaStream()) 135 | }) 136 | peer2.on('connect', function () { 137 | t.pass('peer2 connect') 138 | peer2.addStream(common.getMediaStream()) 139 | }) 140 | peer1.on('stream', function () { 141 | t.pass('peer1 got stream') 142 | }) 143 | peer2.on('stream', function () { 144 | t.pass('peer2 got stream') 145 | }) 146 | }) 147 | 148 | test('add stream on non-initiator only', function (t) { 149 | t.plan(3) 150 | 151 | const peer1 = new Peer({ 152 | config, 153 | initiator: true, 154 | wrtc: common.wrtc 155 | }) 156 | const peer2 = new Peer({ 157 | config, 158 | wrtc: common.wrtc, 159 | stream: common.getMediaStream() 160 | }) 161 | 162 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 163 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 164 | 165 | peer1.on('connect', function () { 166 | t.pass('peer1 connect') 167 | }) 168 | peer2.on('connect', function () { 169 | t.pass('peer2 connect') 170 | }) 171 | peer1.on('stream', function () { 172 | t.pass('peer1 got stream') 173 | }) 174 | }) 175 | 176 | test('negotiated channels', function (t) { 177 | t.plan(2) 178 | 179 | const peer1 = new Peer({ 180 | config, 181 | initiator: true, 182 | wrtc: common.wrtc, 183 | channelConfig: { 184 | id: 1, 185 | negotiated: true 186 | } 187 | }) 188 | const peer2 = new Peer({ 189 | config, 190 | wrtc: common.wrtc, 191 | channelConfig: { 192 | id: 1, 193 | negotiated: true 194 | } 195 | }) 196 | 197 | peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) }) 198 | peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) }) 199 | 200 | peer1.on('connect', function () { 201 | t.pass('peer1 connect') 202 | }) 203 | peer2.on('connect', function () { 204 | t.pass('peer2 connect') 205 | }) 206 | }) 207 | -------------------------------------------------------------------------------- /test/object-mode.js: -------------------------------------------------------------------------------- 1 | const common = require('./common') 2 | const Peer = require('../') 3 | const test = require('tape') 4 | 5 | let 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 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc, objectMode: true }) 18 | const peer2 = new Peer({ 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 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc, objectMode: true }) 54 | const peer2 = new Peer({ 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 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc, objectMode: true }) 90 | const peer2 = new Peer({ 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 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc, objectMode: true }) 128 | const peer2 = new Peer({ 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/stream.js: -------------------------------------------------------------------------------- 1 | const common = require('./common') 2 | const Peer = require('../') 3 | const str = require('string-to-stream') 4 | const test = require('tape') 5 | 6 | let 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 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc }) 20 | const peer2 = new Peer({ 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 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc }) 56 | const peer2 = new Peer({ 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/trickle.js: -------------------------------------------------------------------------------- 1 | const common = require('./common') 2 | const Peer = require('../') 3 | const test = require('tape') 4 | 5 | let 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 | const peer1 = new Peer({ config, initiator: true, trickle: false, wrtc: common.wrtc }) 18 | const peer2 = new Peer({ config, trickle: false, wrtc: common.wrtc }) 19 | 20 | let numSignal1 = 0 21 | peer1.on('signal', function (data) { 22 | numSignal1 += 1 23 | peer2.signal(data) 24 | }) 25 | 26 | let 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 | const peer1 = new Peer({ config, initiator: true, trickle: false, wrtc: common.wrtc }) 64 | const peer2 = new Peer({ config, wrtc: common.wrtc }) 65 | 66 | let numSignal1 = 0 67 | peer1.on('signal', function (data) { 68 | numSignal1 += 1 69 | peer2.signal(data) 70 | }) 71 | 72 | let 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 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc }) 110 | const peer2 = new Peer({ config, trickle: false, wrtc: common.wrtc }) 111 | 112 | let numSignal1 = 0 113 | peer1.on('signal', function (data) { 114 | numSignal1 += 1 115 | peer2.signal(data) 116 | }) 117 | 118 | let 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('null end candidate does not throw', function (t) { 153 | const peer1 = new Peer({ trickle: true, config, initiator: true, wrtc: common.wrtc }) 154 | const peer2 = new Peer({ trickle: true, config, wrtc: common.wrtc }) 155 | 156 | // translate all falsey candidates to null 157 | let endCandidateSent = false 158 | function endToNull (data) { 159 | if (data.candidate && !data.candidate.candidate) { 160 | data.candidate.candidate = null 161 | endCandidateSent = true 162 | } 163 | return data 164 | } 165 | 166 | peer1.on('error', () => t.fail('peer1 threw error')) 167 | peer2.on('error', () => t.fail('peer2 threw error')) 168 | 169 | peer1.on('signal', data => peer2.signal(endToNull(data))) 170 | peer2.on('signal', data => peer1.signal(endToNull(data))) 171 | 172 | peer1.on('connect', () => { 173 | if (!endCandidateSent) { // force an end candidate to browsers that don't send them 174 | peer1.signal({ candidate: { candidate: null, sdpMLineIndex: 0, sdpMid: '0' } }) 175 | peer2.signal({ candidate: { candidate: null, sdpMLineIndex: 0, sdpMid: '0' } }) 176 | } 177 | t.pass('connected') 178 | t.end() 179 | }) 180 | }) 181 | 182 | test('empty-string end candidate does not throw', function (t) { 183 | const peer1 = new Peer({ trickle: true, config, initiator: true, wrtc: common.wrtc }) 184 | const peer2 = new Peer({ trickle: true, config, wrtc: common.wrtc }) 185 | 186 | // translate all falsey candidates to null 187 | let endCandidateSent = false 188 | function endToEmptyString (data) { 189 | if (data.candidate && !data.candidate.candidate) { 190 | data.candidate.candidate = '' 191 | endCandidateSent = true 192 | } 193 | return data 194 | } 195 | 196 | peer1.on('error', () => t.fail('peer1 threw error')) 197 | peer2.on('error', () => t.fail('peer2 threw error')) 198 | 199 | peer1.on('signal', data => peer2.signal(endToEmptyString(data))) 200 | peer2.on('signal', data => peer1.signal(endToEmptyString(data))) 201 | 202 | peer1.on('connect', () => { 203 | if (!endCandidateSent) { // force an end candidate to browsers that don't send them 204 | peer1.signal({ candidate: { candidate: '', sdpMLineIndex: 0, sdpMid: '0' } }) 205 | peer2.signal({ candidate: { candidate: '', sdpMLineIndex: 0, sdpMid: '0' } }) 206 | } 207 | t.pass('connected') 208 | t.end() 209 | }) 210 | }) 211 | 212 | test('mDNS candidate does not throw', function (t) { 213 | const peer1 = new Peer({ trickle: true, config, initiator: true, wrtc: common.wrtc }) 214 | const peer2 = new Peer({ trickle: true, config, wrtc: common.wrtc }) 215 | 216 | peer1.on('error', () => t.fail('peer1 threw error')) 217 | peer2.on('error', () => t.fail('peer2 threw error')) 218 | 219 | peer1.on('signal', data => peer2.signal(data)) 220 | peer2.on('signal', data => peer1.signal(data)) 221 | 222 | peer1.on('connect', () => { 223 | // force an mDNS candidate to browsers that don't send them 224 | const candidate = 'candidate:2053030672 1 udp 2113937151 ede93942-fbc5-4323-9b73-169de626e467.local 55741 typ host generation 0 ufrag HNmH network-cost 999' 225 | peer1.signal({ candidate: { candidate, sdpMLineIndex: 0, sdpMid: '0' } }) 226 | peer2.signal({ candidate: { candidate, sdpMLineIndex: 0, sdpMid: '0' } }) 227 | t.pass('connected') 228 | t.end() 229 | }) 230 | }) 231 | 232 | test('ice candidates received before description', function (t) { 233 | t.plan(3) 234 | 235 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc }) 236 | const peer2 = new Peer({ config, wrtc: common.wrtc }) 237 | 238 | const signalQueue1 = [] 239 | peer1.on('signal', function (data) { 240 | signalQueue1.push(data) 241 | if (data.candidate) { 242 | while (signalQueue1[0]) peer2.signal(signalQueue1.pop()) 243 | } 244 | }) 245 | 246 | const signalQueue2 = [] 247 | peer2.on('signal', function (data) { 248 | signalQueue2.push(data) 249 | if (data.candidate) { 250 | while (signalQueue2[0]) peer1.signal(signalQueue2.pop()) 251 | } 252 | }) 253 | 254 | peer1.on('connect', function () { 255 | t.pass('peers connected') 256 | 257 | peer2.on('connect', function () { 258 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 259 | peer1.destroy() 260 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 261 | peer2.destroy() 262 | }) 263 | }) 264 | }) 265 | -------------------------------------------------------------------------------- /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 | const 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 | --------------------------------------------------------------------------------