mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
fix(spf): keep the live reload loop alive across transient fetch failures
The reload loop's try/catch wrapped the while, so a single fetch/parse failure (e.g. a transient "TypeError: Failed to fetch", a CDN blip) ended the loop — the live playlist would stop refreshing and playback would eventually stall at the last-known window. Move the try inside the loop: on a non-abort error, log and retry on the next cadence; only abort (source change / destroy) exits. Adds a fake-timer test asserting the loop retries after a rejected fetch and resolves on the next attempt. Verified against a live Mux CMAF stream (window keeps advancing). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b4c7db2653
commit
fb5e6cbf95
@@ -115,8 +115,8 @@ function setupTrackReload<K extends SelectedTrackKey>({
|
||||
const trackId = state[selectedKey].get()!;
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
while (!ac.signal.aborted) {
|
||||
while (!ac.signal.aborted) {
|
||||
try {
|
||||
const presentation = peek(state.presentation);
|
||||
if (!isResolvedPresentation(presentation)) break;
|
||||
// The track currently in the presentation is the prior snapshot
|
||||
@@ -143,11 +143,19 @@ function setupTrackReload<K extends SelectedTrackKey>({
|
||||
const target = meta?.targetDuration || FALLBACK_TARGET_DURATION;
|
||||
// Spec: reload ~target duration; half that when the playlist was unchanged.
|
||||
await sleep((changed ? target : target / 2) * 1000, ac.signal);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'AbortError') return;
|
||||
// A transient fetch/parse failure must not kill the loop — a live
|
||||
// playlist has to keep refreshing. Log and retry on the next
|
||||
// cadence (the `while` re-checks `aborted`).
|
||||
// TODO(error-management): route to a state-error slot once one exists.
|
||||
console.error(`[reload:${type}] media-playlist reload failed; retrying:`, error);
|
||||
try {
|
||||
await sleep(FALLBACK_TARGET_DURATION * 1000, ac.signal);
|
||||
} catch {
|
||||
return; // aborted during the retry wait
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'AbortError') return;
|
||||
// TODO(error-management): route to a state-error slot once one exists.
|
||||
console.error(`[reload:${type}] media-playlist reload failed:`, error);
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
@@ -120,3 +120,39 @@ describe('reloadAudioTrack', () => {
|
||||
reactor.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('reload resilience', () => {
|
||||
it('survives a transient fetch failure and retries (does not kill the loop)', async () => {
|
||||
vi.useFakeTimers();
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
try {
|
||||
const state = {
|
||||
presentation: signal<MaybeResolvedPresentation | undefined>(makePresentation()),
|
||||
selectedVideoTrackId: signal<string | undefined>('v-1'),
|
||||
};
|
||||
let calls = 0;
|
||||
const fetchResolvableText = vi.fn(() => {
|
||||
calls += 1;
|
||||
return calls === 1 ? Promise.reject(new TypeError('Failed to fetch')) : Promise.resolve(MEDIA_PLAYLIST);
|
||||
});
|
||||
|
||||
const reactor = reloadVideoTrack.setup({ state, config: { fetchResolvableText } });
|
||||
|
||||
// First attempt rejects: logged, but the loop is still alive (track not yet resolved).
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(calls).toBe(1);
|
||||
expect(errorSpy).toHaveBeenCalled();
|
||||
expect(isResolvedTrack(findTrack(state.presentation.get()!, 'video', 'v-1')!)).toBe(false);
|
||||
|
||||
// Retry cadence elapses → second attempt succeeds (would never happen if the
|
||||
// loop had died on the first failure).
|
||||
await vi.advanceTimersByTimeAsync(6000);
|
||||
expect(calls).toBe(2);
|
||||
expect(isResolvedTrack(findTrack(state.presentation.get()!, 'video', 'v-1')!)).toBe(true);
|
||||
|
||||
reactor.destroy();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user