Skip to content

Commit

Permalink
Add datachannel tests, interop tests. Fix feross#670. Fix feross#721.…
Browse files Browse the repository at this point in the history
… Remove duplicate code.
  • Loading branch information
t-mullen committed Aug 8, 2020
2 parents b8a4ec0 + 12cf1d7 commit 6406610
Show file tree
Hide file tree
Showing 21 changed files with 1,468 additions and 125 deletions.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ addons:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
sauce_connect: true
sauce_connect:
no_ssl_bump_domains: airtap.local
hosts:
- airtap.local
env:
Expand Down
63 changes: 56 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ 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
- `channelName` - custom webrtc data channel name. Must be the same on both peers.
- `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)
Expand Down Expand Up @@ -365,9 +365,9 @@ Add a `RTCRtpTransceiver` to the connection. Can be used to add transceivers bef

### `datachannel = peer.createDataChannel(channelName, channelConfig)`

Used to create additional DataChannel objects. DataChannels are instances of `stream.Duplex`.
Used to create additional `DataChannel` objects. `DataChannel`s are instances of `stream.Duplex`.

Firefox currently [does not support](https://bugzilla.mozilla.org/show_bug.cgi?id=1513107) creating new datachannels after closing any datachannel.
NOTE: Firefox currently [does not support](https://bugzilla.mozilla.org/show_bug.cgi?id=1513107) creating new datachannels after closing any datachannel.

### `peer.destroy([err])`

Expand All @@ -376,6 +376,21 @@ 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.

### `datachannel.send(data)`
Send text/binary data to the remote peer. Similar to `peer.send(data)`.

Note: If this method is called before the `datachannel.on('open')` event has fired, then data
will be buffered.

### `datachannel.end()`
Closes and destroys the `DataChannel` after waiting for it to flush.

### `datachannel.close([err])`
Immediately closes and destroys the DataChannel without waiting for it to flush.

If the optional `err` parameter is passed, then it will be emitted as an `'error'`
event on the DataChannel.

### `Peer.WEBRTC_SUPPORT`

Detect native WebRTC support in the javascript environment.
Expand All @@ -392,7 +407,7 @@ if (Peer.WEBRTC_SUPPORT) {

### duplex stream

`Peer` objects are instances of `stream.Duplex`. They behave very similarly to a
`Peer` and `DataChannel` 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.

Expand Down Expand Up @@ -427,7 +442,7 @@ 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).
Received a message from the remote peer (via the default data channel).

`data` will be either a `String` or a `Buffer/Uint8Array` (see [buffer](https://github.com/feross/buffer)).

Expand All @@ -453,32 +468,66 @@ Received a remote audio/video track. Streams may contain multiple tracks.

### `peer.on('datachannel', function (datachannel) {})`

Received an additional DataChannel. This fires after the remote peer calls `peer.createDataChannel()`.
Received an additional DataChannel. This fires after the remote peer calls `peer.createDataChannel()`. It will not fire for the default DataChannel.

### `peer.on('negotiate', () => {})`

The peer has completed a round of (re)negotiation, but may not be connected yet.

### `peer.on('close', () => {})`

Called when the peer connection has closed.
Fired 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.

### `datachannel.on('open', () => {})`

Fired when the DataChannel has opened and is ready to use.

### `datachannel.on('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)).

### `datachannel.on('close', () => {})`

Fired when the DataChannel has closed.

### `datachannel.on('error', (err) => {})`

Fired when a fatal error occurs on the DataChannel.

`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_PC_CONSTRUCTOR`
- `ERR_DESTROYED`
- `ERR_CREATE_OFFER`
- `ERR_CREATE_ANSWER`
- `ERR_SET_LOCAL_DESCRIPTION`
- `ERR_SET_REMOTE_DESCRIPTION`
- `ERR_ADD_ICE_CANDIDATE`
- `ERR_ADD_TRANSCEIVER`
- `ERR_SENDER_REMOVED`
- `ERR_SENDER_ALREADY_ADDED`
- `ERR_TRACK_NOT_ADDED`
- `ERR_REMOVE_TRACK`
- `ERR_UNSUPPORTED_REPLACETRACK`
- `ERR_ICE_CONNECTION_FAILURE`
- `ERR_ICE_CONNECTION_CLOSED`
- `ERR_SIGNALING`
- `ERR_DATA_CHANNEL`
- `ERR_INVALID_CHANNEL_NAME`
- `ERR_CONNECTION_FAILURE`


Expand Down
58 changes: 32 additions & 26 deletions datachannel.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ var MAX_BUFFERED_AMOUNT = 64 * 1024
var CHANNEL_CLOSING_TIMEOUT = 5 * 1000
var CHANNEL_CLOSE_DELAY = 3 * 1000

function makeError (message, code) {
var err = new Error(message)
function makeError (err, code) {
if (typeof err === 'string') err = new Error(err)
if (err.error instanceof Error) err = err.error
err.code = code
return err
}
Expand All @@ -26,15 +27,17 @@ class DataChannel extends stream.Duplex {

super(opts)

this.closed = false
this._chunk = null
this._cb = null
this._interval = null
this._channel = null
this._fresh = true
this._open = false

this.channelName = opts.channelName || null
this.channelConfig = opts.channelConfig || DataChannel.channelConfig
this.negotiated = this.channelConfig.negotiated
this.channelNegotiated = this.channelConfig.negotiated

// HACK: Chrome will sometimes get stuck in readyState "closing", let's check for this condition
var isClosing = false
Expand Down Expand Up @@ -71,7 +74,7 @@ class DataChannel extends stream.Duplex {
this._onChannelClose()
}
this._channel.onerror = err => {
this.destroy(makeError(err, 'ERR_DATA_CHANNEL'))
this.close(makeError(err, 'ERR_DATA_CHANNEL'))
}

this._onFinishBound = () => {
Expand All @@ -83,13 +86,13 @@ class DataChannel extends stream.Duplex {
_read () { }

_write (chunk, encoding, cb) {
if (this.destroyed) return cb(makeError('cannot write after channel is destroyed', 'ERR_DATA_CHANNEL'))
if (this.closed) return cb(makeError('cannot write after channel is closed', 'ERR_DATA_CHANNEL'))

if (this._channel && this._channel.readyState === 'open') {
try {
this.send(chunk)
} catch (err) {
return this.destroy(makeError(err, 'ERR_DATA_CHANNEL'))
this.close(makeError(err, 'ERR_DATA_CHANNEL'))
}
if (this._channel.bufferedAmount > MAX_BUFFERED_AMOUNT) {
this._debug('start backpressure: bufferedAmount %d', this._channel.bufferedAmount)
Expand All @@ -107,18 +110,18 @@ class DataChannel extends stream.Duplex {
// When stream finishes writing, close socket. Half open connections are not
// supported.
_onFinish () {
if (this.destroyed) return
if (this.closed) return

// Wait a bit before destroying so the socket flushes.
// Wait a bit before closing so the socket flushes.
// TODO: is there a more reliable way to accomplish this?
const destroySoon = () => {
setTimeout(() => this.destroy(), 1000)
const closeSoon = () => {
setTimeout(() => this.close(), 1000)
}

if (this._connected) {
destroySoon()
if (this._open) {
closeSoon()
} else {
this.once('connect', destroySoon)
this.once('open', closeSoon)
}
}

Expand All @@ -130,14 +133,14 @@ class DataChannel extends stream.Duplex {
}

_onChannelMessage (event) {
if (this.destroyed) return
if (this.closed) return
var data = event.data
if (data instanceof ArrayBuffer) data = Buffer.from(data)
this.push(data)
}

_onChannelBufferedAmountLow () {
if (this.destroyed || !this._cb) return
if (this.closed || !this._cb) return
this._debug('ending backpressure: bufferedAmount %d', this._channel.bufferedAmount)
var cb = this._cb
this._cb = null
Expand All @@ -146,6 +149,7 @@ class DataChannel extends stream.Duplex {

_onChannelOpen () {
this._debug('on channel open', this.channelName)
this._open = true
this.emit('open')
this._sendChunk()

Expand All @@ -156,17 +160,17 @@ class DataChannel extends stream.Duplex {

_onChannelClose () {
this._debug('on channel close')
this.destroy()
return this.close()
}

_sendChunk () { // called when peer connects or this._channel set
if (this.destroyed) return
if (this.closed) return

if (this._chunk) {
try {
this.send(this._chunk)
} catch (err) {
return this.destroy(makeError(err, 'ERR_DATA_CHANNEL'))
return this.close(makeError(err, 'ERR_DATA_CHANNEL'))
}
this._chunk = null
this._debug('sent chunk from "write before connect"')
Expand Down Expand Up @@ -196,17 +200,19 @@ class DataChannel extends stream.Duplex {
this._channel.send(chunk)
}

// 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, () => { })
DataChannel.prototype._destroy.call(this, err, () => { })
}

_destroy (err, cb) {
if (this.destroyed) return
this.close(err)
cb()
}

this._debug('destroy datachannel (error: %s)', err && (err.message || err))
close (err) {
if (this.closed) return

this._debug('close datachannel (error: %s)', err && (err.message || err))

if (this._channel) {
if (this._fresh) { // HACK: Safari sometimes cannot close channels immediately after opening them
Expand All @@ -227,7 +233,8 @@ class DataChannel extends stream.Duplex {
if (!this._readableState.ended) this.push(null)
if (!this._writableState.finished) this.end()

this.destroyed = true
this.closed = true
this._open = false

clearInterval(this._closingInterval)
this._closingInterval = null
Expand All @@ -244,7 +251,6 @@ class DataChannel extends stream.Duplex {

if (err) this.emit('error', err)
this.emit('close')
cb()
}

_debug () {
Expand Down
Loading

0 comments on commit 6406610

Please sign in to comment.