Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/spreed.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVincent Petry <vincent@nextcloud.com>2021-06-21 22:26:01 +0300
committerGitHub <noreply@github.com>2021-06-21 22:26:01 +0300
commit4a49dc593c39fae2b5911d9b0dd5c61924957ac8 (patch)
tree6328bf57b6730551cf33f7ad6f2067304034c64c
parentd3811374a64ad913e9fba0db99c4b69e039f5a7a (diff)
parenta9ca74c3a726a2b1c1f09714b952960c2b05db17 (diff)
Merge pull request #5660 from nextcloud/stop-sent-streams-when-media-is-disabledv12.0.0-alpha.3
Stop sent streams when media is disabled
-rw-r--r--src/utils/webrtc/simplewebrtc/localmedia.js16
-rw-r--r--src/utils/webrtc/simplewebrtc/peer.js175
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)
}
}