mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
fix(voice): guard stale screen-share negotiation and probes (#2389)
This commit is contained in:
1 parent
59f6217267
commit
f1734704ef
2 files changed
+60
-14
No files matched your search
@@ -506,6 +506,8 @@ class ScreenShareCodecNegotiation {
|
|||||||
private rtcConnectionId = createId('rtc');
|
private rtcConnectionId = createId('rtc');
|
||||||
private mediaSessionId = createId('media');
|
private mediaSessionId = createId('media');
|
||||||
private negotiationSnapshot = createScreenShareCodecNegotiationSnapshot();
|
private negotiationSnapshot = createScreenShareCodecNegotiationSnapshot();
|
||||||
|
private bindingRevision = 0;
|
||||||
|
private selectionRevision = 0;
|
||||||
|
|
||||||
getSelectedCodec(): VideoCodec | null {
|
getSelectedCodec(): VideoCodec | null {
|
||||||
return this.selectedCodec;
|
return this.selectedCodec;
|
||||||
@@ -582,6 +584,7 @@ class ScreenShareCodecNegotiation {
|
|||||||
this.dispose();
|
this.dispose();
|
||||||
this.room = room;
|
this.room = room;
|
||||||
this.bindOptions = options;
|
this.bindOptions = options;
|
||||||
|
const bindingRevision = this.bindingRevision;
|
||||||
const onDataReceived = (
|
const onDataReceived = (
|
||||||
payload: Uint8Array,
|
payload: Uint8Array,
|
||||||
participant: Participant | undefined,
|
participant: Participant | undefined,
|
||||||
@@ -589,23 +592,24 @@ class ScreenShareCodecNegotiation {
|
|||||||
topic?: string,
|
topic?: string,
|
||||||
): void => {
|
): void => {
|
||||||
if (topic !== PROTOCOL_TOPIC || !participant) return;
|
if (topic !== PROTOCOL_TOPIC || !participant) return;
|
||||||
this.handleDataMessage(room, participant, payload, options);
|
this.handleDataMessage(room, participant, payload, options, bindingRevision);
|
||||||
};
|
};
|
||||||
const onParticipantConnected = (): void => {
|
const onParticipantConnected = (): void => {
|
||||||
void this.publishLocalCapabilities(room, 'participant-connected', options);
|
void this.publishBoundLocalCapabilities(room, 'participant-connected', options, bindingRevision);
|
||||||
};
|
};
|
||||||
const onParticipantDisconnected = (participant: Participant): void => {
|
const onParticipantDisconnected = (participant: Participant): void => {
|
||||||
|
if (!this.isBindingCurrent(room, bindingRevision)) return;
|
||||||
this.remoteCodecsByIdentity.delete(participant.identity);
|
this.remoteCodecsByIdentity.delete(participant.identity);
|
||||||
void this.updateSelection(room, 'participant-disconnected', options);
|
void this.updateSelection(room, 'participant-disconnected', options, bindingRevision);
|
||||||
};
|
};
|
||||||
const onReconnected = (): void => {
|
const onReconnected = (): void => {
|
||||||
void this.publishLocalCapabilities(room, 'reconnected', options);
|
void this.publishBoundLocalCapabilities(room, 'reconnected', options, bindingRevision);
|
||||||
};
|
};
|
||||||
room.on(RoomEvent.DataReceived, onDataReceived);
|
room.on(RoomEvent.DataReceived, onDataReceived);
|
||||||
room.on(RoomEvent.ParticipantConnected, onParticipantConnected);
|
room.on(RoomEvent.ParticipantConnected, onParticipantConnected);
|
||||||
room.on(RoomEvent.ParticipantDisconnected, onParticipantDisconnected);
|
room.on(RoomEvent.ParticipantDisconnected, onParticipantDisconnected);
|
||||||
room.on(RoomEvent.Reconnected, onReconnected);
|
room.on(RoomEvent.Reconnected, onReconnected);
|
||||||
void this.publishLocalCapabilities(room, 'connected', options);
|
void this.publishBoundLocalCapabilities(room, 'connected', options, bindingRevision);
|
||||||
this.bindDisposer = () => {
|
this.bindDisposer = () => {
|
||||||
room.off(RoomEvent.DataReceived, onDataReceived);
|
room.off(RoomEvent.DataReceived, onDataReceived);
|
||||||
room.off(RoomEvent.ParticipantConnected, onParticipantConnected);
|
room.off(RoomEvent.ParticipantConnected, onParticipantConnected);
|
||||||
@@ -616,6 +620,8 @@ class ScreenShareCodecNegotiation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
|
this.bindingRevision += 1;
|
||||||
|
this.selectionRevision += 1;
|
||||||
this.bindDisposer?.();
|
this.bindDisposer?.();
|
||||||
this.bindDisposer = null;
|
this.bindDisposer = null;
|
||||||
this.room = null;
|
this.room = null;
|
||||||
@@ -631,16 +637,26 @@ class ScreenShareCodecNegotiation {
|
|||||||
room: Room | null = this.room,
|
room: Room | null = this.room,
|
||||||
reason: NegotiationReason = 'manual',
|
reason: NegotiationReason = 'manual',
|
||||||
options: BindOptions = this.bindOptions,
|
options: BindOptions = this.bindOptions,
|
||||||
|
): Promise<CodecNegotiationSelection | null> {
|
||||||
|
if (!room) return null;
|
||||||
|
return await this.publishBoundLocalCapabilities(room, reason, options, this.bindingRevision);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async publishBoundLocalCapabilities(
|
||||||
|
room: Room,
|
||||||
|
reason: NegotiationReason,
|
||||||
|
options: BindOptions,
|
||||||
|
bindingRevision: number,
|
||||||
): Promise<CodecNegotiationSelection | null> {
|
): Promise<CodecNegotiationSelection | null> {
|
||||||
if (typeof window === 'undefined') return null;
|
if (typeof window === 'undefined') return null;
|
||||||
if (!room?.localParticipant) return null;
|
if (!room.localParticipant || !this.isBindingCurrent(room, bindingRevision)) return null;
|
||||||
this.room = room;
|
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
loadGpuEncoderReport(),
|
loadGpuEncoderReport(),
|
||||||
loadNativeHardwareEncoderCapabilities(),
|
loadNativeHardwareEncoderCapabilities(),
|
||||||
loadVideoDecoderExclusions(),
|
loadVideoDecoderExclusions(),
|
||||||
loadOpenH264Status(),
|
loadOpenH264Status(),
|
||||||
]);
|
]);
|
||||||
|
if (!this.isBindingCurrent(room, bindingRevision)) return null;
|
||||||
this.localCodecs = buildLocalCodecAdvertisements();
|
this.localCodecs = buildLocalCodecAdvertisements();
|
||||||
const message: FluxerSelectProtocolMessage = {
|
const message: FluxerSelectProtocolMessage = {
|
||||||
op: SELECT_PROTOCOL_OP,
|
op: SELECT_PROTOCOL_OP,
|
||||||
@@ -655,15 +671,23 @@ class ScreenShareCodecNegotiation {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
await this.publishMessage(room, message);
|
await this.publishMessage(room, message);
|
||||||
return await this.updateSelection(room, reason, options);
|
if (!this.isBindingCurrent(room, bindingRevision)) return null;
|
||||||
|
return await this.updateSelection(room, reason, options, bindingRevision);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleDataMessage(room: Room, participant: Participant, payload: Uint8Array, options: BindOptions): void {
|
private handleDataMessage(
|
||||||
|
room: Room,
|
||||||
|
participant: Participant,
|
||||||
|
payload: Uint8Array,
|
||||||
|
options: BindOptions,
|
||||||
|
bindingRevision: number,
|
||||||
|
): void {
|
||||||
|
if (!this.isBindingCurrent(room, bindingRevision)) return;
|
||||||
const message = parseMessage(payload);
|
const message = parseMessage(payload);
|
||||||
if (!message) return;
|
if (!message) return;
|
||||||
if (message.op === SELECT_PROTOCOL_OP) {
|
if (message.op === SELECT_PROTOCOL_OP) {
|
||||||
this.remoteCodecsByIdentity.set(participant.identity, message.d.codecs);
|
this.remoteCodecsByIdentity.set(participant.identity, message.d.codecs);
|
||||||
void this.updateSelection(room, 'data', options);
|
void this.updateSelection(room, 'data', options, bindingRevision);
|
||||||
} else if (message.op === SESSION_UPDATE_OP) {
|
} else if (message.op === SESSION_UPDATE_OP) {
|
||||||
this.remoteCodecsByIdentity.set(participant.identity, message.d.codecs);
|
this.remoteCodecsByIdentity.set(participant.identity, message.d.codecs);
|
||||||
logger.debug('Received remote codec session update', {
|
logger.debug('Received remote codec session update', {
|
||||||
@@ -672,7 +696,7 @@ class ScreenShareCodecNegotiation {
|
|||||||
mediaSessionId: message.d.media_session_id,
|
mediaSessionId: message.d.media_session_id,
|
||||||
reason: message.d.reason,
|
reason: message.d.reason,
|
||||||
});
|
});
|
||||||
void this.updateSelection(room, 'data', options);
|
void this.updateSelection(room, 'data', options, bindingRevision);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,8 +704,9 @@ class ScreenShareCodecNegotiation {
|
|||||||
room: Room,
|
room: Room,
|
||||||
reason: NegotiationReason,
|
reason: NegotiationReason,
|
||||||
options: BindOptions,
|
options: BindOptions,
|
||||||
|
bindingRevision: number,
|
||||||
): Promise<CodecNegotiationSelection | null> {
|
): Promise<CodecNegotiationSelection | null> {
|
||||||
if (!room.localParticipant) return null;
|
if (!room.localParticipant || !this.isBindingCurrent(room, bindingRevision)) return null;
|
||||||
if (this.localCodecs.length === 0) this.localCodecs = buildLocalCodecAdvertisements();
|
if (this.localCodecs.length === 0) this.localCodecs = buildLocalCodecAdvertisements();
|
||||||
if (reason === 'participant-disconnected' && room.remoteParticipants.size === 0 && this.selectedCodec) {
|
if (reason === 'participant-disconnected' && room.remoteParticipants.size === 0 && this.selectedCodec) {
|
||||||
logger.debug('Keeping active screen share codec after last viewer disconnected', {
|
logger.debug('Keeping active screen share codec after last viewer disconnected', {
|
||||||
@@ -703,13 +728,20 @@ class ScreenShareCodecNegotiation {
|
|||||||
if (!selection) return null;
|
if (!selection) return null;
|
||||||
this.selectedCodec = selection.codec;
|
this.selectedCodec = selection.codec;
|
||||||
if (selection.codec === previousCodec) return selection;
|
if (selection.codec === previousCodec) return selection;
|
||||||
|
const selectionRevision = ++this.selectionRevision;
|
||||||
this.mediaSessionId = createId('media');
|
this.mediaSessionId = createId('media');
|
||||||
logger.info('Selected screen share codec from XState capability intersection', selection);
|
logger.info('Selected screen share codec from XState capability intersection', selection);
|
||||||
await this.publishSessionUpdate(room, selection);
|
await this.publishSessionUpdate(room, selection);
|
||||||
|
if (!this.isBindingCurrent(room, bindingRevision) || selectionRevision !== this.selectionRevision) return null;
|
||||||
await options.onSelectedCodecChanged?.(selection);
|
await options.onSelectedCodecChanged?.(selection);
|
||||||
|
if (!this.isBindingCurrent(room, bindingRevision) || selectionRevision !== this.selectionRevision) return null;
|
||||||
return selection;
|
return selection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isBindingCurrent(room: Room, bindingRevision: number): boolean {
|
||||||
|
return this.room === room && this.bindingRevision === bindingRevision;
|
||||||
|
}
|
||||||
|
|
||||||
private async publishSessionUpdate(room: Room, selection: CodecNegotiationSelection): Promise<void> {
|
private async publishSessionUpdate(room: Room, selection: CodecNegotiationSelection): Promise<void> {
|
||||||
const message: FluxerSessionUpdateMessage = {
|
const message: FluxerSessionUpdateMessage = {
|
||||||
op: SESSION_UPDATE_OP,
|
op: SESSION_UPDATE_OP,
|
||||||
|
|||||||
@@ -324,6 +324,14 @@ function safeSetEnabled(publication: RemoteTrackPublication, enabled: boolean):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRemoteTrackStatsReport(track: RemoteTrack): Promise<RTCStatsReport | undefined> | undefined {
|
||||||
|
try {
|
||||||
|
return track.getRTCStatsReport?.();
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function publishMigrationMessage(
|
function publishMigrationMessage(
|
||||||
participant: LocalParticipant,
|
participant: LocalParticipant,
|
||||||
message: ScreenShareMigrationMessage,
|
message: ScreenShareMigrationMessage,
|
||||||
@@ -820,6 +828,7 @@ class ScreenSharePublicationMigration extends Store {
|
|||||||
let disposed = false;
|
let disposed = false;
|
||||||
let video: HTMLVideoElement | null = null;
|
let video: HTMLVideoElement | null = null;
|
||||||
let statsInterval: NodeJS.Timeout | null = null;
|
let statsInterval: NodeJS.Timeout | null = null;
|
||||||
|
let statsRequestPending = false;
|
||||||
const cleanups: Array<() => void> = [];
|
const cleanups: Array<() => void> = [];
|
||||||
const complete = (): void => {
|
const complete = (): void => {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
@@ -850,8 +859,10 @@ class ScreenSharePublicationMigration extends Store {
|
|||||||
cleanups.push(() => clearTimeout(expireTimer));
|
cleanups.push(() => clearTimeout(expireTimer));
|
||||||
|
|
||||||
const checkStats = (): void => {
|
const checkStats = (): void => {
|
||||||
const statsPromise = track.getRTCStatsReport?.();
|
if (disposed || statsRequestPending) return;
|
||||||
|
const statsPromise = getRemoteTrackStatsReport(track);
|
||||||
if (!statsPromise) return;
|
if (!statsPromise) return;
|
||||||
|
statsRequestPending = true;
|
||||||
void statsPromise
|
void statsPromise
|
||||||
.then((report) => {
|
.then((report) => {
|
||||||
if (!report) return;
|
if (!report) return;
|
||||||
@@ -870,7 +881,10 @@ class ScreenSharePublicationMigration extends Store {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
statsRequestPending = false;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
statsInterval = setInterval(checkStats, REMOTE_READY_PROBE_STATS_INTERVAL_MS);
|
statsInterval = setInterval(checkStats, REMOTE_READY_PROBE_STATS_INTERVAL_MS);
|
||||||
checkStats();
|
checkStats();
|
||||||
|
|||||||
Reference in new issue
Block a user