From e8add3eb85c0f0e8162e91353e7f9232c7b6f310 Mon Sep 17 00:00:00 2001 From: Feross Aboukhadijeh Date: Tue, 6 Aug 2019 15:24:40 -0700 Subject: [PATCH] BREAKING: Use ES class; drop `inherits` dep Use an ES class so we can drop the `inherits` dependency. All the WebRTC environments we support have supported ES classes for a long time. All usages of `Peer()` must be replaced with `new Peer()` now. This was always the way code examples in the readme have been written. --- index.js | 1708 +++++++++++++++++++++++++------------------------- package.json | 1 - 2 files changed, 837 insertions(+), 872 deletions(-) diff --git a/index.js b/index.js index 3a3ade8e..5d50dce1 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,5 @@ -module.exports = Peer - var debug = require('debug')('simple-peer') var getBrowserRTC = require('get-browser-rtc') -var inherits = require('inherits') var randombytes = require('randombytes') var stream = require('readable-stream') @@ -10,1027 +7,996 @@ var MAX_BUFFERED_AMOUNT = 64 * 1024 var ICECOMPLETE_TIMEOUT = 5 * 1000 var CHANNEL_CLOSING_TIMEOUT = 5 * 1000 -inherits(Peer, stream.Duplex) +// HACK: Filter trickle lines when trickle is disabled #354 +function filterTrickle (sdp) { + return sdp.replace(/a=ice-options:trickle\s\n/g, '') +} + +function makeError (message, code) { + var err = new Error(message) + err.code = code + return err +} + +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 */ -function Peer (opts) { - var self = this - if (!(self instanceof Peer)) return new Peer(opts) - - self._id = randombytes(4).toString('hex').slice(0, 7) - self._debug('new peer %o', opts) - - opts = Object.assign({ - allowHalfOpen: false - }, opts) - - stream.Duplex.call(self, opts) - - self.channelName = opts.initiator - ? opts.channelName || randombytes(20).toString('hex') - : null - - self.initiator = opts.initiator || false - self.channelConfig = opts.channelConfig || Peer.channelConfig - self.config = Object.assign({}, Peer.config, opts.config) - self.offerOptions = opts.offerOptions || {} - self.answerOptions = opts.answerOptions || {} - self.sdpTransform = opts.sdpTransform || function (sdp) { return sdp } - self.streams = opts.streams || (opts.stream ? [opts.stream] : []) // support old "stream" option - self.trickle = opts.trickle !== undefined ? opts.trickle : true - self.allowHalfTrickle = opts.allowHalfTrickle !== undefined ? opts.allowHalfTrickle : false - self.iceCompleteTimeout = opts.iceCompleteTimeout || ICECOMPLETE_TIMEOUT - - self.destroyed = false - self._connected = false - - self.remoteAddress = undefined - self.remoteFamily = undefined - self.remotePort = undefined - self.localAddress = undefined - self.localFamily = undefined - self.localPort = undefined - - self._wrtc = (opts.wrtc && typeof opts.wrtc === 'object') - ? opts.wrtc - : getBrowserRTC() - - if (!self._wrtc) { - if (typeof window === 'undefined') { - throw makeError('No WebRTC support: Specify `opts.wrtc` option in this environment', 'ERR_WEBRTC_SUPPORT') - } else { - throw makeError('No WebRTC support: Not a supported browser', 'ERR_WEBRTC_SUPPORT') +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.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._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 makeError('No WebRTC support: Specify `opts.wrtc` option in this environment', 'ERR_WEBRTC_SUPPORT') + } else { + throw makeError('No WebRTC support: Not a supported browser', 'ERR_WEBRTC_SUPPORT') + } } - } - self._pcReady = false - self._channelReady = false - self._iceComplete = false // ice candidate trickle done (got null candidate) - self._iceCompleteTimer = null // send an offer/answer anyway after some timeout - self._channel = null - self._pendingCandidates = [] - - self._isNegotiating = !self.initiator // is this peer waiting for negotiation to complete? - self._batchedNegotiation = false // batch synchronous negotiations - self._queuedNegotiation = false // is there a queued negotiation request? - self._sendersAwaitingStable = [] - self._senderMap = new Map() - self._firstStable = true - self._closingInterval = null - - self._remoteTracks = [] - self._remoteStreams = [] - - self._chunk = null - self._cb = null - self._interval = null - - try { - self._pc = new (self._wrtc.RTCPeerConnection)(self.config) - } catch (err) { - setTimeout(() => self.destroy(makeError(err, 'ERR_PC_CONSTRUCTOR')), 0) - return - } + 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 = [] - // We prefer feature detection whenever possible, but sometimes that's not - // possible for certain implementations. - self._isReactNativeWebrtc = typeof self._pc._peerConnectionId === 'number' + this._isNegotiating = !this.initiator // is this peer waiting for negotiation to complete? + this._batchedNegotiation = false // batch synchronous negotiations + this._queuedNegotiation = false // is there a queued negotiation request? + this._sendersAwaitingStable = [] + this._senderMap = new Map() + this._firstStable = true + this._closingInterval = null - self._pc.oniceconnectionstatechange = function () { - self._onIceStateChange() - } - self._pc.onicegatheringstatechange = function () { - self._onIceStateChange() - } - self._pc.onsignalingstatechange = function () { - self._onSignalingStateChange() - } - self._pc.onicecandidate = function (event) { - self._onIceCandidate(event) - } + this._remoteTracks = [] + this._remoteStreams = [] - // Other spec events, unused by this implementation: - // - onconnectionstatechange - // - onicecandidateerror - // - onfingerprintfailure - // - onnegotiationneeded + this._chunk = null + this._cb = null + this._interval = null - if (self.initiator) { - self._setupData({ - channel: self._pc.createDataChannel(self.channelName, self.channelConfig) - }) - } else { - self._pc.ondatachannel = function (event) { - self._setupData(event) + try { + this._pc = new (this._wrtc.RTCPeerConnection)(this.config) + } catch (err) { + setTimeout(() => this.destroy(makeError(err, 'ERR_PC_CONSTRUCTOR')), 0) + return } - } - if (self.streams) { - self.streams.forEach(function (stream) { - self.addStream(stream) - }) - } - self._pc.ontrack = function (event) { - self._onTrack(event) - } + // We prefer feature detection whenever possible, but sometimes that's not + // possible for certain implementations. + this._isReactNativeWebrtc = typeof this._pc._peerConnectionId === 'number' - if (self.initiator) { - self._needsNegotiation() - } + this._pc.oniceconnectionstatechange = () => { + this._onIceStateChange() + } + this._pc.onicegatheringstatechange = () => { + this._onIceStateChange() + } + this._pc.onsignalingstatechange = () => { + this._onSignalingStateChange() + } + this._pc.onicecandidate = event => { + this._onIceCandidate(event) + } - self._onFinishBound = function () { - self._onFinish() - } - self.once('finish', self._onFinishBound) -} + // Other spec events, unused by this implementation: + // - onconnectionstatechange + // - onicecandidateerror + // - onfingerprintfailure + // - onnegotiationneeded -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' - }, - { - urls: 'stun:global.stun.twilio.com:3478?transport=udp' + if (this.initiator) { + this._setupData({ + channel: this._pc.createDataChannel(this.channelName, this.channelConfig) + }) + } else { + this._pc.ondatachannel = event => { + this._setupData(event) + } } - ], - sdpSemantics: 'unified-plan' -} -Peer.channelConfig = {} -Object.defineProperty(Peer.prototype, 'bufferSize', { - get: function () { - var self = this - return (self._channel && self._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 -Object.defineProperty(Peer.prototype, 'connected', { - get: function () { - var self = this - return (self._connected && self._channel.readyState === 'open') - } -}) - -Peer.prototype.address = function () { - var self = this - return { port: self.localPort, family: self.localFamily, address: self.localAddress } -} + if (this.streams) { + this.streams.forEach(stream => { + this.addStream(stream) + }) + } + this._pc.ontrack = event => { + this._onTrack(event) + } -Peer.prototype.signal = function (data) { - var self = this - if (self.destroyed) throw makeError('cannot signal after peer is destroyed', 'ERR_SIGNALING') - if (typeof data === 'string') { - try { - data = JSON.parse(data) - } catch (err) { - data = {} + if (this.initiator) { + this._needsNegotiation() } - } - self._debug('signal()') - if (data.renegotiate && self.initiator) { - self._debug('got request to renegotiate') - self._needsNegotiation() - } - if (data.transceiverRequest && self.initiator) { - self._debug('got request for transceiver') - self.addTransceiver(data.transceiverRequest.kind, data.transceiverRequest.init) - } - if (data.candidate) { - if (self._pc.localDescription && self._pc.localDescription.type && self._pc.remoteDescription && self._pc.remoteDescription.type) { - self._addIceCandidate(data.candidate) - } else { - self._pendingCandidates.push(data.candidate) + this._onFinishBound = () => { + this._onFinish() } + this.once('finish', this._onFinishBound) } - if (data.sdp) { - self._pc.setRemoteDescription(new (self._wrtc.RTCSessionDescription)(data)).then(function () { - if (self.destroyed) return - self._pendingCandidates.forEach(function (candidate) { - self._addIceCandidate(candidate) - }) - self._pendingCandidates = [] + get bufferSize () { + return (this._channel && this._channel.bufferedAmount) || 0 + } - if (self._pc.remoteDescription.type === 'offer') self._createAnswer() - }).catch(function (err) { self.destroy(makeError(err, 'ERR_SET_REMOTE_DESCRIPTION')) }) + // 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') } - if (!data.sdp && !data.candidate && !data.renegotiate && !data.transceiverRequest) { - self.destroy(makeError('signal() called with invalid signal data', 'ERR_SIGNALING')) + + address () { + return { port: this.localPort, family: this.localFamily, address: this.localAddress } } -} -Peer.prototype._addIceCandidate = function (candidate) { - var self = this - var iceCandidateObj = new self._wrtc.RTCIceCandidate(candidate) - self._pc.addIceCandidate(iceCandidateObj).catch(function (err) { - if (!iceCandidateObj.address || iceCandidateObj.address.endsWith('.local')) { - warn('Ignoring unsupported ICE candidate.') - } else { - self.destroy(makeError(err, 'ERR_ADD_ICE_CANDIDATE')) + signal (data) { + if (this.destroyed) throw makeError('cannot signal after peer is destroyed', 'ERR_SIGNALING') + if (typeof data === 'string') { + try { + data = JSON.parse(data) + } catch (err) { + data = {} + } } - }) -} - -/** - * Send text/binary data to the remote peer. - * @param {ArrayBufferView|ArrayBuffer|Buffer|string|Blob} chunk - */ -Peer.prototype.send = function (chunk) { - var self = this - self._channel.send(chunk) -} + this._debug('signal()') -/** - * Add a Transceiver to the connection. - * @param {String} kind - * @param {Object} init - */ -Peer.prototype.addTransceiver = function (kind, init) { - var self = this + 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.localDescription && this._pc.localDescription.type && 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 - self._debug('addTransceiver()') + this._pendingCandidates.forEach(candidate => { + this._addIceCandidate(candidate) + }) + this._pendingCandidates = [] - if (self.initiator) { - try { - self._pc.addTransceiver(kind, init) - self._needsNegotiation() - } catch (err) { - self.destroy(makeError(err, 'ERR_ADD_TRANSCEIVER')) + if (this._pc.remoteDescription.type === 'offer') this._createAnswer() + }) + .catch(err => { + this.destroy(makeError(err, 'ERR_SET_REMOTE_DESCRIPTION')) + }) + } + if (!data.sdp && !data.candidate && !data.renegotiate && !data.transceiverRequest) { + this.destroy(makeError('signal() called with invalid signal data', 'ERR_SIGNALING')) } - } else { - self.emit('signal', { // request initiator to renegotiate - transceiverRequest: { kind, init } - }) } -} -/** - * Add a MediaStream to the connection. - * @param {MediaStream} stream - */ -Peer.prototype.addStream = function (stream) { - var self = this - - self._debug('addStream()') - - stream.getTracks().forEach(function (track) { - self.addTrack(track, stream) - }) -} - -/** - * Add a MediaStreamTrack to the connection. - * @param {MediaStreamTrack} track - * @param {MediaStream} stream - */ -Peer.prototype.addTrack = function (track, stream) { - var self = this - - self._debug('addTrack()') - - var submap = self._senderMap.get(track) || new Map() // nested Maps map [track, stream] to sender - var sender = submap.get(stream) - if (!sender) { - sender = self._pc.addTrack(track, stream) - submap.set(stream, sender) - self._senderMap.set(track, submap) - self._needsNegotiation() - } else if (sender.removed) { - throw makeError('Track has been removed. You should enable/disable tracks that you want to re-add.', 'ERR_SENDER_REMOVED') - } else { - throw makeError('Track has already been added to that stream.', 'ERR_SENDER_ALREADY_ADDED') + _addIceCandidate (candidate) { + var 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(makeError(err, 'ERR_ADD_ICE_CANDIDATE')) + } + }) } -} -/** - * Replace a MediaStreamTrack by another in the connection. - * @param {MediaStreamTrack} oldTrack - * @param {MediaStreamTrack} newTrack - * @param {MediaStream} stream - */ -Peer.prototype.replaceTrack = function (oldTrack, newTrack, stream) { - var self = this + /** + * Send text/binary data to the remote peer. + * @param {ArrayBufferView|ArrayBuffer|Buffer|string|Blob} chunk + */ + send (chunk) { + this._channel.send(chunk) + } + + /** + * Add a Transceiver to the connection. + * @param {String} kind + * @param {Object} init + */ + addTransceiver (kind, init) { + this._debug('addTransceiver()') + + if (this.initiator) { + try { + this._pc.addTransceiver(kind, init) + this._needsNegotiation() + } catch (err) { + this.destroy(makeError(err, 'ERR_ADD_TRANSCEIVER')) + } + } else { + this.emit('signal', { // request initiator to renegotiate + transceiverRequest: { kind, init } + }) + } + } - self._debug('replaceTrack()') + /** + * Add a MediaStream to the connection. + * @param {MediaStream} stream + */ + addStream (stream) { + this._debug('addStream()') - var submap = self._senderMap.get(oldTrack) - var sender = submap ? submap.get(stream) : null - if (!sender) { - throw makeError('Cannot replace track that was never added.', 'ERR_TRACK_NOT_ADDED') + stream.getTracks().forEach(track => { + this.addTrack(track, stream) + }) } - if (newTrack) self._senderMap.set(newTrack, submap) - if (sender.replaceTrack != null) { - sender.replaceTrack(newTrack) - } else { - self.destroy(makeError('replaceTrack is not supported in this browser', 'ERR_UNSUPPORTED_REPLACETRACK')) + /** + * Add a MediaStreamTrack to the connection. + * @param {MediaStreamTrack} track + * @param {MediaStream} stream + */ + addTrack (track, stream) { + this._debug('addTrack()') + + var submap = this._senderMap.get(track) || new Map() // nested Maps map [track, stream] to sender + var 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 makeError('Track has been removed. You should enable/disable tracks that you want to re-add.', 'ERR_SENDER_REMOVED') + } else { + throw makeError('Track has already been added to that stream.', 'ERR_SENDER_ALREADY_ADDED') + } } -} -/** - * Remove a MediaStreamTrack from the connection. - * @param {MediaStreamTrack} track - * @param {MediaStream} stream - */ -Peer.prototype.removeTrack = function (track, stream) { - var self = this + /** + * Replace a MediaStreamTrack by another in the connection. + * @param {MediaStreamTrack} oldTrack + * @param {MediaStreamTrack} newTrack + * @param {MediaStream} stream + */ + replaceTrack (oldTrack, newTrack, stream) { + this._debug('replaceTrack()') - self._debug('removeSender()') + var submap = this._senderMap.get(oldTrack) + var sender = submap ? submap.get(stream) : null + if (!sender) { + throw makeError('Cannot replace track that was never added.', 'ERR_TRACK_NOT_ADDED') + } + if (newTrack) this._senderMap.set(newTrack, submap) - var submap = self._senderMap.get(track) - var sender = submap ? submap.get(stream) : null - if (!sender) { - throw makeError('Cannot remove track that was never added.', 'ERR_TRACK_NOT_ADDED') - } - try { - sender.removed = true - self._pc.removeTrack(sender) - } catch (err) { - if (err.name === 'NS_ERROR_UNEXPECTED') { - self._sendersAwaitingStable.push(sender) // HACK: Firefox must wait until (signalingState === stable) https://bugzilla.mozilla.org/show_bug.cgi?id=1133874 + if (sender.replaceTrack != null) { + sender.replaceTrack(newTrack) } else { - self.destroy(makeError(err, 'ERR_REMOVE_TRACK')) + this.destroy(makeError('replaceTrack is not supported in this browser', 'ERR_UNSUPPORTED_REPLACETRACK')) } } - self._needsNegotiation() -} -/** - * Remove a MediaStream from the connection. - * @param {MediaStream} stream - */ -Peer.prototype.removeStream = function (stream) { - var self = this + /** + * Remove a MediaStreamTrack from the connection. + * @param {MediaStreamTrack} track + * @param {MediaStream} stream + */ + removeTrack (track, stream) { + this._debug('removeSender()') - self._debug('removeSenders()') + var submap = this._senderMap.get(track) + var sender = submap ? submap.get(stream) : null + if (!sender) { + throw makeError('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(makeError(err, 'ERR_REMOVE_TRACK')) + } + } + this._needsNegotiation() + } - stream.getTracks().forEach(function (track) { - self.removeTrack(track, stream) - }) -} + /** + * Remove a MediaStream from the connection. + * @param {MediaStream} stream + */ + removeStream (stream) { + this._debug('removeSenders()') -Peer.prototype._needsNegotiation = function () { - var self = this - - self._debug('_needsNegotiation') - if (self._batchedNegotiation) return // batch synchronous renegotiations - self._batchedNegotiation = true - setTimeout(function () { - self._batchedNegotiation = false - self._debug('starting batched negotiation') - self.negotiate() - }, 0) -} + stream.getTracks().forEach(track => { + this.removeTrack(track, stream) + }) + } -Peer.prototype.negotiate = function () { - var self = this + _needsNegotiation () { + this._debug('_needsNegotiation') + if (this._batchedNegotiation) return // batch synchronous renegotiations + this._batchedNegotiation = true + setTimeout(() => { + this._batchedNegotiation = false + this._debug('starting batched negotiation') + this.negotiate() + }, 0) + } - if (self.initiator) { - if (self._isNegotiating) { - self._queuedNegotiation = true - self._debug('already negotiating, queueing') + negotiate () { + 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 { - self._debug('start negotiation') - setTimeout(() => { // HACK: Chrome crashes if we immediately call createOffer - self._createOffer() - }, 0) - } - } else { - if (!self._isNegotiating) { - self._debug('requesting negotiation from initiator') - self.emit('signal', { // request initiator to renegotiate - renegotiate: true - }) + if (!this._isNegotiating) { + this._debug('requesting negotiation from initiator') + this.emit('signal', { // request initiator to renegotiate + renegotiate: true + }) + } } + this._isNegotiating = true } - self._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 -Peer.prototype.destroy = function (err) { - var self = this - self._destroy(err, function () {}) -} + // 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, () => {}) + } -Peer.prototype._destroy = function (err, cb) { - var self = this - if (self.destroyed) return + _destroy (err, cb) { + if (this.destroyed) return - self._debug('destroy (error: %s)', err && (err.message || err)) + this._debug('destroy (error: %s)', err && (err.message || err)) - self.readable = self.writable = false + this.readable = this.writable = false - if (!self._readableState.ended) self.push(null) - if (!self._writableState.finished) self.end() + if (!this._readableState.ended) this.push(null) + if (!this._writableState.finished) this.end() - self.destroyed = true - self._connected = false - self._pcReady = false - self._channelReady = false - self._remoteTracks = null - self._remoteStreams = null - self._senderMap = null + this.destroyed = true + this._connected = false + this._pcReady = false + this._channelReady = false + this._remoteTracks = null + this._remoteStreams = null + this._senderMap = null - clearInterval(self._closingInterval) - self._closingInterval = null + clearInterval(this._closingInterval) + this._closingInterval = null - clearInterval(self._interval) - self._interval = null - self._chunk = null - self._cb = null + clearInterval(this._interval) + this._interval = null + this._chunk = null + this._cb = null - if (self._onFinishBound) self.removeListener('finish', self._onFinishBound) - self._onFinishBound = null + if (this._onFinishBound) this.removeListener('finish', this._onFinishBound) + this._onFinishBound = null - if (self._channel) { - try { - self._channel.close() - } catch (err) {} + if (this._channel) { + try { + this._channel.close() + } catch (err) {} - self._channel.onmessage = null - self._channel.onopen = null - self._channel.onclose = null - self._channel.onerror = null - } - if (self._pc) { - try { - self._pc.close() - } catch (err) {} - - self._pc.oniceconnectionstatechange = null - self._pc.onicegatheringstatechange = null - self._pc.onsignalingstatechange = null - self._pc.onicecandidate = null - self._pc.ontrack = null - self._pc.ondatachannel = null + 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) {} + + 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() } - self._pc = null - self._channel = null - if (err) self.emit('error', err) - self.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(makeError('Data channel event is missing `channel` property', 'ERR_DATA_CHANNEL')) + } -Peer.prototype._setupData = function (event) { - var self = this - 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 self.destroy(makeError('Data channel event is missing `channel` property', 'ERR_DATA_CHANNEL')) - } + this._channel = event.channel + this._channel.binaryType = 'arraybuffer' - self._channel = event.channel - self._channel.binaryType = 'arraybuffer' + if (typeof this._channel.bufferedAmountLowThreshold === 'number') { + this._channel.bufferedAmountLowThreshold = MAX_BUFFERED_AMOUNT + } - if (typeof self._channel.bufferedAmountLowThreshold === 'number') { - self._channel.bufferedAmountLowThreshold = MAX_BUFFERED_AMOUNT - } + this.channelName = this._channel.label - self.channelName = self._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 = err => { + this.destroy(makeError(err, 'ERR_DATA_CHANNEL')) + } - self._channel.onmessage = function (event) { - self._onChannelMessage(event) - } - self._channel.onbufferedamountlow = function () { - self._onChannelBufferedAmountLow() - } - self._channel.onopen = function () { - self._onChannelOpen() - } - self._channel.onclose = function () { - self._onChannelClose() - } - self._channel.onerror = function (err) { - self.destroy(makeError(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 + var 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) } - // 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 - var isClosing = false - self._closingInterval = setInterval(function () { // No "onclosing" event - if (self._channel && self._channel.readyState === 'closing') { - if (isClosing) self._onChannelClose() // closing timed out: equivalent to onclose firing - isClosing = true + _read () {} + + _write (chunk, encoding, cb) { + if (this.destroyed) return cb(makeError('cannot write after peer is destroyed', 'ERR_DATA_CHANNEL')) + + if (this._connected) { + try { + this.send(chunk) + } catch (err) { + return this.destroy(makeError(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 { - isClosing = false + this._debug('write before connect') + this._chunk = chunk + this._cb = cb } - }, CHANNEL_CLOSING_TIMEOUT) -} - -Peer.prototype._read = function () {} + } -Peer.prototype._write = function (chunk, encoding, cb) { - var self = this - if (self.destroyed) return cb(makeError('cannot write after peer is destroyed', 'ERR_DATA_CHANNEL')) + // When stream finishes writing, close socket. Half open connections are not + // supported. + _onFinish () { + if (this.destroyed) return - if (self._connected) { - try { - self.send(chunk) - } catch (err) { - return self.destroy(makeError(err, 'ERR_DATA_CHANNEL')) + // 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 (self._channel.bufferedAmount > MAX_BUFFERED_AMOUNT) { - self._debug('start backpressure: bufferedAmount %d', self._channel.bufferedAmount) - self._cb = cb + + if (this._connected) { + destroySoon() } else { - cb(null) + this.once('connect', destroySoon) } - } else { - self._debug('write before connect') - self._chunk = chunk - self._cb = cb } -} -// When stream finishes writing, close socket. Half open connections are not -// supported. -Peer.prototype._onFinish = function () { - var self = this - if (self.destroyed) return + _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 + var signal = this._pc.localDescription || offer + this._debug('signal') + this.emit('signal', { + type: signal.type, + sdp: signal.sdp + }) + } - if (self._connected) { - destroySoon() - } else { - self.once('connect', destroySoon) - } + const onSuccess = () => { + this._debug('createOffer success') + if (this.destroyed) return + if (this.trickle || this._iceComplete) sendOffer() + else this.once('_iceComplete', sendOffer) // wait for candidates + } - // Wait a bit before destroying so the socket flushes. - // TODO: is there a more reliable way to accomplish this? - function destroySoon () { - setTimeout(function () { - self.destroy() - }, 1000) - } -} + const onError = err => { + this.destroy(makeError(err, 'ERR_SET_LOCAL_DESCRIPTION')) + } -Peer.prototype._startIceCompleteTimeout = function () { - var self = this - if (self.destroyed) return - if (self._iceCompleteTimer) return - self._debug('started iceComplete timeout') - self._iceCompleteTimer = setTimeout(function () { - if (!self._iceComplete) { - self._iceComplete = true - self._debug('iceComplete timeout completed') - self.emit('iceTimeout') - self.emit('_iceComplete') - } - }, self.iceCompleteTimeout) -} + this._pc.setLocalDescription(offer) + .then(onSuccess) + .catch(onError) + }) + .catch(err => { + this.destroy(makeError(err, 'ERR_CREATE_OFFER')) + }) + } -Peer.prototype._createOffer = function () { - var self = this - if (self.destroyed) return - - self._pc.createOffer(self.offerOptions).then(function (offer) { - if (self.destroyed) return - if (!self.trickle && !self.allowHalfTrickle) offer.sdp = filterTrickle(offer.sdp) - offer.sdp = self.sdpTransform(offer.sdp) - self._pc.setLocalDescription(offer).then(onSuccess).catch(onError) - - function onSuccess () { - self._debug('createOffer success') - if (self.destroyed) return - if (self.trickle || self._iceComplete) sendOffer() - else self.once('_iceComplete', sendOffer) // wait for candidates + _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) + } + }) } + } - function onError (err) { - self.destroy(makeError(err, 'ERR_SET_LOCAL_DESCRIPTION')) - } + _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 + var signal = this._pc.localDescription || answer + this._debug('signal') + this.emit('signal', { + type: signal.type, + sdp: signal.sdp + }) + if (!this.initiator) this._requestMissingTransceivers() + } - function sendOffer () { - if (self.destroyed) return - var signal = self._pc.localDescription || offer - self._debug('signal') - self.emit('signal', { - type: signal.type, - sdp: signal.sdp - }) - } - }).catch(function (err) { self.destroy(makeError(err, 'ERR_CREATE_OFFER')) }) -} + const onSuccess = () => { + if (this.destroyed) return + if (this.trickle || this._iceComplete) sendAnswer() + else this.once('_iceComplete', sendAnswer) + } -Peer.prototype._requestMissingTransceivers = function () { - var self = this + const onError = err => { + this.destroy(makeError(err, 'ERR_SET_LOCAL_DESCRIPTION')) + } - if (self._pc.getTransceivers) { - self._pc.getTransceivers().forEach(transceiver => { - if (!transceiver.mid && transceiver.sender.track && !transceiver.requested) { - transceiver.requested = true // HACK: Safari returns negotiated transceivers with a null mid - self.addTransceiver(transceiver.sender.track.kind) - } - }) + this._pc.setLocalDescription(answer) + .then(onSuccess) + .catch(onError) + }) + .catch(err => { + this.destroy(makeError(err, 'ERR_CREATE_ANSWER')) + }) } -} -Peer.prototype._createAnswer = function () { - var self = this - if (self.destroyed) return + _onIceStateChange () { + if (this.destroyed) return + var iceConnectionState = this._pc.iceConnectionState + var iceGatheringState = this._pc.iceGatheringState - self._pc.createAnswer(self.answerOptions).then(function (answer) { - if (self.destroyed) return - if (!self.trickle && !self.allowHalfTrickle) answer.sdp = filterTrickle(answer.sdp) - answer.sdp = self.sdpTransform(answer.sdp) - self._pc.setLocalDescription(answer).then(onSuccess).catch(onError) + this._debug( + 'iceStateChange (connection: %s) (gathering: %s)', + iceConnectionState, + iceGatheringState + ) + this.emit('iceStateChange', iceConnectionState, iceGatheringState) - function onSuccess () { - if (self.destroyed) return - if (self.trickle || self._iceComplete) sendAnswer() - else self.once('_iceComplete', sendAnswer) + if (iceConnectionState === 'connected' || iceConnectionState === 'completed') { + this._pcReady = true + this._maybeReady() } - - function onError (err) { - self.destroy(makeError(err, 'ERR_SET_LOCAL_DESCRIPTION')) + if (iceConnectionState === 'failed') { + this.destroy(makeError('Ice connection failed.', 'ERR_ICE_CONNECTION_FAILURE')) } - - function sendAnswer () { - if (self.destroyed) return - var signal = self._pc.localDescription || answer - self._debug('signal') - self.emit('signal', { - type: signal.type, - sdp: signal.sdp - }) - if (!self.initiator) self._requestMissingTransceivers() + if (iceConnectionState === 'closed') { + this.destroy(makeError('Ice connection closed.', 'ERR_ICE_CONNECTION_CLOSED')) } - }).catch(function (err) { self.destroy(makeError(err, 'ERR_CREATE_ANSWER')) }) -} - -Peer.prototype._onIceStateChange = function () { - var self = this - if (self.destroyed) return - var iceConnectionState = self._pc.iceConnectionState - var iceGatheringState = self._pc.iceGatheringState - - self._debug( - 'iceStateChange (connection: %s) (gathering: %s)', - iceConnectionState, - iceGatheringState - ) - self.emit('iceStateChange', iceConnectionState, iceGatheringState) - - if (iceConnectionState === 'connected' || iceConnectionState === 'completed') { - self._pcReady = true - self._maybeReady() - } - if (iceConnectionState === 'failed') { - self.destroy(makeError('Ice connection failed.', 'ERR_ICE_CONNECTION_FAILURE')) - } - if (iceConnectionState === 'closed') { - self.destroy(makeError('Ice connection closed.', 'ERR_ICE_CONNECTION_CLOSED')) } -} - -Peer.prototype.getStats = function (cb) { - var self = this - // Promise-based getStats() (standard) - if (self._pc.getStats.length === 0) { - self._pc.getStats().then(function (res) { - var reports = [] - res.forEach(function (report) { - reports.push(flattenValues(report)) - }) - cb(null, reports) - }, function (err) { cb(err) }) - - // Two-parameter callback-based getStats() (deprecated, former standard) - } else if (self._isReactNativeWebrtc) { - self._pc.getStats(null, function (res) { - var reports = [] - res.forEach(function (report) { - reports.push(flattenValues(report)) - }) - cb(null, reports) - }, function (err) { cb(err) }) - - // Single-parameter callback-based getStats() (non-standard) - } else if (self._pc.getStats.length > 0) { - self._pc.getStats(function (res) { - // If we destroy connection in `connect` callback this code might happen to run when actual connection is already closed - if (self.destroyed) return - - var reports = [] - res.result().forEach(function (result) { - var report = {} - result.names().forEach(function (name) { - report[name] = result.stat(name) + 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) }) - report.id = result.id - report.type = result.type - report.timestamp = result.timestamp - reports.push(flattenValues(report)) - }) - cb(null, reports) - }, function (err) { cb(err) }) + } + return report + } - // Unknown browser, skip getStats() since it's anyone's guess which style of - // getStats() they implement. - } else { - cb(null, []) - } + // Promise-based getStats() (standard) + if (this._pc.getStats.length === 0) { + this._pc.getStats() + .then(res => { + var reports = [] + res.forEach(report => { + reports.push(flattenValues(report)) + }) + cb(null, reports) + }, err => cb(err)) + + // Two-parameter callback-based getStats() (deprecated, former standard) + } else if (this._isReactNativeWebrtc) { + this._pc.getStats(null, res => { + var 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 + + var reports = [] + res.result().forEach(result => { + var 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)) - // statreports can come with a value array instead of properties - function flattenValues (report) { - if (Object.prototype.toString.call(report.values) === '[object Array]') { - report.values.forEach(function (value) { - Object.assign(report, value) - }) + // Unknown browser, skip getStats() since it's anyone's guess which style of + // getStats() they implement. + } else { + cb(null, []) } - return report } -} -Peer.prototype._maybeReady = function () { - var self = this - self._debug('maybeReady pc %s channel %s', self._pcReady, self._channelReady) - if (self._connected || self._connecting || !self._pcReady || !self._channelReady) return + _maybeReady () { + this._debug('maybeReady pc %s channel %s', this._pcReady, this._channelReady) + if (this._connected || this._connecting || !this._pcReady || !this._channelReady) return - self._connecting = true + this._connecting = true - // HACK: We can't rely on order here, for details see https://github.com/js-platform/node-webrtc/issues/339 - function findCandidatePair () { - if (self.destroyed) return + // 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 - self.getStats(function (err, items) { - if (self.destroyed) return + this.getStats((err, items) => { + if (this.destroyed) return - // Treat getStats error as non-fatal. It's not essential. - if (err) items = [] + // Treat getStats error as non-fatal. It's not essential. + if (err) items = [] - var remoteCandidates = {} - var localCandidates = {} - var candidatePairs = {} - var foundSelectedCandidatePair = false + var remoteCandidates = {} + var localCandidates = {} + var candidatePairs = {} + var foundSelectedCandidatePair = false - items.forEach(function (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 - } - }) + 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 + } + }) - items.forEach(function (item) { - // Spec-compliant - if (item.type === 'transport' && item.selectedCandidatePairId) { - setSelectedCandidatePair(candidatePairs[item.selectedCandidatePairId]) + const setSelectedCandidatePair = selectedCandidatePair => { + foundSelectedCandidatePair = true + + var 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' + } + + var 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 + ) } - // Old implementations - if ( - (item.type === 'googCandidatePair' && item.googActiveConnection === 'true') || - ((item.type === 'candidatepair' || item.type === 'candidate-pair') && item.selected) - ) { - setSelectedCandidatePair(item) - } - }) + 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) + } + }) - function setSelectedCandidatePair (selectedCandidatePair) { - foundSelectedCandidatePair = true - - var local = localCandidates[selectedCandidatePair.localCandidateId] - - if (local && (local.ip || local.address)) { - // Spec - self.localAddress = local.ip || local.address - self.localPort = Number(local.port) - } else if (local && local.ipAddress) { - // Firefox - self.localAddress = local.ipAddress - self.localPort = Number(local.portNumber) - } else if (typeof selectedCandidatePair.googLocalAddress === 'string') { - // TODO: remove this once Chrome 58 is released - local = selectedCandidatePair.googLocalAddress.split(':') - self.localAddress = local[0] - self.localPort = Number(local[1]) - } - if (self.localAddress) { - self.localFamily = self.localAddress.includes(':') ? 'IPv6' : 'IPv4' + // 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 } - var remote = remoteCandidates[selectedCandidatePair.remoteCandidateId] - - if (remote && (remote.ip || remote.address)) { - // Spec - self.remoteAddress = remote.ip || remote.address - self.remotePort = Number(remote.port) - } else if (remote && remote.ipAddress) { - // Firefox - self.remoteAddress = remote.ipAddress - self.remotePort = Number(remote.portNumber) - } else if (typeof selectedCandidatePair.googRemoteAddress === 'string') { - // TODO: remove this once Chrome 58 is released - remote = selectedCandidatePair.googRemoteAddress.split(':') - self.remoteAddress = remote[0] - self.remotePort = Number(remote[1]) - } - if (self.remoteAddress) { - self.remoteFamily = self.remoteAddress.includes(':') ? 'IPv6' : 'IPv4' + if (this._chunk) { + try { + this.send(this._chunk) + } catch (err) { + return this.destroy(makeError(err, 'ERR_DATA_CHANNEL')) + } + this._chunk = null + this._debug('sent chunk from "write before connect"') + + var cb = this._cb + this._cb = null + cb(null) } - self._debug( - 'connect local: %s:%s remote: %s:%s', - self.localAddress, self.localPort, self.remoteAddress, self.remotePort - ) - } - - // 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 { - self._connecting = false - self._connected = true - } - - if (self._chunk) { - try { - self.send(self._chunk) - } catch (err) { - return self.destroy(makeError(err, 'ERR_DATA_CHANNEL')) + // 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() } - self._chunk = null - self._debug('sent chunk from "write before connect"') - - var cb = self._cb - self._cb = null - cb(null) - } - // If `bufferedAmountLowThreshold` and 'onbufferedamountlow' are unsupported, - // fallback to using setInterval to implement backpressure. - if (typeof self._channel.bufferedAmountLowThreshold !== 'number') { - self._interval = setInterval(function () { self._onInterval() }, 150) - if (self._interval.unref) self._interval.unref() - } - - self._debug('connect') - self.emit('connect') - }) + this._debug('connect') + this.emit('connect') + }) + } + findCandidatePair() } - findCandidatePair() -} -Peer.prototype._onInterval = function () { - var self = this - if (!self._cb || !self._channel || self._channel.bufferedAmount > MAX_BUFFERED_AMOUNT) { - return + _onInterval () { + if (!this._cb || !this._channel || this._channel.bufferedAmount > MAX_BUFFERED_AMOUNT) { + return + } + this._onChannelBufferedAmountLow() } - self._onChannelBufferedAmountLow() -} -Peer.prototype._onSignalingStateChange = function () { - var self = this - if (self.destroyed) return + _onSignalingStateChange () { + if (this.destroyed) return - if (self._pc.signalingState === 'stable' && !self._firstStable) { - self._isNegotiating = false + if (this._pc.signalingState === 'stable' && !this._firstStable) { + this._isNegotiating = false - // HACK: Firefox doesn't yet support removing tracks when signalingState !== 'stable' - self._debug('flushing sender queue', self._sendersAwaitingStable) - self._sendersAwaitingStable.forEach(function (sender) { - self._pc.removeTrack(sender) - self._queuedNegotiation = true - }) - self._sendersAwaitingStable = [] + // 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 (self._queuedNegotiation) { - self._debug('flushing negotiation queue') - self._queuedNegotiation = false - self._needsNegotiation() // negotiate again + if (this._queuedNegotiation) { + this._debug('flushing negotiation queue') + this._queuedNegotiation = false + this._needsNegotiation() // negotiate again + } + + this._debug('negotiate') + this.emit('negotiate') } + this._firstStable = false - self._debug('negotiate') - self.emit('negotiate') + this._debug('signalingStateChange %s', this._pc.signalingState) + this.emit('signalingStateChange', this._pc.signalingState) } - self._firstStable = false - self._debug('signalingStateChange %s', self._pc.signalingState) - self.emit('signalingStateChange', self._pc.signalingState) -} + _onIceCandidate (event) { + if (this.destroyed) return + if (event.candidate && this.trickle) { + this.emit('signal', { + 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() + } + } -Peer.prototype._onIceCandidate = function (event) { - var self = this - if (self.destroyed) return - if (event.candidate && self.trickle) { - self.emit('signal', { - candidate: { - candidate: event.candidate.candidate, - sdpMLineIndex: event.candidate.sdpMLineIndex, - sdpMid: event.candidate.sdpMid - } - }) - } else if (!event.candidate && !self._iceComplete) { - self._iceComplete = true - self.emit('_iceComplete') + _onChannelMessage (event) { + if (this.destroyed) return + var data = event.data + if (data instanceof ArrayBuffer) data = Buffer.from(data) + this.push(data) } - // as soon as we've received one valid candidate start timeout - if (event.candidate) { - self._startIceCompleteTimeout() + + _onChannelBufferedAmountLow () { + if (this.destroyed || !this._cb) return + this._debug('ending backpressure: bufferedAmount %d', this._channel.bufferedAmount) + var cb = this._cb + this._cb = null + cb(null) } -} -Peer.prototype._onChannelMessage = function (event) { - var self = this - if (self.destroyed) return - var data = event.data - if (data instanceof ArrayBuffer) data = Buffer.from(data) - self.push(data) -} + _onChannelOpen () { + if (this._connected || this.destroyed) return + this._debug('on channel open') + this._channelReady = true + this._maybeReady() + } -Peer.prototype._onChannelBufferedAmountLow = function () { - var self = this - if (self.destroyed || !self._cb) return - self._debug('ending backpressure: bufferedAmount %d', self._channel.bufferedAmount) - var cb = self._cb - self._cb = null - cb(null) -} + _onChannelClose () { + if (this.destroyed) return + this._debug('on channel close') + this.destroy() + } -Peer.prototype._onChannelOpen = function () { - var self = this - if (self._connected || self.destroyed) return - self._debug('on channel open') - self._channelReady = true - self._maybeReady() -} + _onTrack (event) { + if (this.destroyed) return -Peer.prototype._onChannelClose = function () { - var self = this - if (self.destroyed) return - self._debug('on channel close') - self.destroy() -} + event.streams.forEach(eventStream => { + this._debug('on track') + this.emit('track', event.track, eventStream) -Peer.prototype._onTrack = function (event) { - var self = this - if (self.destroyed) return + this._remoteTracks.push({ + track: event.track, + stream: eventStream + }) - event.streams.forEach(function (eventStream) { - self._debug('on track') - self.emit('track', event.track, 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 - self._remoteTracks.push({ - track: event.track, - stream: eventStream + this._remoteStreams.push(eventStream) + setTimeout(() => { + this.emit('stream', eventStream) // ensure all tracks have been added + }, 0) }) + } - if (self._remoteStreams.some(function (remoteStream) { - return remoteStream.id === eventStream.id - })) return // Only fire one 'stream' event, even though there may be multiple tracks per stream - - self._remoteStreams.push(eventStream) - setTimeout(function () { - self.emit('stream', eventStream) // ensure all tracks have been added - }, 0) - }) + _debug () { + var args = [].slice.call(arguments) + args[0] = '[' + this._id + '] ' + args[0] + debug.apply(null, args) + } } -Peer.prototype._debug = function () { - var self = this - var args = [].slice.call(arguments) - args[0] = '[' + self._id + '] ' + args[0] - debug.apply(null, args) -} +Peer.WEBRTC_SUPPORT = !!getBrowserRTC() -// HACK: Filter trickle lines when trickle is disabled #354 -function filterTrickle (sdp) { - return sdp.replace(/a=ice-options:trickle\s\n/g, '') +/** + * 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' + }, + { + urls: 'stun:global.stun.twilio.com:3478?transport=udp' + } + ], + sdpSemantics: 'unified-plan' } -function makeError (message, code) { - var err = new Error(message) - err.code = code - return err -} +Peer.channelConfig = {} -function warn (message) { - console.warn(message) -} +module.exports = Peer diff --git a/package.json b/package.json index 9845e0ab..bcb67f0a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "dependencies": { "debug": "^4.0.1", "get-browser-rtc": "^1.0.0", - "inherits": "^2.0.1", "randombytes": "^2.0.3", "readable-stream": "^3.4.0" },