diff --git a/packages/media-signaling/src/lib/Call.ts b/packages/media-signaling/src/lib/Call.ts index 1faad62b6216d..dbd4d02396b5b 100644 --- a/packages/media-signaling/src/lib/Call.ts +++ b/packages/media-signaling/src/lib/Call.ts @@ -571,16 +571,18 @@ export class ClientMediaCall implements IClientMediaCall { } const hadVideoTrack = this.hasScreenVideoTrack(); - if (hadVideoTrack && newVideoTrack !== this.screenVideoTrack) { - this.config.logger?.debug('ClientMediaCall.setScreenVideoTrack.stopOldTrack'); - this.screenVideoTrack?.stop(); - } - + const oldVideoTrack = this.screenVideoTrack; this.screenVideoTrack = newVideoTrack; if (this.webrtcProcessor) { await this.webrtcProcessor.setScreenVideoTrack(newVideoTrack); } + // Only stop the track after we replaced it on the transceiver, as we don't want the transceiver to stop if there's another track + if (hadVideoTrack && newVideoTrack !== oldVideoTrack) { + this.config.logger?.debug('ClientMediaCall.setScreenVideoTrack.stopOldTrack'); + oldVideoTrack?.stop(); + } + if (newVideoTrack && !hadVideoTrack) { await this.negotiationManager.processNegotiations(); } diff --git a/packages/media-signaling/src/lib/media/MediaStreamWrapper.ts b/packages/media-signaling/src/lib/media/MediaStreamWrapper.ts index 042b1b23c1620..26f7905a6fce9 100644 --- a/packages/media-signaling/src/lib/media/MediaStreamWrapper.ts +++ b/packages/media-signaling/src/lib/media/MediaStreamWrapper.ts @@ -33,9 +33,9 @@ export class MediaStreamWrapper implements IMediaStreamWrapper { private videoTrack: MediaStreamTrackWrapper | null = null; - private audioSender: RTCRtpSender | null = null; + private audioTransceiver: RTCRtpTransceiver | null = null; - private videoSender: RTCRtpSender | null = null; + private videoTransceiver: RTCRtpTransceiver | null = null; private stopped = false; @@ -197,17 +197,61 @@ export class MediaStreamWrapper implements IMediaStreamWrapper { this.emitter.emit('trackChanged', { track: newTrack, kind }); } + private getCurrentTransceiver(kind: MediaStreamTrack['kind']): RTCRtpTransceiver | null { + const transceiver = kind === 'audio' ? this.audioTransceiver : this.videoTransceiver; + if (!transceiver) { + return null; + } + + // transceiver.stopped is not available on safari + if ('stopped' in transceiver && transceiver.stopped) { + return null; + } + + if (transceiver.currentDirection === 'stopped' || transceiver.direction === 'stopped') { + return null; + } + + return transceiver; + } + + private setCurrentTransceiver(kind: MediaStreamTrack['kind'], transceiver: RTCRtpTransceiver | null) { + const oldTransceiver = kind === 'audio' ? this.audioTransceiver : this.videoTransceiver; + if ((!oldTransceiver && !transceiver) || oldTransceiver === transceiver) { + return; + } + + const action = transceiver ? 'Changed' : 'Removed'; + + this.logger?.debug(`${action} current ${kind} transceiver for ${this.tag} stream`); + if (kind === 'audio') { + this.audioTransceiver = transceiver; + } else { + this.videoTransceiver = transceiver; + } + } + private async syncTrackChange(kind: MediaStreamTrack['kind'], track: MediaStreamTrack | null): Promise { if (this.remote) { return; } - const sender = kind === 'audio' ? this.audioSender : this.videoSender; - if (sender) { + if (this.stopped || ['closed', 'failed'].includes(this.peer.connectionState)) { + return; + } + + const currentTransceiver = this.getCurrentTransceiver(kind); + if (currentTransceiver?.sender) { // If we already have a sender of the same kind for this stream, we can just replace the track with no issues // TODO: safe guard against edge cases where this would fail (eg: changing number of audio channels or increasing video quality) this.logger?.debug('MediaStreamWrapper.setPeerTrack.replaceTrack', kind); - await sender.replaceTrack(track); - return; + try { + await currentTransceiver.sender.replaceTrack(track); + // Only return early if the track was successfully replaced on a transceiver - otherwise we add it to a new one + return; + } catch (err) { + this.logger?.error('MediaStreamWrapper.setPeerTrack.replaceTrack failed', kind, err); + this.setCurrentTransceiver(kind, null); + } } if (!track) { @@ -219,13 +263,7 @@ export class MediaStreamWrapper implements IMediaStreamWrapper { this.peer.addTrack(track, this.stream); const transceiver = this.peer.getTransceivers().find((t) => t.sender.track === track); - if (transceiver) { - if (kind === 'audio') { - this.audioSender = transceiver.sender; - } else { - this.videoSender = transceiver.sender; - } - } + this.setCurrentTransceiver(kind, transceiver ?? null); } private wrapTrack(kind: MediaStreamTrack['kind'], track: MediaStreamTrack | null) { diff --git a/packages/media-signaling/src/lib/services/webrtc/Processor.ts b/packages/media-signaling/src/lib/services/webrtc/Processor.ts index a4159dee123ec..e09a016f73238 100644 --- a/packages/media-signaling/src/lib/services/webrtc/Processor.ts +++ b/packages/media-signaling/src/lib/services/webrtc/Processor.ts @@ -380,7 +380,16 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor { acceptableDirection: RTCRtpTransceiverDirection, ): void { const transceivers = this.getTransceivers(kind); + let hasAnyValidTransceiver = false; + let hasAnyStoppedTransceiver = false; for (const transceiver of transceivers) { + if (transceiver.currentDirection === 'stopped') { + hasAnyStoppedTransceiver = true; + continue; + } + + hasAnyValidTransceiver = true; + if (transceiver.direction !== desiredDirection) { continue; } @@ -393,6 +402,19 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor { transceiver.direction = transceiver.currentDirection; } } + + if (desiredDirection.includes('send') && !hasAnyValidTransceiver && hasAnyStoppedTransceiver) { + this.reactToStoppedTransceiver(kind); + } + } + + private reactToStoppedTransceiver(kind: 'audio' | 'video') { + this.config.logger?.error(`The ${kind} transceiver has stopped`); + if (kind === 'video' && this.screenVideoTrack) { + void this.streams.screenShareLocal.setTrack(kind, null).catch((err) => { + this.config.logger?.error('Failed to remove track from screen share media stream', err); + }); + } } private requestDirection(