Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7e978b4
cherry pick changes related to adjusting audio transceiver directions
pierre-lehnen-rc Oct 29, 2025
0adee6c
fix: (wip) weak glare protection on webrtc renegotiations
pierre-lehnen-rc Oct 29, 2025
3aeaf48
server side handling of negotiation requests
pierre-lehnen-rc Oct 29, 2025
387c24a
wip - client side
pierre-lehnen-rc Oct 30, 2025
63a046f
negotiation queue
pierre-lehnen-rc Oct 31, 2025
f2961b8
working negotiation queue
pierre-lehnen-rc Nov 3, 2025
3ee2715
cleaning comments
pierre-lehnen-rc Nov 3, 2025
f8477e0
update direction
pierre-lehnen-rc Nov 3, 2025
cf23a46
cleanup
pierre-lehnen-rc Nov 3, 2025
dd30d3b
changeset
pierre-lehnen-rc Nov 3, 2025
e804cb0
Merge branch 'fix/voip-weak-glare-protection' into voip/audio-transce…
pierre-lehnen-rc Nov 3, 2025
eb91d32
audio direction sync
pierre-lehnen-rc Nov 3, 2025
c0371bd
code review
pierre-lehnen-rc Nov 4, 2025
c16935b
Merge branch 'fix/voip-weak-glare-protection' into voip/audio-transce…
pierre-lehnen-rc Nov 4, 2025
b723e67
code review
pierre-lehnen-rc Nov 4, 2025
056357b
code review
pierre-lehnen-rc Nov 4, 2025
d46b081
cleanup
pierre-lehnen-rc Nov 4, 2025
3ab5819
Merge branch 'fix/voip-weak-glare-protection' into voip/audio-transce…
pierre-lehnen-rc Nov 4, 2025
865a204
sync audio direction on negotiations
pierre-lehnen-rc Nov 4, 2025
d94d8d1
chore: change audio direction on webrtc negotiations based on the cal…
pierre-lehnen-rc Nov 4, 2025
07ebb90
chore: identify if a call was put on hold by the remote user
pierre-lehnen-rc Nov 4, 2025
dc0f48a
Merge branch 'chore/sync-on-hold-audio-direction' into voip/audio-tra…
pierre-lehnen-rc Nov 4, 2025
14adf4b
Merge branch 'chore/identify-remote-held' into voip/audio-transceiver…
pierre-lehnen-rc Nov 4, 2025
f4fec35
chore: trigger a renegotiation when a call is put on hold
pierre-lehnen-rc Nov 4, 2025
0e74d93
Merge branch 'chore/held-call-trigger-renegotiation' into voip/audio-…
pierre-lehnen-rc Nov 4, 2025
5066215
Merge branch 'feat/voip-sync-audio-direction' into voip/audio-transce…
pierre-lehnen-rc Nov 5, 2025
e9f9836
Merge branch 'feat/voip-sync-audio-direction' into voip/audio-transce…
pierre-lehnen-rc Nov 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export interface IClientMediaCall {
held: boolean;
/* busy = state >= 'accepted' && state < 'hangup' */
busy: boolean;
/* if the other side has put the call on hold */
remoteHeld: boolean;

contact: CallContact;
transferredBy: CallContact | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface IWebRTCProcessor extends IServiceProcessor<WebRTCInternalStateM
localAudioLevel: number;

getStats(selector?: MediaStreamTrack | null): Promise<RTCStatsReport | null>;
isRemoteHeld(): boolean;
}

export type WebRTCProcessorConfig = {
Expand Down
23 changes: 23 additions & 0 deletions packages/media-signaling/src/lib/Call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ export class ClientMediaCall implements IClientMediaCall {
return this.webrtcProcessor.held;
}

private _remoteHeld: boolean;

public get remoteHeld(): boolean {
return this._remoteHeld;
}

/** indicates the call is past the "dialing" stage and not yet over */
public get busy(): boolean {
return !this.isPendingAcceptance() && !this.isOver();
Expand Down Expand Up @@ -208,6 +214,7 @@ export class ClientMediaCall implements IClientMediaCall {
this._contact = null;
this._transferredBy = null;
this._service = null;
this._remoteHeld = false;

this.negotiationManager = new NegotiationManager(this, { logger: config.logger });
}
Expand Down Expand Up @@ -987,6 +994,20 @@ export class ClientMediaCall implements IClientMediaCall {
this.stateTimeoutHandlers.clear();
}

private updateRemoteHeld(): void {
if (!this.webrtcProcessor) {
return;
}

const isRemoteHeld = this.webrtcProcessor.isRemoteHeld();
if (isRemoteHeld === this._remoteHeld) {
return;
}

this._remoteHeld = isRemoteHeld;
this.emitter.emit('trackStateChange');
}

private onWebRTCInternalStateChange(stateName: keyof WebRTCInternalStateMap): void {
this.config.logger?.debug('ClientMediaCall.onWebRTCInternalStateChange');
if (!this.webrtcProcessor) {
Expand All @@ -1006,6 +1027,8 @@ export class ClientMediaCall implements IClientMediaCall {

this.requestStateReport();
}

this.updateRemoteHeld();
}

private onNegotiationNeeded(oldNegotiationId: string): void {
Expand Down
51 changes: 51 additions & 0 deletions packages/media-signaling/src/lib/services/webrtc/Processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor {
this._held = held;
this.localStream.setEnabled(!held && !this._muted);
this.remoteStream.setEnabled(!held);

this.updateAudioDirectionWithoutNegotiation();
}

public stop(): void {
Expand Down Expand Up @@ -257,6 +259,33 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor {
return this.peer.signalingState === 'stable';
}

public isRemoteHeld(): boolean {
if (this.stopped) {
return false;
}

if (['closed', 'failed', 'new'].includes(this.peer.connectionState)) {
return false;
}

let anyTransceiverNotSending = false;
const transceivers = this.getAudioTransceivers();

for (const transceiver of transceivers) {
if (!transceiver.currentDirection || transceiver.currentDirection === 'stopped') {
continue;
}

if (transceiver.currentDirection.includes('send')) {
return false;
}

anyTransceiverNotSending = true;
}

return anyTransceiverNotSending;
}

public getLocalDescription(): RTCSessionDescriptionInit | null {
this.config.logger?.debug('MediaCallWebRTCProcessor.getLocalDescription');
if (this.stopped) {
Expand Down Expand Up @@ -354,6 +383,28 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor {
}
}

private updateAudioDirectionWithoutNegotiation(): void {
// If the signaling state is not stable, then a negotiation is already happening and the audio direction will be updated by them
if (this.peer.signalingState !== 'stable') {
return;
}

const desiredDirection = this.held ? 'sendonly' : 'sendrecv';
const acceptableDirection = this.held ? 'inactive' : 'recvonly';

const transceivers = this.getAudioTransceivers();
for (const transceiver of transceivers) {
// If the last direction we requested still matches our current requirements, then we don't need to change our request
if ([desiredDirection, acceptableDirection, 'stopped'].includes(transceiver.direction)) {
continue;
}

// If the current state of the call doesn't match what we are requesting here, the browser will trigger the negotiation-needed event for us
this.config.logger?.debug(`Changing desired audio direction from ${transceiver.direction} to ${desiredDirection}.`);
transceiver.direction = desiredDirection;
}
}

private getAudioTransceivers(): RTCRtpTransceiver[] {
return this.peer
.getTransceivers()
Expand Down
Loading