Repository: feross/simple-peer
Branch: master
Commit: f1a492d1999c
Files: 25
Total size: 111.4 KB
Directory structure:
gitextract_bqa_c8qr/
├── .airtap.yml
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ ├── config.yml
│ │ └── feature_request.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── dependabot.yml
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── 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
├── stream.js
├── trickle.js
└── z-cleanup.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .airtap.yml
================================================
sauce_connect: true
browsers:
- name: firefox
version: latest
- name: chrome
version: latest
- name: safari
version: latest
- name: edge
version: latest
- name: and_chr
version: latest
- name: ios_saf
version: latest
providers:
- airtap-sauce
presets:
local:
providers: airtap-manual
browsers:
- name: manual
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: "🐞 Bug report"
about: Report an issue with this software
title: ''
labels: ''
assignees: ''
---
<!-- THIS IS NOT A SUPPORT FORUM. USE THIS FORM TO FILE BUGS. -->
**What version of this package are you using?**
**What operating system, Node.js, and npm version?**
**What happened?**
<!-- Please provide a minimal reproducible example that demonstrates your issue. -->
**What did you expect to happen?**
**Are you willing to submit a pull request to fix this bug?**
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: ❓ Ask a question
url: https://discord.gg/CNxFAzdEmr
about: Ask questions about this software
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: "⭐️ Feature request"
about: Request a new feature to be added
title: ''
labels: ''
assignees: ''
---
<!-- THIS IS NOT A SUPPORT FORUM. USE THIS FORM TO DISCUSS FEATURE DEVELOPMENT. -->
**What version of this package are you using?**
**What problem do you want to solve?**
**What do you think is the correct solution to this problem?**
**Are you willing to submit a pull request to implement this change?**
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
**What is the purpose of this pull request? (put an "X" next to item)**
[ ] Documentation update
[ ] Bug fix
[ ] New feature
[ ] Other, please explain:
**What changes did you make? (Give an overview)**
**Which issue (if any) does this pull request address?**
**Is there anything you'd like reviewers to focus on?**
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: daily
labels:
- dependency
versioning-strategy: increase
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
labels:
- dependency
================================================
FILE: .github/workflows/ci.yml
================================================
name: ci
'on':
- push
- pull_request
jobs:
test:
name: Node ${{ matrix.node }} / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
environment: ci
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
node:
- '14'
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
- run: npm install
- run: npm run build --if-present
- run: echo "127.0.0.1 airtap.local" | sudo tee -a /etc/hosts
- run: npm test
env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
================================================
FILE: .gitignore
================================================
.nyc_output
node_modules
package-lock.json
================================================
FILE: .npmignore
================================================
.airtap.yml
.nyc_output
.github/
img/
perf/
test/
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) Feross Aboukhadijeh
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
# 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]
[ci-image]: https://img.shields.io/github/workflow/status/feross/simple-peer/ci/master
[ci-url]: https://github.com/feross/simple-peer/actions
[coveralls-image]: https://coveralls.io/repos/github/feross/simple-peer/badge.svg?branch=master
[coveralls-url]: https://coveralls.io/github/feross/simple-peer?branch=master
[npm-image]: https://img.shields.io/npm/v/simple-peer.svg
[npm-url]: https://npmjs.org/package/simple-peer
[downloads-image]: https://img.shields.io/npm/dm/simple-peer.svg
[downloads-url]: https://npmjs.org/package/simple-peer
[standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg
[standard-url]: https://standardjs.com
[sauce-image]: https://saucelabs.com/buildstatus/simple-peer
[sauce-url]: https://saucelabs.com/u/simple-peer
#### Simple WebRTC video, voice, and data channels
<h5 align="center">
Sponsored by <a href="http://dfinity.org/"><img src="https://cdn.rawgit.com/feross/simple-peer/master/img/dfinity-sponsor.png" alt="DFINITY" width=250 valign="middle"></a>
</h5>
> We are hiring a peer-to-peer WebRTC mobile Web application expert.
>
> [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.
>
> 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).
## features
- concise, **node.js style** API for [WebRTC](https://en.wikipedia.org/wiki/WebRTC)
- **works in node and the browser!**
- supports **video/voice streams**
- supports **data channel**
- text and binary data
- node.js [duplex stream](http://nodejs.org/api/stream.html) interface
- supports advanced options like:
- enable/disable [trickle ICE candidates](http://webrtchacks.com/trickle-ice/)
- manually set config options
- transceivers and renegotiation
This package is used by [WebTorrent](https://webtorrent.io) and [many others](#who-is-using-simple-peer).
- [install](#install)
- [examples](#usage)
* [A simpler example](#a-simpler-example)
* [data channels](#data-channels)
* [video/voice](#videovoice)
* [dynamic video/voice](#dynamic-videovoice)
* [in node](#in-node)
- [api](#api)
- [events](#events)
- [error codes](#error-codes)
- [connecting more than 2 peers?](#connecting-more-than-2-peers)
- [memory usage](#memory-usage)
- [connection does not work on some networks?](#connection-does-not-work-on-some-networks)
- [Who is using `simple-peer`?](#who-is-using-simple-peer)
- [license](#license)
## install
```
npm install simple-peer
```
This package works in the browser with [browserify](https://browserify.org). If
you do not use a bundler, you can use the `simplepeer.min.js` standalone script
directly in a `<script>` tag. This exports a `SimplePeer` constructor on
`window`. Wherever you see `Peer` in the examples below, substitute that with
`SimplePeer`.
## usage
Let's create an html page that lets you manually connect two peers:
```html
<html>
<body>
<style>
#outgoing {
width: 600px;
word-wrap: break-word;
white-space: normal;
}
</style>
<form>
<textarea id="incoming"></textarea>
<button type="submit">submit</button>
</form>
<pre id="outgoing"></pre>
<script src="simplepeer.min.js"></script>
<script>
const p = new SimplePeer({
initiator: location.hash === '#1',
trickle: false
})
p.on('error', err => console.log('error', err))
p.on('signal', data => {
console.log('SIGNAL', JSON.stringify(data))
document.querySelector('#outgoing').textContent = JSON.stringify(data)
})
document.querySelector('form').addEventListener('submit', ev => {
ev.preventDefault()
p.signal(JSON.parse(document.querySelector('#incoming').value))
})
p.on('connect', () => {
console.log('CONNECT')
p.send('whatever' + Math.random())
})
p.on('data', data => {
console.log('data: ' + data)
})
</script>
</body>
</html>
```
Visit `index.html#1` from one browser (the initiator) and `index.html` from another
browser (the receiver).
An "offer" will be generated by the initiator. Paste this into the receiver's form and
hit submit. The receiver generates an "answer". Paste this into the initiator's form and
hit submit.
Now you have a direct P2P connection between two browsers!
### A simpler example
This example create two peers **in the same web page**.
In a real-world application, *you would never do this*. The sender and receiver `Peer`
instances would exist in separate browsers. A "signaling server" (usually implemented with
websockets) would be used to exchange signaling data between the two browsers until a
peer-to-peer connection is established.
### data channels
```js
var Peer = require('simple-peer')
var peer1 = new Peer({ initiator: true })
var peer2 = new Peer()
peer1.on('signal', data => {
// when peer1 has signaling data, give it to peer2 somehow
peer2.signal(data)
})
peer2.on('signal', data => {
// when peer2 has signaling data, give it to peer1 somehow
peer1.signal(data)
})
peer1.on('connect', () => {
// wait for 'connect' event before using the data channel
peer1.send('hey peer2, how is it going?')
})
peer2.on('data', data => {
// got a data channel message
console.log('got a message from peer1: ' + data)
})
```
### video/voice
Video/voice is also super simple! In this example, peer1 sends video to peer2.
```js
var Peer = require('simple-peer')
// get video/voice stream
navigator.mediaDevices.getUserMedia({
video: true,
audio: true
}).then(gotMedia).catch(() => {})
function gotMedia (stream) {
var peer1 = new Peer({ initiator: true, stream: stream })
var peer2 = new Peer()
peer1.on('signal', data => {
peer2.signal(data)
})
peer2.on('signal', data => {
peer1.signal(data)
})
peer2.on('stream', stream => {
// got remote video stream, now let's show it in a video tag
var video = document.querySelector('video')
if ('srcObject' in video) {
video.srcObject = stream
} else {
video.src = window.URL.createObjectURL(stream) // for older browsers
}
video.play()
})
}
```
For two-way video, simply pass a `stream` option into both `Peer` constructors. Simple!
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).
### dynamic video/voice
It is also possible to establish a data-only connection at first, and later add
a video/voice stream, if desired.
```js
var Peer = require('simple-peer') // create peer without waiting for media
var peer1 = new Peer({ initiator: true }) // you don't need streams here
var peer2 = new Peer()
peer1.on('signal', data => {
peer2.signal(data)
})
peer2.on('signal', data => {
peer1.signal(data)
})
peer2.on('stream', stream => {
// got remote video stream, now let's show it in a video tag
var video = document.querySelector('video')
if ('srcObject' in video) {
video.srcObject = stream
} else {
video.src = window.URL.createObjectURL(stream) // for older browsers
}
video.play()
})
function addMedia (stream) {
peer1.addStream(stream) // <- add streams to peer dynamically
}
// then, anytime later...
navigator.mediaDevices.getUserMedia({
video: true,
audio: true
}).then(addMedia).catch(() => {})
```
### in node
To use this library in node, pass in `opts.wrtc` as a parameter (see [the constructor options](#peer--new-peeropts)):
```js
var Peer = require('simple-peer')
var wrtc = require('wrtc')
var peer1 = new Peer({ initiator: true, wrtc: wrtc })
var peer2 = new Peer({ wrtc: wrtc })
```
## api
### `peer = new Peer([opts])`
Create a new WebRTC peer connection.
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.
If `opts` is specified, then the default options (shown below) will be overridden.
```
{
initiator: false,
channelConfig: {},
channelName: '<random string>',
config: { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:global.stun.twilio.com:3478?transport=udp' }] },
offerOptions: {},
answerOptions: {},
sdpTransform: function (sdp) { return sdp },
stream: false,
streams: [],
trickle: true,
allowHalfTrickle: false,
wrtc: {}, // RTCPeerConnection/RTCSessionDescription/RTCIceCandidate
objectMode: false
}
```
The options do the following:
- `initiator` - set to `true` if this is the initiating peer
- `channelConfig` - custom webrtc data channel configuration (used by [`createDataChannel`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createDataChannel))
- `channelName` - custom webrtc data channel name
- `config` - custom webrtc configuration (used by [`RTCPeerConnection`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection) constructor)
- `offerOptions` - custom offer options (used by [`createOffer`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer) method)
- `answerOptions` - custom answer options (used by [`createAnswer`](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createAnswer) method)
- `sdpTransform` - function to transform the generated SDP signaling data (for advanced users)
- `stream` - if video/voice is desired, pass stream returned from [`getUserMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia)
- `streams` - an array of MediaStreams returned from [`getUserMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia)
- `trickle` - set to `false` to disable [trickle ICE](http://webrtchacks.com/trickle-ice/) and get a single 'signal' event (slower)
- `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:
- [`RTCPeerConnection`](https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection)
- [`RTCSessionDescription`](https://www.w3.org/TR/webrtc/#dom-rtcsessiondescription)
- [`RTCIceCandidate`](https://www.w3.org/TR/webrtc/#dom-rtcicecandidate)
- `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.
### `peer.signal(data)`
Call this method whenever the remote peer emits a `peer.on('signal')` event.
The `data` will encapsulate a webrtc offer, answer, or ice candidate. These messages help
the peers to eventually establish a direct connection to each other. The contents of these
strings are an implementation detail that can be ignored by the user of this module;
simply pass the data from 'signal' events to the remote peer and call `peer.signal(data)`
to get connected.
### `peer.send(data)`
Send text/binary data to the remote peer. `data` can be any of several types: `String`,
`Buffer` (see [buffer](https://github.com/feross/buffer)), `ArrayBufferView` (`Uint8Array`,
etc.), `ArrayBuffer`, or `Blob` (in browsers that support it).
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.
### `peer.addStream(stream)`
Add a `MediaStream` to the connection.
### `peer.removeStream(stream)`
Remove a `MediaStream` from the connection.
### `peer.addTrack(track, stream)`
Add a `MediaStreamTrack` to the connection. Must also pass the `MediaStream` you want to attach it to.
### `peer.removeTrack(track, stream)`
Remove a `MediaStreamTrack` from the connection. Must also pass the `MediaStream` that it was attached to.
### `peer.replaceTrack(oldTrack, newTrack, stream)`
Replace a `MediaStreamTrack` with another track. Must also pass the `MediaStream` that the old track was attached to.
### `peer.addTransceiver(kind, init)`
Add a `RTCRtpTransceiver` to the connection. Can be used to add transceivers before adding tracks. Automatically called as neccesary by `addTrack`.
### `peer.destroy([err])`
Destroy and cleanup this peer connection.
If the optional `err` parameter is passed, then it will be emitted as an `'error'`
event on the stream.
### `Peer.WEBRTC_SUPPORT`
Detect native WebRTC support in the javascript environment.
```js
var Peer = require('simple-peer')
if (Peer.WEBRTC_SUPPORT) {
// webrtc support!
} else {
// fallback
}
```
### duplex stream
`Peer` objects are instances of `stream.Duplex`. They behave very similarly to a
`net.Socket` from the node core `net` module. The duplex stream reads/writes to the data
channel.
```js
var peer = new Peer(opts)
// ... signaling ...
peer.write(new Buffer('hey'))
peer.on('data', function (chunk) {
console.log('got a chunk', chunk)
})
```
## events
`Peer` objects are instance of `EventEmitter`. Take a look at the [nodejs events documentation](https://nodejs.org/api/events.html) for more information.
Example of removing all registered **close**-event listeners:
```js
peer.removeAllListeners('close')
```
### `peer.on('signal', data => {})`
Fired when the peer wants to send signaling data to the remote peer.
**It is the responsibility of the application developer (that's you!) to get this data to
the other peer.** This usually entails using a websocket signaling server. This data is an
`Object`, so remember to call `JSON.stringify(data)` to serialize it first. Then, simply
call `peer.signal(data)` on the remote peer.
(Be sure to listen to this event immediately to avoid missing it. For `initiator: true`
peers, it fires right away. For `initatior: false` peers, it fires when the remote
offer is received.)
### `peer.on('connect', () => {})`
Fired when the peer connection and data channel are ready to use.
### `peer.on('data', data => {})`
Received a message from the remote peer (via the data channel).
`data` will be either a `String` or a `Buffer/Uint8Array` (see [buffer](https://github.com/feross/buffer)).
### `peer.on('stream', stream => {})`
Received a remote video stream, which can be displayed in a video tag:
```js
peer.on('stream', stream => {
var video = document.querySelector('video')
if ('srcObject' in video) {
video.srcObject = stream
} else {
video.src = window.URL.createObjectURL(stream)
}
video.play()
})
```
### `peer.on('track', (track, stream) => {})`
Received a remote audio/video track. Streams may contain multiple tracks.
### `peer.on('close', () => {})`
Called when the peer connection has closed.
### `peer.on('error', (err) => {})`
Fired when a fatal error occurs. Usually, this means bad signaling data was received from the remote peer.
`err` is an `Error` object.
## error codes
Errors returned by the `error` event have an `err.code` property that will indicate the origin of the failure.
Possible error codes:
- `ERR_WEBRTC_SUPPORT`
- `ERR_CREATE_OFFER`
- `ERR_CREATE_ANSWER`
- `ERR_SET_LOCAL_DESCRIPTION`
- `ERR_SET_REMOTE_DESCRIPTION`
- `ERR_ADD_ICE_CANDIDATE`
- `ERR_ICE_CONNECTION_FAILURE`
- `ERR_SIGNALING`
- `ERR_DATA_CHANNEL`
- `ERR_CONNECTION_FAILURE`
## connecting more than 2 peers?
The simplest way to do that is to create a full-mesh topology. That means that every peer
opens a connection to every other peer. To illustrate:

To broadcast a message, just iterate over all the peers and call `peer.send`.
So, say you have 3 peers. Then, when a peer wants to send some data it must send it 2
times, once to each of the other peers. So you're going to want to be a bit careful about
the size of the data you send.
Full mesh topologies don't scale well when the number of peers is very large. The total
number of edges in the network will be 
where `n` is the number of peers.
For clarity, here is the code to connect 3 peers together:
#### Peer 1
```js
// These are peer1's connections to peer2 and peer3
var peer2 = new Peer({ initiator: true })
var peer3 = new Peer({ initiator: true })
peer2.on('signal', data => {
// send this signaling data to peer2 somehow
})
peer2.on('connect', () => {
peer2.send('hi peer2, this is peer1')
})
peer2.on('data', data => {
console.log('got a message from peer2: ' + data)
})
peer3.on('signal', data => {
// send this signaling data to peer3 somehow
})
peer3.on('connect', () => {
peer3.send('hi peer3, this is peer1')
})
peer3.on('data', data => {
console.log('got a message from peer3: ' + data)
})
```
#### Peer 2
```js
// These are peer2's connections to peer1 and peer3
var peer1 = new Peer()
var peer3 = new Peer({ initiator: true })
peer1.on('signal', data => {
// send this signaling data to peer1 somehow
})
peer1.on('connect', () => {
peer1.send('hi peer1, this is peer2')
})
peer1.on('data', data => {
console.log('got a message from peer1: ' + data)
})
peer3.on('signal', data => {
// send this signaling data to peer3 somehow
})
peer3.on('connect', () => {
peer3.send('hi peer3, this is peer2')
})
peer3.on('data', data => {
console.log('got a message from peer3: ' + data)
})
```
#### Peer 3
```js
// These are peer3's connections to peer1 and peer2
var peer1 = new Peer()
var peer2 = new Peer()
peer1.on('signal', data => {
// send this signaling data to peer1 somehow
})
peer1.on('connect', () => {
peer1.send('hi peer1, this is peer3')
})
peer1.on('data', data => {
console.log('got a message from peer1: ' + data)
})
peer2.on('signal', data => {
// send this signaling data to peer2 somehow
})
peer2.on('connect', () => {
peer2.send('hi peer2, this is peer3')
})
peer2.on('data', data => {
console.log('got a message from peer2: ' + data)
})
```
## memory usage
If you call `peer.send(buf)`, `simple-peer` is not keeping a reference to `buf`
and sending the buffer at some later point in time. We immediately call
`channel.send()` on the data channel. So it should be fine to mutate the buffer
right afterward.
However, beware that `peer.write(buf)` (a writable stream method) does not have
the same contract. It will potentially buffer the data and call
`channel.send()` at a future point in time, so definitely don't assume it's
safe to mutate the buffer.
## connection does not work on some networks?
If a direct connection fails, in particular, because of NAT traversal and/or firewalls,
WebRTC ICE uses an intermediary (relay) TURN server. In other words, ICE will first use
STUN with UDP to directly connect peers and, if that fails, will fall back to a TURN relay
server.
In order to use a TURN server, you must specify the `config` option to the `Peer`
constructor. See the API docs above.
[](https://github.com/feross/standard)
## Who is using `simple-peer`?
- [WebTorrent](http://webtorrent.io) - Streaming torrent client in the browser
- [Virus Cafe](https://virus.cafe) - Make a friend in 2 minutes
- [Instant.io](https://instant.io) - Secure, anonymous, streaming file transfer
- [Zencastr](https://zencastr.com) - Easily record your remote podcast interviews in studio quality.
- [Friends](https://github.com/moose-team/friends) - Peer-to-peer chat powered by the web
- [Socket.io-p2p](https://github.com/socketio/socket.io-p2p) - Official Socket.io P2P communication library
- [ScreenCat](https://maxogden.github.io/screencat/) - Screen sharing + remote collaboration app
- [WebCat](https://www.npmjs.com/package/webcat) - P2P pipe across the web using Github private/public key for auth
- [RTCCat](https://www.npmjs.com/package/rtcat) - WebRTC netcat
- [PeerNet](https://www.npmjs.com/package/peernet) - Peer-to-peer gossip network using randomized algorithms
- [PusherTC](http://pushertc.herokuapp.com) - Video chat with using Pusher. See [guide](http://blog.carbonfive.com/2014/10/16/webrtc-made-simple/).
- [lxjs-chat](https://github.com/feross/lxjs-chat) - Omegle-like video chat site
- [Whiteboard](https://github.com/feross/whiteboard) - P2P Whiteboard powered by WebRTC and WebTorrent
- [Peer Calls](https://peercalls.com) - WebRTC group video calling. Create a room. Share the link.
- [Netsix](https://mmorainville.github.io/netsix-gh-pages/) - Send videos to your friends using WebRTC so that they can watch them right away.
- [Stealthy](https://www.stealthy.im) - Stealthy is a decentralized, end-to-end encrypted, p2p chat application.
- [oorja.io](https://github.com/akshayKMR/oorja) - Effortless video-voice chat with realtime collaborative features. Extensible using react components 🙌
- [TalktoMe](https://talktome.universal-apps.xyz) - Skype alternative for audio/video conferencing based on WebRTC, but without the loss of packets.
- [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
- [Detox](https://github.com/Detox) - Overlay network for distributed anonymous P2P communications entirely in the browser
- [Metastream](https://github.com/samuelmaddock/metastream) - Watch streaming media with friends.
- [firepeer](https://github.com/natzcam/firepeer) - secure signalling and authentication using firebase realtime database
- [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)).
- [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/)).
- [Firstdate.co](https://firstdate.co) - Online video dating for actually meeting people and not just messaging them
- [TensorChat](https://github.com/EhsaanIqbal/tensorchat) - It's simple - Create. Share. Chat.
- [On/Office](https://onoffice.app) - View your desktop in a WebVR-powered environment
- [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
- [Ciphora](https://github.com/HR/ciphora) - A peer-to-peer end-to-end encrypted messaging chat app.
- [Whisthub](https://www.whisthub.com) - Online card game Color Whist with the possibility to start a video chat while playing.
- [Brie.fi/ng](https://brie.fi/ng) - Secure anonymous video chat
- [Peer.School](https://github.com/holtwick/peer2school) - Simple virtual classroom starting from the 1st class including video chat and real time whiteboard
- [FileFire](https://filefire.ca) - Transfer large files and folders at high speed without size limits.
- [safeShare](https://github.com/vj-abishek/airdrop) - Transfer files easily with text and voice communication.
- [CubeChat](https://cubechat.io) - Party in 3D 🎉
- [Homely School](https://homelyschool.com) - A virtual schooling system
- [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)
- [Share-Anywhere](https://share-anywhere.com/) - Cross-platform file transfer
- [QuaranTime.io](https://quarantime.io/) - The Activity board-game in video!
- [Trango](https://web.trango.io) - Cross-platform calling and file sharing solution.
- [P2PT](https://github.com/subins2000/p2pt) - Use WebTorrent trackers as signalling servers for making WebRTC connections
- [Dots](https://github.com/subins2000/vett) - Online multiplayer Dots & Boxes game. [Play Here!](https://vett.space)
- [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.
- [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)
- [Speakrandom](https://speakrandom.com) - Voice-chat social network using simple-peer to create audio conferences!
- [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.
- *Your app here! - send a PR!*
## license
MIT. Copyright (c) [Feross Aboukhadijeh](http://feross.org).
================================================
FILE: index.js
================================================
/*! simple-peer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
const debug = require('debug')('simple-peer')
const getBrowserRTC = require('get-browser-rtc')
const randombytes = require('randombytes')
const stream = require('readable-stream')
const queueMicrotask = require('queue-microtask') // TODO: remove when Node 10 is not supported
const errCode = require('err-code')
const { Buffer } = require('buffer')
const MAX_BUFFERED_AMOUNT = 64 * 1024
const ICECOMPLETE_TIMEOUT = 5 * 1000
const CHANNEL_CLOSING_TIMEOUT = 5 * 1000
// HACK: Filter trickle lines when trickle is disabled #354
function filterTrickle (sdp) {
return sdp.replace(/a=ice-options:trickle\s\n/g, '')
}
function warn (message) {
console.warn(message)
}
/**
* WebRTC peer connection. Same API as node core `net.Socket`, plus a few extra methods.
* Duplex stream.
* @param {Object} opts
*/
class Peer extends stream.Duplex {
constructor (opts) {
opts = Object.assign({
allowHalfOpen: false
}, opts)
super(opts)
this._id = randombytes(4).toString('hex').slice(0, 7)
this._debug('new peer %o', opts)
this.channelName = opts.initiator
? opts.channelName || randombytes(20).toString('hex')
: null
this.initiator = opts.initiator || false
this.channelConfig = opts.channelConfig || Peer.channelConfig
this.channelNegotiated = this.channelConfig.negotiated
this.config = Object.assign({}, Peer.config, opts.config)
this.offerOptions = opts.offerOptions || {}
this.answerOptions = opts.answerOptions || {}
this.sdpTransform = opts.sdpTransform || (sdp => sdp)
this.streams = opts.streams || (opts.stream ? [opts.stream] : []) // support old "stream" option
this.trickle = opts.trickle !== undefined ? opts.trickle : true
this.allowHalfTrickle = opts.allowHalfTrickle !== undefined ? opts.allowHalfTrickle : false
this.iceCompleteTimeout = opts.iceCompleteTimeout || ICECOMPLETE_TIMEOUT
this.destroyed = false
this.destroying = false
this._connected = false
this.remoteAddress = undefined
this.remoteFamily = undefined
this.remotePort = undefined
this.localAddress = undefined
this.localFamily = undefined
this.localPort = undefined
this._wrtc = (opts.wrtc && typeof opts.wrtc === 'object')
? opts.wrtc
: getBrowserRTC()
if (!this._wrtc) {
if (typeof window === 'undefined') {
throw errCode(new Error('No WebRTC support: Specify `opts.wrtc` option in this environment'), 'ERR_WEBRTC_SUPPORT')
} else {
throw errCode(new Error('No WebRTC support: Not a supported browser'), 'ERR_WEBRTC_SUPPORT')
}
}
this._pcReady = false
this._channelReady = false
this._iceComplete = false // ice candidate trickle done (got null candidate)
this._iceCompleteTimer = null // send an offer/answer anyway after some timeout
this._channel = null
this._pendingCandidates = []
this._isNegotiating = false // is this peer waiting for negotiation to complete?
this._firstNegotiation = true
this._batchedNegotiation = false // batch synchronous negotiations
this._queuedNegotiation = false // is there a queued negotiation request?
this._sendersAwaitingStable = []
this._senderMap = new Map()
this._closingInterval = null
this._remoteTracks = []
this._remoteStreams = []
this._chunk = null
this._cb = null
this._interval = null
try {
this._pc = new (this._wrtc.RTCPeerConnection)(this.config)
} catch (err) {
this.destroy(errCode(err, 'ERR_PC_CONSTRUCTOR'))
return
}
// We prefer feature detection whenever possible, but sometimes that's not
// possible for certain implementations.
this._isReactNativeWebrtc = typeof this._pc._peerConnectionId === 'number'
this._pc.oniceconnectionstatechange = () => {
this._onIceStateChange()
}
this._pc.onicegatheringstatechange = () => {
this._onIceStateChange()
}
this._pc.onconnectionstatechange = () => {
this._onConnectionStateChange()
}
this._pc.onsignalingstatechange = () => {
this._onSignalingStateChange()
}
this._pc.onicecandidate = event => {
this._onIceCandidate(event)
}
// HACK: Fix for odd Firefox behavior, see: https://github.com/feross/simple-peer/pull/783
if (typeof this._pc.peerIdentity === 'object') {
this._pc.peerIdentity.catch(err => {
this.destroy(errCode(err, 'ERR_PC_PEER_IDENTITY'))
})
}
// Other spec events, unused by this implementation:
// - onconnectionstatechange
// - onicecandidateerror
// - onfingerprintfailure
// - onnegotiationneeded
if (this.initiator || this.channelNegotiated) {
this._setupData({
channel: this._pc.createDataChannel(this.channelName, this.channelConfig)
})
} else {
this._pc.ondatachannel = event => {
this._setupData(event)
}
}
if (this.streams) {
this.streams.forEach(stream => {
this.addStream(stream)
})
}
this._pc.ontrack = event => {
this._onTrack(event)
}
this._debug('initial negotiation')
this._needsNegotiation()
this._onFinishBound = () => {
this._onFinish()
}
this.once('finish', this._onFinishBound)
}
get bufferSize () {
return (this._channel && this._channel.bufferedAmount) || 0
}
// HACK: it's possible channel.readyState is "closing" before peer.destroy() fires
// https://bugs.chromium.org/p/chromium/issues/detail?id=882743
get connected () {
return (this._connected && this._channel.readyState === 'open')
}
address () {
return { port: this.localPort, family: this.localFamily, address: this.localAddress }
}
signal (data) {
if (this.destroying) return
if (this.destroyed) throw errCode(new Error('cannot signal after peer is destroyed'), 'ERR_DESTROYED')
if (typeof data === 'string') {
try {
data = JSON.parse(data)
} catch (err) {
data = {}
}
}
this._debug('signal()')
if (data.renegotiate && this.initiator) {
this._debug('got request to renegotiate')
this._needsNegotiation()
}
if (data.transceiverRequest && this.initiator) {
this._debug('got request for transceiver')
this.addTransceiver(data.transceiverRequest.kind, data.transceiverRequest.init)
}
if (data.candidate) {
if (this._pc.remoteDescription && this._pc.remoteDescription.type) {
this._addIceCandidate(data.candidate)
} else {
this._pendingCandidates.push(data.candidate)
}
}
if (data.sdp) {
this._pc.setRemoteDescription(new (this._wrtc.RTCSessionDescription)(data))
.then(() => {
if (this.destroyed) return
this._pendingCandidates.forEach(candidate => {
this._addIceCandidate(candidate)
})
this._pendingCandidates = []
if (this._pc.remoteDescription.type === 'offer') this._createAnswer()
})
.catch(err => {
this.destroy(errCode(err, 'ERR_SET_REMOTE_DESCRIPTION'))
})
}
if (!data.sdp && !data.candidate && !data.renegotiate && !data.transceiverRequest) {
this.destroy(errCode(new Error('signal() called with invalid signal data'), 'ERR_SIGNALING'))
}
}
_addIceCandidate (candidate) {
const iceCandidateObj = new this._wrtc.RTCIceCandidate(candidate)
this._pc.addIceCandidate(iceCandidateObj)
.catch(err => {
if (!iceCandidateObj.address || iceCandidateObj.address.endsWith('.local')) {
warn('Ignoring unsupported ICE candidate.')
} else {
this.destroy(errCode(err, 'ERR_ADD_ICE_CANDIDATE'))
}
})
}
/**
* Send text/binary data to the remote peer.
* @param {ArrayBufferView|ArrayBuffer|Buffer|string|Blob} chunk
*/
send (chunk) {
if (this.destroying) return
if (this.destroyed) throw errCode(new Error('cannot send after peer is destroyed'), 'ERR_DESTROYED')
this._channel.send(chunk)
}
/**
* Add a Transceiver to the connection.
* @param {String} kind
* @param {Object} init
*/
addTransceiver (kind, init) {
if (this.destroying) return
if (this.destroyed) throw errCode(new Error('cannot addTransceiver after peer is destroyed'), 'ERR_DESTROYED')
this._debug('addTransceiver()')
if (this.initiator) {
try {
this._pc.addTransceiver(kind, init)
this._needsNegotiation()
} catch (err) {
this.destroy(errCode(err, 'ERR_ADD_TRANSCEIVER'))
}
} else {
this.emit('signal', { // request initiator to renegotiate
type: 'transceiverRequest',
transceiverRequest: { kind, init }
})
}
}
/**
* Add a MediaStream to the connection.
* @param {MediaStream} stream
*/
addStream (stream) {
if (this.destroying) return
if (this.destroyed) throw errCode(new Error('cannot addStream after peer is destroyed'), 'ERR_DESTROYED')
this._debug('addStream()')
stream.getTracks().forEach(track => {
this.addTrack(track, stream)
})
}
/**
* Add a MediaStreamTrack to the connection.
* @param {MediaStreamTrack} track
* @param {MediaStream} stream
*/
addTrack (track, stream) {
if (this.destroying) return
if (this.destroyed) throw errCode(new Error('cannot addTrack after peer is destroyed'), 'ERR_DESTROYED')
this._debug('addTrack()')
const submap = this._senderMap.get(track) || new Map() // nested Maps map [track, stream] to sender
let sender = submap.get(stream)
if (!sender) {
sender = this._pc.addTrack(track, stream)
submap.set(stream, sender)
this._senderMap.set(track, submap)
this._needsNegotiation()
} else if (sender.removed) {
throw errCode(new Error('Track has been removed. You should enable/disable tracks that you want to re-add.'), 'ERR_SENDER_REMOVED')
} else {
throw errCode(new Error('Track has already been added to that stream.'), 'ERR_SENDER_ALREADY_ADDED')
}
}
/**
* Replace a MediaStreamTrack by another in the connection.
* @param {MediaStreamTrack} oldTrack
* @param {MediaStreamTrack} newTrack
* @param {MediaStream} stream
*/
replaceTrack (oldTrack, newTrack, stream) {
if (this.destroying) return
if (this.destroyed) throw errCode(new Error('cannot replaceTrack after peer is destroyed'), 'ERR_DESTROYED')
this._debug('replaceTrack()')
const submap = this._senderMap.get(oldTrack)
const sender = submap ? submap.get(stream) : null
if (!sender) {
throw errCode(new Error('Cannot replace track that was never added.'), 'ERR_TRACK_NOT_ADDED')
}
if (newTrack) this._senderMap.set(newTrack, submap)
if (sender.replaceTrack != null) {
sender.replaceTrack(newTrack)
} else {
this.destroy(errCode(new Error('replaceTrack is not supported in this browser'), 'ERR_UNSUPPORTED_REPLACETRACK'))
}
}
/**
* Remove a MediaStreamTrack from the connection.
* @param {MediaStreamTrack} track
* @param {MediaStream} stream
*/
removeTrack (track, stream) {
if (this.destroying) return
if (this.destroyed) throw errCode(new Error('cannot removeTrack after peer is destroyed'), 'ERR_DESTROYED')
this._debug('removeSender()')
const submap = this._senderMap.get(track)
const sender = submap ? submap.get(stream) : null
if (!sender) {
throw errCode(new Error('Cannot remove track that was never added.'), 'ERR_TRACK_NOT_ADDED')
}
try {
sender.removed = true
this._pc.removeTrack(sender)
} catch (err) {
if (err.name === 'NS_ERROR_UNEXPECTED') {
this._sendersAwaitingStable.push(sender) // HACK: Firefox must wait until (signalingState === stable) https://bugzilla.mozilla.org/show_bug.cgi?id=1133874
} else {
this.destroy(errCode(err, 'ERR_REMOVE_TRACK'))
}
}
this._needsNegotiation()
}
/**
* Remove a MediaStream from the connection.
* @param {MediaStream} stream
*/
removeStream (stream) {
if (this.destroying) return
if (this.destroyed) throw errCode(new Error('cannot removeStream after peer is destroyed'), 'ERR_DESTROYED')
this._debug('removeSenders()')
stream.getTracks().forEach(track => {
this.removeTrack(track, stream)
})
}
_needsNegotiation () {
this._debug('_needsNegotiation')
if (this._batchedNegotiation) return // batch synchronous renegotiations
this._batchedNegotiation = true
queueMicrotask(() => {
this._batchedNegotiation = false
if (this.initiator || !this._firstNegotiation) {
this._debug('starting batched negotiation')
this.negotiate()
} else {
this._debug('non-initiator initial negotiation request discarded')
}
this._firstNegotiation = false
})
}
negotiate () {
if (this.destroying) return
if (this.destroyed) throw errCode(new Error('cannot negotiate after peer is destroyed'), 'ERR_DESTROYED')
if (this.initiator) {
if (this._isNegotiating) {
this._queuedNegotiation = true
this._debug('already negotiating, queueing')
} else {
this._debug('start negotiation')
setTimeout(() => { // HACK: Chrome crashes if we immediately call createOffer
this._createOffer()
}, 0)
}
} else {
if (this._isNegotiating) {
this._queuedNegotiation = true
this._debug('already negotiating, queueing')
} else {
this._debug('requesting negotiation from initiator')
this.emit('signal', { // request initiator to renegotiate
type: 'renegotiate',
renegotiate: true
})
}
}
this._isNegotiating = true
}
// TODO: Delete this method once readable-stream is updated to contain a default
// implementation of destroy() that automatically calls _destroy()
// See: https://github.com/nodejs/readable-stream/issues/283
destroy (err) {
this._destroy(err, () => {})
}
_destroy (err, cb) {
if (this.destroyed || this.destroying) return
this.destroying = true
this._debug('destroying (error: %s)', err && (err.message || err))
queueMicrotask(() => { // allow events concurrent with the call to _destroy() to fire (see #692)
this.destroyed = true
this.destroying = false
this._debug('destroy (error: %s)', err && (err.message || err))
this.readable = this.writable = false
if (!this._readableState.ended) this.push(null)
if (!this._writableState.finished) this.end()
this._connected = false
this._pcReady = false
this._channelReady = false
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
if (this._onFinishBound) this.removeListener('finish', this._onFinishBound)
this._onFinishBound = null
if (this._channel) {
try {
this._channel.close()
} catch (err) {}
// allow events concurrent with destruction to be handled
this._channel.onmessage = null
this._channel.onopen = null
this._channel.onclose = null
this._channel.onerror = null
}
if (this._pc) {
try {
this._pc.close()
} catch (err) {}
// allow events concurrent with destruction to be handled
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
if (err) this.emit('error', err)
this.emit('close')
cb()
})
}
_setupData (event) {
if (!event.channel) {
// In some situations `pc.createDataChannel()` returns `undefined` (in wrtc),
// which is invalid behavior. Handle it gracefully.
// See: https://github.com/feross/simple-peer/issues/163
return this.destroy(errCode(new Error('Data channel event is missing `channel` property'), 'ERR_DATA_CHANNEL'))
}
this._channel = event.channel
this._channel.binaryType = 'arraybuffer'
if (typeof this._channel.bufferedAmountLowThreshold === 'number') {
this._channel.bufferedAmountLowThreshold = MAX_BUFFERED_AMOUNT
}
this.channelName = this._channel.label
this._channel.onmessage = event => {
this._onChannelMessage(event)
}
this._channel.onbufferedamountlow = () => {
this._onChannelBufferedAmountLow()
}
this._channel.onopen = () => {
this._onChannelOpen()
}
this._channel.onclose = () => {
this._onChannelClose()
}
this._channel.onerror = event => {
const err = event.error instanceof Error
? event.error
: new Error(`Datachannel error: ${event.message} ${event.filename}:${event.lineno}:${event.colno}`)
this.destroy(errCode(err, 'ERR_DATA_CHANNEL'))
}
// HACK: Chrome will sometimes get stuck in readyState "closing", let's check for this condition
// https://bugs.chromium.org/p/chromium/issues/detail?id=882743
let isClosing = false
this._closingInterval = setInterval(() => { // No "onclosing" event
if (this._channel && this._channel.readyState === 'closing') {
if (isClosing) this._onChannelClose() // closing timed out: equivalent to onclose firing
isClosing = true
} else {
isClosing = false
}
}, CHANNEL_CLOSING_TIMEOUT)
}
_read () {}
_write (chunk, encoding, cb) {
if (this.destroyed) return cb(errCode(new Error('cannot write after peer is destroyed'), 'ERR_DATA_CHANNEL'))
if (this._connected) {
try {
this.send(chunk)
} catch (err) {
return this.destroy(errCode(err, 'ERR_DATA_CHANNEL'))
}
if (this._channel.bufferedAmount > MAX_BUFFERED_AMOUNT) {
this._debug('start backpressure: bufferedAmount %d', this._channel.bufferedAmount)
this._cb = cb
} else {
cb(null)
}
} else {
this._debug('write before connect')
this._chunk = chunk
this._cb = cb
}
}
// When stream finishes writing, close socket. Half open connections are not
// supported.
_onFinish () {
if (this.destroyed) return
// Wait a bit before destroying so the socket flushes.
// TODO: is there a more reliable way to accomplish this?
const destroySoon = () => {
setTimeout(() => this.destroy(), 1000)
}
if (this._connected) {
destroySoon()
} else {
this.once('connect', destroySoon)
}
}
_startIceCompleteTimeout () {
if (this.destroyed) return
if (this._iceCompleteTimer) return
this._debug('started iceComplete timeout')
this._iceCompleteTimer = setTimeout(() => {
if (!this._iceComplete) {
this._iceComplete = true
this._debug('iceComplete timeout completed')
this.emit('iceTimeout')
this.emit('_iceComplete')
}
}, this.iceCompleteTimeout)
}
_createOffer () {
if (this.destroyed) return
this._pc.createOffer(this.offerOptions)
.then(offer => {
if (this.destroyed) return
if (!this.trickle && !this.allowHalfTrickle) offer.sdp = filterTrickle(offer.sdp)
offer.sdp = this.sdpTransform(offer.sdp)
const sendOffer = () => {
if (this.destroyed) return
const signal = this._pc.localDescription || offer
this._debug('signal')
this.emit('signal', {
type: signal.type,
sdp: signal.sdp
})
}
const onSuccess = () => {
this._debug('createOffer success')
if (this.destroyed) return
if (this.trickle || this._iceComplete) sendOffer()
else this.once('_iceComplete', sendOffer) // wait for candidates
}
const onError = err => {
this.destroy(errCode(err, 'ERR_SET_LOCAL_DESCRIPTION'))
}
this._pc.setLocalDescription(offer)
.then(onSuccess)
.catch(onError)
})
.catch(err => {
this.destroy(errCode(err, 'ERR_CREATE_OFFER'))
})
}
_requestMissingTransceivers () {
if (this._pc.getTransceivers) {
this._pc.getTransceivers().forEach(transceiver => {
if (!transceiver.mid && transceiver.sender.track && !transceiver.requested) {
transceiver.requested = true // HACK: Safari returns negotiated transceivers with a null mid
this.addTransceiver(transceiver.sender.track.kind)
}
})
}
}
_createAnswer () {
if (this.destroyed) return
this._pc.createAnswer(this.answerOptions)
.then(answer => {
if (this.destroyed) return
if (!this.trickle && !this.allowHalfTrickle) answer.sdp = filterTrickle(answer.sdp)
answer.sdp = this.sdpTransform(answer.sdp)
const sendAnswer = () => {
if (this.destroyed) return
const signal = this._pc.localDescription || answer
this._debug('signal')
this.emit('signal', {
type: signal.type,
sdp: signal.sdp
})
if (!this.initiator) this._requestMissingTransceivers()
}
const onSuccess = () => {
if (this.destroyed) return
if (this.trickle || this._iceComplete) sendAnswer()
else this.once('_iceComplete', sendAnswer)
}
const onError = err => {
this.destroy(errCode(err, 'ERR_SET_LOCAL_DESCRIPTION'))
}
this._pc.setLocalDescription(answer)
.then(onSuccess)
.catch(onError)
})
.catch(err => {
this.destroy(errCode(err, 'ERR_CREATE_ANSWER'))
})
}
_onConnectionStateChange () {
if (this.destroyed) return
if (this._pc.connectionState === 'failed') {
this.destroy(errCode(new Error('Connection failed.'), 'ERR_CONNECTION_FAILURE'))
}
}
_onIceStateChange () {
if (this.destroyed) return
const iceConnectionState = this._pc.iceConnectionState
const iceGatheringState = this._pc.iceGatheringState
this._debug(
'iceStateChange (connection: %s) (gathering: %s)',
iceConnectionState,
iceGatheringState
)
this.emit('iceStateChange', iceConnectionState, iceGatheringState)
if (iceConnectionState === 'connected' || iceConnectionState === 'completed') {
this._pcReady = true
this._maybeReady()
}
if (iceConnectionState === 'failed') {
this.destroy(errCode(new Error('Ice connection failed.'), 'ERR_ICE_CONNECTION_FAILURE'))
}
if (iceConnectionState === 'closed') {
this.destroy(errCode(new Error('Ice connection closed.'), 'ERR_ICE_CONNECTION_CLOSED'))
}
}
getStats (cb) {
// statreports can come with a value array instead of properties
const flattenValues = report => {
if (Object.prototype.toString.call(report.values) === '[object Array]') {
report.values.forEach(value => {
Object.assign(report, value)
})
}
return report
}
// Promise-based getStats() (standard)
if (this._pc.getStats.length === 0 || this._isReactNativeWebrtc) {
this._pc.getStats()
.then(res => {
const reports = []
res.forEach(report => {
reports.push(flattenValues(report))
})
cb(null, reports)
}, err => cb(err))
// Single-parameter callback-based getStats() (non-standard)
} else if (this._pc.getStats.length > 0) {
this._pc.getStats(res => {
// If we destroy connection in `connect` callback this code might happen to run when actual connection is already closed
if (this.destroyed) return
const reports = []
res.result().forEach(result => {
const report = {}
result.names().forEach(name => {
report[name] = result.stat(name)
})
report.id = result.id
report.type = result.type
report.timestamp = result.timestamp
reports.push(flattenValues(report))
})
cb(null, reports)
}, err => cb(err))
// Unknown browser, skip getStats() since it's anyone's guess which style of
// getStats() they implement.
} else {
cb(null, [])
}
}
_maybeReady () {
this._debug('maybeReady pc %s channel %s', this._pcReady, this._channelReady)
if (this._connected || this._connecting || !this._pcReady || !this._channelReady) return
this._connecting = true
// HACK: We can't rely on order here, for details see https://github.com/js-platform/node-webrtc/issues/339
const findCandidatePair = () => {
if (this.destroyed) return
this.getStats((err, items) => {
if (this.destroyed) return
// Treat getStats error as non-fatal. It's not essential.
if (err) items = []
const remoteCandidates = {}
const localCandidates = {}
const candidatePairs = {}
let foundSelectedCandidatePair = false
items.forEach(item => {
// TODO: Once all browsers support the hyphenated stats report types, remove
// the non-hypenated ones
if (item.type === 'remotecandidate' || item.type === 'remote-candidate') {
remoteCandidates[item.id] = item
}
if (item.type === 'localcandidate' || item.type === 'local-candidate') {
localCandidates[item.id] = item
}
if (item.type === 'candidatepair' || item.type === 'candidate-pair') {
candidatePairs[item.id] = item
}
})
const setSelectedCandidatePair = selectedCandidatePair => {
foundSelectedCandidatePair = true
let local = localCandidates[selectedCandidatePair.localCandidateId]
if (local && (local.ip || local.address)) {
// Spec
this.localAddress = local.ip || local.address
this.localPort = Number(local.port)
} else if (local && local.ipAddress) {
// Firefox
this.localAddress = local.ipAddress
this.localPort = Number(local.portNumber)
} else if (typeof selectedCandidatePair.googLocalAddress === 'string') {
// TODO: remove this once Chrome 58 is released
local = selectedCandidatePair.googLocalAddress.split(':')
this.localAddress = local[0]
this.localPort = Number(local[1])
}
if (this.localAddress) {
this.localFamily = this.localAddress.includes(':') ? 'IPv6' : 'IPv4'
}
let remote = remoteCandidates[selectedCandidatePair.remoteCandidateId]
if (remote && (remote.ip || remote.address)) {
// Spec
this.remoteAddress = remote.ip || remote.address
this.remotePort = Number(remote.port)
} else if (remote && remote.ipAddress) {
// Firefox
this.remoteAddress = remote.ipAddress
this.remotePort = Number(remote.portNumber)
} else if (typeof selectedCandidatePair.googRemoteAddress === 'string') {
// TODO: remove this once Chrome 58 is released
remote = selectedCandidatePair.googRemoteAddress.split(':')
this.remoteAddress = remote[0]
this.remotePort = Number(remote[1])
}
if (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
)
}
items.forEach(item => {
// Spec-compliant
if (item.type === 'transport' && item.selectedCandidatePairId) {
setSelectedCandidatePair(candidatePairs[item.selectedCandidatePairId])
}
// Old implementations
if (
(item.type === 'googCandidatePair' && item.googActiveConnection === 'true') ||
((item.type === 'candidatepair' || item.type === 'candidate-pair') && item.selected)
) {
setSelectedCandidatePair(item)
}
})
// Ignore candidate pair selection in browsers like Safari 11 that do not have any local or remote candidates
// But wait until at least 1 candidate pair is available
if (!foundSelectedCandidatePair && (!Object.keys(candidatePairs).length || Object.keys(localCandidates).length)) {
setTimeout(findCandidatePair, 100)
return
} else {
this._connecting = false
this._connected = true
}
if (this._chunk) {
try {
this.send(this._chunk)
} catch (err) {
return this.destroy(errCode(err, 'ERR_DATA_CHANNEL'))
}
this._chunk = null
this._debug('sent chunk from "write before connect"')
const cb = this._cb
this._cb = null
cb(null)
}
// If `bufferedAmountLowThreshold` and 'onbufferedamountlow' are unsupported,
// fallback to using setInterval to implement backpressure.
if (typeof this._channel.bufferedAmountLowThreshold !== 'number') {
this._interval = setInterval(() => this._onInterval(), 150)
if (this._interval.unref) this._interval.unref()
}
this._debug('connect')
this.emit('connect')
})
}
findCandidatePair()
}
_onInterval () {
if (!this._cb || !this._channel || this._channel.bufferedAmount > MAX_BUFFERED_AMOUNT) {
return
}
this._onChannelBufferedAmountLow()
}
_onSignalingStateChange () {
if (this.destroyed) return
if (this._pc.signalingState === 'stable') {
this._isNegotiating = false
// HACK: Firefox doesn't yet support removing tracks when signalingState !== 'stable'
this._debug('flushing sender queue', this._sendersAwaitingStable)
this._sendersAwaitingStable.forEach(sender => {
this._pc.removeTrack(sender)
this._queuedNegotiation = true
})
this._sendersAwaitingStable = []
if (this._queuedNegotiation) {
this._debug('flushing negotiation queue')
this._queuedNegotiation = false
this._needsNegotiation() // negotiate again
} else {
this._debug('negotiated')
this.emit('negotiated')
}
}
this._debug('signalingStateChange %s', this._pc.signalingState)
this.emit('signalingStateChange', this._pc.signalingState)
}
_onIceCandidate (event) {
if (this.destroyed) return
if (event.candidate && this.trickle) {
this.emit('signal', {
type: 'candidate',
candidate: {
candidate: event.candidate.candidate,
sdpMLineIndex: event.candidate.sdpMLineIndex,
sdpMid: event.candidate.sdpMid
}
})
} else if (!event.candidate && !this._iceComplete) {
this._iceComplete = true
this.emit('_iceComplete')
}
// as soon as we've received one valid candidate start timeout
if (event.candidate) {
this._startIceCompleteTimeout()
}
}
_onChannelMessage (event) {
if (this.destroyed) return
let data = event.data
if (data instanceof ArrayBuffer) data = Buffer.from(data)
this.push(data)
}
_onChannelBufferedAmountLow () {
if (this.destroyed || !this._cb) return
this._debug('ending backpressure: bufferedAmount %d', this._channel.bufferedAmount)
const cb = this._cb
this._cb = null
cb(null)
}
_onChannelOpen () {
if (this._connected || this.destroyed) return
this._debug('on channel open')
this._channelReady = true
this._maybeReady()
}
_onChannelClose () {
if (this.destroyed) return
this._debug('on channel close')
this.destroy()
}
_onTrack (event) {
if (this.destroyed) return
event.streams.forEach(eventStream => {
this._debug('on track')
this.emit('track', event.track, eventStream)
this._remoteTracks.push({
track: event.track,
stream: eventStream
})
if (this._remoteStreams.some(remoteStream => {
return remoteStream.id === eventStream.id
})) return // Only fire one 'stream' event, even though there may be multiple tracks per stream
this._remoteStreams.push(eventStream)
queueMicrotask(() => {
this._debug('on stream')
this.emit('stream', eventStream) // ensure all tracks have been added
})
})
}
_debug () {
const args = [].slice.call(arguments)
args[0] = '[' + this._id + '] ' + args[0]
debug.apply(null, args)
}
}
Peer.WEBRTC_SUPPORT = !!getBrowserRTC()
/**
* Expose peer and data channel config for overriding all Peer
* instances. Otherwise, just set opts.config or opts.channelConfig
* when constructing a Peer.
*/
Peer.config = {
iceServers: [
{
urls: [
'stun:stun.l.google.com:19302',
'stun:global.stun.twilio.com:3478'
]
}
],
sdpSemantics: 'unified-plan'
}
Peer.channelConfig = {}
module.exports = Peer
================================================
FILE: package.json
================================================
{
"name": "simple-peer",
"description": "Simple one-to-one WebRTC video/voice and data channels",
"version": "9.11.1",
"author": {
"name": "Feross Aboukhadijeh",
"email": "feross@feross.org",
"url": "https://feross.org"
},
"bugs": {
"url": "https://github.com/feross/simple-peer/issues"
},
"dependencies": {
"buffer": "^6.0.3",
"debug": "^4.3.2",
"err-code": "^3.0.1",
"get-browser-rtc": "^1.1.0",
"queue-microtask": "^1.2.3",
"randombytes": "^2.1.0",
"readable-stream": "^3.6.0"
},
"devDependencies": {
"airtap": "^4.0.3",
"airtap-manual": "^1.0.0",
"airtap-sauce": "^1.1.0",
"babel-minify": "^0.5.1",
"bowser": "^2.11.0",
"browserify": "^17.0.0",
"coveralls": "^3.1.1",
"nyc": "^15.1.0",
"prettier-bytes": "^1.0.4",
"simple-get": "^4.0.0",
"speedometer": "^1.1.0",
"standard": "*",
"string-to-stream": "^3.0.1",
"tape": "^5.5.2",
"thunky": "^1.1.0",
"wrtc": "^0.4.7",
"ws": "^7.5.3"
},
"keywords": [
"data",
"data channel",
"data channel stream",
"data channels",
"p2p",
"peer",
"peer",
"peer-to-peer",
"stream",
"video",
"voice",
"webrtc",
"webrtc stream"
],
"license": "MIT",
"main": "index.js",
"repository": {
"type": "git",
"url": "git://github.com/feross/simple-peer.git"
},
"scripts": {
"build": "browserify -s SimplePeer -r . | minify > simplepeer.min.js",
"size": "npm run build && cat simplepeer.min.js | gzip | wc -c",
"// test": "standard && npm run test-node && npm run test-browser",
"test": "standard && npm run test-browser",
"test-browser": "airtap --coverage --concurrency 1 -- test/*.js",
"test-browser-local": "airtap --coverage --preset local -- test/*.js",
"test-node": "WRTC=wrtc tape test/*.js",
"coverage": "nyc report --reporter=text-lcov | coveralls"
},
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
}
================================================
FILE: perf/receive.js
================================================
// run in a browser and look at console for speed
// beefy perf/receive.js
// 7.6MB
const prettierBytes = require('prettier-bytes')
const speedometer = require('speedometer')
const Peer = require('simple-peer')
const speed = speedometer()
let peer
const socket = new window.WebSocket('ws://localhost:8080')
socket.addEventListener('message', onMessage)
function onMessage (event) {
const message = event.data
if (message === 'ready') {
if (peer) return
peer = new Peer()
peer.on('signal', function (signal) {
socket.send(JSON.stringify(signal))
})
peer.on('data', function (message) {
speed(message.length)
})
} else {
peer.signal(JSON.parse(message))
}
}
setInterval(function () {
console.log(prettierBytes(speed()))
}, 1000)
================================================
FILE: perf/send.js
================================================
// run in a browser, with:
// beefy perf/send.js
const Peer = require('simple-peer')
const stream = require('readable-stream')
const buf = Buffer.alloc(10000)
const endless = new stream.Readable({
read: function () {
this.push(buf)
}
})
let peer
const socket = new window.WebSocket('ws://localhost:8080')
socket.addEventListener('message', onMessage)
function onMessage (event) {
const message = event.data
if (message === 'ready') {
if (peer) return
peer = new Peer({ initiator: true })
peer.on('signal', function (signal) {
socket.send(JSON.stringify(signal))
})
peer.on('connect', function () {
endless.pipe(peer)
})
} else {
peer.signal(JSON.parse(message))
}
}
================================================
FILE: perf/server.js
================================================
// run in a terminal, to do signaling for peers
const ws = require('ws')
const server = new ws.Server({
port: 8080
})
const sockets = []
server.on('connection', function (socket) {
sockets.push(socket)
socket.on('message', onMessage)
socket.on('close', function () {
sockets.splice(sockets.indexOf(socket), 1)
})
function onMessage (message) {
sockets
.filter(s => s !== socket)
.forEach(socket => socket.send(message))
}
if (sockets.length === 2) {
sockets.forEach(socket => socket.send('ready'))
}
})
================================================
FILE: test/basic.js
================================================
const common = require('./common')
const Peer = require('../')
const test = require('tape')
let config
test('get config', function (t) {
common.getConfig(function (err, _config) {
if (err) return t.fail(err)
config = _config
t.end()
})
})
test('detect WebRTC support', function (t) {
t.equal(Peer.WEBRTC_SUPPORT, typeof window !== 'undefined', 'builtin webrtc support')
t.end()
})
test('create peer without options', function (t) {
t.plan(1)
if (process.browser) {
let peer
t.doesNotThrow(function () {
peer = new Peer()
})
peer.destroy()
} else {
t.pass('Skip no-option test in Node.js, since the wrtc option is required')
}
})
test('can detect error when RTCPeerConstructor throws', function (t) {
t.plan(1)
const peer = new Peer({ wrtc: { RTCPeerConnection: null } })
peer.once('error', function () {
t.pass('got error event')
peer.destroy()
})
})
test('signal event gets emitted', function (t) {
t.plan(2)
const peer = new Peer({ config, initiator: true, wrtc: common.wrtc })
peer.once('signal', function () {
t.pass('got signal event')
peer.on('close', function () { t.pass('peer destroyed') })
peer.destroy()
})
})
test('signal event does not get emitted by non-initiator', function (t) {
const peer = new Peer({ config, initiator: false, wrtc: common.wrtc })
peer.once('signal', function () {
t.fail('got signal event')
peer.on('close', function () { t.pass('peer destroyed') })
peer.destroy()
})
setTimeout(() => {
t.pass('did not get signal after 1000ms')
t.end()
}, 1000)
})
test('signal event does not get emitted by non-initiator with stream', function (t) {
const peer = new Peer({
config,
stream: common.getMediaStream(),
initiator: false,
wrtc: common.wrtc
})
peer.once('signal', function () {
t.fail('got signal event')
peer.on('close', function () { t.pass('peer destroyed') })
peer.destroy()
})
setTimeout(() => {
t.pass('did not get signal after 1000ms')
t.end()
}, 1000)
})
test('data send/receive text', function (t) {
t.plan(10)
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc })
const peer2 = new Peer({ config, wrtc: common.wrtc })
let numSignal1 = 0
peer1.on('signal', function (data) {
numSignal1 += 1
peer2.signal(data)
})
let numSignal2 = 0
peer2.on('signal', function (data) {
numSignal2 += 1
peer1.signal(data)
})
peer1.on('connect', tryTest)
peer2.on('connect', tryTest)
function tryTest () {
if (!peer1.connected || !peer2.connected) return
t.ok(numSignal1 >= 1)
t.ok(numSignal2 >= 1)
t.equal(peer1.initiator, true, 'peer1 is initiator')
t.equal(peer2.initiator, false, 'peer2 is not initiator')
peer1.send('sup peer2')
peer2.on('data', function (data) {
t.ok(Buffer.isBuffer(data), 'data is Buffer')
t.equal(data.toString(), 'sup peer2', 'got correct message')
peer2.send('sup peer1')
peer1.on('data', function (data) {
t.ok(Buffer.isBuffer(data), 'data is Buffer')
t.equal(data.toString(), 'sup peer1', 'got correct message')
peer1.on('close', function () { t.pass('peer1 destroyed') })
peer1.destroy()
peer2.on('close', function () { t.pass('peer2 destroyed') })
peer2.destroy()
})
})
}
})
test('sdpTransform function is called', function (t) {
t.plan(3)
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc })
const peer2 = new Peer({ config, sdpTransform, wrtc: common.wrtc })
function sdpTransform (sdp) {
t.equal(typeof sdp, 'string', 'got a string as SDP')
setTimeout(function () {
peer1.on('close', function () { t.pass('peer1 destroyed') })
peer1.destroy()
peer2.on('close', function () { t.pass('peer2 destroyed') })
peer2.destroy()
}, 0)
return sdp
}
peer1.on('signal', function (data) {
peer2.signal(data)
})
peer2.on('signal', function (data) {
peer1.signal(data)
})
})
test('old constraint formats are used', function (t) {
t.plan(3)
const constraints = {
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
}
}
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc, constraints })
const peer2 = new Peer({ config, wrtc: common.wrtc, constraints })
peer1.on('signal', function (data) {
peer2.signal(data)
})
peer2.on('signal', function (data) {
peer1.signal(data)
})
peer1.on('connect', function () {
t.pass('peers connected')
peer1.on('close', function () { t.pass('peer1 destroyed') })
peer1.destroy()
peer2.on('close', function () { t.pass('peer2 destroyed') })
peer2.destroy()
})
})
test('new constraint formats are used', function (t) {
t.plan(3)
const constraints = {
offerToReceiveAudio: true,
offerToReceiveVideo: true
}
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc, constraints })
const peer2 = new Peer({ config, wrtc: common.wrtc, constraints })
peer1.on('signal', function (data) {
peer2.signal(data)
})
peer2.on('signal', function (data) {
peer1.signal(data)
})
peer1.on('connect', function () {
t.pass('peers connected')
peer1.on('close', function () { t.pass('peer1 destroyed') })
peer1.destroy()
peer2.on('close', function () { t.pass('peer2 destroyed') })
peer2.destroy()
})
})
test('ensure remote address and port are available right after connection', function (t) {
if (common.isBrowser('safari') || common.isBrowser('ios')) {
t.pass('Skip on Safari and iOS which do not support modern getStats() calls')
t.end()
return
}
if (common.isBrowser('chrome') || common.isBrowser('edge')) {
t.pass('Skip on Chrome and Edge which hide local IPs with mDNS')
t.end()
return
}
t.plan(7)
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc })
const peer2 = new Peer({ config, wrtc: common.wrtc })
peer1.on('signal', function (data) {
peer2.signal(data)
})
peer2.on('signal', function (data) {
peer1.signal(data)
})
peer1.on('connect', function () {
t.pass('peers connected')
t.ok(peer1.remoteAddress, 'peer1 remote address is present')
t.ok(peer1.remotePort, 'peer1 remote port is present')
peer2.on('connect', function () {
t.ok(peer2.remoteAddress, 'peer2 remote address is present')
t.ok(peer2.remotePort, 'peer2 remote port is present')
peer1.on('close', function () { t.pass('peer1 destroyed') })
peer1.destroy()
peer2.on('close', function () { t.pass('peer2 destroyed') })
peer2.destroy()
})
})
})
test('ensure iceStateChange fires when connection failed', (t) => {
t.plan(1)
const peer = new Peer({ config, initiator: true, wrtc: common.wrtc })
peer.on('iceStateChange', (connectionState, gatheringState) => {
t.pass('got iceStateChange')
t.end()
})
// simulate concurrent iceConnectionStateChange and destroy()
peer.destroy()
peer._pc.oniceconnectionstatechange()
})
================================================
FILE: test/binary.js
================================================
const common = require('./common')
const Peer = require('../')
const test = require('tape')
let config
test('get config', function (t) {
common.getConfig(function (err, _config) {
if (err) return t.fail(err)
config = _config
t.end()
})
})
test('data send/receive Buffer', function (t) {
t.plan(6)
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc })
const peer2 = new Peer({ config, wrtc: common.wrtc })
peer1.on('signal', function (data) {
peer2.signal(data)
})
peer2.on('signal', function (data) {
peer1.signal(data)
})
peer1.on('connect', tryTest)
peer2.on('connect', tryTest)
function tryTest () {
if (!peer1.connected || !peer2.connected) return
peer1.send(Buffer.from([0, 1, 2]))
peer2.on('data', function (data) {
t.ok(Buffer.isBuffer(data), 'data is Buffer')
t.deepEqual(data, Buffer.from([0, 1, 2]), 'got correct message')
peer2.send(Buffer.from([0, 2, 4]))
peer1.on('data', function (data) {
t.ok(Buffer.isBuffer(data), 'data is Buffer')
t.deepEqual(data, Buffer.from([0, 2, 4]), 'got correct message')
peer1.on('close', function () { t.pass('peer1 destroyed') })
peer1.destroy()
peer2.on('close', function () { t.pass('peer2 destroyed') })
peer2.destroy()
})
})
}
})
test('data send/receive Uint8Array', function (t) {
t.plan(6)
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc })
const peer2 = new Peer({ config, wrtc: common.wrtc })
peer1.on('signal', function (data) {
peer2.signal(data)
})
peer2.on('signal', function (data) {
peer1.signal(data)
})
peer1.on('connect', tryTest)
peer2.on('connect', tryTest)
function tryTest () {
if (!peer1.connected || !peer2.connected) return
peer1.send(new Uint8Array([0, 1, 2]))
peer2.on('data', function (data) {
// binary types always get converted to Buffer
// See: https://github.com/feross/simple-peer/issues/138#issuecomment-278240571
t.ok(Buffer.isBuffer(data), 'data is Buffer')
t.deepEqual(data, Buffer.from([0, 1, 2]), 'got correct message')
peer2.send(new Uint8Array([0, 2, 4]))
peer1.on('data', function (data) {
t.ok(Buffer.isBuffer(data), 'data is Buffer')
t.deepEqual(data, Buffer.from([0, 2, 4]), 'got correct message')
peer1.on('close', function () { t.pass('peer1 destroyed') })
peer1.destroy()
peer2.on('close', function () { t.pass('peer2 destroyed') })
peer2.destroy()
})
})
}
})
test('data send/receive ArrayBuffer', function (t) {
t.plan(6)
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc })
const peer2 = new Peer({ config, wrtc: common.wrtc })
peer1.on('signal', function (data) {
peer2.signal(data)
})
peer2.on('signal', function (data) {
peer1.signal(data)
})
peer1.on('connect', tryTest)
peer2.on('connect', tryTest)
function tryTest () {
if (!peer1.connected || !peer2.connected) return
peer1.send(new Uint8Array([0, 1, 2]).buffer)
peer2.on('data', function (data) {
t.ok(Buffer.isBuffer(data), 'data is Buffer')
t.deepEqual(data, Buffer.from([0, 1, 2]), 'got correct message')
peer2.send(new Uint8Array([0, 2, 4]).buffer)
peer1.on('data', function (data) {
t.ok(Buffer.isBuffer(data), 'data is Buffer')
t.deepEqual(data, Buffer.from([0, 2, 4]), 'got correct message')
peer1.on('close', function () { t.pass('peer1 destroyed') })
peer1.destroy()
peer2.on('close', function () { t.pass('peer2 destroyed') })
peer2.destroy()
})
})
}
})
================================================
FILE: test/common.js
================================================
const get = require('simple-get')
const thunky = require('thunky')
const bowser = require('bowser')
exports.getConfig = thunky(function (cb) {
// Includes TURN -- needed for tests to pass on Sauce Labs
// https://github.com/feross/simple-peer/issues/41
// WARNING: This is *NOT* a public endpoint. Do not depend on it in your app.
get.concat('https://instant.io/__rtcConfig__', function (err, res, data) {
if (err) return cb(err)
data = data.toString()
try {
data = JSON.parse(data)
} catch (err) {
cb(err)
return
}
cb(null, data)
})
})
// For testing on node, we must provide a WebRTC implementation
if (process.env.WRTC === 'wrtc') {
exports.wrtc = require('wrtc')
}
// create a test MediaStream with two tracks
let canvas
exports.getMediaStream = function () {
if (exports.wrtc) {
const source = new exports.wrtc.nonstandard.RTCVideoSource()
const tracks = [source.createTrack(), source.createTrack()]
return new exports.wrtc.MediaStream(tracks)
} else {
if (!canvas) {
canvas = document.createElement('canvas')
canvas.width = canvas.height = 100
canvas.getContext('2d') // initialize canvas
}
const stream = canvas.captureStream(30)
stream.addTrack(stream.getTracks()[0].clone()) // should have 2 tracks
return stream
}
}
exports.isBrowser = function (name) {
if (typeof (window) === 'undefined') return false
const satifyObject = {}
if (name === 'ios') { // bowser can't directly name iOS Safari
satifyObject.mobile = { safari: '>=0' }
} else {
satifyObject[name] = '>=0'
}
return bowser.getParser(window.navigator.userAgent).satisfies(satifyObject)
}
================================================
FILE: test/multistream.js
================================================
const common = require('./common')
const Peer = require('../')
const test = require('tape')
let config
test('get config', function (t) {
common.getConfig(function (err, _config) {
if (err) return t.fail(err)
config = _config
t.end()
})
})
test('multistream', function (t) {
if (common.isBrowser('ios')) {
t.pass('Skip on iOS emulator which does not support this reliably') // iOS emulator issue #486
t.end()
return
}
t.plan(20)
const peer1 = new Peer({
config,
initiator: true,
wrtc: common.wrtc,
streams: (new Array(10)).fill(null).map(function () { return common.getMediaStream() })
})
const peer2 = new Peer({
config,
wrtc: common.wrtc,
streams: (new Array(10)).fill(null).map(function () { return common.getMediaStream() })
})
peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) })
peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) })
const receivedIds = {}
peer1.on('stream', function (stream) {
t.pass('peer1 got stream')
if (receivedIds[stream.id]) {
t.fail('received one unique stream per event')
} else {
receivedIds[stream.id] = true
}
})
peer2.on('stream', function (stream) {
t.pass('peer2 got stream')
if (receivedIds[stream.id]) {
t.fail('received one unique stream per event')
} else {
receivedIds[stream.id] = true
}
})
t.on('end', () => {
peer1.destroy()
peer2.destroy()
})
})
test('multistream (track event)', function (t) {
t.plan(20)
const peer1 = new Peer({
config,
initiator: true,
wrtc: common.wrtc,
streams: (new Array(5)).fill(null).map(function () { return common.getMediaStream() })
})
const peer2 = new Peer({
config,
wrtc: common.wrtc,
streams: (new Array(5)).fill(null).map(function () { return common.getMediaStream() })
})
peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) })
peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) })
const receivedIds = {}
peer1.on('track', function (track) {
t.pass('peer1 got track')
if (receivedIds[track.id]) {
t.fail('received one unique track per event')
} else {
receivedIds[track.id] = true
}
})
peer2.on('track', function (track) {
t.pass('peer2 got track')
if (receivedIds[track.id]) {
t.fail('received one unique track per event')
} else {
receivedIds[track.id] = true
}
})
t.on('end', () => {
peer1.destroy()
peer2.destroy()
})
})
test('multistream on non-initiator only', function (t) {
t.plan(30)
const peer1 = new Peer({
config,
initiator: true,
wrtc: common.wrtc,
streams: []
})
const peer2 = new Peer({
config,
wrtc: common.wrtc,
streams: (new Array(10)).fill(null).map(function () { return common.getMediaStream() })
})
peer1.on('signal', function (data) {
if (data.transceiverRequest) t.pass('got transceiverRequest')
if (!peer2.destroyed) peer2.signal(data)
})
peer2.on('signal', function (data) {
if (data.transceiverRequest) t.pass('got transceiverRequest')
if (!peer1.destroyed) peer1.signal(data)
})
const receivedIds = {}
peer1.on('stream', function (stream) {
t.pass('peer1 got stream')
if (receivedIds[stream.id]) {
t.fail('received one unique stream per event')
} else {
receivedIds[stream.id] = true
}
})
t.on('end', () => {
peer1.destroy()
peer2.destroy()
})
})
test('delayed stream on non-initiator', function (t) {
if (common.isBrowser('ios')) {
t.pass('Skip on iOS which does not support this reliably')
t.end()
return
}
t.timeoutAfter(15000)
t.plan(1)
const peer1 = new Peer({
config,
trickle: true,
initiator: true,
wrtc: common.wrtc,
streams: [common.getMediaStream()]
})
const peer2 = new Peer({
config,
trickle: true,
wrtc: common.wrtc,
streams: []
})
peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) })
peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) })
setTimeout(() => {
peer2.addStream(common.getMediaStream())
}, 10000)
peer1.on('stream', function () {
t.pass('peer1 got stream')
})
t.on('end', () => {
peer1.destroy()
peer2.destroy()
})
})
test('incremental multistream', function (t) {
if (common.isBrowser('ios')) {
t.pass('Skip on iOS emulator which does not support this reliably') // iOS emulator issue #486
t.end()
return
}
t.plan(12)
const peer1 = new Peer({
config,
initiator: true,
wrtc: common.wrtc,
streams: []
})
const peer2 = new Peer({
config,
wrtc: common.wrtc,
streams: []
})
peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) })
peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) })
peer1.on('connect', function () {
t.pass('peer1 connected')
peer1.addStream(common.getMediaStream())
})
peer2.on('connect', function () {
t.pass('peer2 connected')
peer2.addStream(common.getMediaStream())
})
const receivedIds = {}
let count1 = 0
peer1.on('stream', function (stream) {
t.pass('peer1 got stream')
if (receivedIds[stream.id]) {
t.fail('received one unique stream per event')
} else {
receivedIds[stream.id] = true
}
count1++
if (count1 < 5) {
peer1.addStream(common.getMediaStream())
}
})
let count2 = 0
peer2.on('stream', function (stream) {
t.pass('peer2 got stream')
if (receivedIds[stream.id]) {
t.fail('received one unique stream per event')
} else {
receivedIds[stream.id] = true
}
count2++
if (count2 < 5) {
peer2.addStream(common.getMediaStream())
}
})
t.on('end', () => {
peer1.destroy()
peer2.destroy()
})
})
test('incremental multistream (track event)', function (t) {
t.plan(22)
const peer1 = new Peer({
config,
initiator: true,
wrtc: common.wrtc,
streams: []
})
const peer2 = new Peer({
config,
wrtc: common.wrtc,
streams: []
})
peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) })
peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) })
peer1.on('connect', function () {
t.pass('peer1 connected')
peer1.addStream(common.getMediaStream())
})
peer2.on('connect', function () {
t.pass('peer2 connected')
peer2.addStream(common.getMediaStream())
})
const receivedIds = {}
let count1 = 0
peer1.on('track', function (track) {
t.pass('peer1 got track')
if (receivedIds[track.id]) {
t.fail('received one unique track per event')
} else {
receivedIds[track.id] = true
}
count1++
if (count1 % 2 === 0 && count1 < 10) {
peer1.addStream(common.getMediaStream())
}
})
let count2 = 0
peer2.on('track', function (track) {
t.pass('peer2 got track')
if (receivedIds[track.id]) {
t.fail('received one unique track per event')
} else {
receivedIds[track.id] = true
}
count2++
if (count2 % 2 === 0 && count2 < 10) {
peer2.addStream(common.getMediaStream())
}
})
t.on('end', () => {
peer1.destroy()
peer2.destroy()
})
})
test('incremental multistream on non-initiator only', function (t) {
if (common.isBrowser('ios')) {
t.pass('Skip on iOS emulator which does not support this reliably') // iOS emulator issue #486
t.end()
return
}
t.plan(7)
const peer1 = new Peer({
config,
initiator: true,
wrtc: common.wrtc,
streams: []
})
const peer2 = new Peer({
config,
wrtc: common.wrtc,
streams: []
})
peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) })
peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) })
peer1.on('connect', function () {
t.pass('peer1 connected')
})
peer2.on('connect', function () {
t.pass('peer2 connected')
peer2.addStream(common.getMediaStream())
})
const receivedIds = {}
let count = 0
peer1.on('stream', function (stream) {
t.pass('peer1 got stream')
if (receivedIds[stream.id]) {
t.fail('received one unique stream per event')
} else {
receivedIds[stream.id] = true
}
count++
if (count < 5) {
peer2.addStream(common.getMediaStream())
}
})
t.on('end', () => {
peer1.destroy()
peer2.destroy()
})
})
test('incremental multistream on non-initiator only (track event)', function (t) {
t.plan(12)
const peer1 = new Peer({
config,
initiator: true,
wrtc: common.wrtc,
streams: []
})
const peer2 = new Peer({
config,
wrtc: common.wrtc,
streams: []
})
peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) })
peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) })
peer1.on('connect', function () {
t.pass('peer1 connected')
})
peer2.on('connect', function () {
t.pass('peer2 connected')
peer2.addStream(common.getMediaStream())
})
const receivedIds = {}
let count = 0
peer1.on('track', function (track) {
t.pass('peer1 got track')
if (receivedIds[track.id]) {
t.fail('received one unique track per event')
} else {
receivedIds[track.id] = true
}
count++
if (count % 2 === 0 && count < 10) {
peer2.addStream(common.getMediaStream())
}
})
t.on('end', () => {
peer1.destroy()
peer2.destroy()
})
})
test('addStream after removeStream', function (t) {
if (common.isBrowser('ios')) {
t.pass('Skip on iOS which does not support this reliably')
t.end()
return
}
t.plan(2)
const stream1 = common.getMediaStream()
const stream2 = common.getMediaStream()
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc })
const peer2 = new Peer({ config, wrtc: common.wrtc, streams: [stream1] })
peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) })
peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) })
peer1.once('stream', () => {
t.pass('peer1 got first stream')
peer2.removeStream(stream1)
setTimeout(() => {
peer1.once('stream', () => {
t.pass('peer1 got second stream')
})
peer2.addStream(stream2)
}, 1000)
})
t.on('end', () => {
peer1.destroy()
peer2.destroy()
})
})
test('removeTrack immediately', function (t) {
t.plan(2)
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc })
const peer2 = new Peer({ config, wrtc: common.wrtc })
peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) })
peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) })
const stream1 = common.getMediaStream()
const stream2 = common.getMediaStream()
peer1.addTrack(stream1.getTracks()[0], stream1)
peer2.addTrack(stream2.getTracks()[0], stream2)
peer1.removeTrack(stream1.getTracks()[0], stream1)
peer2.removeTrack(stream2.getTracks()[0], stream2)
peer1.on('track', function (track, stream) {
t.fail('peer1 did not get track event')
})
peer2.on('track', function (track, stream) {
t.fail('peer2 did not get track event')
})
peer1.on('connect', function () {
t.pass('peer1 connected')
})
peer2.on('connect', function () {
t.pass('peer2 connected')
})
t.on('end', () => {
peer1.destroy()
peer2.destroy()
})
})
test('replaceTrack', function (t) {
t.plan(4)
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc })
const peer2 = new Peer({ config, wrtc: common.wrtc })
peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) })
peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) })
const stream1 = common.getMediaStream()
const stream2 = common.getMediaStream()
peer1.addTrack(stream1.getTracks()[0], stream1)
peer2.addTrack(stream2.getTracks()[0], stream2)
peer1.replaceTrack(stream1.getTracks()[0], stream2.getTracks()[0], stream1)
peer2.replaceTrack(stream2.getTracks()[0], stream1.getTracks()[0], stream2)
peer1.on('track', function (track, stream) {
t.pass('peer1 got track event')
peer2.replaceTrack(stream2.getTracks()[0], null, stream2)
})
peer2.on('track', function (track, stream) {
t.pass('peer2 got track event')
peer1.replaceTrack(stream1.getTracks()[0], null, stream1)
})
peer1.on('connect', function () {
t.pass('peer1 connected')
})
peer2.on('connect', function () {
t.pass('peer2 connected')
})
t.on('end', () => {
peer1.destroy()
peer2.destroy()
})
})
================================================
FILE: test/negotiation.js
================================================
const common = require('./common')
const Peer = require('../')
const test = require('tape')
let config
test('get config', function (t) {
common.getConfig(function (err, _config) {
if (err) return t.fail(err)
config = _config
t.end()
})
})
test('single negotiation', function (t) {
t.plan(10)
const peer1 = new Peer({ config, initiator: true, stream: common.getMediaStream(), wrtc: common.wrtc })
const peer2 = new Peer({ config, stream: common.getMediaStream(), wrtc: common.wrtc })
peer1.on('signal', function (data) {
if (data.renegotiate) t.fail('got unexpected request to renegotiate')
if (!peer2.destroyed) peer2.signal(data)
})
peer2.on('signal', function (data) {
if (data.renegotiate) t.fail('got unexpected request to renegotiate')
if (!peer1.destroyed) peer1.signal(data)
})
peer1.on('connect', function () {
t.pass('peer1 connected')
})
peer2.on('connect', function () {
t.pass('peer2 connected')
})
peer1.on('stream', function (stream) {
t.pass('peer1 got stream')
})
peer2.on('stream', function (stream) {
t.pass('peer2 got stream')
})
let trackCount1 = 0
peer1.on('track', function (track) {
t.pass('peer1 got track')
trackCount1++
if (trackCount1 >= 2) {
t.pass('got correct number of tracks')
}
})
let trackCount2 = 0
peer2.on('track', function (track) {
t.pass('peer2 got track')
trackCount2++
if (trackCount2 >= 2) {
t.pass('got correct number of tracks')
}
})
})
test('manual renegotiation', function (t) {
t.plan(2)
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc })
const peer2 = new Peer({ config, wrtc: common.wrtc })
peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) })
peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) })
peer1.on('connect', function () {
peer1.negotiate()
peer1.on('negotiated', function () {
t.pass('peer1 negotiated')
})
peer2.on('negotiated', function () {
t.pass('peer2 negotiated')
})
})
})
test('repeated manual renegotiation', function (t) {
t.plan(6)
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc })
const peer2 = new Peer({ config, wrtc: common.wrtc })
peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) })
peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) })
peer1.once('connect', function () {
peer1.negotiate()
})
peer1.once('negotiated', function () {
t.pass('peer1 negotiated')
peer1.negotiate()
peer1.once('negotiated', function () {
t.pass('peer1 negotiated again')
peer1.negotiate()
peer1.once('negotiated', function () {
t.pass('peer1 negotiated again')
})
})
})
peer2.once('negotiated', function () {
t.pass('peer2 negotiated')
peer2.negotiate()
peer2.once('negotiated', function () {
t.pass('peer2 negotiated again')
peer1.negotiate()
peer1.once('negotiated', function () {
t.pass('peer1 negotiated again')
})
})
})
})
test('renegotiation after addStream', function (t) {
if (common.isBrowser('ios')) {
t.pass('Skip on iOS which does not support this reliably')
t.end()
return
}
t.plan(4)
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc })
const peer2 = new Peer({ config, wrtc: common.wrtc })
peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) })
peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) })
peer1.on('connect', function () {
t.pass('peer1 connect')
peer1.addStream(common.getMediaStream())
})
peer2.on('connect', function () {
t.pass('peer2 connect')
peer2.addStream(common.getMediaStream())
})
peer1.on('stream', function () {
t.pass('peer1 got stream')
})
peer2.on('stream', function () {
t.pass('peer2 got stream')
})
})
test('add stream on non-initiator only', function (t) {
t.plan(3)
const peer1 = new Peer({
config,
initiator: true,
wrtc: common.wrtc
})
const peer2 = new Peer({
config,
wrtc: common.wrtc,
stream: common.getMediaStream()
})
peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) })
peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) })
peer1.on('connect', function () {
t.pass('peer1 connect')
})
peer2.on('connect', function () {
t.pass('peer2 connect')
})
peer1.on('stream', function () {
t.pass('peer1 got stream')
})
})
test('negotiated channels', function (t) {
t.plan(2)
const peer1 = new Peer({
config,
initiator: true,
wrtc: common.wrtc,
channelConfig: {
id: 1,
negotiated: true
}
})
const peer2 = new Peer({
config,
wrtc: common.wrtc,
channelConfig: {
id: 1,
negotiated: true
}
})
peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) })
peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) })
peer1.on('connect', function () {
t.pass('peer1 connect')
})
peer2.on('connect', function () {
t.pass('peer2 connect')
})
})
================================================
FILE: test/object-mode.js
================================================
const common = require('./common')
const Peer = require('../')
const test = require('tape')
let config
test('get config', function (t) {
common.getConfig(function (err, _config) {
if (err) return t.fail(err)
config = _config
t.end()
})
})
test('data send/receive string {objectMode: true}', function (t) {
t.plan(6)
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc, objectMode: true })
const peer2 = new Peer({ config, wrtc: common.wrtc, objectMode: true })
peer1.on('signal', function (data) {
peer2.signal(data)
})
peer2.on('signal', function (data) {
peer1.signal(data)
})
peer1.on('connect', tryTest)
peer2.on('connect', tryTest)
function tryTest () {
if (!peer1.connected || !peer2.connected) return
peer1.send('this is a string')
peer2.on('data', function (data) {
t.equal(typeof data, 'string', 'data is a string')
t.equal(data, 'this is a string', 'got correct message')
peer2.send('this is another string')
peer1.on('data', function (data) {
t.equal(typeof data, 'string', 'data is a string')
t.equal(data, 'this is another string', 'got correct message')
peer1.on('close', function () { t.pass('peer1 destroyed') })
peer1.destroy()
peer2.on('close', function () { t.pass('peer2 destroyed') })
peer2.destroy()
})
})
}
})
test('data send/receive Buffer {objectMode: true}', function (t) {
t.plan(6)
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc, objectMode: true })
const peer2 = new Peer({ config, wrtc: common.wrtc, objectMode: true })
peer1.on('signal', function (data) {
peer2.signal(data)
})
peer2.on('signal', function (data) {
peer1.signal(data)
})
peer1.on('connect', tryTest)
peer2.on('connect', tryTest)
function tryTest () {
if (!peer1.connected || !peer2.connected) return
peer1.send(Buffer.from('this is a Buffer'))
peer2.on('data', function (data) {
t.ok(Buffer.isBuffer(data), 'data is a Buffer')
t.deepEqual(data, Buffer.from('this is a Buffer'), 'got correct message')
peer2.send(Buffer.from('this is another Buffer'))
peer1.on('data', function (data) {
t.ok(Buffer.isBuffer(data), 'data is a Buffer')
t.deepEqual(data, Buffer.from('this is another Buffer'), 'got correct message')
peer1.on('close', function () { t.pass('peer1 destroyed') })
peer1.destroy()
peer2.on('close', function () { t.pass('peer2 destroyed') })
peer2.destroy()
})
})
}
})
test('data send/receive Uint8Array {objectMode: true}', function (t) {
t.plan(6)
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc, objectMode: true })
const peer2 = new Peer({ config, wrtc: common.wrtc, objectMode: true })
peer1.on('signal', function (data) {
peer2.signal(data)
})
peer2.on('signal', function (data) {
peer1.signal(data)
})
peer1.on('connect', tryTest)
peer2.on('connect', tryTest)
function tryTest () {
if (!peer1.connected || !peer2.connected) return
peer1.send(new Uint8Array([0, 1, 2]))
peer2.on('data', function (data) {
// binary types always get converted to Buffer
// See: https://github.com/feross/simple-peer/issues/138#issuecomment-278240571
t.ok(Buffer.isBuffer(data), 'data is a Buffer')
t.deepEqual(data, Buffer.from([0, 1, 2]), 'got correct message')
peer2.send(new Uint8Array([1, 2, 3]))
peer1.on('data', function (data) {
t.ok(Buffer.isBuffer(data), 'data is a Buffer')
t.deepEqual(data, Buffer.from([1, 2, 3]), 'got correct message')
peer1.on('close', function () { t.pass('peer1 destroyed') })
peer1.destroy()
peer2.on('close', function () { t.pass('peer2 destroyed') })
peer2.destroy()
})
})
}
})
test('data send/receive ArrayBuffer {objectMode: true}', function (t) {
t.plan(6)
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc, objectMode: true })
const peer2 = new Peer({ config, wrtc: common.wrtc, objectMode: true })
peer1.on('signal', function (data) {
peer2.signal(data)
})
peer2.on('signal', function (data) {
peer1.signal(data)
})
peer1.on('connect', tryTest)
peer2.on('connect', tryTest)
function tryTest () {
if (!peer1.connected || !peer2.connected) return
peer1.send(new Uint8Array([0, 1, 2]).buffer)
peer2.on('data', function (data) {
t.ok(Buffer.isBuffer(data), 'data is a Buffer')
t.deepEqual(data, Buffer.from([0, 1, 2]), 'got correct message')
peer2.send(new Uint8Array([1, 2, 3]).buffer)
peer1.on('data', function (data) {
t.ok(Buffer.isBuffer(data), 'data is a Buffer')
t.deepEqual(data, Buffer.from([1, 2, 3]), 'got correct message')
peer1.on('close', function () { t.pass('peer1 destroyed') })
peer1.destroy()
peer2.on('close', function () { t.pass('peer2 destroyed') })
peer2.destroy()
})
})
}
})
================================================
FILE: test/stream.js
================================================
const common = require('./common')
const Peer = require('../')
const str = require('string-to-stream')
const test = require('tape')
let config
test('get config', function (t) {
common.getConfig(function (err, _config) {
if (err) return t.fail(err)
config = _config
t.end()
})
})
test('duplex stream: send data before "connect" event', function (t) {
t.plan(9)
t.timeoutAfter(20000)
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc })
const peer2 = new Peer({ config, wrtc: common.wrtc })
peer1.on('signal', function (data) { if (!peer2.destroyed) peer2.signal(data) })
peer2.on('signal', function (data) { if (!peer1.destroyed) peer1.signal(data) })
str('abc').pipe(peer1)
peer1.on('data', function () {
t.fail('peer1 should not get data')
})
peer1.on('finish', function () {
t.pass('got peer1 "finish"')
t.ok(peer1._writableState.finished)
})
peer1.on('end', function () {
t.pass('got peer1 "end"')
t.ok(peer1._readableState.ended)
})
peer2.on('data', function (chunk) {
t.equal(chunk.toString(), 'abc', 'got correct message')
})
peer2.on('finish', function () {
t.pass('got peer2 "finish"')
t.ok(peer2._writableState.finished)
})
peer2.on('end', function () {
t.pass('got peer2 "end"')
t.ok(peer2._readableState.ended)
})
})
test('duplex stream: send data one-way', function (t) {
t.plan(9)
t.timeoutAfter(20000)
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc })
const peer2 = new Peer({ config, wrtc: common.wrtc })
peer1.on('signal', function (data) { peer2.signal(data) })
peer2.on('signal', function (data) { peer1.signal(data) })
peer1.on('connect', tryTest)
peer2.on('connect', tryTest)
function tryTest () {
if (!peer1.connected || !peer2.connected) return
peer1.on('data', function () {
t.fail('peer1 should not get data')
})
peer1.on('finish', function () {
t.pass('got peer1 "finish"')
t.ok(peer1._writableState.finished)
})
peer1.on('end', function () {
t.pass('got peer1 "end"')
t.ok(peer1._readableState.ended)
})
peer2.on('data', function (chunk) {
t.equal(chunk.toString(), 'abc', 'got correct message')
})
peer2.on('finish', function () {
t.pass('got peer2 "finish"')
t.ok(peer2._writableState.finished)
})
peer2.on('end', function () {
t.pass('got peer2 "end"')
t.ok(peer2._readableState.ended)
})
str('abc').pipe(peer1)
}
})
================================================
FILE: test/trickle.js
================================================
const common = require('./common')
const Peer = require('../')
const test = require('tape')
let config
test('get config', function (t) {
common.getConfig(function (err, _config) {
if (err) return t.fail(err)
config = _config
t.end()
})
})
test('disable trickle', function (t) {
t.plan(8)
const peer1 = new Peer({ config, initiator: true, trickle: false, wrtc: common.wrtc })
const peer2 = new Peer({ config, trickle: false, wrtc: common.wrtc })
let numSignal1 = 0
peer1.on('signal', function (data) {
numSignal1 += 1
peer2.signal(data)
})
let numSignal2 = 0
peer2.on('signal', function (data) {
numSignal2 += 1
peer1.signal(data)
})
peer1.on('connect', tryTest)
peer2.on('connect', tryTest)
function tryTest () {
if (!peer1.connected || !peer2.connected) return
t.equal(numSignal1, 1, 'only one `signal` event')
t.equal(numSignal2, 1, 'only one `signal` event')
t.equal(peer1.initiator, true, 'peer1 is initiator')
t.equal(peer2.initiator, false, 'peer2 is not initiator')
peer1.send('sup peer2')
peer2.on('data', function (data) {
t.equal(data.toString(), 'sup peer2', 'got correct message')
peer2.send('sup peer1')
peer1.on('data', function (data) {
t.equal(data.toString(), 'sup peer1', 'got correct message')
peer1.on('close', function () { t.pass('peer1 destroyed') })
peer1.destroy()
peer2.on('close', function () { t.pass('peer2 destroyed') })
peer2.destroy()
})
})
}
})
test('disable trickle (only initiator)', function (t) {
t.plan(8)
const peer1 = new Peer({ config, initiator: true, trickle: false, wrtc: common.wrtc })
const peer2 = new Peer({ config, wrtc: common.wrtc })
let numSignal1 = 0
peer1.on('signal', function (data) {
numSignal1 += 1
peer2.signal(data)
})
let numSignal2 = 0
peer2.on('signal', function (data) {
numSignal2 += 1
peer1.signal(data)
})
peer1.on('connect', tryTest)
peer2.on('connect', tryTest)
function tryTest () {
if (!peer1.connected || !peer2.connected) return
t.equal(numSignal1, 1, 'only one `signal` event for initiator')
t.ok(numSignal2 >= 1, 'at least one `signal` event for receiver')
t.equal(peer1.initiator, true, 'peer1 is initiator')
t.equal(peer2.initiator, false, 'peer2 is not initiator')
peer1.send('sup peer2')
peer2.on('data', function (data) {
t.equal(data.toString(), 'sup peer2', 'got correct message')
peer2.send('sup peer1')
peer1.on('data', function (data) {
t.equal(data.toString(), 'sup peer1', 'got correct message')
peer1.on('close', function () { t.pass('peer1 destroyed') })
peer1.destroy()
peer2.on('close', function () { t.pass('peer2 destroyed') })
peer2.destroy()
})
})
}
})
test('disable trickle (only receiver)', function (t) {
t.plan(8)
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc })
const peer2 = new Peer({ config, trickle: false, wrtc: common.wrtc })
let numSignal1 = 0
peer1.on('signal', function (data) {
numSignal1 += 1
peer2.signal(data)
})
let numSignal2 = 0
peer2.on('signal', function (data) {
numSignal2 += 1
peer1.signal(data)
})
peer1.on('connect', tryTest)
peer2.on('connect', tryTest)
function tryTest () {
if (!peer1.connected || !peer2.connected) return
t.ok(numSignal1 >= 1, 'at least one `signal` event for initiator')
t.equal(numSignal2, 1, 'only one `signal` event for receiver')
t.equal(peer1.initiator, true, 'peer1 is initiator')
t.equal(peer2.initiator, false, 'peer2 is not initiator')
peer1.send('sup peer2')
peer2.on('data', function (data) {
t.equal(data.toString(), 'sup peer2', 'got correct message')
peer2.send('sup peer1')
peer1.on('data', function (data) {
t.equal(data.toString(), 'sup peer1', 'got correct message')
peer1.on('close', function () { t.pass('peer1 destroyed') })
peer1.destroy()
peer2.on('close', function () { t.pass('peer2 destroyed') })
peer2.destroy()
})
})
}
})
test('null end candidate does not throw', function (t) {
const peer1 = new Peer({ trickle: true, config, initiator: true, wrtc: common.wrtc })
const peer2 = new Peer({ trickle: true, config, wrtc: common.wrtc })
// translate all falsey candidates to null
let endCandidateSent = false
function endToNull (data) {
if (data.candidate && !data.candidate.candidate) {
data.candidate.candidate = null
endCandidateSent = true
}
return data
}
peer1.on('error', () => t.fail('peer1 threw error'))
peer2.on('error', () => t.fail('peer2 threw error'))
peer1.on('signal', data => peer2.signal(endToNull(data)))
peer2.on('signal', data => peer1.signal(endToNull(data)))
peer1.on('connect', () => {
if (!endCandidateSent) { // force an end candidate to browsers that don't send them
peer1.signal({ candidate: { candidate: null, sdpMLineIndex: 0, sdpMid: '0' } })
peer2.signal({ candidate: { candidate: null, sdpMLineIndex: 0, sdpMid: '0' } })
}
t.pass('connected')
t.end()
})
})
test('empty-string end candidate does not throw', function (t) {
const peer1 = new Peer({ trickle: true, config, initiator: true, wrtc: common.wrtc })
const peer2 = new Peer({ trickle: true, config, wrtc: common.wrtc })
// translate all falsey candidates to null
let endCandidateSent = false
function endToEmptyString (data) {
if (data.candidate && !data.candidate.candidate) {
data.candidate.candidate = ''
endCandidateSent = true
}
return data
}
peer1.on('error', () => t.fail('peer1 threw error'))
peer2.on('error', () => t.fail('peer2 threw error'))
peer1.on('signal', data => peer2.signal(endToEmptyString(data)))
peer2.on('signal', data => peer1.signal(endToEmptyString(data)))
peer1.on('connect', () => {
if (!endCandidateSent) { // force an end candidate to browsers that don't send them
peer1.signal({ candidate: { candidate: '', sdpMLineIndex: 0, sdpMid: '0' } })
peer2.signal({ candidate: { candidate: '', sdpMLineIndex: 0, sdpMid: '0' } })
}
t.pass('connected')
t.end()
})
})
test('mDNS candidate does not throw', function (t) {
const peer1 = new Peer({ trickle: true, config, initiator: true, wrtc: common.wrtc })
const peer2 = new Peer({ trickle: true, config, wrtc: common.wrtc })
peer1.on('error', () => t.fail('peer1 threw error'))
peer2.on('error', () => t.fail('peer2 threw error'))
peer1.on('signal', data => peer2.signal(data))
peer2.on('signal', data => peer1.signal(data))
peer1.on('connect', () => {
// force an mDNS candidate to browsers that don't send them
const candidate = 'candidate:2053030672 1 udp 2113937151 ede93942-fbc5-4323-9b73-169de626e467.local 55741 typ host generation 0 ufrag HNmH network-cost 999'
peer1.signal({ candidate: { candidate, sdpMLineIndex: 0, sdpMid: '0' } })
peer2.signal({ candidate: { candidate, sdpMLineIndex: 0, sdpMid: '0' } })
t.pass('connected')
t.end()
})
})
test('ice candidates received before description', function (t) {
t.plan(3)
const peer1 = new Peer({ config, initiator: true, wrtc: common.wrtc })
const peer2 = new Peer({ config, wrtc: common.wrtc })
const signalQueue1 = []
peer1.on('signal', function (data) {
signalQueue1.push(data)
if (data.candidate) {
while (signalQueue1[0]) peer2.signal(signalQueue1.pop())
}
})
const signalQueue2 = []
peer2.on('signal', function (data) {
signalQueue2.push(data)
if (data.candidate) {
while (signalQueue2[0]) peer1.signal(signalQueue2.pop())
}
})
peer1.on('connect', function () {
t.pass('peers connected')
peer2.on('connect', function () {
peer1.on('close', function () { t.pass('peer1 destroyed') })
peer1.destroy()
peer2.on('close', function () { t.pass('peer2 destroyed') })
peer2.destroy()
})
})
})
================================================
FILE: test/z-cleanup.js
================================================
// This test file runs after all the others. This is where we can run the cleanup
// code that is required
const test = require('tape')
test('cleanup', function (t) {
// Shut down the process and any daemons
t.end()
if (process && process.exit) {
process.exit(0)
}
})
gitextract_bqa_c8qr/
├── .airtap.yml
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ ├── config.yml
│ │ └── feature_request.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── dependabot.yml
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── 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
├── stream.js
├── trickle.js
└── z-cleanup.js
SYMBOL INDEX (62 symbols across 9 files)
FILE: index.js
constant MAX_BUFFERED_AMOUNT (line 10) | const MAX_BUFFERED_AMOUNT = 64 * 1024
constant ICECOMPLETE_TIMEOUT (line 11) | const ICECOMPLETE_TIMEOUT = 5 * 1000
constant CHANNEL_CLOSING_TIMEOUT (line 12) | const CHANNEL_CLOSING_TIMEOUT = 5 * 1000
function filterTrickle (line 15) | function filterTrickle (sdp) {
function warn (line 19) | function warn (message) {
class Peer (line 28) | class Peer extends stream.Duplex {
method constructor (line 29) | constructor (opts) {
method bufferSize (line 168) | get bufferSize () {
method connected (line 174) | get connected () {
method address (line 178) | address () {
method signal (line 182) | signal (data) {
method _addIceCandidate (line 230) | _addIceCandidate (candidate) {
method send (line 246) | send (chunk) {
method addTransceiver (line 257) | addTransceiver (kind, init) {
method addStream (line 281) | addStream (stream) {
method addTrack (line 296) | addTrack (track, stream) {
method replaceTrack (line 321) | replaceTrack (oldTrack, newTrack, stream) {
method removeTrack (line 345) | removeTrack (track, stream) {
method removeStream (line 372) | removeStream (stream) {
method _needsNegotiation (line 382) | _needsNegotiation () {
method negotiate (line 398) | negotiate () {
method destroy (line 430) | destroy (err) {
method _destroy (line 434) | _destroy (err, cb) {
method _setupData (line 502) | _setupData (event) {
method _read (line 551) | _read () {}
method _write (line 553) | _write (chunk, encoding, cb) {
method _onFinish (line 577) | _onFinish () {
method _startIceCompleteTimeout (line 593) | _startIceCompleteTimeout () {
method _createOffer (line 607) | _createOffer () {
method _requestMissingTransceivers (line 646) | _requestMissingTransceivers () {
method _createAnswer (line 657) | _createAnswer () {
method _onConnectionStateChange (line 696) | _onConnectionStateChange () {
method _onIceStateChange (line 703) | _onIceStateChange () {
method getStats (line 727) | getStats (cb) {
method _maybeReady (line 776) | _maybeReady () {
method _onInterval (line 916) | _onInterval () {
method _onSignalingStateChange (line 923) | _onSignalingStateChange () {
method _onIceCandidate (line 951) | _onIceCandidate (event) {
method _onChannelMessage (line 972) | _onChannelMessage (event) {
method _onChannelBufferedAmountLow (line 979) | _onChannelBufferedAmountLow () {
method _onChannelOpen (line 987) | _onChannelOpen () {
method _onChannelClose (line 994) | _onChannelClose () {
method _onTrack (line 1000) | _onTrack (event) {
method _debug (line 1024) | _debug () {
FILE: perf/receive.js
function onMessage (line 18) | function onMessage (event) {
FILE: perf/send.js
function onMessage (line 21) | function onMessage (event) {
FILE: perf/server.js
function onMessage (line 18) | function onMessage (message) {
FILE: test/basic.js
function tryTest (line 108) | function tryTest () {
function sdpTransform (line 141) | function sdpTransform (sdp) {
FILE: test/binary.js
function tryTest (line 28) | function tryTest () {
function tryTest (line 64) | function tryTest () {
function tryTest (line 102) | function tryTest () {
FILE: test/object-mode.js
function tryTest (line 28) | function tryTest () {
function tryTest (line 64) | function tryTest () {
function tryTest (line 100) | function tryTest () {
function tryTest (line 138) | function tryTest () {
FILE: test/stream.js
function tryTest (line 62) | function tryTest () {
FILE: test/trickle.js
function tryTest (line 35) | function tryTest () {
function tryTest (line 81) | function tryTest () {
function tryTest (line 127) | function tryTest () {
function endToNull (line 158) | function endToNull (data) {
function endToEmptyString (line 188) | function endToEmptyString (data) {
Condensed preview — 25 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (119K chars).
[
{
"path": ".airtap.yml",
"chars": 363,
"preview": "sauce_connect: true\nbrowsers:\n - name: firefox\n version: latest\n - name: chrome\n version: latest\n - name: safar"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 482,
"preview": "---\nname: \"🐞 Bug report\"\nabout: Report an issue with this software\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n<!-- THIS IS"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 154,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: ❓ Ask a question\n url: https://discord.gg/CNxFAzdEmr\n about: "
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 422,
"preview": "---\nname: \"⭐️ Feature request\"\nabout: Request a new feature to be added\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n<!-- TH"
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 319,
"preview": "**What is the purpose of this pull request? (put an \"X\" next to item)**\n\n[ ] Documentation update\n[ ] Bug fix\n[ ] New fe"
},
{
"path": ".github/dependabot.yml",
"chars": 287,
"preview": "version: 2\nupdates:\n - package-ecosystem: npm\n directory: /\n schedule:\n interval: daily\n labels:\n - "
},
{
"path": ".github/workflows/ci.yml",
"chars": 700,
"preview": "name: ci\n'on':\n - push\n - pull_request\njobs:\n test:\n name: Node ${{ matrix.node }} / ${{ matrix.os }}\n runs-on:"
},
{
"path": ".gitignore",
"chars": 43,
"preview": ".nyc_output\nnode_modules\npackage-lock.json\n"
},
{
"path": ".npmignore",
"chars": 50,
"preview": ".airtap.yml\n.nyc_output\n.github/\nimg/\nperf/\ntest/\n"
},
{
"path": "LICENSE",
"chars": 1081,
"preview": "The MIT License (MIT)\n\nCopyright (c) Feross Aboukhadijeh\n\nPermission is hereby granted, free of charge, to any person ob"
},
{
"path": "README.md",
"chars": 25683,
"preview": "# simple-peer [![ci][ci-image]][ci-url] [![coveralls][coveralls-image]][coveralls-url] [![npm][npm-image]][npm-url] [![d"
},
{
"path": "index.js",
"chars": 33562,
"preview": "/*! simple-peer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */\nconst debug = require('debug')('sim"
},
{
"path": "package.json",
"chars": 2199,
"preview": "{\n \"name\": \"simple-peer\",\n \"description\": \"Simple one-to-one WebRTC video/voice and data channels\",\n \"version\": \"9.11"
},
{
"path": "perf/receive.js",
"chars": 787,
"preview": "// run in a browser and look at console for speed\n// beefy perf/receive.js\n\n// 7.6MB\n\nconst prettierBytes = require('p"
},
{
"path": "perf/send.js",
"chars": 731,
"preview": "// run in a browser, with:\n// beefy perf/send.js\n\nconst Peer = require('simple-peer')\nconst stream = require('readable"
},
{
"path": "perf/server.js",
"chars": 551,
"preview": "// run in a terminal, to do signaling for peers\n\nconst ws = require('ws')\n\nconst server = new ws.Server({\n port: 8080\n}"
},
{
"path": "test/basic.js",
"chars": 7133,
"preview": "const common = require('./common')\nconst Peer = require('../')\nconst test = require('tape')\n\nlet config\ntest('get config"
},
{
"path": "test/binary.js",
"chars": 3691,
"preview": "const common = require('./common')\nconst Peer = require('../')\nconst test = require('tape')\n\nlet config\ntest('get config"
},
{
"path": "test/common.js",
"chars": 1691,
"preview": "const get = require('simple-get')\nconst thunky = require('thunky')\nconst bowser = require('bowser')\n\nexports.getConfig ="
},
{
"path": "test/multistream.js",
"chars": 12866,
"preview": "const common = require('./common')\nconst Peer = require('../')\nconst test = require('tape')\n\nlet config\ntest('get config"
},
{
"path": "test/negotiation.js",
"chars": 5300,
"preview": "const common = require('./common')\nconst Peer = require('../')\nconst test = require('tape')\n\nlet config\ntest('get config"
},
{
"path": "test/object-mode.js",
"chars": 5056,
"preview": "const common = require('./common')\nconst Peer = require('../')\nconst test = require('tape')\n\nlet config\ntest('get config"
},
{
"path": "test/stream.js",
"chars": 2527,
"preview": "const common = require('./common')\nconst Peer = require('../')\nconst str = require('string-to-stream')\nconst test = requ"
},
{
"path": "test/trickle.js",
"chars": 8068,
"preview": "const common = require('./common')\nconst Peer = require('../')\nconst test = require('tape')\n\nlet config\ntest('get config"
},
{
"path": "test/z-cleanup.js",
"chars": 282,
"preview": "// This test file runs after all the others. This is where we can run the cleanup\n// code that is required\n\nconst test ="
}
]
About this extraction
This page contains the full source code of the feross/simple-peer GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 25 files (111.4 KB), approximately 30.8k tokens, and a symbol index with 62 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.