Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions JitsiConference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3546,6 +3546,16 @@ export default class JitsiConference extends Listenable {
);

emitter.emit(JitsiConferenceEvents.TRACK_ADDED, track);

Copy link
Member

@jallamsetty1 jallamsetty1 Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Always apply the final mute state (pending or current).
const muteState = track._getPendingMuteState() ?? track.isMuted();
track._clearPendingMuteState();
track.setMute(muteState);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if we should always call setMute() track. This should handle the case when presence is available during track creation and there are no subsequent mute events after that initial presence update.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nevermind, I think case is already handled.

// Apply any pending mute state that arrived before the track was created
// Always call setMute when there's a pending state, even if it matches current state,
// because setMute() is designed to emit on the first call regardless of state change
const pendingMutedState = track._getPendingMuteState();

if (pendingMutedState !== undefined) {
track._clearPendingMuteState();
track.setMute(pendingMutedState);
}
}


Expand Down
42 changes: 41 additions & 1 deletion modules/RTC/JitsiRemoteTrack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ export default class JitsiRemoteTrack extends JitsiTrack {
private _rtc: RTC;
private _muted: boolean;
private _hasBeenMuted: boolean;
private _setMuteCalled: boolean;
private _ssrc: number;
private _pendingMuteState?: boolean;

public ownerEndpointId: string;
public isP2P: boolean;
Expand Down Expand Up @@ -101,6 +103,7 @@ export default class JitsiRemoteTrack extends JitsiTrack {
this._ssrc = ssrc;
this.ownerEndpointId = ownerEndpointId;
this._muted = muted;
this._setMuteCalled = false;
this.isP2P = isP2P;
this._sourceName = sourceName;
this._trackStreamingStatus = null;
Expand Down Expand Up @@ -423,6 +426,37 @@ export default class JitsiRemoteTrack extends JitsiTrack {
return this._enteredForwardedSourcesTimestamp;
}

/**
* Sets the pending mute state for this track,
* received before the track was fully initialized.
*
* @param {boolean | undefined} state - The pending mute state
* @internal
*/
_setPendingMuteState(state: boolean | undefined): void {
this._pendingMuteState = state;
}

/**
* Returns the pending mute state for this track,
* received before the track was fully initialized.
*
* @returns {boolean | undefined} the pending mute state
* @internal
*/
_getPendingMuteState(): boolean | undefined {
return this._pendingMuteState;
}

/**
* Clears the pending mute state for this track.
*
* @internal
*/
_clearPendingMuteState(): void {
this._pendingMuteState = undefined;
}

/**
* Removes attached event listeners and dispose TrackStreamingStatus .
*
Expand All @@ -444,10 +478,16 @@ export default class JitsiRemoteTrack extends JitsiTrack {
* @internal
*/
setMute(value: boolean): void {
if (this._muted === value) {
const isFirstCall = !this._setMuteCalled;
const stateChanged = this._muted !== value;

// Skip if state didn't change, unless this is the first call (to emit initial state)
if (!stateChanged && !isFirstCall) {
return;
}

this._setMuteCalled = true;

if (value) {
this._hasBeenMuted = true;
}
Expand Down
21 changes: 20 additions & 1 deletion modules/RTC/TraceablePeerConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export default class TraceablePeerConnection {
private _pcId: string;
private _remoteUfrag: string;
private _signalingLayer: SignalingLayer;
private _pendingMuteUpdates: Map<string, boolean>;
/**
* @internal
*/
Expand Down Expand Up @@ -324,6 +325,13 @@ export default class TraceablePeerConnection {
*/
this.remoteTracks = new Map();

/**
* Stores pending mute updates for sources that haven't had tracks created yet.
* @type {Map<string, boolean>}
* @private
*/
this._pendingMuteUpdates = new Map();

/**
* A map which stores local tracks mapped by {@link JitsiLocalTrack.rtcId}
* @type {Map<number, JitsiLocalTrack>}
Expand Down Expand Up @@ -1203,11 +1211,14 @@ export default class TraceablePeerConnection {
const track = this.getRemoteTracks().slice().reverse().find(t => t.getSourceName() === sourceName);

if (!track) {
logger.debug(`Remote track not found for source=${sourceName}, mute update failed!`);
// Store the pending mute update to be applied when the track is created
this._pendingMuteUpdates.set(sourceName, isMuted);

return;
}

// Clear any pending mute update since we're applying the current state to an existing track
this._pendingMuteUpdates.delete(sourceName);
track.setMute(isMuted);
}

Expand Down Expand Up @@ -1692,6 +1703,14 @@ export default class TraceablePeerConnection {
userTracksByMediaType.add(remoteTrack);
}

// Store pending mute update on the track itself so JitsiConference can apply it after attaching listeners
if (this._pendingMuteUpdates.has(sourceName)) {
const pendingMutedState = this._pendingMuteUpdates.get(sourceName);

remoteTrack._setPendingMuteState(pendingMutedState);
this._pendingMuteUpdates.delete(sourceName);
}

this.eventEmitter.emit(RTCEvents.REMOTE_TRACK_ADDED, remoteTrack, this);
}

Expand Down