diff --git a/packages/media-signaling/src/lib/services/webrtc/Processor.ts b/packages/media-signaling/src/lib/services/webrtc/Processor.ts index 0841262434fe0..fc51f816dcdaa 100644 --- a/packages/media-signaling/src/lib/services/webrtc/Processor.ts +++ b/packages/media-signaling/src/lib/services/webrtc/Processor.ts @@ -109,10 +109,9 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor { await this.initialization; if (!this.addedEmptyTransceiver) { + this.config.logger?.debug('MediaCallWebRTCProcessor.createOffer.addEmptyTransceiver'); // If there's no audio transceivers yet, add a new one; since it's an offer, the track can be set later - const transceivers = this.peer - .getTransceivers() - .filter((transceiver) => transceiver.sender.track?.kind === 'audio' || transceiver.receiver.track?.kind === 'audio'); + const transceivers = this.getAudioTransceivers(); if (!transceivers.length) { this.peer.addTransceiver('audio', { direction: 'sendrecv' }); @@ -120,6 +119,8 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor { } } + this.updateAudioDirectionBeforeNegotiation(); + if (iceRestart) { this.restartIce(); } @@ -169,9 +170,7 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor { await this.initialization; - const transceivers = this.peer - .getTransceivers() - .filter((transceiver) => transceiver.sender.track?.kind === 'audio' || transceiver.receiver.track?.kind === 'audio'); + const transceivers = this.getAudioTransceivers(); if (!transceivers.length) { throw new Error('no-audio-transceiver'); @@ -193,6 +192,10 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor { } await this.peer.setLocalDescription(sdp); + + if (sdp.type === 'answer') { + this.updateAudioDirectionAfterNegotiation(); + } } public async setRemoteDescription(sdp: RTCSessionDescriptionInit): Promise { @@ -207,7 +210,15 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor { throw new Error('unsupported-description-type'); } + if (sdp.type === 'offer') { + this.updateAudioDirectionBeforeNegotiation(); + } + await this.peer.setRemoteDescription(sdp); + + if (sdp.type === 'answer') { + this.updateAudioDirectionAfterNegotiation(); + } } public getInternalState(stateName: K): ServiceStateValue { @@ -298,6 +309,57 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor { this.emitter.emit('internalStateChange', stateName); } + private updateAudioDirectionBeforeNegotiation(): void { + // Before the negotiation, we set the direction based on our own state only + // We'll tell the SDK that we want to send audio and, depending on the "on hold" state, also receive it + const desiredDirection = this.held ? 'sendonly' : 'sendrecv'; + + const transceivers = this.getAudioTransceivers(); + for (const transceiver of transceivers) { + if (transceiver.direction === 'stopped') { + continue; + } + + if (transceiver.direction !== desiredDirection) { + this.config.logger?.debug(`Changing audio direction from ${transceiver.direction} to ${desiredDirection}`); + } + + transceiver.direction = desiredDirection; + } + } + + private updateAudioDirectionAfterNegotiation(): void { + // Before the negotiation started, we told the browser we wanted to send audio - but we don't care if actually send or not, it's up to the other side to determine if they want to receive. + // If the other side doesn't want to receive audio, the negotiation will result in a state where "direction" and "currentDirection" don't match + // But if the only difference is that we said we want to send audio and are not sending it, then we can change what we say we want to reflect the current state + + // If we didn't do this, everything would still work, but the browser would trigger redundant renegotiations whenever the directions mismatch + + const desiredDirection = this.held ? 'sendonly' : 'sendrecv'; + const acceptableDirection = this.held ? 'inactive' : 'recvonly'; + + const transceivers = this.getAudioTransceivers(); + for (const transceiver of transceivers) { + if (transceiver.direction !== desiredDirection) { + continue; + } + if (!transceiver.currentDirection || ['stopped', desiredDirection].includes(transceiver.currentDirection)) { + continue; + } + + if (transceiver.currentDirection === acceptableDirection) { + this.config.logger?.debug(`Changing audio direction from ${transceiver.direction} to match ${transceiver.currentDirection}.`); + transceiver.direction = transceiver.currentDirection; + } + } + } + + private getAudioTransceivers(): RTCRtpTransceiver[] { + return this.peer + .getTransceivers() + .filter((transceiver) => transceiver.sender.track?.kind === 'audio' || transceiver.receiver.track?.kind === 'audio'); + } + private registerPeerEvents() { const { peer } = this;