diff --git a/index.js b/index.js index b715fb6f..320b21dc 100644 --- a/index.js +++ b/index.js @@ -51,6 +51,7 @@ class Peer extends stream.Duplex { this.trickle = opts.trickle !== undefined ? opts.trickle : true this.allowHalfTrickle = opts.allowHalfTrickle !== undefined ? opts.allowHalfTrickle : false this.iceCompleteTimeout = opts.iceCompleteTimeout || ICECOMPLETE_TIMEOUT + this.iceRestartEnabled = 'iceRestartEnabled' in opts ? opts.iceRestartEnabled : true this.destroyed = false this.destroying = false @@ -180,8 +181,7 @@ class Peer extends stream.Duplex { } signal (data) { - if (this.destroying) return - if (this.destroyed) throw errCode(new Error('cannot signal after peer is destroyed'), 'ERR_DESTROYED') + if (this.destroyed) throw errCode(new Error('cannot signal after peer is destroyed'), 'ERR_SIGNALING') if (typeof data === 'string') { try { data = JSON.parse(data) @@ -244,8 +244,6 @@ class Peer extends stream.Duplex { * @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) } @@ -255,8 +253,6 @@ class Peer extends stream.Duplex { * @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) { @@ -279,8 +275,6 @@ class Peer extends stream.Duplex { * @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 => { @@ -294,8 +288,6 @@ class Peer extends stream.Duplex { * @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 @@ -319,8 +311,6 @@ class Peer extends stream.Duplex { * @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) @@ -343,8 +333,6 @@ class Peer extends stream.Duplex { * @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) @@ -370,8 +358,6 @@ class Peer extends stream.Duplex { * @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 => { @@ -395,10 +381,7 @@ class Peer extends stream.Duplex { }) } - negotiate () { - if (this.destroying) return - if (this.destroyed) throw errCode(new Error('cannot negotiate after peer is destroyed'), 'ERR_DESTROYED') - + negotiate (restart = false) { if (this.initiator) { if (this._isNegotiating) { this._queuedNegotiation = true @@ -406,9 +389,10 @@ class Peer extends stream.Duplex { } else { this._debug('start negotiation') setTimeout(() => { // HACK: Chrome crashes if we immediately call createOffer - this._createOffer() + this._createOffer(restart) }, 0) } + this._isRestarting = restart } else { if (this._isNegotiating) { this._queuedNegotiation = true @@ -424,11 +408,21 @@ class Peer extends stream.Duplex { this._isNegotiating = true } + restart () { + if (this.initiator) { + if (this._isRestarting) { + this._debug('already restarting, ignoring') + } else { + this._pc.restartIce() + } + } + } + // 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, () => {}) + this._destroy(err, () => { }) } _destroy (err, cb) { @@ -469,7 +463,7 @@ class Peer extends stream.Duplex { if (this._channel) { try { this._channel.close() - } catch (err) {} + } catch (err) { } // allow events concurrent with destruction to be handled this._channel.onmessage = null @@ -480,7 +474,7 @@ class Peer extends stream.Duplex { if (this._pc) { try { this._pc.close() - } catch (err) {} + } catch (err) { } // allow events concurrent with destruction to be handled this._pc.oniceconnectionstatechange = null @@ -545,7 +539,7 @@ class Peer extends stream.Duplex { }, CHANNEL_CLOSING_TIMEOUT) } - _read () {} + _read () { } _write (chunk, encoding, cb) { if (this.destroyed) return cb(errCode(new Error('cannot write after peer is destroyed'), 'ERR_DATA_CHANNEL')) @@ -692,8 +686,10 @@ class Peer extends stream.Duplex { _onConnectionStateChange () { if (this.destroyed) return - if (this._pc.connectionState === 'failed') { + if (this._pc.connectionState === 'failed' && !this.iceRestartEnabled) { this.destroy(errCode(new Error('Connection failed.'), 'ERR_CONNECTION_FAILURE')) + } else if (this._pc.connectionState === 'failed' && this.iceRestartEnabled) { + this._pc.restartIce() } } @@ -709,11 +705,20 @@ class Peer extends stream.Duplex { ) this.emit('iceStateChange', iceConnectionState, iceGatheringState) - if (iceConnectionState === 'connected' || iceConnectionState === 'completed') { + if (iceConnectionState === 'connected' || iceGatheringState === 'completed') { + this._isRestarting = false this._pcReady = true this._maybeReady() } - if (iceConnectionState === 'failed') { + + if (iceConnectionState === 'failed' && this.iceRestartEnabled) { + if (this.initiator && !this._isRestarting) { + this._isNegotiating = false + this._isRestarting = true + + this._needsNegotiation(true) + } + } else if (iceConnectionState === 'failed' && !this.iceRestartEnabled) { this.destroy(errCode(new Error('Ice connection failed.'), 'ERR_ICE_CONNECTION_FAILURE')) } if (iceConnectionState === 'closed') { @@ -743,7 +748,7 @@ class Peer extends stream.Duplex { cb(null, reports) }, err => cb(err)) - // Single-parameter callback-based getStats() (non-standard) + // 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 @@ -763,8 +768,8 @@ class Peer extends stream.Duplex { cb(null, reports) }, err => cb(err)) - // Unknown browser, skip getStats() since it's anyone's guess which style of - // getStats() they implement. + // Unknown browser, skip getStats() since it's anyone's guess which style of + // getStats() they implement. } else { cb(null, []) } @@ -772,7 +777,7 @@ class Peer extends stream.Duplex { _maybeReady () { this._debug('maybeReady pc %s channel %s', this._pcReady, this._channelReady) - if (this._connected || this._connecting || !this._pcReady || !this._channelReady) return + if (((this._connected || this._connecting) && !this._isRestarting) || !this._pcReady || !this._channelReady) return this._connecting = true diff --git a/package.json b/package.json index 740778d3..2abeb96e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "simple-peer", "description": "Simple one-to-one WebRTC video/voice and data channels", - "version": "9.9.4", + "version": "9.10.0", "author": { "name": "Feross Aboukhadijeh", "email": "feross@feross.org", @@ -31,7 +31,7 @@ "speedometer": "^1.1.0", "standard": "*", "string-to-stream": "^3.0.1", - "tape": "^5.0.1", + "tape": "^5.3.1", "thunky": "^1.1.0", "wrtc": "^0.4.6", "ws": "^7.3.1" diff --git a/test/negotiation.js b/test/negotiation.js index a6c4a879..5b16add5 100644 --- a/test/negotiation.js +++ b/test/negotiation.js @@ -204,3 +204,38 @@ test('negotiated channels', function (t) { t.pass('peer2 connect') }) }) + +test('renegotiation after restart', 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) + }) + + peer1.on('connect', function () { + peer1.addStream(common.getMediaStream()) + }) + peer2.on('connect', function () { + peer2.addStream(common.getMediaStream()) + }) + + peer1.on('stream', function () { + t.pass('got peer1 stream') + }) + + peer2.on('stream', function () { + t.pass('got peer2 stream') + peer1.restart() + }) + + let tracks = 1 + peer2.on('track', function () { + t.pass(`got peer2 track ${tracks++}`) + }) +})