fix(voice): confirm a decode stall before withdrawing a codec (#2661)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-11 12:53:29 +02:00
1 parent 7d8778495f
commit 74782dc4f2
4 files changed
+281 -27

No files matched your search

@@ -2,7 +2,7 @@
import type {FluxerCodecAdvertisement} from '@app/features/voice/engine/ScreenShareCodecNegotiation';
import type {HardwareEncodeReport} from '@app/features/voice/utils/GpuEncoderCapabilities';
import {beforeEach, describe, expect, it, vi} from 'vitest';
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
const gpuReport: HardwareEncodeReport = {
av1: 'hardware',
@@ -77,13 +77,15 @@ const {
buildLocalCodecAdvertisements,
computeNegotiatedVideoCodec,
} = await import('./ScreenShareCodecNegotiation');
const {findStalledVideoDecoder} = await import('@app/features/voice/utils/ScreenShareCodecDiagnostics');
const {findStalledVideoDecoder, scheduleScreenShareDecoderVerification} = await import(
'@app/features/voice/utils/ScreenShareCodecDiagnostics'
);
const {getVideoDecoderExclusionsSync, markScreenShareDecodeFailure, resetVideoDecoderExclusions} = await import(
'@app/features/voice/utils/VideoDecoderCapabilities'
);
const {resetCachedCodecCapabilities} = await import('@app/features/voice/utils/CodecCapabilityDetector');
function stalledScreenShareStats(mimeType: string): RTCStatsReport {
function screenShareStats(mimeType: string, frames: {framesReceived: number; framesDecoded: number}): RTCStatsReport {
const entries: Array<Record<string, unknown>> = [
{id: 'codec-1', type: 'codec', mimeType},
{
@@ -93,13 +95,23 @@ function stalledScreenShareStats(mimeType: string): RTCStatsReport {
codecId: 'codec-1',
packetsReceived: 4200,
bytesReceived: 3_500_000,
framesReceived: 180,
framesDecoded: 0,
decoderImplementation: 'D3D11VideoDecoder',
powerEfficientDecoder: true,
...frames,
},
];
return new Map(entries.map((entry) => [entry.id as string, entry])) as unknown as RTCStatsReport;
}
function stalledScreenShareStats(mimeType: string): RTCStatsReport {
return screenShareStats(mimeType, {framesReceived: 180, framesDecoded: 0});
}
function statsSequence(...reports: Array<RTCStatsReport>): () => Promise<RTCStatsReport | undefined> {
const queue = [...reports];
return () => Promise.resolve(queue.shift());
}
function decodeAdvertisedFor(name: 'H264' | 'VP8' | 'VP9'): boolean | undefined {
return buildLocalCodecAdvertisements().find((codec) => codec.name === name)?.decode;
}
@@ -164,3 +176,119 @@ describe('a stalled H.264 screen share decode', () => {
expect(decodeAdvertisedFor('VP8')).toBe(true);
});
});
describe('confirming a screen share decode stall before a codec is withdrawn', () => {
beforeEach(() => {
vi.useFakeTimers();
resetVideoDecoderExclusions();
resetCachedCodecCapabilities();
ScreenShareCodecNegotiation.dispose();
});
afterEach(() => {
vi.useRealTimers();
});
it('withdraws nothing from a single stalled sample', async () => {
const onDecodeFailure = vi.fn();
scheduleScreenShareDecoderVerification(
statsSequence(
screenShareStats('video/H264', {framesReceived: 180, framesDecoded: 0}),
screenShareStats('video/H264', {framesReceived: 240, framesDecoded: 0}),
),
undefined,
onDecodeFailure,
);
await vi.advanceTimersByTimeAsync(5000);
expect(onDecodeFailure).not.toHaveBeenCalled();
expect(decodeAdvertisedFor('H264')).toBe(true);
});
it('withdraws nothing when packets arrive but no whole frames do', async () => {
const onDecodeFailure = vi.fn();
scheduleScreenShareDecoderVerification(
statsSequence(
screenShareStats('video/H264', {framesReceived: 0, framesDecoded: 0}),
screenShareStats('video/H264', {framesReceived: 0, framesDecoded: 0}),
),
undefined,
onDecodeFailure,
);
await vi.advanceTimersByTimeAsync(5000);
expect(vi.getTimerCount()).toBe(0);
await vi.advanceTimersByTimeAsync(5000);
expect(onDecodeFailure).not.toHaveBeenCalled();
expect(decodeAdvertisedFor('H264')).toBe(true);
});
it('withdraws the codec exactly once when a second sample confirms the stall', async () => {
const withdrawn: Array<string> = [];
scheduleScreenShareDecoderVerification(
statsSequence(
screenShareStats('video/H264', {framesReceived: 180, framesDecoded: 0}),
screenShareStats('video/H264', {framesReceived: 240, framesDecoded: 0}),
),
undefined,
(failure) => {
if (markScreenShareDecodeFailure(failure.codec, 'screen-share-decode-stalled')) {
withdrawn.push(failure.codec);
}
},
);
await vi.advanceTimersByTimeAsync(5000);
expect(withdrawn).toEqual([]);
expect(decodeAdvertisedFor('H264')).toBe(true);
await vi.advanceTimersByTimeAsync(5000);
expect(withdrawn).toEqual(['h264']);
expect(decodeAdvertisedFor('H264')).toBe(false);
});
it('withdraws nothing when the second sample decoded a frame', async () => {
const onDecodeFailure = vi.fn();
scheduleScreenShareDecoderVerification(
statsSequence(
screenShareStats('video/H264', {framesReceived: 180, framesDecoded: 0}),
screenShareStats('video/H264', {framesReceived: 240, framesDecoded: 12}),
),
undefined,
onDecodeFailure,
);
await vi.advanceTimersByTimeAsync(10000);
expect(onDecodeFailure).not.toHaveBeenCalled();
expect(decodeAdvertisedFor('H264')).toBe(true);
});
it('withdraws nothing when no new frames arrived between the samples', async () => {
const onDecodeFailure = vi.fn();
scheduleScreenShareDecoderVerification(
statsSequence(
screenShareStats('video/H264', {framesReceived: 180, framesDecoded: 0}),
screenShareStats('video/H264', {framesReceived: 180, framesDecoded: 0}),
),
undefined,
onDecodeFailure,
);
await vi.advanceTimersByTimeAsync(10000);
expect(onDecodeFailure).not.toHaveBeenCalled();
expect(decodeAdvertisedFor('H264')).toBe(true);
});
it('withdraws nothing when the track goes away between the samples', async () => {
const onDecodeFailure = vi.fn();
const cancel = scheduleScreenShareDecoderVerification(
statsSequence(
screenShareStats('video/H264', {framesReceived: 180, framesDecoded: 0}),
screenShareStats('video/H264', {framesReceived: 240, framesDecoded: 0}),
),
undefined,
onDecodeFailure,
);
await vi.advanceTimersByTimeAsync(5000);
expect(vi.getTimerCount()).toBe(1);
cancel();
expect(vi.getTimerCount()).toBe(0);
await vi.advanceTimersByTimeAsync(5000);
expect(onDecodeFailure).not.toHaveBeenCalled();
expect(decodeAdvertisedFor('H264')).toBe(true);
});
});
@@ -141,7 +141,7 @@ export function bindRoomEvents(
): void {
const guard = dependencies.connection.createGuardedHandler;
const participantSpeakingDisposers = new Map<string, ParticipantSpeakingDisposer>();
const screenShareDecoderVerificationTimers = new Map<string, NodeJS.Timeout>();
const screenShareDecoderVerificationCancels = new Map<string, () => void>();
const remoteTrackLifecycleDisposers = new Map<string, () => void>();
let codecNegotiationDisposer: (() => void) | null = null;
let screenShareMigrationDisposer: (() => void) | null = null;
@@ -227,16 +227,16 @@ export function bindRoomEvents(
};
const clearScreenShareDecoderVerification = (trackSid: string | undefined): void => {
if (!trackSid) return;
const timer = screenShareDecoderVerificationTimers.get(trackSid);
if (!timer) return;
clearTimeout(timer);
screenShareDecoderVerificationTimers.delete(trackSid);
const cancel = screenShareDecoderVerificationCancels.get(trackSid);
if (!cancel) return;
cancel();
screenShareDecoderVerificationCancels.delete(trackSid);
};
const clearAllScreenShareDecoderVerifications = (): void => {
for (const timer of screenShareDecoderVerificationTimers.values()) {
clearTimeout(timer);
for (const cancel of screenShareDecoderVerificationCancels.values()) {
cancel();
}
screenShareDecoderVerificationTimers.clear();
screenShareDecoderVerificationCancels.clear();
};
const bindCodecNegotiation = (): void => {
codecNegotiationDisposer?.();
@@ -258,12 +258,12 @@ export function bindRoomEvents(
if (pub.source !== Track.Source.ScreenShare || pub.kind !== Track.Kind.Video) return;
clearScreenShareDecoderVerification(pub.trackSid);
const trackSid = pub.trackSid;
screenShareDecoderVerificationTimers.set(
screenShareDecoderVerificationCancels.set(
trackSid,
scheduleScreenShareDecoderVerification(
() => track.getRTCStatsReport(),
() => {
screenShareDecoderVerificationTimers.delete(trackSid);
screenShareDecoderVerificationCancels.delete(trackSid);
},
(failure) => {
if (!markScreenShareDecodeFailure(failure.codec, 'screen-share-decode-stalled')) return;