mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
fix(spf): derive the live window from any resolved track during selection churn
liveWindowFromState keyed the live window on the *selected* timeline-bearing track, so when an ABR/user switch briefly left the newly-selected rendition unresolved (a shell, no segments), the window blinked to null. That flipped seekToLiveEdge out of `live` and stalled the seekable-range writer. The live window is a presentation-level property — all renditions are time-aligned and share the anchor — so fall back to any resolved track of the timeline-bearing type when the selected one isn't resolved yet, rather than returning null. A deselected track's window may trail live by up to one reload during the switch gap; the selected track's fresh window resumes the moment it resolves. 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
7c99f266a6
commit
4572730e0b
@@ -14,6 +14,7 @@
|
||||
import type { ReadonlySignal } from '../../core/signals/primitives';
|
||||
import { type LiveWindow, liveWindowFor } from '../../media/live-window';
|
||||
import type { MaybeResolvedPresentation } from '../../media/types';
|
||||
import { getTracksByType } from '../../media/utils/tracks';
|
||||
|
||||
export interface LiveWindowState {
|
||||
presentation: ReadonlySignal<MaybeResolvedPresentation | undefined>;
|
||||
@@ -31,7 +32,28 @@ export function liveTrackId(state: LiveWindowState): string | undefined {
|
||||
}
|
||||
|
||||
export function liveWindowFromState(state: LiveWindowState): LiveWindow | null {
|
||||
return liveWindowFor(state.presentation.get(), liveTrackId(state));
|
||||
const presentation = state.presentation.get();
|
||||
|
||||
// Prefer the selected timeline-bearing track when it's resolved.
|
||||
const selected = liveWindowFor(presentation, liveTrackId(state));
|
||||
if (selected) return selected;
|
||||
|
||||
// The selected track isn't resolved yet (e.g. briefly mid ABR / user switch).
|
||||
// The live window is a presentation-level property — all renditions are
|
||||
// time-aligned and share the anchor — so fall back to any resolved track of the
|
||||
// timeline-bearing type rather than letting the window blink to `null`. A null
|
||||
// blink would flip `seekToLiveEdge` out of `live` and re-fire its one-time edge
|
||||
// seek (yanking a viewer who has scrubbed back), and stall the seekable-range
|
||||
// writer. A deselected track's window may trail live by up to one reload during
|
||||
// the switch gap — acceptable, and the selected track's fresh window resumes the
|
||||
// moment it resolves.
|
||||
if (!presentation) return null;
|
||||
const type = state.selectedVideoTrackId?.get() ? 'video' : 'audio';
|
||||
for (const track of getTracksByType(presentation, type)) {
|
||||
const window = liveWindowFor(presentation, track.id);
|
||||
if (window) return window;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type AudioTrack,
|
||||
type MaybeResolvedPresentation,
|
||||
MEDIA_PLAYLIST_METADATA_KEY,
|
||||
type PartiallyResolvedVideoTrack,
|
||||
type Presentation,
|
||||
type SelectionSet,
|
||||
type VideoSelectionSet,
|
||||
@@ -98,6 +99,29 @@ describe('liveWindowFromState', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to a resolved track when the selected track is not yet resolved (mid-switch)', () => {
|
||||
// ABR/user switch in flight: the selected rendition (v-2) is still a shell.
|
||||
// The window is presentation-level, so it must come from the resolved sibling
|
||||
// (v-1) rather than blinking to null.
|
||||
const shell: PartiallyResolvedVideoTrack = {
|
||||
type: 'video',
|
||||
id: 'v-2',
|
||||
url: 'https://example.com/v2.m3u8',
|
||||
mimeType: 'video/mp4',
|
||||
codecs: ['avc1.640028'],
|
||||
bandwidth: 2_000_000,
|
||||
};
|
||||
const pres: Presentation = {
|
||||
id: 'pres-1',
|
||||
url: 'https://example.com/master.m3u8',
|
||||
startTime: 0,
|
||||
selectionSets: [
|
||||
{ id: 'v-set', type: 'video', switchingSets: [{ id: 'vss', type: 'video', tracks: [videoTrack(100), shell] }] },
|
||||
],
|
||||
};
|
||||
expect(liveWindowFromState(state({ presentation: pres, videoId: 'v-2' }))).toEqual({ start: 100, end: 110 });
|
||||
});
|
||||
|
||||
it('returns null when no track is selected', () => {
|
||||
const pres = presentation({ video: videoTrack(100) });
|
||||
expect(liveWindowFromState(state({ presentation: pres, videoId: undefined, audioId: undefined }))).toBeNull();
|
||||
|
||||
Reference in New Issue
Block a user