refactor(spf): make updateMediaSourceDuration the sole duration writer

syncLiveSeekableRange dropped its defensive mediaSource.duration = Infinity
write — it duplicated updateMediaSourceDuration (the canonical owner). Per the
W3C MSE spec, setLiveSeekableRange requires only readyState === 'open', not a
set duration, so this behavior never needed it. Verified live (Mux LL-HLS):
with the write removed, duration is Infinity and the initial seek lands at the
holdback from the first frame — updateMediaSourceDuration's async write resolves
fast at startup (buffers idle), ahead of seekToLiveEdge's seek.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2026-06-25 10:00:20 -07:00
co-authored by Claude Opus 4.8
parent 7ccac9168e
commit 3859da3dbd
2 changed files with 10 additions and 14 deletions
@@ -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.
}
});
}
@@ -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();
});