├── .airtap.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── .tool-versions ├── 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 ├── test ├── basic.js ├── binary.js ├── common.js ├── multistream.js ├── negotiation.js ├── object-mode.js ├── trickle.js └── z-cleanup.js └── tinysimplepeer.min.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 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 18.7.0 2 | -------------------------------------------------------------------------------- /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 | # tiny-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 | This is a fork of [simple-peer](https://github.com/feross/simple-peer) which drops `stream.Duplex` support but retains all other functionality. This reduces the overhead of sending/receiving data channel messages and also radically reduces the bundle size of this library. The remainder of this README is the original documentation of `simple-peer` with slight modifications to reflect the changes in `tiny-simple-peer`. 19 | 20 |
21 | Sponsored by    DFINITY 22 |
23 | 24 | > We are hiring a peer-to-peer WebRTC mobile Web application expert. 25 | > 26 | > [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. 27 | > 28 | > 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). 29 | 30 | ## features 31 | 32 | - concise, **node.js style** API for [WebRTC](https://en.wikipedia.org/wiki/WebRTC) 33 | - **works in node and the browser!** 34 | - supports **video/voice streams** 35 | - supports **data channel** 36 | - text and binary data 37 | - node.js [duplex stream](http://nodejs.org/api/stream.html) interface 38 | - supports advanced options like: 39 | - enable/disable [trickle ICE candidates](http://webrtchacks.com/trickle-ice/) 40 | - manually set config options 41 | - transceivers and renegotiation 42 | 43 | This package is used by [WebTorrent](https://webtorrent.io) and [many others](#who-is-using-simple-peer). 44 | 45 | - [install](#install) 46 | - [examples](#usage) 47 | * [A simpler example](#a-simpler-example) 48 | * [data channels](#data-channels) 49 | * [video/voice](#videovoice) 50 | * [dynamic video/voice](#dynamic-videovoice) 51 | * [in node](#in-node) 52 | - [api](#api) 53 | - [events](#events) 54 | - [error codes](#error-codes) 55 | - [connecting more than 2 peers?](#connecting-more-than-2-peers) 56 | - [memory usage](#memory-usage) 57 | - [connection does not work on some networks?](#connection-does-not-work-on-some-networks) 58 | - [Who is using `simple-peer`?](#who-is-using-simple-peer) 59 | - [license](#license) 60 | 61 | ## install 62 | 63 | ``` 64 | npm install simple-peer 65 | ``` 66 | 67 | This package works in the browser with [browserify](https://browserify.org). If 68 | you do not use a bundler, you can use the `tinysimplepeer.min.js` standalone script 69 | directly in a ` 93 | 120 | 121 | 122 | ``` 123 | 124 | Visit `index.html#1` from one browser (the initiator) and `index.html` from another 125 | browser (the receiver). 126 | 127 | An "offer" will be generated by the initiator. Paste this into the receiver's form and 128 | hit submit. The receiver generates an "answer". Paste this into the initiator's form and 129 | hit submit. 130 | 131 | Now you have a direct P2P connection between two browsers! 132 | 133 | ### A simpler example 134 | 135 | This example create two peers **in the same web page**. 136 | 137 | In a real-world application, *you would never do this*. The sender and receiver `Peer` 138 | instances would exist in separate browsers. A "signaling server" (usually implemented with 139 | websockets) would be used to exchange signaling data between the two browsers until a 140 | peer-to-peer connection is established. 141 | 142 | ### data channels 143 | 144 | ```js 145 | var Peer = require('simple-peer') 146 | 147 | var peer1 = new Peer({ initiator: true }) 148 | var peer2 = new Peer() 149 | 150 | peer1.on('signal', data => { 151 | // when peer1 has signaling data, give it to peer2 somehow 152 | peer2.signal(data) 153 | }) 154 | 155 | peer2.on('signal', data => { 156 | // when peer2 has signaling data, give it to peer1 somehow 157 | peer1.signal(data) 158 | }) 159 | 160 | peer1.on('connect', () => { 161 | // wait for 'connect' event before using the data channel 162 | peer1.send('hey peer2, how is it going?') 163 | }) 164 | 165 | peer2.on('data', data => { 166 | // got a data channel message 167 | console.log('got a message from peer1: ' + data) 168 | }) 169 | ``` 170 | 171 | ### video/voice 172 | 173 | Video/voice is also super simple! In this example, peer1 sends video to peer2. 174 | 175 | ```js 176 | var Peer = require('simple-peer') 177 | 178 | // get video/voice stream 179 | navigator.mediaDevices.getUserMedia({ 180 | video: true, 181 | audio: true 182 | }).then(gotMedia).catch(() => {}) 183 | 184 | function gotMedia (stream) { 185 | var peer1 = new Peer({ initiator: true, stream: stream }) 186 | var peer2 = new Peer() 187 | 188 | peer1.on('signal', data => { 189 | peer2.signal(data) 190 | }) 191 | 192 | peer2.on('signal', data => { 193 | peer1.signal(data) 194 | }) 195 | 196 | peer2.on('stream', stream => { 197 | // got remote video stream, now let's show it in a video tag 198 | var video = document.querySelector('video') 199 | 200 | if ('srcObject' in video) { 201 | video.srcObject = stream 202 | } else { 203 | video.src = window.URL.createObjectURL(stream) // for older browsers 204 | } 205 | 206 | video.play() 207 | }) 208 | } 209 | ``` 210 | 211 | For two-way video, simply pass a `stream` option into both `Peer` constructors. Simple! 212 | 213 | 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). 214 | 215 | ### dynamic video/voice 216 | 217 | It is also possible to establish a data-only connection at first, and later add 218 | a video/voice stream, if desired. 219 | 220 | ```js 221 | var Peer = require('simple-peer') // create peer without waiting for media 222 | 223 | var peer1 = new Peer({ initiator: true }) // you don't need streams here 224 | var peer2 = new Peer() 225 | 226 | peer1.on('signal', data => { 227 | peer2.signal(data) 228 | }) 229 | 230 | peer2.on('signal', data => { 231 | peer1.signal(data) 232 | }) 233 | 234 | peer2.on('stream', stream => { 235 | // got remote video stream, now let's show it in a video tag 236 | var video = document.querySelector('video') 237 | 238 | if ('srcObject' in video) { 239 | video.srcObject = stream 240 | } else { 241 | video.src = window.URL.createObjectURL(stream) // for older browsers 242 | } 243 | 244 | video.play() 245 | }) 246 | 247 | function addMedia (stream) { 248 | peer1.addStream(stream) // <- add streams to peer dynamically 249 | } 250 | 251 | // then, anytime later... 252 | navigator.mediaDevices.getUserMedia({ 253 | video: true, 254 | audio: true 255 | }).then(addMedia).catch(() => {}) 256 | ``` 257 | 258 | ### in node 259 | 260 | To use this library in node, pass in `opts.wrtc` as a parameter (see [the constructor options](#peer--new-peeropts)): 261 | 262 | ```js 263 | var Peer = require('simple-peer') 264 | var wrtc = require('wrtc') 265 | 266 | var peer1 = new Peer({ initiator: true, wrtc: wrtc }) 267 | var peer2 = new Peer({ wrtc: wrtc }) 268 | ``` 269 | 270 | ## api 271 | 272 | ### `peer = new Peer([opts])` 273 | 274 | Create a new WebRTC peer connection. 275 | 276 | 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. 277 | 278 | If `opts` is specified, then the default options (shown below) will be overridden. 279 | 280 | ``` 281 | { 282 | initiator: false, 283 | channelConfig: {}, 284 | channelName: '', 285 | config: { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:global.stun.twilio.com:3478?transport=udp' }] }, 286 | offerOptions: {}, 287 | answerOptions: {}, 288 | sdpTransform: function (sdp) { return sdp }, 289 | stream: false, 290 | streams: [], 291 | trickle: true, 292 | allowHalfTrickle: false, 293 | wrtc: {}, // RTCPeerConnection/RTCSessionDescription/RTCIceCandidate 294 | objectMode: false 295 | } 296 | ``` 297 | 298 | The options do the following: 299 | 300 | - `initiator` - set to `true` if this is the initiating peer 301 | - `channelConfig` - custom webrtc data channel configuration (used by [`createDataChannel`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createDataChannel)) 302 | - `channelName` - custom webrtc data channel name 303 | - `config` - custom webrtc configuration (used by [`RTCPeerConnection`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection) constructor) 304 | - `offerOptions` - custom offer options (used by [`createOffer`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer) method) 305 | - `answerOptions` - custom answer options (used by [`createAnswer`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createAnswer) method) 306 | - `sdpTransform` - function to transform the generated SDP signaling data (for advanced users) 307 | - `stream` - if video/voice is desired, pass stream returned from [`getUserMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) 308 | - `streams` - an array of MediaStreams returned from [`getUserMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) 309 | - `trickle` - set to `false` to disable [trickle ICE](http://webrtchacks.com/trickle-ice/) and get a single 'signal' event (slower) 310 | - `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: 311 | - [`RTCPeerConnection`](https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection) 312 | - [`RTCSessionDescription`](https://www.w3.org/TR/webrtc/#dom-rtcsessiondescription) 313 | - [`RTCIceCandidate`](https://www.w3.org/TR/webrtc/#dom-rtcicecandidate) 314 | 315 | - `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. 316 | 317 | ### `peer.signal(data)` 318 | 319 | Call this method whenever the remote peer emits a `peer.on('signal')` event. 320 | 321 | The `data` will encapsulate a webrtc offer, answer, or ice candidate. These messages help 322 | the peers to eventually establish a direct connection to each other. The contents of these 323 | strings are an implementation detail that can be ignored by the user of this module; 324 | simply pass the data from 'signal' events to the remote peer and call `peer.signal(data)` 325 | to get connected. 326 | 327 | ### `peer.send(data)` 328 | 329 | Send text/binary data to the remote peer. `data` can be any of several types: `String`, 330 | `Buffer` (see [buffer](https://github.com/feross/buffer)), `ArrayBufferView` (`Uint8Array`, 331 | etc.), `ArrayBuffer`, or `Blob` (in browsers that support it). 332 | 333 | 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. 334 | 335 | ### `peer.addStream(stream)` 336 | 337 | Add a `MediaStream` to the connection. 338 | 339 | ### `peer.removeStream(stream)` 340 | 341 | Remove a `MediaStream` from the connection. 342 | 343 | ### `peer.addTrack(track, stream)` 344 | 345 | Add a `MediaStreamTrack` to the connection. Must also pass the `MediaStream` you want to attach it to. 346 | 347 | ### `peer.removeTrack(track, stream)` 348 | 349 | Remove a `MediaStreamTrack` from the connection. Must also pass the `MediaStream` that it was attached to. 350 | 351 | ### `peer.replaceTrack(oldTrack, newTrack, stream)` 352 | 353 | Replace a `MediaStreamTrack` with another track. Must also pass the `MediaStream` that the old track was attached to. 354 | 355 | ### `peer.addTransceiver(kind, init)` 356 | 357 | Add a `RTCRtpTransceiver` to the connection. Can be used to add transceivers before adding tracks. Automatically called as neccesary by `addTrack`. 358 | 359 | ### `peer.destroy([err])` 360 | 361 | Destroy and cleanup this peer connection. 362 | 363 | If the optional `err` parameter is passed, then it will be emitted as an `'error'` 364 | event on the stream. 365 | 366 | ### `Peer.WEBRTC_SUPPORT` 367 | 368 | Detect native WebRTC support in the javascript environment. 369 | 370 | ```js 371 | var Peer = require('simple-peer') 372 | 373 | if (Peer.WEBRTC_SUPPORT) { 374 | // webrtc support! 375 | } else { 376 | // fallback 377 | } 378 | ``` 379 | 380 | ## events 381 | 382 | `Peer` objects are instance of `EventEmitter`. Take a look at the [nodejs events documentation](https://nodejs.org/api/events.html) for more information. 383 | 384 | Example of removing all registered **close**-event listeners: 385 | ```js 386 | peer.removeAllListeners('close') 387 | ``` 388 | 389 | ### `peer.on('signal', data => {})` 390 | 391 | Fired when the peer wants to send signaling data to the remote peer. 392 | 393 | **It is the responsibility of the application developer (that's you!) to get this data to 394 | the other peer.** This usually entails using a websocket signaling server. This data is an 395 | `Object`, so remember to call `JSON.stringify(data)` to serialize it first. Then, simply 396 | call `peer.signal(data)` on the remote peer. 397 | 398 | (Be sure to listen to this event immediately to avoid missing it. For `initiator: true` 399 | peers, it fires right away. For `initatior: false` peers, it fires when the remote 400 | offer is received.) 401 | 402 | ### `peer.on('connect', () => {})` 403 | 404 | Fired when the peer connection and data channel are ready to use. 405 | 406 | ### `peer.on('data', data => {})` 407 | 408 | Received a message from the remote peer (via the data channel). 409 | 410 | `data` will be either a `String` or a `Buffer/Uint8Array` (see [buffer](https://github.com/feross/buffer)). 411 | 412 | ### `peer.on('stream', stream => {})` 413 | 414 | Received a remote video stream, which can be displayed in a video tag: 415 | 416 | ```js 417 | peer.on('stream', stream => { 418 | var video = document.querySelector('video') 419 | if ('srcObject' in video) { 420 | video.srcObject = stream 421 | } else { 422 | video.src = window.URL.createObjectURL(stream) 423 | } 424 | video.play() 425 | }) 426 | ``` 427 | 428 | ### `peer.on('track', (track, stream) => {})` 429 | 430 | Received a remote audio/video track. Streams may contain multiple tracks. 431 | 432 | ### `peer.on('close', () => {})` 433 | 434 | Called when the peer connection has closed. 435 | 436 | ### `peer.on('error', (err) => {})` 437 | 438 | Fired when a fatal error occurs. Usually, this means bad signaling data was received from the remote peer. 439 | 440 | `err` is an `Error` object. 441 | 442 | ## error codes 443 | 444 | Errors returned by the `error` event have an `err.code` property that will indicate the origin of the failure. 445 | 446 | Possible error codes: 447 | - `ERR_WEBRTC_SUPPORT` 448 | - `ERR_CREATE_OFFER` 449 | - `ERR_CREATE_ANSWER` 450 | - `ERR_SET_LOCAL_DESCRIPTION` 451 | - `ERR_SET_REMOTE_DESCRIPTION` 452 | - `ERR_ADD_ICE_CANDIDATE` 453 | - `ERR_ICE_CONNECTION_FAILURE` 454 | - `ERR_SIGNALING` 455 | - `ERR_DATA_CHANNEL` 456 | - `ERR_CONNECTION_FAILURE` 457 | 458 | 459 | ## connecting more than 2 peers? 460 | 461 | The simplest way to do that is to create a full-mesh topology. That means that every peer 462 | opens a connection to every other peer. To illustrate: 463 | 464 | ![full mesh topology](img/full-mesh.png) 465 | 466 | To broadcast a message, just iterate over all the peers and call `peer.send`. 467 | 468 | So, say you have 3 peers. Then, when a peer wants to send some data it must send it 2 469 | times, once to each of the other peers. So you're going to want to be a bit careful about 470 | the size of the data you send. 471 | 472 | Full mesh topologies don't scale well when the number of peers is very large. The total 473 | number of edges in the network will be ![full mesh formula](img/full-mesh-formula.png) 474 | where `n` is the number of peers. 475 | 476 | For clarity, here is the code to connect 3 peers together: 477 | 478 | #### Peer 1 479 | 480 | ```js 481 | // These are peer1's connections to peer2 and peer3 482 | var peer2 = new Peer({ initiator: true }) 483 | var peer3 = new Peer({ initiator: true }) 484 | 485 | peer2.on('signal', data => { 486 | // send this signaling data to peer2 somehow 487 | }) 488 | 489 | peer2.on('connect', () => { 490 | peer2.send('hi peer2, this is peer1') 491 | }) 492 | 493 | peer2.on('data', data => { 494 | console.log('got a message from peer2: ' + data) 495 | }) 496 | 497 | peer3.on('signal', data => { 498 | // send this signaling data to peer3 somehow 499 | }) 500 | 501 | peer3.on('connect', () => { 502 | peer3.send('hi peer3, this is peer1') 503 | }) 504 | 505 | peer3.on('data', data => { 506 | console.log('got a message from peer3: ' + data) 507 | }) 508 | ``` 509 | 510 | #### Peer 2 511 | 512 | ```js 513 | // These are peer2's connections to peer1 and peer3 514 | var peer1 = new Peer() 515 | var peer3 = new Peer({ initiator: true }) 516 | 517 | peer1.on('signal', data => { 518 | // send this signaling data to peer1 somehow 519 | }) 520 | 521 | peer1.on('connect', () => { 522 | peer1.send('hi peer1, this is peer2') 523 | }) 524 | 525 | peer1.on('data', data => { 526 | console.log('got a message from peer1: ' + data) 527 | }) 528 | 529 | peer3.on('signal', data => { 530 | // send this signaling data to peer3 somehow 531 | }) 532 | 533 | peer3.on('connect', () => { 534 | peer3.send('hi peer3, this is peer2') 535 | }) 536 | 537 | peer3.on('data', data => { 538 | console.log('got a message from peer3: ' + data) 539 | }) 540 | ``` 541 | 542 | #### Peer 3 543 | 544 | ```js 545 | // These are peer3's connections to peer1 and peer2 546 | var peer1 = new Peer() 547 | var peer2 = new Peer() 548 | 549 | peer1.on('signal', data => { 550 | // send this signaling data to peer1 somehow 551 | }) 552 | 553 | peer1.on('connect', () => { 554 | peer1.send('hi peer1, this is peer3') 555 | }) 556 | 557 | peer1.on('data', data => { 558 | console.log('got a message from peer1: ' + data) 559 | }) 560 | 561 | peer2.on('signal', data => { 562 | // send this signaling data to peer2 somehow 563 | }) 564 | 565 | peer2.on('connect', () => { 566 | peer2.send('hi peer2, this is peer3') 567 | }) 568 | 569 | peer2.on('data', data => { 570 | console.log('got a message from peer2: ' + data) 571 | }) 572 | ``` 573 | 574 | ## memory usage 575 | 576 | If you call `peer.send(buf)`, `simple-peer` is not keeping a reference to `buf` 577 | and sending the buffer at some later point in time. We immediately call 578 | `channel.send()` on the data channel. So it should be fine to mutate the buffer 579 | right afterward. 580 | 581 | However, beware that `peer.write(buf)` (a writable stream method) does not have 582 | the same contract. It will potentially buffer the data and call 583 | `channel.send()` at a future point in time, so definitely don't assume it's 584 | safe to mutate the buffer. 585 | 586 | 587 | ## connection does not work on some networks? 588 | 589 | If a direct connection fails, in particular, because of NAT traversal and/or firewalls, 590 | WebRTC ICE uses an intermediary (relay) TURN server. In other words, ICE will first use 591 | STUN with UDP to directly connect peers and, if that fails, will fall back to a TURN relay 592 | server. 593 | 594 | In order to use a TURN server, you must specify the `config` option to the `Peer` 595 | constructor. See the API docs above. 596 | 597 | [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) 598 | 599 | 600 | ## Who is using `simple-peer`? 601 | 602 | - [WebTorrent](http://webtorrent.io) - Streaming torrent client in the browser 603 | - [Virus Cafe](https://virus.cafe) - Make a friend in 2 minutes 604 | - [Instant.io](https://instant.io) - Secure, anonymous, streaming file transfer 605 | - [Zencastr](https://zencastr.com) - Easily record your remote podcast interviews in studio quality. 606 | - [Friends](https://github.com/moose-team/friends) - Peer-to-peer chat powered by the web 607 | - [Socket.io-p2p](https://github.com/socketio/socket.io-p2p) - Official Socket.io P2P communication library 608 | - [ScreenCat](https://maxogden.github.io/screencat/) - Screen sharing + remote collaboration app 609 | - [WebCat](https://www.npmjs.com/package/webcat) - P2P pipe across the web using Github private/public key for auth 610 | - [RTCCat](https://www.npmjs.com/package/rtcat) - WebRTC netcat 611 | - [PeerNet](https://www.npmjs.com/package/peernet) - Peer-to-peer gossip network using randomized algorithms 612 | - [PusherTC](http://pushertc.herokuapp.com) - Video chat with using Pusher. See [guide](http://blog.carbonfive.com/2014/10/16/webrtc-made-simple/). 613 | - [lxjs-chat](https://github.com/feross/lxjs-chat) - Omegle-like video chat site 614 | - [Whiteboard](https://github.com/feross/whiteboard) - P2P Whiteboard powered by WebRTC and WebTorrent 615 | - [Peer Calls](https://peercalls.com) - WebRTC group video calling. Create a room. Share the link. 616 | - [Netsix](https://mmorainville.github.io/netsix-gh-pages/) - Send videos to your friends using WebRTC so that they can watch them right away. 617 | - [Stealthy](https://www.stealthy.im) - Stealthy is a decentralized, end-to-end encrypted, p2p chat application. 618 | - [oorja.io](https://github.com/akshayKMR/oorja) - Effortless video-voice chat with realtime collaborative features. Extensible using react components 🙌 619 | - [TalktoMe](https://talktome.universal-apps.xyz) - Skype alternative for audio/video conferencing based on WebRTC, but without the loss of packets. 620 | - [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 621 | - [Detox](https://github.com/Detox) - Overlay network for distributed anonymous P2P communications entirely in the browser 622 | - [Metastream](https://github.com/samuelmaddock/metastream) - Watch streaming media with friends. 623 | - [firepeer](https://github.com/natzcam/firepeer) - secure signalling and authentication using firebase realtime database 624 | - [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)). 625 | - [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/)). 626 | - [Firstdate.co](https://firstdate.co) - Online video dating for actually meeting people and not just messaging them 627 | - [TensorChat](https://github.com/EhsaanIqbal/tensorchat) - It's simple - Create. Share. Chat. 628 | - [On/Office](https://onoffice.app) - View your desktop in a WebVR-powered environment 629 | - [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 630 | - [Ciphora](https://github.com/HR/ciphora) - A peer-to-peer end-to-end encrypted messaging chat app. 631 | - [Whisthub](https://www.whisthub.com) - Online card game Color Whist with the possibility to start a video chat while playing. 632 | - [Brie.fi/ng](https://brie.fi/ng) - Secure anonymous video chat 633 | - [Peer.School](https://github.com/holtwick/peer2school) - Simple virtual classroom starting from the 1st class including video chat and real time whiteboard 634 | - [FileFire](https://filefire.ca) - Transfer large files and folders at high speed without size limits. 635 | - [safeShare](https://github.com/vj-abishek/airdrop) - Transfer files easily with text and voice communication. 636 | - [CubeChat](https://cubechat.io) - Party in 3D 🎉 637 | - [Homely School](https://homelyschool.com) - A virtual schooling system 638 | - [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) 639 | - [Share-Anywhere](https://share-anywhere.com/) - Cross-platform file transfer 640 | - [QuaranTime.io](https://quarantime.io/) - The Activity board-game in video! 641 | - [Trango](https://web.trango.io) - Cross-platform calling and file sharing solution. 642 | - [P2PT](https://github.com/subins2000/p2pt) - Use WebTorrent trackers as signalling servers for making WebRTC connections 643 | - [Dots](https://github.com/subins2000/vett) - Online multiplayer Dots & Boxes game. [Play Here!](https://vett.space) 644 | - [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. 645 | - [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) 646 | - [Speakrandom](https://speakrandom.com) - Voice-chat social network using simple-peer to create audio conferences! 647 | - [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. 648 | 649 | 650 | 651 | - *Your app here! - send a PR!* 652 | 653 | ## license 654 | 655 | MIT. Copyright (c) [Feross Aboukhadijeh](http://feross.org). 656 | -------------------------------------------------------------------------------- /img/dfinity-sponsor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gfodor/tiny-simple-peer/df91e392a378081082d04e91de2661b05a5aaa6f/img/dfinity-sponsor.png -------------------------------------------------------------------------------- /img/full-mesh-formula.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gfodor/tiny-simple-peer/df91e392a378081082d04e91de2661b05a5aaa6f/img/full-mesh-formula.png -------------------------------------------------------------------------------- /img/full-mesh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gfodor/tiny-simple-peer/df91e392a378081082d04e91de2661b05a5aaa6f/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 randomstring = require('random-string') 5 | const queueMicrotask = require('queue-microtask') 6 | const { EventEmitter } = require('events') 7 | const errCode = require('err-code') 8 | 9 | const MAX_BUFFERED_AMOUNT = 64 * 1024 10 | const ICECOMPLETE_TIMEOUT = 5 * 1000 11 | const CHANNEL_CLOSING_TIMEOUT = 5 * 1000 12 | 13 | // HACK: Filter trickle lines when trickle is disabled #354 14 | function filterTrickle (sdp) { 15 | return sdp.replace(/a=ice-options:trickle\s\n/g, '') 16 | } 17 | 18 | function warn (message) { 19 | console.warn(message) 20 | } 21 | 22 | /** 23 | * WebRTC peer connection. Same API as node core `net.Socket`, plus a few extra methods. 24 | * Duplex stream. 25 | * @param {Object} opts 26 | */ 27 | class Peer extends EventEmitter { 28 | constructor (opts) { 29 | opts = Object.assign({ 30 | allowHalfOpen: false 31 | }, opts) 32 | 33 | super(opts) 34 | 35 | this.id = opts.id || randomstring({ length: 20 }) 36 | this._debug('new peer %o', opts) 37 | 38 | this.channelName = opts.initiator 39 | ? opts.channelName || randomstring({ length: 20 }) 40 | : null 41 | 42 | this.initiator = opts.initiator || false 43 | this.channelConfig = opts.channelConfig || Peer.channelConfig 44 | this.channelNegotiated = this.channelConfig.negotiated 45 | this.config = Object.assign({}, Peer.config, opts.config) 46 | this.proprietaryConstraints = Object.assign({}, Peer.proprietaryConstraints, opts.proprietaryConstraints) 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, this.proprietaryConstraints) 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 | destroy (err) { 428 | this._destroy(err, () => {}) 429 | } 430 | 431 | _destroy (err, cb) { 432 | if (this.destroyed || this.destroying) return 433 | this.destroying = true 434 | 435 | this._debug('destroying (error: %s)', err && (err.message || err)) 436 | 437 | queueMicrotask(() => { // allow events concurrent with the call to _destroy() to fire (see #692) 438 | this.destroyed = true 439 | this.destroying = false 440 | 441 | this._debug('destroy (error: %s)', err && (err.message || err)) 442 | 443 | this._connected = false 444 | this._pcReady = false 445 | this._channelReady = false 446 | this._remoteTracks = null 447 | this._remoteStreams = null 448 | this._senderMap = null 449 | 450 | clearInterval(this._closingInterval) 451 | this._closingInterval = null 452 | 453 | clearInterval(this._interval) 454 | this._interval = null 455 | this._chunk = null 456 | this._cb = null 457 | 458 | if (this._onFinishBound) this.removeListener('finish', this._onFinishBound) 459 | this._onFinishBound = null 460 | 461 | if (this._channel) { 462 | try { 463 | this._channel.close() 464 | } catch (err) {} 465 | 466 | // allow events concurrent with destruction to be handled 467 | this._channel.onmessage = null 468 | this._channel.onopen = null 469 | this._channel.onclose = null 470 | this._channel.onerror = null 471 | } 472 | if (this._pc) { 473 | try { 474 | this._pc.close() 475 | } catch (err) {} 476 | 477 | // allow events concurrent with destruction to be handled 478 | this._pc.oniceconnectionstatechange = null 479 | this._pc.onicegatheringstatechange = null 480 | this._pc.onsignalingstatechange = null 481 | this._pc.onicecandidate = null 482 | this._pc.ontrack = null 483 | this._pc.ondatachannel = null 484 | } 485 | this._pc = null 486 | this._channel = null 487 | 488 | if (err) this.emit('error', err) 489 | this.emit('close') 490 | cb() 491 | }) 492 | } 493 | 494 | _setupData (event) { 495 | if (!event.channel) { 496 | // In some situations `pc.createDataChannel()` returns `undefined` (in wrtc), 497 | // which is invalid behavior. Handle it gracefully. 498 | // See: https://github.com/feross/simple-peer/issues/163 499 | return this.destroy(errCode(new Error('Data channel event is missing `channel` property'), 'ERR_DATA_CHANNEL')) 500 | } 501 | 502 | this._channel = event.channel 503 | this._channel.binaryType = 'arraybuffer' 504 | 505 | if (typeof this._channel.bufferedAmountLowThreshold === 'number') { 506 | this._channel.bufferedAmountLowThreshold = MAX_BUFFERED_AMOUNT 507 | } 508 | 509 | this.channelName = this._channel.label 510 | 511 | this._channel.onmessage = event => { 512 | this._onChannelMessage(event) 513 | } 514 | this._channel.onbufferedamountlow = () => { 515 | this._onChannelBufferedAmountLow() 516 | } 517 | this._channel.onopen = () => { 518 | this._onChannelOpen() 519 | } 520 | this._channel.onclose = () => { 521 | this._onChannelClose() 522 | } 523 | this._channel.onerror = event => { 524 | const err = event.error instanceof Error 525 | ? event.error 526 | : new Error(`Datachannel error: ${event.message} ${event.filename}:${event.lineno}:${event.colno}`) 527 | this.destroy(errCode(err, 'ERR_DATA_CHANNEL')) 528 | } 529 | 530 | // HACK: Chrome will sometimes get stuck in readyState "closing", let's check for this condition 531 | // https://bugs.chromium.org/p/chromium/issues/detail?id=882743 532 | let isClosing = false 533 | this._closingInterval = setInterval(() => { // No "onclosing" event 534 | if (this._channel && this._channel.readyState === 'closing') { 535 | if (isClosing) this._onChannelClose() // closing timed out: equivalent to onclose firing 536 | isClosing = true 537 | } else { 538 | isClosing = false 539 | } 540 | }, CHANNEL_CLOSING_TIMEOUT) 541 | } 542 | 543 | _read () {} 544 | 545 | _write (chunk, encoding, cb) { 546 | if (this.destroyed) return cb(errCode(new Error('cannot write after peer is destroyed'), 'ERR_DATA_CHANNEL')) 547 | 548 | if (this._connected) { 549 | try { 550 | this.send(chunk) 551 | } catch (err) { 552 | return this.destroy(errCode(err, 'ERR_DATA_CHANNEL')) 553 | } 554 | if (this._channel.bufferedAmount > MAX_BUFFERED_AMOUNT) { 555 | this._debug('start backpressure: bufferedAmount %d', this._channel.bufferedAmount) 556 | this._cb = cb 557 | } else { 558 | cb(null) 559 | } 560 | } else { 561 | this._debug('write before connect') 562 | this._chunk = chunk 563 | this._cb = cb 564 | } 565 | } 566 | 567 | // When stream finishes writing, close socket. Half open connections are not 568 | // supported. 569 | _onFinish () { 570 | if (this.destroyed) return 571 | 572 | // Wait a bit before destroying so the socket flushes. 573 | // TODO: is there a more reliable way to accomplish this? 574 | const destroySoon = () => { 575 | setTimeout(() => this.destroy(), 1000) 576 | } 577 | 578 | if (this._connected) { 579 | destroySoon() 580 | } else { 581 | this.once('connect', destroySoon) 582 | } 583 | } 584 | 585 | _startIceCompleteTimeout () { 586 | if (this.destroyed) return 587 | if (this._iceCompleteTimer) return 588 | this._debug('started iceComplete timeout') 589 | this._iceCompleteTimer = setTimeout(() => { 590 | if (!this._iceComplete) { 591 | this._iceComplete = true 592 | this._debug('iceComplete timeout completed') 593 | this.emit('iceTimeout') 594 | this.emit('_iceComplete') 595 | } 596 | }, this.iceCompleteTimeout) 597 | } 598 | 599 | _createOffer () { 600 | if (this.destroyed) return 601 | 602 | this._pc.createOffer(this.offerOptions) 603 | .then(offer => { 604 | if (this.destroyed) return 605 | if (!this.trickle && !this.allowHalfTrickle) offer.sdp = filterTrickle(offer.sdp) 606 | offer.sdp = this.sdpTransform(offer.sdp) 607 | 608 | const sendOffer = () => { 609 | if (this.destroyed) return 610 | const signal = this._pc.localDescription || offer 611 | this._debug('signal') 612 | this.emit('signal', { 613 | type: signal.type, 614 | sdp: signal.sdp 615 | }) 616 | } 617 | 618 | const onSuccess = () => { 619 | this._debug('createOffer success') 620 | if (this.destroyed) return 621 | if (this.trickle || this._iceComplete) sendOffer() 622 | else this.once('_iceComplete', sendOffer) // wait for candidates 623 | } 624 | 625 | const onError = err => { 626 | this.destroy(errCode(err, 'ERR_SET_LOCAL_DESCRIPTION')) 627 | } 628 | 629 | this._pc.setLocalDescription(offer) 630 | .then(onSuccess) 631 | .catch(onError) 632 | }) 633 | .catch(err => { 634 | this.destroy(errCode(err, 'ERR_CREATE_OFFER')) 635 | }) 636 | } 637 | 638 | _requestMissingTransceivers () { 639 | if (this._pc.getTransceivers) { 640 | this._pc.getTransceivers().forEach(transceiver => { 641 | if (!transceiver.mid && transceiver.sender.track && !transceiver.requested) { 642 | transceiver.requested = true // HACK: Safari returns negotiated transceivers with a null mid 643 | this.addTransceiver(transceiver.sender.track.kind) 644 | } 645 | }) 646 | } 647 | } 648 | 649 | _createAnswer () { 650 | if (this.destroyed) return 651 | 652 | this._pc.createAnswer(this.answerOptions) 653 | .then(answer => { 654 | if (this.destroyed) return 655 | if (!this.trickle && !this.allowHalfTrickle) answer.sdp = filterTrickle(answer.sdp) 656 | answer.sdp = this.sdpTransform(answer.sdp) 657 | 658 | const sendAnswer = () => { 659 | if (this.destroyed) return 660 | const signal = this._pc.localDescription || answer 661 | this._debug('signal') 662 | this.emit('signal', { 663 | type: signal.type, 664 | sdp: signal.sdp 665 | }) 666 | if (!this.initiator) this._requestMissingTransceivers() 667 | } 668 | 669 | const onSuccess = () => { 670 | if (this.destroyed) return 671 | if (this.trickle || this._iceComplete) sendAnswer() 672 | else this.once('_iceComplete', sendAnswer) 673 | } 674 | 675 | const onError = err => { 676 | this.destroy(errCode(err, 'ERR_SET_LOCAL_DESCRIPTION')) 677 | } 678 | 679 | this._pc.setLocalDescription(answer) 680 | .then(onSuccess) 681 | .catch(onError) 682 | }) 683 | .catch(err => { 684 | this.destroy(errCode(err, 'ERR_CREATE_ANSWER')) 685 | }) 686 | } 687 | 688 | _onConnectionStateChange () { 689 | if (this.destroyed) return 690 | if (this._pc.connectionState === 'failed') { 691 | this.destroy(errCode(new Error('Connection failed.'), 'ERR_CONNECTION_FAILURE')) 692 | } 693 | } 694 | 695 | _onIceStateChange () { 696 | if (this.destroyed) return 697 | const iceConnectionState = this._pc.iceConnectionState 698 | const iceGatheringState = this._pc.iceGatheringState 699 | 700 | this._debug( 701 | 'iceStateChange (connection: %s) (gathering: %s)', 702 | iceConnectionState, 703 | iceGatheringState 704 | ) 705 | this.emit('iceStateChange', iceConnectionState, iceGatheringState) 706 | 707 | if (iceConnectionState === 'connected' || iceConnectionState === 'completed') { 708 | this._pcReady = true 709 | this._maybeReady() 710 | } 711 | if (iceConnectionState === 'failed') { 712 | this.destroy(errCode(new Error('Ice connection failed.'), 'ERR_ICE_CONNECTION_FAILURE')) 713 | } 714 | if (iceConnectionState === 'closed') { 715 | this.destroy(errCode(new Error('Ice connection closed.'), 'ERR_ICE_CONNECTION_CLOSED')) 716 | } 717 | } 718 | 719 | getStats (cb) { 720 | // statreports can come with a value array instead of properties 721 | const flattenValues = report => { 722 | if (Object.prototype.toString.call(report.values) === '[object Array]') { 723 | report.values.forEach(value => { 724 | Object.assign(report, value) 725 | }) 726 | } 727 | return report 728 | } 729 | 730 | // Promise-based getStats() (standard) 731 | if (this._pc.getStats.length === 0 || this._isReactNativeWebrtc) { 732 | this._pc.getStats() 733 | .then(res => { 734 | const reports = [] 735 | res.forEach(report => { 736 | reports.push(flattenValues(report)) 737 | }) 738 | cb(null, reports) 739 | }, err => cb(err)) 740 | 741 | // Single-parameter callback-based getStats() (non-standard) 742 | } else if (this._pc.getStats.length > 0) { 743 | this._pc.getStats(res => { 744 | // If we destroy connection in `connect` callback this code might happen to run when actual connection is already closed 745 | if (this.destroyed) return 746 | 747 | const reports = [] 748 | res.result().forEach(result => { 749 | const report = {} 750 | result.names().forEach(name => { 751 | report[name] = result.stat(name) 752 | }) 753 | report.id = result.id 754 | report.type = result.type 755 | report.timestamp = result.timestamp 756 | reports.push(flattenValues(report)) 757 | }) 758 | cb(null, reports) 759 | }, err => cb(err)) 760 | 761 | // Unknown browser, skip getStats() since it's anyone's guess which style of 762 | // getStats() they implement. 763 | } else { 764 | cb(null, []) 765 | } 766 | } 767 | 768 | _maybeReady () { 769 | this._debug('maybeReady pc %s channel %s', this._pcReady, this._channelReady) 770 | if (this._connected || this._connecting || !this._pcReady || !this._channelReady) return 771 | 772 | this._connecting = true 773 | 774 | // HACK: We can't rely on order here, for details see https://github.com/js-platform/node-webrtc/issues/339 775 | const findCandidatePair = () => { 776 | if (this.destroyed) return 777 | 778 | this.getStats((err, items) => { 779 | if (this.destroyed) return 780 | 781 | // Treat getStats error as non-fatal. It's not essential. 782 | if (err) items = [] 783 | 784 | const remoteCandidates = {} 785 | const localCandidates = {} 786 | const candidatePairs = {} 787 | let foundSelectedCandidatePair = false 788 | 789 | items.forEach(item => { 790 | // TODO: Once all browsers support the hyphenated stats report types, remove 791 | // the non-hypenated ones 792 | if (item.type === 'remotecandidate' || item.type === 'remote-candidate') { 793 | remoteCandidates[item.id] = item 794 | } 795 | if (item.type === 'localcandidate' || item.type === 'local-candidate') { 796 | localCandidates[item.id] = item 797 | } 798 | if (item.type === 'candidatepair' || item.type === 'candidate-pair') { 799 | candidatePairs[item.id] = item 800 | } 801 | }) 802 | 803 | const setSelectedCandidatePair = selectedCandidatePair => { 804 | foundSelectedCandidatePair = true 805 | 806 | let local = localCandidates[selectedCandidatePair.localCandidateId] 807 | 808 | if (local && (local.ip || local.address)) { 809 | // Spec 810 | this.localAddress = local.ip || local.address 811 | this.localPort = Number(local.port) 812 | } else if (local && local.ipAddress) { 813 | // Firefox 814 | this.localAddress = local.ipAddress 815 | this.localPort = Number(local.portNumber) 816 | } else if (typeof selectedCandidatePair.googLocalAddress === 'string') { 817 | // TODO: remove this once Chrome 58 is released 818 | local = selectedCandidatePair.googLocalAddress.split(':') 819 | this.localAddress = local[0] 820 | this.localPort = Number(local[1]) 821 | } 822 | if (this.localAddress) { 823 | this.localFamily = this.localAddress.includes(':') ? 'IPv6' : 'IPv4' 824 | } 825 | 826 | let remote = remoteCandidates[selectedCandidatePair.remoteCandidateId] 827 | 828 | if (remote && (remote.ip || remote.address)) { 829 | // Spec 830 | this.remoteAddress = remote.ip || remote.address 831 | this.remotePort = Number(remote.port) 832 | } else if (remote && remote.ipAddress) { 833 | // Firefox 834 | this.remoteAddress = remote.ipAddress 835 | this.remotePort = Number(remote.portNumber) 836 | } else if (typeof selectedCandidatePair.googRemoteAddress === 'string') { 837 | // TODO: remove this once Chrome 58 is released 838 | remote = selectedCandidatePair.googRemoteAddress.split(':') 839 | this.remoteAddress = remote[0] 840 | this.remotePort = Number(remote[1]) 841 | } 842 | if (this.remoteAddress) { 843 | this.remoteFamily = this.remoteAddress.includes(':') ? 'IPv6' : 'IPv4' 844 | } 845 | 846 | this._debug( 847 | 'connect local: %s:%s remote: %s:%s', 848 | this.localAddress, 849 | this.localPort, 850 | this.remoteAddress, 851 | this.remotePort 852 | ) 853 | } 854 | 855 | items.forEach(item => { 856 | // Spec-compliant 857 | if (item.type === 'transport' && item.selectedCandidatePairId) { 858 | setSelectedCandidatePair(candidatePairs[item.selectedCandidatePairId]) 859 | } 860 | 861 | // Old implementations 862 | if ( 863 | (item.type === 'googCandidatePair' && item.googActiveConnection === 'true') || 864 | ((item.type === 'candidatepair' || item.type === 'candidate-pair') && item.selected) 865 | ) { 866 | setSelectedCandidatePair(item) 867 | } 868 | }) 869 | 870 | // Ignore candidate pair selection in browsers like Safari 11 that do not have any local or remote candidates 871 | // But wait until at least 1 candidate pair is available 872 | if (!foundSelectedCandidatePair && (!Object.keys(candidatePairs).length || Object.keys(localCandidates).length)) { 873 | setTimeout(findCandidatePair, 100) 874 | return 875 | } else { 876 | this._connecting = false 877 | this._connected = true 878 | } 879 | 880 | if (this._chunk) { 881 | try { 882 | this.send(this._chunk) 883 | } catch (err) { 884 | return this.destroy(errCode(err, 'ERR_DATA_CHANNEL')) 885 | } 886 | this._chunk = null 887 | this._debug('sent chunk from "write before connect"') 888 | 889 | const cb = this._cb 890 | this._cb = null 891 | cb(null) 892 | } 893 | 894 | // If `bufferedAmountLowThreshold` and 'onbufferedamountlow' are unsupported, 895 | // fallback to using setInterval to implement backpressure. 896 | if (typeof this._channel.bufferedAmountLowThreshold !== 'number') { 897 | this._interval = setInterval(() => this._onInterval(), 150) 898 | if (this._interval.unref) this._interval.unref() 899 | } 900 | 901 | this._debug('connect') 902 | this.emit('connect') 903 | }) 904 | } 905 | findCandidatePair() 906 | } 907 | 908 | _onInterval () { 909 | if (!this._cb || !this._channel || this._channel.bufferedAmount > MAX_BUFFERED_AMOUNT) { 910 | return 911 | } 912 | this._onChannelBufferedAmountLow() 913 | } 914 | 915 | _onSignalingStateChange () { 916 | if (this.destroyed) return 917 | 918 | if (this._pc.signalingState === 'stable') { 919 | this._isNegotiating = false 920 | 921 | // HACK: Firefox doesn't yet support removing tracks when signalingState !== 'stable' 922 | this._debug('flushing sender queue', this._sendersAwaitingStable) 923 | this._sendersAwaitingStable.forEach(sender => { 924 | this._pc.removeTrack(sender) 925 | this._queuedNegotiation = true 926 | }) 927 | this._sendersAwaitingStable = [] 928 | 929 | if (this._queuedNegotiation) { 930 | this._debug('flushing negotiation queue') 931 | this._queuedNegotiation = false 932 | this._needsNegotiation() // negotiate again 933 | } else { 934 | this._debug('negotiated') 935 | this.emit('negotiated') 936 | } 937 | } 938 | 939 | this._debug('signalingStateChange %s', this._pc.signalingState) 940 | this.emit('signalingStateChange', this._pc.signalingState) 941 | } 942 | 943 | _onIceCandidate (event) { 944 | if (this.destroyed) return 945 | if (event.candidate && this.trickle) { 946 | this.emit('signal', { 947 | type: 'candidate', 948 | candidate: { 949 | candidate: event.candidate.candidate, 950 | sdpMLineIndex: event.candidate.sdpMLineIndex, 951 | sdpMid: event.candidate.sdpMid 952 | } 953 | }) 954 | } else if (!event.candidate && !this._iceComplete) { 955 | this._iceComplete = true 956 | this.emit('_iceComplete') 957 | } 958 | // as soon as we've received one valid candidate start timeout 959 | if (event.candidate) { 960 | this._startIceCompleteTimeout() 961 | } 962 | } 963 | 964 | _onChannelMessage (event) { 965 | if (this.destroyed) return 966 | this.emit('data', event.data) 967 | } 968 | 969 | _onChannelBufferedAmountLow () { 970 | if (this.destroyed || !this._cb) return 971 | this._debug('ending backpressure: bufferedAmount %d', this._channel.bufferedAmount) 972 | const cb = this._cb 973 | this._cb = null 974 | cb(null) 975 | } 976 | 977 | _onChannelOpen () { 978 | if (this._connected || this.destroyed) return 979 | this._debug('on channel open') 980 | this._channelReady = true 981 | this._maybeReady() 982 | } 983 | 984 | _onChannelClose () { 985 | if (this.destroyed) return 986 | this._debug('on channel close') 987 | this.destroy() 988 | } 989 | 990 | _onTrack (event) { 991 | if (this.destroyed) return 992 | const { track, receiver, streams } = event 993 | 994 | streams.forEach(eventStream => { 995 | this._debug('on track') 996 | this.emit('track', track, eventStream, receiver) 997 | 998 | this._remoteTracks.push({ track, stream: eventStream }) 999 | 1000 | if (this._remoteStreams.some(remoteStream => { 1001 | return remoteStream.id === eventStream.id 1002 | })) return // Only fire one 'stream' event, even though there may be multiple tracks per stream 1003 | 1004 | this._remoteStreams.push(eventStream) 1005 | queueMicrotask(() => { 1006 | this._debug('on stream') 1007 | this.emit('stream', eventStream, receiver) // ensure all tracks have been added 1008 | }) 1009 | }) 1010 | } 1011 | 1012 | _debug () { 1013 | const args = [].slice.call(arguments) 1014 | args[0] = '[' + this.id + '] ' + args[0] 1015 | debug.apply(null, args) 1016 | } 1017 | } 1018 | 1019 | Peer.WEBRTC_SUPPORT = !!getBrowserRTC() 1020 | 1021 | /** 1022 | * Expose peer and data channel config for overriding all Peer 1023 | * instances. Otherwise, just set opts.config or opts.channelConfig 1024 | * when constructing a Peer. 1025 | */ 1026 | Peer.config = { 1027 | iceServers: [ 1028 | { 1029 | urls: [ 1030 | 'stun:stun.l.google.com:19302', 1031 | 'stun:global.stun.twilio.com:3478' 1032 | ] 1033 | } 1034 | ], 1035 | sdpSemantics: 'unified-plan' 1036 | } 1037 | 1038 | Peer.channelConfig = {} 1039 | Peer.proprietaryConstraints = {} 1040 | 1041 | module.exports = Peer 1042 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiny-simple-peer", 3 | "description": "Simple one-to-one WebRTC video/voice and data channels, which drops stream support from simple-peer to improve performance and reduce bundle size", 4 | "version": "10.1.2", 5 | "engines": { 6 | "node": ">=10" 7 | }, 8 | "contributors": [ 9 | { 10 | "name": "Feross Aboukhadijeh", 11 | "email": "feross@feross.org", 12 | "url": "https://feross.org" 13 | }, 14 | { 15 | "name": "Greg Fodor", 16 | "email": "gfodor@gmail.com", 17 | "url": "https://github.com/gfodor" 18 | } 19 | ], 20 | "bugs": { 21 | "url": "https://github.com/gfodor/tiny-simple-peer/issues" 22 | }, 23 | "dependencies": { 24 | "debug": "^4.3.2", 25 | "err-code": "^3.0.1", 26 | "events": "^3.3.0", 27 | "get-browser-rtc": "^1.1.0", 28 | "queue-microtask": "^1.2.3", 29 | "random-string": "^0.2.0" 30 | }, 31 | "devDependencies": { 32 | "airtap": "^4.0.3", 33 | "airtap-manual": "^1.0.0", 34 | "airtap-sauce": "^1.1.0", 35 | "babel-minify": "^0.5.1", 36 | "bowser": "^2.11.0", 37 | "browserify": "^17.0.0", 38 | "coveralls": "^3.1.1", 39 | "nyc": "^15.1.0", 40 | "prettier-bytes": "^1.0.4", 41 | "simple-get": "^4.0.0", 42 | "speedometer": "^1.1.0", 43 | "standard": "*", 44 | "string-to-stream": "^3.0.1", 45 | "tape": "^5.5.2", 46 | "thunky": "^1.1.0", 47 | "wrtc": "^0.4.7", 48 | "ws": "^7.5.3" 49 | }, 50 | "keywords": [ 51 | "data", 52 | "data channel", 53 | "data channel stream", 54 | "data channels", 55 | "p2p", 56 | "peer", 57 | "peer", 58 | "peer-to-peer", 59 | "stream", 60 | "video", 61 | "voice", 62 | "webrtc", 63 | "webrtc stream" 64 | ], 65 | "license": "MIT", 66 | "main": "index.js", 67 | "repository": { 68 | "type": "git", 69 | "url": "git://github.com/gfodor/tiny-simple-peer.git" 70 | }, 71 | "scripts": { 72 | "build": "browserify -s TinySimplePeer -r . | minify > tinysimplepeer.min.js", 73 | "size": "npm run build && cat tinysimplepeer.min.js | gzip | wc -c", 74 | "// test": "standard && npm run test-node && npm run test-browser", 75 | "test": "standard && npm run test-browser", 76 | "test-browser": "airtap --coverage --concurrency 1 -- test/*.js", 77 | "test-browser-local": "airtap --coverage --preset local -- test/*.js", 78 | "test-node": "WRTC=wrtc tape test/*.js", 79 | "coverage": "nyc report --reporter=text-lcov | coveralls" 80 | }, 81 | "funding": [ 82 | { 83 | "type": "github", 84 | "url": "https://github.com/sponsors/feross" 85 | }, 86 | { 87 | "type": "patreon", 88 | "url": "https://www.patreon.com/feross" 89 | }, 90 | { 91 | "type": "consulting", 92 | "url": "https://feross.org/support" 93 | } 94 | ] 95 | } 96 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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('create peer with a custom id', function (t) { 34 | t.plan(2) 35 | 36 | if (process.browser) { 37 | let peer 38 | t.doesNotThrow(function () { 39 | peer = new Peer({ id: 'hello' }) 40 | }) 41 | t.equal(peer.id, 'hello') 42 | peer.destroy() 43 | } else { 44 | t.pass('Skip no-option test in Node.js, since the wrtc option is required') 45 | } 46 | }) 47 | 48 | test('can detect error when RTCPeerConstructor throws', function (t) { 49 | t.plan(1) 50 | 51 | const peer = new Peer({ wrtc: { RTCPeerConnection: null } }) 52 | peer.once('error', function () { 53 | t.pass('got error event') 54 | peer.destroy() 55 | }) 56 | }) 57 | 58 | test('signal event gets emitted', function (t) { 59 | t.plan(2) 60 | 61 | const peer = new Peer({ config, initiator: true, wrtc: common.wrtc }) 62 | peer.once('signal', function () { 63 | t.pass('got signal event') 64 | peer.on('close', function () { t.pass('peer destroyed') }) 65 | peer.destroy() 66 | }) 67 | }) 68 | 69 | test('signal event does not get emitted by non-initiator', function (t) { 70 | const peer = new Peer({ config, initiator: false, wrtc: common.wrtc }) 71 | peer.once('signal', function () { 72 | t.fail('got signal event') 73 | peer.on('close', function () { t.pass('peer destroyed') }) 74 | peer.destroy() 75 | }) 76 | 77 | setTimeout(() => { 78 | t.pass('did not get signal after 1000ms') 79 | t.end() 80 | }, 1000) 81 | }) 82 | 83 | test('signal event does not get emitted by non-initiator with stream', function (t) { 84 | const peer = new Peer({ 85 | config, 86 | stream: common.getMediaStream(), 87 | initiator: false, 88 | wrtc: common.wrtc 89 | }) 90 | peer.once('signal', function () { 91 | t.fail('got signal event') 92 | peer.on('close', function () { t.pass('peer destroyed') }) 93 | peer.destroy() 94 | }) 95 | 96 | setTimeout(() => { 97 | t.pass('did not get signal after 1000ms') 98 | t.end() 99 | }, 1000) 100 | }) 101 | 102 | test('data send/receive text', function (t) { 103 | t.plan(10) 104 | 105 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc }) 106 | const peer2 = new Peer({ config, wrtc: common.wrtc }) 107 | 108 | let numSignal1 = 0 109 | peer1.on('signal', function (data) { 110 | numSignal1 += 1 111 | peer2.signal(data) 112 | }) 113 | 114 | let numSignal2 = 0 115 | peer2.on('signal', function (data) { 116 | numSignal2 += 1 117 | peer1.signal(data) 118 | }) 119 | 120 | peer1.on('connect', tryTest) 121 | peer2.on('connect', tryTest) 122 | 123 | function tryTest () { 124 | if (!peer1.connected || !peer2.connected) return 125 | 126 | t.ok(numSignal1 >= 1) 127 | t.ok(numSignal2 >= 1) 128 | t.equal(peer1.initiator, true, 'peer1 is initiator') 129 | t.equal(peer2.initiator, false, 'peer2 is not initiator') 130 | 131 | peer1.send('sup peer2') 132 | peer2.on('data', function (data) { 133 | t.ok(typeof data === 'string', 'data is string') 134 | t.equal(data.toString(), 'sup peer2', 'got correct message') 135 | 136 | peer2.send('sup peer1') 137 | peer1.on('data', function (data) { 138 | t.ok(typeof data === 'string', 'data is string') 139 | t.equal(data.toString(), 'sup peer1', 'got correct message') 140 | 141 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 142 | peer1.destroy() 143 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 144 | peer2.destroy() 145 | }) 146 | }) 147 | } 148 | }) 149 | 150 | test('sdpTransform function is called', function (t) { 151 | t.plan(3) 152 | 153 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc }) 154 | const peer2 = new Peer({ config, sdpTransform, wrtc: common.wrtc }) 155 | 156 | function sdpTransform (sdp) { 157 | t.equal(typeof sdp, 'string', 'got a string as SDP') 158 | setTimeout(function () { 159 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 160 | peer1.destroy() 161 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 162 | peer2.destroy() 163 | }, 0) 164 | return sdp 165 | } 166 | 167 | peer1.on('signal', function (data) { 168 | peer2.signal(data) 169 | }) 170 | 171 | peer2.on('signal', function (data) { 172 | peer1.signal(data) 173 | }) 174 | }) 175 | 176 | test('old constraint formats are used', function (t) { 177 | t.plan(3) 178 | 179 | const constraints = { 180 | mandatory: { 181 | OfferToReceiveAudio: true, 182 | OfferToReceiveVideo: true 183 | } 184 | } 185 | 186 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc, constraints }) 187 | const peer2 = new Peer({ config, wrtc: common.wrtc, constraints }) 188 | 189 | peer1.on('signal', function (data) { 190 | peer2.signal(data) 191 | }) 192 | 193 | peer2.on('signal', function (data) { 194 | peer1.signal(data) 195 | }) 196 | 197 | peer1.on('connect', function () { 198 | t.pass('peers connected') 199 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 200 | peer1.destroy() 201 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 202 | peer2.destroy() 203 | }) 204 | }) 205 | 206 | test('new constraint formats are used', function (t) { 207 | t.plan(3) 208 | 209 | const constraints = { 210 | offerToReceiveAudio: true, 211 | offerToReceiveVideo: true 212 | } 213 | 214 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc, constraints }) 215 | const peer2 = new Peer({ config, wrtc: common.wrtc, constraints }) 216 | 217 | peer1.on('signal', function (data) { 218 | peer2.signal(data) 219 | }) 220 | 221 | peer2.on('signal', function (data) { 222 | peer1.signal(data) 223 | }) 224 | 225 | peer1.on('connect', function () { 226 | t.pass('peers connected') 227 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 228 | peer1.destroy() 229 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 230 | peer2.destroy() 231 | }) 232 | }) 233 | 234 | test('ensure remote address and port are available right after connection', function (t) { 235 | if (common.isBrowser('safari') || common.isBrowser('ios')) { 236 | t.pass('Skip on Safari and iOS which do not support modern getStats() calls') 237 | t.end() 238 | return 239 | } 240 | if (common.isBrowser('chrome') || common.isBrowser('edge')) { 241 | t.pass('Skip on Chrome and Edge which hide local IPs with mDNS') 242 | t.end() 243 | return 244 | } 245 | 246 | t.plan(7) 247 | 248 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc }) 249 | const peer2 = new Peer({ config, wrtc: common.wrtc }) 250 | 251 | peer1.on('signal', function (data) { 252 | peer2.signal(data) 253 | }) 254 | 255 | peer2.on('signal', function (data) { 256 | peer1.signal(data) 257 | }) 258 | 259 | peer1.on('connect', function () { 260 | t.pass('peers connected') 261 | 262 | t.ok(peer1.remoteAddress, 'peer1 remote address is present') 263 | t.ok(peer1.remotePort, 'peer1 remote port is present') 264 | 265 | peer2.on('connect', function () { 266 | t.ok(peer2.remoteAddress, 'peer2 remote address is present') 267 | t.ok(peer2.remotePort, 'peer2 remote port is present') 268 | 269 | peer1.on('close', function () { t.pass('peer1 destroyed') }) 270 | peer1.destroy() 271 | peer2.on('close', function () { t.pass('peer2 destroyed') }) 272 | peer2.destroy() 273 | }) 274 | }) 275 | }) 276 | 277 | test('ensure iceStateChange fires when connection failed', (t) => { 278 | t.plan(1) 279 | const peer = new Peer({ config, initiator: true, wrtc: common.wrtc }) 280 | 281 | peer.on('iceStateChange', (connectionState, gatheringState) => { 282 | t.pass('got iceStateChange') 283 | t.end() 284 | }) 285 | 286 | // simulate concurrent iceConnectionStateChange and destroy() 287 | peer.destroy() 288 | peer._pc.oniceconnectionstatechange() 289 | }) 290 | -------------------------------------------------------------------------------- /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 ArrayBuffer', 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(new Uint8Array([0, 1, 2]).buffer) 32 | peer2.on('data', function (data) { 33 | t.ok(data instanceof ArrayBuffer, 'data is ArrayBuffer') 34 | t.deepEqual([...new Uint8Array(data)], [0, 1, 2], 'got correct message') 35 | 36 | peer2.send(Buffer.from([0, 2, 4])) 37 | peer1.on('data', function (data) { 38 | t.ok(data instanceof ArrayBuffer, 'data is ArrayBuffer') 39 | t.deepEqual([...new Uint8Array(data)], [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(data instanceof ArrayBuffer, 'data is ArrayBuffer') 72 | t.deepEqual([...new Uint8Array(data)], [0, 1, 2], 'got correct message') 73 | 74 | peer2.send(new Uint8Array([0, 2, 4])) 75 | peer1.on('data', function (data) { 76 | t.ok(data instanceof ArrayBuffer, 'data is ArrayBuffer') 77 | t.deepEqual([...new Uint8Array(data)], [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(data instanceof ArrayBuffer, 'data is ArrayBuffer') 108 | t.deepEqual([...new Uint8Array(data)], [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(data instanceof ArrayBuffer, 'data is ArrayBuffer') 113 | t.deepEqual([...new Uint8Array(data)], [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 Uint8Array {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(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(data instanceof ArrayBuffer, 'data is a ArrayBuffer') 72 | t.deepEqual([...new Uint8Array(data)], [0, 1, 2], 'got correct message') 73 | 74 | peer2.send(new Uint8Array([1, 2, 3])) 75 | peer1.on('data', function (data) { 76 | t.ok(data instanceof ArrayBuffer, 'data is a ArrayBuffer') 77 | t.deepEqual([...new Uint8Array(data)], [1, 2, 3], '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 {objectMode: true}', function (t) { 89 | t.plan(6) 90 | 91 | const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc, objectMode: true }) 92 | const peer2 = new Peer({ config, wrtc: common.wrtc, objectMode: true }) 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(data instanceof ArrayBuffer, 'data is a ArrayBuffer') 108 | t.deepEqual([...new Uint8Array(data)], [0, 1, 2], 'got correct message') 109 | 110 | peer2.send(new Uint8Array([1, 2, 3]).buffer) 111 | peer1.on('data', function (data) { 112 | t.ok(data instanceof ArrayBuffer, 'data is a ArrayBuffer') 113 | t.deepEqual([...new Uint8Array(data)], [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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tinysimplepeer.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 n;n="undefined"==typeof window?"undefined"==typeof global?"undefined"==typeof self?this:self:global:window,n.TinySimplePeer=e()}})(function(){var e=Math.abs;return function(){function s(d,e,n){function t(o,i){if(!e[o]){if(!d[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:{}};d[o][0].call(a.exports,function(e){var r=d[o][1][e];return t(r||e)},a,a.exports,s,d,e,n)}return e[o].exports}for(var r="function"==typeof require&&require,o=0;o{"%%"===e||(r++,"%c"===e&&(o=r))}),e.splice(o,0,t)}function i(){let e;try{e=t.storage.getItem("debug")}catch(e){}return!e&&"undefined"!=typeof o&&"env"in o&&(e=o.env.DEBUG),e}t.formatArgs=r,t.save=function(e){try{e?t.storage.setItem("debug",e):t.storage.removeItem("debug")}catch(e){}},t.load=i,t.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+)/))},t.storage=function(){try{return localStorage}catch(e){}}(),t.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`."))}})(),t.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"],t.log=console.debug||console.log||(()=>{}),n.exports=e("./common")(t);const{formatters:s}=n.exports;s.j=function(e){try{return JSON.stringify(e)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}}}).call(this)}).call(this,e("_process"))},{"./common":2,_process:7}],2:[function(n,t){t.exports=function(t){function r(e){function n(...e){if(!n.enabled)return;const o=n,i=+new Date,s=i-(t||i);o.diff=s,o.prev=t,o.curr=i,t=i,e[0]=r.coerce(e[0]),"string"!=typeof e[0]&&e.unshift("%O");let a=0;e[0]=e[0].replace(/%([a-zA-Z%])/g,(n,t)=>{if("%%"===n)return"%";a++;const i=r.formatters[t];if("function"==typeof i){const t=e[a];n=i.call(o,t),e.splice(a,1),a--}return n}),r.formatArgs.call(o,e);const d=o.log||r.log;d.apply(o,e)}let t,i,s,a=null;return n.namespace=e,n.useColors=r.useColors(),n.color=r.selectColor(e),n.extend=o,n.destroy=r.destroy,Object.defineProperty(n,"enabled",{enumerable:!0,configurable:!1,get:()=>null===a?(i!==r.namespaces&&(i=r.namespaces,s=r.enabled(e)),s):a,set:e=>{a=e}}),"function"==typeof r.init&&r.init(n),n}function o(e,n){const t=r(this.namespace+("undefined"==typeof n?":":n)+e);return t.log=this.log,t}function i(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(i),...r.skips.map(i).map(e=>"-"+e)].join(",");return r.enable(""),e},r.enable=function(e){r.save(e),r.namespaces=e,r.names=[],r.skips=[];let n;const t=("string"==typeof e?e:"").split(/[\s,]+/),o=t.length;for(n=0;n{r[e]=t[e]}),r.names=[],r.skips=[],r.formatters={},r.selectColor=function(n){let t=0;for(let e=0;ea&&!c.warned){c.warned=!0;var l=new Error("Possible EventEmitter memory leak detected. "+c.length+" "+(n+" listeners added. Use emitter.setMaxListeners() to increase limit"));l.name="MaxListenersExceededWarning",l.emitter=e,l.type=n,l.count=c.length,t(l)}return e}function a(){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 d(e,n,t){var r={fired:!1,wrapFn:void 0,target:e,type:n,listener:t},o=a.bind(r);return o.listener=t,r.wrapFn=o,o}function c(e,n,t){var r=e._events;if(r===void 0)return[];var o=r[n];return void 0===o?[]:"function"==typeof o?t?[o.listener||o]:[o]:t?_(o):p(o,o.length)}function l(e){var n=this._events;if(n!==void 0){var t=n[e];if("function"==typeof t)return 1;if(void 0!==t)return t.length}return 0}function p(e,t){for(var n=Array(t),r=0;re||h(e))throw new RangeError("The value of \"defaultMaxListeners\" is out of range. It must be a non-negative number. Received "+e+".");b=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||h(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 i(this)},r.prototype.emit=function(e){for(var n=[],t=1;ts)return this;0===s?t.shift():u(t,s),1===t.length&&(r[e]=t[0]),void 0!==r.removeListener&&this.emit("removeListener",e,d||n)}return this},r.prototype.off=r.prototype.removeListener,r.prototype.removeAllListeners=function(e){var n,t,r;if(t=this._events,void 0===t)return this;if(void 0===t.removeListener)return 0===arguments.length?(this._events=Object.create(null),this._eventsCount=0):void 0!==t[e]&&(0==--this._eventsCount?this._events=Object.create(null):delete t[e]),this;if(0===arguments.length){var o,s=Object.keys(t);for(r=0;r=1.5*o?"s":"")}var c=24*(60*60000);t.exports=function(e,n){n=n||{};var t=typeof e;if("string"==t&&0 */let t;n.exports="function"==typeof queueMicrotask?queueMicrotask.bind("undefined"==typeof window?e:window):e=>(t||(t=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)},{}],9:[function(e,n,t){"use strict";function r(e){return e||(e={}),{length:e.length||8,numeric:"boolean"!=typeof e.numeric||e.numeric,letters:"boolean"!=typeof e.letters||e.letters,special:"boolean"==typeof e.special&&e.special,exclude:Array.isArray(e.exclude)?e.exclude:[]}}function o(e){var n="";e.numeric&&(n+="0123456789"),e.letters&&(n+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"),e.special&&(n+="!$%^&*()_+|~-=`{}[]:;<>?,./");for(var t=0;t<=e.exclude.length;t++)n=n.replace(e.exclude[t],"");return n}var i="0123456789",s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",a="!$%^&*()_+|~-=`{}[]:;<>?,./";n.exports=function n(e){e=r(e);var t,s,a="",d=e.length,c=e.exclude,l=o(e);for(t=1;t<=d;t++)a+=l.substring(s=Math.floor(Math.random()*l.length),s+1);return a}},{}],"/":[function(e,n,t){function r(e){return e.replace(/a=ice-options:trickle\s\n/g,"")}function o(e){console.warn(e)}/*! simple-peer. MIT License. Feross Aboukhadijeh */const i=e("debug")("simple-peer"),s=e("get-browser-rtc"),a=e("random-string"),d=e("queue-microtask"),{EventEmitter:c}=e("events"),l=e("err-code"),p=65536,u=5000,_=5000;class g extends c{constructor(e){if(e=Object.assign({allowHalfOpen:!1},e),super(e),this.id=e.id||a({length:20}),this._debug("new peer %o",e),this.channelName=e.initiator?e.channelName||a({length:20}):null,this.initiator=e.initiator||!1,this.channelConfig=e.channelConfig||g.channelConfig,this.channelNegotiated=this.channelConfig.negotiated,this.config=Object.assign({},g.config,e.config),this.proprietaryConstraints=Object.assign({},g.proprietaryConstraints,e.proprietaryConstraints),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||u,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:s(),!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,this.proprietaryConstraints)}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(n){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 n=new this._wrtc.RTCIceCandidate(e);this._pc.addIceCandidate(n).catch(e=>{!n.address||n.address.endsWith(".local")?o("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,n){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,n),this._needsNegotiation()}catch(e){this.destroy(l(e,"ERR_ADD_TRANSCEIVER"))}else this.emit("signal",{type:"transceiverRequest",transceiverRequest:{kind:e,init:n}})}}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(n=>{this.addTrack(n,e)})}}addTrack(e,n){if(this.destroying)return;if(this.destroyed)throw l(new Error("cannot addTrack after peer is destroyed"),"ERR_DESTROYED");this._debug("addTrack()");const t=this._senderMap.get(e)||new Map;let r=t.get(n);if(!r)r=this._pc.addTrack(e,n),t.set(n,r),this._senderMap.set(e,t),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,n,t){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),o=r?r.get(t):null;if(!o)throw l(new Error("Cannot replace track that was never added."),"ERR_TRACK_NOT_ADDED");n&&this._senderMap.set(n,r),null==o.replaceTrack?this.destroy(l(new Error("replaceTrack is not supported in this browser"),"ERR_UNSUPPORTED_REPLACETRACK")):o.replaceTrack(n)}removeTrack(e,n){if(this.destroying)return;if(this.destroyed)throw l(new Error("cannot removeTrack after peer is destroyed"),"ERR_DESTROYED");this._debug("removeSender()");const t=this._senderMap.get(e),r=t?t.get(n):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(n=>{this.removeTrack(n,e)})}}_needsNegotiation(){this._debug("_needsNegotiation"),this._batchedNegotiation||(this._batchedNegotiation=!0,d(()=>{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,n){this.destroyed||this.destroying||(this.destroying=!0,this._debug("destroying (error: %s)",e&&(e.message||e)),d(()=>{if(this.destroyed=!0,this.destroying=!1,this._debug("destroy (error: %s)",e&&(e.message||e)),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"),n()}))}_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=p),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 n=e.error instanceof Error?e.error:new Error(`Datachannel error: ${e.message} ${e.filename}:${e.lineno}:${e.colno}`);this.destroy(l(n,"ERR_DATA_CHANNEL"))};let n=!1;this._closingInterval=setInterval(()=>{this._channel&&"closing"===this._channel.readyState?(n&&this._onChannelClose(),n=!0):n=!1},_)}_read(){}_write(e,n,t){if(this.destroyed)return t(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>p?(this._debug("start backpressure: bufferedAmount %d",this._channel.bufferedAmount),this._cb=t):t(null)}else this._debug("write before connect"),this._chunk=e,this._cb=t}_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=r(e.sdp)),e.sdp=this.sdpTransform(e.sdp);const n=()=>{if(!this.destroyed){const n=this._pc.localDescription||e;this._debug("signal"),this.emit("signal",{type:n.type,sdp:n.sdp})}},t=()=>{this._debug("createOffer success"),this.destroyed||(this.trickle||this._iceComplete?n():this.once("_iceComplete",n))},o=e=>{this.destroy(l(e,"ERR_SET_LOCAL_DESCRIPTION"))};this._pc.setLocalDescription(e).then(t).catch(o)}).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=r(e.sdp)),e.sdp=this.sdpTransform(e.sdp);const n=()=>{if(!this.destroyed){const n=this._pc.localDescription||e;this._debug("signal"),this.emit("signal",{type:n.type,sdp:n.sdp}),this.initiator||this._requestMissingTransceivers()}},t=()=>{this.destroyed||(this.trickle||this._iceComplete?n():this.once("_iceComplete",n))},o=e=>{this.destroy(l(e,"ERR_SET_LOCAL_DESCRIPTION"))};this._pc.setLocalDescription(e).then(t).catch(o)}).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,n=this._pc.iceGatheringState;this._debug("iceStateChange (connection: %s) (gathering: %s)",e,n),this.emit("iceStateChange",e,n),("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 n=e=>("[object Array]"===Object.prototype.toString.call(e.values)&&e.values.forEach(n=>{Object.assign(e,n)}),e);0===this._pc.getStats.length||this._isReactNativeWebrtc?this._pc.getStats().then(t=>{const r=[];t.forEach(e=>{r.push(n(e))}),e(null,r)},n=>e(n)):0{if(this.destroyed)return;const r=[];t.result().forEach(e=>{const t={};e.names().forEach(n=>{t[n]=e.stat(n)}),t.id=e.id,t.type=e.type,t.timestamp=e.timestamp,r.push(n(t))}),e(null,r)},n=>e(n)):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((n,t)=>{if(this.destroyed)return;n&&(t=[]);const r={},o={},i={};let s=!1;t.forEach(e=>{("remotecandidate"===e.type||"remote-candidate"===e.type)&&(r[e.id]=e),("localcandidate"===e.type||"local-candidate"===e.type)&&(o[e.id]=e),("candidatepair"===e.type||"candidate-pair"===e.type)&&(i[e.id]=e)});const a=e=>{s=!0;let n=o[e.localCandidateId];n&&(n.ip||n.address)?(this.localAddress=n.ip||n.address,this.localPort=+n.port):n&&n.ipAddress?(this.localAddress=n.ipAddress,this.localPort=+n.portNumber):"string"==typeof e.googLocalAddress&&(n=e.googLocalAddress.split(":"),this.localAddress=n[0],this.localPort=+n[1]),this.localAddress&&(this.localFamily=this.localAddress.includes(":")?"IPv6":"IPv4");let t=r[e.remoteCandidateId];t&&(t.ip||t.address)?(this.remoteAddress=t.ip||t.address,this.remotePort=+t.port):t&&t.ipAddress?(this.remoteAddress=t.ipAddress,this.remotePort=+t.portNumber):"string"==typeof e.googRemoteAddress&&(t=e.googRemoteAddress.split(":"),this.remoteAddress=t[0],this.remotePort=+t[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(t.forEach(e=>{"transport"===e.type&&e.selectedCandidatePairId&&a(i[e.selectedCandidatePairId]),("googCandidatePair"===e.type&&"true"===e.googActiveConnection||("candidatepair"===e.type||"candidate-pair"===e.type)&&e.selected)&&a(e)}),!s&&(!Object.keys(i).length||Object.keys(o).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>p)&&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){this.destroyed||this.emit("data",e.data)}_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){if(this.destroyed)return;const{track:n,receiver:t,streams:r}=e;r.forEach(e=>{this._debug("on track"),this.emit("track",n,e,t),this._remoteTracks.push({track:n,stream:e}),this._remoteStreams.some(n=>n.id===e.id)||(this._remoteStreams.push(e),d(()=>{this._debug("on stream"),this.emit("stream",e,t)}))})}_debug(){const e=[].slice.call(arguments);e[0]="["+this.id+"] "+e[0],i.apply(null,e)}}g.WEBRTC_SUPPORT=!!s(),g.config={iceServers:[{urls:["stun:stun.l.google.com:19302","stun:global.stun.twilio.com:3478"]}],sdpSemantics:"unified-plan"},g.channelConfig={},g.proprietaryConstraints={},n.exports=g},{debug:1,"err-code":3,events:4,"get-browser-rtc":5,"queue-microtask":8,"random-string":9}]},{},[])("/")}); --------------------------------------------------------------------------------