mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
fix(spf): set mediaSource.duration = Infinity eagerly for live
For live, presentation.duration is Infinity as soon as the presentation resolves — before any segment appends. updateMediaSourceDuration is composed before the buffer actors, so its entry runs while the MediaSource is freshly open and empty: write Infinity synchronously there (no buffered clamp needed, Infinity ≥ any range). This gets ahead of the first append, which would otherwise set duration to the buffered end (MSE coded-frame-processing) and pin the live stream to a finite, live-edge duration — the async wait-for-idle path loses that race because a live loader appends continuously. VoD keeps the existing wait-open/idle + clamp + write-once-while-NaN path. Verified against a live Mux CMAF stream: mediaSource.duration is Infinity from the start, no finite transient. 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
6780a44ccb
commit
b4c7db2653
@@ -331,4 +331,21 @@ describe('updateMediaSourceDuration', () => {
|
||||
|
||||
reactor.destroy();
|
||||
});
|
||||
|
||||
it('writes Infinity for live even when an append already set a finite duration', async () => {
|
||||
// Live race: the first segment append sets duration to the buffered end
|
||||
// before this behavior writes. The once-while-NaN guard would leave it
|
||||
// finite; for Infinity we override (Infinity ≥ any buffered range).
|
||||
const { state, context, reactor } = setupUpdateMediaSourceDuration();
|
||||
|
||||
const mockMediaSource = makeMediaSource({ duration: 30 });
|
||||
context.mediaSource.set(mockMediaSource);
|
||||
state.presentation.set({ duration: Number.POSITIVE_INFINITY } as Presentation);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockMediaSource.duration).toBe(Number.POSITIVE_INFINITY);
|
||||
});
|
||||
|
||||
reactor.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,13 +2,22 @@
|
||||
* **Propagate `presentation.duration` to `mediaSource.duration` — exactly
|
||||
* once per MediaSource.**
|
||||
*
|
||||
* When the presentation has a valid positive duration (including `Infinity`
|
||||
* for live) and a MediaSource is in context, writes the value through to
|
||||
* `mediaSource.duration` on initial setup — once, while `mediaSource.duration`
|
||||
* is still `NaN`. Once any non-NaN value is present (set by us, or by
|
||||
* `endOfStream` from the buffered end), the behavior leaves the property
|
||||
* alone; re-syncing a drift against `presentation.duration` would race with
|
||||
* concurrent `appendBuffer()` calls.
|
||||
* Two paths, by whether the presentation is live:
|
||||
*
|
||||
* - **Live** (`presentation.duration === Infinity`): written **synchronously**
|
||||
* on entry. The presentation declares `Infinity` as soon as it resolves —
|
||||
* before any segment append — and this behavior is composed before the buffer
|
||||
* actors, so it runs while the MediaSource is freshly open and empty. Writing
|
||||
* now (no buffered clamp needed; `Infinity` ≥ any range) gets ahead of the
|
||||
* first append, which would otherwise set `duration` to the buffered end and
|
||||
* pin the live stream to a finite (live-edge) duration.
|
||||
*
|
||||
* - **VoD** (finite): the value is written once, after `mediaSource` is open and
|
||||
* all SourceBuffers are idle, clamped to be ≥ the highest buffered range (MSE
|
||||
* spec). Written only while `mediaSource.duration` is still `NaN`; once any
|
||||
* non-NaN value is present (us on a prior entry, or `endOfStream` from the
|
||||
* buffered end), the property is left alone — re-syncing would race
|
||||
* concurrent `appendBuffer()` calls.
|
||||
*
|
||||
* The entry resolves three async preconditions in order before writing:
|
||||
*
|
||||
@@ -88,11 +97,25 @@ function updateMediaSourceDurationSetup({
|
||||
const presentation = state.presentation.get()!;
|
||||
const mediaSource = context.mediaSource.get()!;
|
||||
|
||||
// Idempotency: someone (us on a prior entry, or concurrent
|
||||
// `endOfStream`) has already written. No-op and leave the state
|
||||
// alone — the next source-reset transition will produce a fresh
|
||||
// entry against the new MediaSource (whose `duration` starts at
|
||||
// `NaN` again).
|
||||
// Live: the presentation declares `Infinity` as soon as it resolves —
|
||||
// before any segment append. This entry runs while the MediaSource is
|
||||
// freshly open and still empty (it's composed before the buffer
|
||||
// actors), so write it now, synchronously: no buffered clamp is needed
|
||||
// (`Infinity` ≥ any range), and getting ahead of the first append is
|
||||
// what stops the append pinning a finite (live-edge) duration. The
|
||||
// async wait-for-idle path below would lose that race — a live loader
|
||||
// appends continuously, so the buffers are rarely all idle.
|
||||
if (presentation.duration === Number.POSITIVE_INFINITY) {
|
||||
if (mediaSource.readyState === 'open' && mediaSource.duration !== Number.POSITIVE_INFINITY) {
|
||||
mediaSource.duration = Number.POSITIVE_INFINITY;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// VoD: write the finite duration once, while it is still `NaN`. Once
|
||||
// any non-NaN value is present (us on a prior entry, or `endOfStream`
|
||||
// from the buffered end), leave it alone — re-syncing a drift against
|
||||
// `presentation.duration` would race concurrent `appendBuffer()`.
|
||||
if (!Number.isNaN(mediaSource.duration)) return;
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
Reference in New Issue
Block a user