mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 13:35:42 +09:00
fix(voice): show buffering while screen share recovery runs (#2682)
This commit is contained in:
1 parent
53b3fa2f4a
commit
c541b86c00
7 files changed
+302
-49
No files matched your search
@@ -5,12 +5,16 @@ import {
|
||||
transitionVoiceMediaGraph,
|
||||
type VoiceMediaGraphSnapshot,
|
||||
} from '@app/features/voice/engine/VoiceMediaGraph';
|
||||
import {selectVoiceMediaGraphStreamTileState} from '@app/features/voice/engine/VoiceMediaGraphTileState';
|
||||
import {
|
||||
selectVoiceMediaGraphStreamTileState,
|
||||
VOICE_MEDIA_GRAPH_FIRST_FRAME_RECOVERY_VISIBLE_MS,
|
||||
} from '@app/features/voice/engine/VoiceMediaGraphTileState';
|
||||
import {VoiceTrackSource} from '@app/features/voice/engine/VoiceTrackSource';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
const STREAM_KEY = 'dm:channel-a:connection-a';
|
||||
const PARTICIPANT_IDENTITY = 'user_2_connection-a';
|
||||
const FAILURE_REPORTED_AT = 300;
|
||||
|
||||
const target = {
|
||||
streamKey: STREAM_KEY,
|
||||
@@ -42,6 +46,24 @@ function attach(graph: VoiceMediaGraphSnapshot): VoiceMediaGraphSnapshot {
|
||||
});
|
||||
}
|
||||
|
||||
function reportFailure(graph: VoiceMediaGraphSnapshot, code: number, reason: string): VoiceMediaGraphSnapshot {
|
||||
return transitionVoiceMediaGraph(graph, {
|
||||
type: 'failure.reported',
|
||||
failure: {
|
||||
code,
|
||||
reason,
|
||||
reportedAt: FAILURE_REPORTED_AT,
|
||||
streamKey: STREAM_KEY,
|
||||
participantIdentity: PARTICIPANT_IDENTITY,
|
||||
source: 'screen_share',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function watchedWithFailure(code: number, reason: string): VoiceMediaGraphSnapshot {
|
||||
return reportFailure(attach(subscribe(createVoiceMediaGraphSnapshot(), true)), code, reason);
|
||||
}
|
||||
|
||||
describe('selectVoiceMediaGraphStreamTileState', () => {
|
||||
it('returns idle when the graph has no state for the tile', () => {
|
||||
expect(selectVoiceMediaGraphStreamTileState(createVoiceMediaGraphSnapshot(), target)).toBe('idle');
|
||||
@@ -140,6 +162,51 @@ describe('selectVoiceMediaGraphStreamTileState', () => {
|
||||
expect(selectVoiceMediaGraphStreamTileState(graph, target)).toBe('subscribedAwaitingFrame');
|
||||
});
|
||||
|
||||
it('returns recovering while a first-frame timeout still has recovery attempts left', () => {
|
||||
const graph = watchedWithFailure(-2303, 'first-frame-timeout');
|
||||
expect(
|
||||
selectVoiceMediaGraphStreamTileState(graph, target, {
|
||||
hasRecoveryBudget: true,
|
||||
nowMs: FAILURE_REPORTED_AT + 1000,
|
||||
}),
|
||||
).toBe('recovering');
|
||||
});
|
||||
|
||||
it('returns failed once the first-frame recovery budget is spent', () => {
|
||||
const graph = watchedWithFailure(-2303, 'first-frame-timeout');
|
||||
expect(
|
||||
selectVoiceMediaGraphStreamTileState(graph, target, {
|
||||
hasRecoveryBudget: false,
|
||||
nowMs: FAILURE_REPORTED_AT + 1000,
|
||||
}),
|
||||
).toBe('failed');
|
||||
});
|
||||
|
||||
it('returns failed once a first-frame timeout has been on screen for the visible cap', () => {
|
||||
const graph = watchedWithFailure(-2303, 'first-frame-timeout');
|
||||
expect(
|
||||
selectVoiceMediaGraphStreamTileState(graph, target, {
|
||||
hasRecoveryBudget: true,
|
||||
nowMs: FAILURE_REPORTED_AT + VOICE_MEDIA_GRAPH_FIRST_FRAME_RECOVERY_VISIBLE_MS,
|
||||
}),
|
||||
).toBe('failed');
|
||||
});
|
||||
|
||||
it('returns failed for a first-frame timeout when the caller tracks no recovery', () => {
|
||||
const graph = watchedWithFailure(-2303, 'first-frame-timeout');
|
||||
expect(selectVoiceMediaGraphStreamTileState(graph, target)).toBe('failed');
|
||||
});
|
||||
|
||||
it('keeps every other failure code failed while recovery attempts remain', () => {
|
||||
const graph = watchedWithFailure(-2202, 'remote-track-subscription-failed');
|
||||
expect(
|
||||
selectVoiceMediaGraphStreamTileState(graph, target, {
|
||||
hasRecoveryBudget: true,
|
||||
nowMs: FAILURE_REPORTED_AT + 1000,
|
||||
}),
|
||||
).toBe('failed');
|
||||
});
|
||||
|
||||
it('returns watchDesired for entry-less streams and idle after the watch ends', () => {
|
||||
let graph = transitionVoiceMediaGraph(createVoiceMediaGraphSnapshot(), {
|
||||
type: 'watchIntent.add',
|
||||
|
||||
@@ -4,11 +4,15 @@ import {
|
||||
selectVoiceMediaGraphAttempt,
|
||||
selectVoiceMediaGraphFailure,
|
||||
selectVoiceMediaGraphSubscriptionEntry,
|
||||
type VoiceMediaGraphFailure,
|
||||
type VoiceMediaGraphSnapshot,
|
||||
type VoiceMediaGraphSubscriptionEntry,
|
||||
} from './VoiceMediaGraph';
|
||||
import {VOICE_MEDIA_GRAPH_FIRST_FRAME_TIMEOUT_FAILURE} from './VoiceMediaGraphDeadlines';
|
||||
import type {VoiceTrackSource} from './VoiceTrackSource';
|
||||
|
||||
export const VOICE_MEDIA_GRAPH_FIRST_FRAME_RECOVERY_VISIBLE_MS = 30_000;
|
||||
|
||||
export type VoiceMediaGraphStreamTileState =
|
||||
| 'idle'
|
||||
| 'watchDesired'
|
||||
@@ -16,6 +20,7 @@ export type VoiceMediaGraphStreamTileState =
|
||||
| 'attaching'
|
||||
| 'subscribedAwaitingFrame'
|
||||
| 'rendering'
|
||||
| 'recovering'
|
||||
| 'failed';
|
||||
|
||||
export interface VoiceMediaGraphStreamTileTarget {
|
||||
@@ -24,14 +29,32 @@ export interface VoiceMediaGraphStreamTileTarget {
|
||||
source: VoiceTrackSource;
|
||||
}
|
||||
|
||||
function tileHasFailure(snapshot: VoiceMediaGraphSnapshot, target: VoiceMediaGraphStreamTileTarget): boolean {
|
||||
if (!target.streamKey && !target.participantIdentity) return false;
|
||||
const failure = selectVoiceMediaGraphFailure(snapshot, {
|
||||
export interface VoiceMediaGraphStreamTileRecovery {
|
||||
hasRecoveryBudget: boolean;
|
||||
nowMs: number;
|
||||
}
|
||||
|
||||
function selectTileFailure(
|
||||
snapshot: VoiceMediaGraphSnapshot,
|
||||
target: VoiceMediaGraphStreamTileTarget,
|
||||
): VoiceMediaGraphFailure | null {
|
||||
if (!target.streamKey && !target.participantIdentity) return null;
|
||||
return selectVoiceMediaGraphFailure(snapshot, {
|
||||
streamKey: target.streamKey,
|
||||
participantIdentity: target.participantIdentity,
|
||||
source: target.source,
|
||||
});
|
||||
return failure !== null;
|
||||
}
|
||||
|
||||
function selectFailureTileState(
|
||||
failure: VoiceMediaGraphFailure,
|
||||
recovery: VoiceMediaGraphStreamTileRecovery | undefined,
|
||||
): VoiceMediaGraphStreamTileState {
|
||||
if (!recovery) return 'failed';
|
||||
if (failure.code !== VOICE_MEDIA_GRAPH_FIRST_FRAME_TIMEOUT_FAILURE.code) return 'failed';
|
||||
if (!recovery.hasRecoveryBudget) return 'failed';
|
||||
if (recovery.nowMs - failure.reportedAt >= VOICE_MEDIA_GRAPH_FIRST_FRAME_RECOVERY_VISIBLE_MS) return 'failed';
|
||||
return 'recovering';
|
||||
}
|
||||
|
||||
function tileIsRendering(
|
||||
@@ -48,11 +71,13 @@ function tileIsRendering(
|
||||
export function selectVoiceMediaGraphStreamTileState(
|
||||
snapshot: VoiceMediaGraphSnapshot,
|
||||
target: VoiceMediaGraphStreamTileTarget,
|
||||
recovery?: VoiceMediaGraphStreamTileRecovery,
|
||||
): VoiceMediaGraphStreamTileState {
|
||||
const entry = target.participantIdentity
|
||||
? selectVoiceMediaGraphSubscriptionEntry(snapshot, target.participantIdentity, target.source)
|
||||
: null;
|
||||
if (tileHasFailure(snapshot, target)) return 'failed';
|
||||
const failure = selectTileFailure(snapshot, target);
|
||||
if (failure !== null) return selectFailureTileState(failure, recovery);
|
||||
if (tileIsRendering(snapshot, target, entry)) return 'rendering';
|
||||
if (entry?.actual.lastError) return 'failed';
|
||||
if (entry?.actual.subscribed === true) return 'subscribedAwaitingFrame';
|
||||
|
||||
Reference in new issue
Block a user