diff options
-rw-r--r-- | src/utils/webrtc/simplewebrtc/localmedia.js | 16 | ||||
-rw-r--r-- | src/utils/webrtc/simplewebrtc/peer.js | 175 |
2 files changed, 185 insertions, 6 deletions
diff --git a/src/utils/webrtc/simplewebrtc/localmedia.js b/src/utils/webrtc/simplewebrtc/localmedia.js index 890961dd4..a7a008923 100644 --- a/src/utils/webrtc/simplewebrtc/localmedia.js +++ b/src/utils/webrtc/simplewebrtc/localmedia.js @@ -634,18 +634,26 @@ LocalMedia.prototype.resume = function() { LocalMedia.prototype._setAudioEnabled = function(bool) { this._audioEnabled = bool - this.localStreams.forEach(function(stream) { - stream.getAudioTracks().forEach(function(track) { + this.localStreams.forEach(stream => { + stream.getAudioTracks().forEach(track => { track.enabled = !!bool + + // MediaStreamTrack does not emit an event when the enabled property + // changes, so it needs to be explicitly notified. + this.emit('localTrackEnabledChanged', track, stream) }) }) } LocalMedia.prototype._setVideoEnabled = function(bool) { this._videoEnabled = bool - this.localStreams.forEach(function(stream) { - stream.getVideoTracks().forEach(function(track) { + this.localStreams.forEach(stream => { + stream.getVideoTracks().forEach(track => { track.enabled = !!bool + + // MediaStreamTrack does not emit an event when the enabled property + // changes, so it needs to be explicitly notified. + this.emit('localTrackEnabledChanged', track, stream) }) }) } diff --git a/src/utils/webrtc/simplewebrtc/peer.js b/src/utils/webrtc/simplewebrtc/peer.js index dbf6a2380..893bdeb77 100644 --- a/src/utils/webrtc/simplewebrtc/peer.js +++ b/src/utils/webrtc/simplewebrtc/peer.js @@ -31,6 +31,9 @@ function Peer(options) { this.receiveMedia = options.receiveMedia || this.parent.config.receiveMedia this.channels = {} this.pendingDCMessages = [] // key (datachannel label) -> value (array[pending messages]) + this._pendingReplaceTracksQueue = [] + this._processPendingReplaceTracksPromise = null + this._initialStreamSetup = false this.sid = options.sid || Date.now().toString() this.pc = new RTCPeerConnection(this.parent.config.peerConnectionConfig) this.pc.addEventListener('icecandidate', this.onIceCandidate.bind(this)) @@ -47,6 +50,28 @@ function Peer(options) { this.pc.addEventListener('negotiationneeded', this.emit.bind(this, 'negotiationNeeded')) this.pc.addEventListener('iceconnectionstatechange', this.emit.bind(this, 'iceConnectionStateChange')) this.pc.addEventListener('iceconnectionstatechange', function() { + if (!options.receiverOnly && self.pc.iceConnectionState !== 'new') { + self._processPendingReplaceTracks().then(finished => { + if (finished === false || self._initialStreamSetup) { + return + } + + // Ensure that initially disabled tracks are stopped after + // establishing a connection. + self.pc.getSenders().forEach(sender => { + if (sender.track) { + // The stream is not known, but it is only used when the + // track is added, so it can be ignored here. + self.handleLocalTrackEnabledChanged(sender.track, null) + } + }) + + self._initialStreamSetup = true + }) + } else { + self._initialStreamSetup = false + } + switch (self.pc.iceConnectionState) { case 'failed': // currently, in chrome only the initiator goes to failed @@ -82,6 +107,9 @@ function Peer(options) { // TODO What would happen if the track is replaced while the peer is // still negotiating the offer and answer? this.parent.on('localTrackReplaced', this.handleLocalTrackReplacedBound) + + this.handleLocalTrackEnabledChangedBound = this.handleLocalTrackEnabledChanged.bind(this) + this.parent.on('localTrackEnabledChanged', this.handleLocalTrackEnabledChangedBound) } } @@ -372,22 +400,123 @@ Peer.prototype.end = function() { this.pc.close() this.handleStreamRemoved() this.parent.off('localTrackReplaced', this.handleLocalTrackReplacedBound) + this.parent.off('localTrackEnabledChanged', this.handleLocalTrackEnabledChangedBound) } Peer.prototype.handleLocalTrackReplaced = function(newTrack, oldTrack, stream) { + this._pendingReplaceTracksQueue.push({ newTrack, oldTrack, stream }) + + this._processPendingReplaceTracks() +} + +/** + * Process pending replace track actions. + * + * All the pending replace track actions are executed from the oldest to the + * newest, waiting until the previous action was executed before executing the + * next one. + * + * The process may be stopped if the connection is lost, or if a track needs to + * be added rather than replaced, which requires a renegotiation. In both cases + * the process will start again once the connection is restablished. + * + * @returns {Promise} a Promise fulfilled when the processing ends; if it was + * completed the resolved value is true, and if it was stopped before + * finishing the resolved value is false. + */ +Peer.prototype._processPendingReplaceTracks = function() { + if (this._processPendingReplaceTracksPromise) { + return this._processPendingReplaceTracksPromise + } + + this._processPendingReplaceTracksPromise = this._processPendingReplaceTracksAsync() + + // For compatibility with older browsers "finally" should not be used on + // Promises. + this._processPendingReplaceTracksPromise.then(() => { + this._processPendingReplaceTracksPromise = null + }).catch(() => { + this._processPendingReplaceTracksPromise = null + }) + + return this._processPendingReplaceTracksPromise +} + +Peer.prototype._processPendingReplaceTracksAsync = async function() { + while (this._pendingReplaceTracksQueue.length > 0) { + if (this.pc.iceConnectionState === 'new') { + // Do not replace the tracks when the connection has not started + // yet, as Firefox can get "stuck" and not replace the tracks even + // if tried later again once connected. + return false + } + + const pending = this._pendingReplaceTracksQueue.shift() + + try { + await this._replaceTrack(pending.newTrack, pending.oldTrack, pending.stream) + } catch (exception) { + // If the track is added instead of replaced a renegotiation will be + // needed, so stop replacing tracks. + return false + } + } + + return true +} + +/** + * Replaces the old track with the new track in the appropriate sender. + * + * If the new track is disabled the old track will be replaced by a null track + * instead, which stops the sent data. The old and new tracks can be the same + * track, which can be used to start or stop sending the track data depending on + * whether the track is enabled or disabled (at the time of being passed to this + * method). + * + * If a new track is provided but no sender was found the new track is added + * instead of replaced (which will require a renegotiation). + * + * The method returns a promise which is fulfilled once the track was replaced + * in the appropriate sender, or immediately if no sender was found and no track + * was added. If a track had to be added the promise is rejected instead. + * + * @param {MediaStreamTrack|null} newTrack the new track to set. + * @param {MediaStreamTrack|null} oldTrack the old track to be replaced. + * @param {MediaStream} stream the stream that the new track belongs to. + * @returns {Promise} + */ +Peer.prototype._replaceTrack = async function(newTrack, oldTrack, stream) { let senderFound = false + // The track should be replaced in just one sender, but an array of promises + // is used to be on the safe side. + const replaceTrackPromises = [] + this.pc.getSenders().forEach(sender => { - if (sender.track !== oldTrack) { + if (sender.track !== oldTrack && sender.trackDisabled !== oldTrack) { + return + } + + if ((sender.track || sender.trackDisabled) && !oldTrack) { return } if (!sender.track && !newTrack) { + // The old track was disabled and thus already stopped, so it does + // not need to be replaced, but the null track needs to be set as + // the disabled track. + if (sender.trackDisabled === oldTrack) { + sender.trackDisabled = newTrack + } + return } if (!sender.kind && sender.track) { sender.kind = sender.track.kind + } else if (!sender.kind && sender.trackDisabled) { + sender.kind = sender.trackDisabled.kind } else if (!sender.kind) { this.pc.getTransceivers().forEach(transceiver => { if (transceiver.sender === sender) { @@ -409,13 +538,40 @@ Peer.prototype.handleLocalTrackReplaced = function(newTrack, oldTrack, stream) { senderFound = true - sender.replaceTrack(newTrack).catch(error => { + // Save reference to trackDisabled to be able to restore it if the track + // can not be replaced. + const oldTrackDisabled = sender.trackDisabled + + if (newTrack && !newTrack.enabled) { + sender.trackDisabled = newTrack + } else { + sender.trackDisabled = null + } + + if (!sender.track && !newTrack.enabled) { + // Nothing to replace now, it will be done once the track is + // enabled. + return + } + + if (sender.track && newTrack && !newTrack.enabled) { + // Replace with a null track to stop the sender. + newTrack = null + } + + const replaceTrackPromise = sender.replaceTrack(newTrack) + + replaceTrackPromise.catch(error => { + sender.trackDisabled = oldTrackDisabled + if (error.name === 'InvalidModificationError') { console.debug('Track could not be replaced, negotiation needed') } else { console.error('Track could not be replaced: ', error, oldTrack, newTrack) } }) + + replaceTrackPromises.push(replaceTrackPromise) }) // If the call started when the audio or video device was not active there @@ -423,6 +579,21 @@ Peer.prototype.handleLocalTrackReplaced = function(newTrack, oldTrack, stream) { // instead of replaced. if (!senderFound && newTrack) { this.pc.addTrack(newTrack, stream) + + return Promise.reject(new Error('Track added instead of replaced')) + } + + return Promise.allSettled(replaceTrackPromises) +} + +Peer.prototype.handleLocalTrackEnabledChanged = function(track, stream) { + const sender = this.pc.getSenders().find(sender => sender.track === track) + const stoppedSender = this.pc.getSenders().find(sender => sender.trackDisabled === track) + + if (track.enabled && stoppedSender) { + this.handleLocalTrackReplacedBound(track, track, stream) + } else if (!track.enabled && sender) { + this.handleLocalTrackReplacedBound(track, track, stream) } } |