diff --git a/packages/spf/src/playback/behaviors/dom/sync-live-seekable-range.ts b/packages/spf/src/playback/behaviors/dom/sync-live-seekable-range.ts index 01b52e2b..7212b4ce 100644 --- a/packages/spf/src/playback/behaviors/dom/sync-live-seekable-range.ts +++ b/packages/spf/src/playback/behaviors/dom/sync-live-seekable-range.ts @@ -11,10 +11,11 @@ * so the range is declared before that behavior seeks the playhead into it (a * seek outside `seekable` is clamped). * - * Out of scope (deliberate, tracked as follow-ups): `clearLiveSeekableRange()` - * on the live→ended transition; and dropping the defensive `duration` write - * below once duration ownership/ordering with `updateMediaSourceDuration` (the - * canonical, but asynchronous, duration writer) is resolved. + * Duration is owned solely by `updateMediaSourceDuration`; this behavior only + * declares the seekable range (`setLiveSeekableRange` requires only + * `readyState === 'open'` per the W3C MSE spec, not a set `duration`). Out of + * scope (tracked follow-up): `clearLiveSeekableRange()` on the live→ended + * transition. */ import type { Behavior } from '../../../core/composition/create-composition'; import { effect } from '../../../core/signals/effect'; @@ -49,15 +50,11 @@ function syncLiveSeekableRangeSetup({ if (!mediaSource || mediaSource.readyState !== 'open' || !liveWindow) return; try { - // A live seekable range needs a set duration. `updateMediaSourceDuration` - // is the canonical duration owner, but writes asynchronously — so guard - // here so this doesn't race ahead of it. (Follow-up: resolve ownership.) - if (Number.isNaN(mediaSource.duration)) mediaSource.duration = Number.POSITIVE_INFINITY; // Re-declared as the window slides so seekable tracks the live window // (the full DVR range remains seekable; seek-to-live-edge starts near the edge). mediaSource.setLiveSeekableRange(liveWindow.start, liveWindow.end); } catch { - // readyState raced closed, or duration set rejected — retried on the next window change. + // readyState raced closed — retried on the next window change. } }); } diff --git a/packages/spf/src/playback/behaviors/dom/tests/sync-live-seekable-range.test.ts b/packages/spf/src/playback/behaviors/dom/tests/sync-live-seekable-range.test.ts index 3826b9cf..869398b0 100644 --- a/packages/spf/src/playback/behaviors/dom/tests/sync-live-seekable-range.test.ts +++ b/packages/spf/src/playback/behaviors/dom/tests/sync-live-seekable-range.test.ts @@ -55,13 +55,12 @@ function run(opts: { presentation?: MaybeResolvedPresentation; trackId?: string; } describe('syncLiveSeekableRange', () => { - it('declares the full live window as seekable and sets Infinity duration', () => { + it('declares the full live window as seekable', () => { const ms = fakeMediaSource(); const cleanup = run({ presentation: makePresentation(), trackId: 'v-1', mediaSource: ms }); // [first.startTime, last.startTime + last.duration] = [100, 110]. expect(ms.setLiveSeekableRange).toHaveBeenCalledWith(100, 110); - expect(ms.duration).toBe(Number.POSITIVE_INFINITY); cleanup(); }); @@ -96,13 +95,13 @@ describe('syncLiveSeekableRange', () => { cleanup(); }); - it('does not overwrite an already-set (non-NaN) duration', () => { + it('leaves duration alone — owned by updateMediaSourceDuration', () => { const ms = fakeMediaSource(); - ms.duration = 500; // e.g. updateMediaSourceDuration already wrote it const cleanup = run({ presentation: makePresentation(), trackId: 'v-1', mediaSource: ms }); - expect(ms.duration).toBe(500); + // Declares the range without touching duration (still NaN from the fake). expect(ms.setLiveSeekableRange).toHaveBeenCalledWith(100, 110); + expect(ms.duration).toBeNaN(); cleanup(); });