Files
v10/dist/dev/playback/behaviors/dom/update-mediasource-duration.js
T

91 lines
4.5 KiB
JavaScript

import { computed } from "../../../core/signals/primitives.js";
import { defineBehavior } from "../../../core/composition/create-composition.js";
import { createMachineReactor } from "../../../core/reactors/create-machine-reactor.js";
import { waitForMediaSourceOpen } from "../../../media/dom/mse/mediasource-setup.js";
import { getMaxBufferedEnd, shouldUpdateDuration, waitForSourceBuffersReady } from "../../../media/dom/mse/duration.js";
//#region src/playback/behaviors/dom/update-mediasource-duration.ts
/**
* **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.
*
* The entry resolves three async preconditions in order before writing:
*
* 1. **MediaSource open** — `waitForMediaSourceOpen` defers until the first
* `sourceopen` event (or any readyState transition, since `'ended'` /
* `'closed'` mean we've missed the window and should bail).
* 2. **All SourceBuffers idle** — `waitForSourceBuffersReady` defers per
* the MSE-spec rule that `duration` cannot be set while any buffer has
* `updating === true`.
* 3. **Buffered-range clamp** — `getMaxBufferedEnd` ensures the written
* duration is at least the highest buffered end (MSE spec disallows
* a smaller `duration` than any buffered range).
*
* The buffer-set helpers operate across `mediaSource.sourceBuffers` (the
* canonical aggregate), so the behavior composes uniformly across
* audio-only, video-only, and mixed configurations.
*
* Single-positive-state reactor (`'preconditions-unmet'` ↔ `'duration-writable'`):
* state derivation is purely signal-driven (presentation validity +
* mediaSource existence). MediaSource lifecycle state (`readyState`) and
* the `duration` itself are non-signal DOM properties resolved inside the
* entry's async sequence. The state-exit cleanup aborts the in-flight
* wait, so source resets and behavior destroy structurally cancel the
* pending write. A post-await re-check of `mediaSource.readyState ===
* 'open'` covers the narrow race where `endOfStream()` synchronously
* transitions readyState to `'ended'` between our `waitForMediaSourceOpen`
* resolution and the `mediaSource.duration` write.
*
* Downstream of `calculatePresentationDuration` (which writes
* `presentation.duration`); concurrent with `endOfStream` (which may later
* write `mediaSource.duration` — the "exactly once" contract keeps us out
* of that path).
*/
function deriveState(presentation, mediaSource) {
return shouldUpdateDuration(presentation, mediaSource) ? "duration-writable" : "preconditions-unmet";
}
function updateMediaSourceDurationSetup({ state, context }) {
const derivedStateSignal = computed(() => deriveState(state.presentation.get(), context.mediaSource.get()));
return createMachineReactor({
initial: "preconditions-unmet",
monitor: () => derivedStateSignal.get(),
states: {
"preconditions-unmet": {},
"duration-writable": { entry: () => {
const presentation = state.presentation.get();
const mediaSource = context.mediaSource.get();
if (!Number.isNaN(mediaSource.duration)) return;
const controller = new AbortController();
const writeWhenReady = async () => {
await waitForMediaSourceOpen(mediaSource, controller.signal);
if (controller.signal.aborted) return;
if (mediaSource.readyState !== "open") return;
await waitForSourceBuffersReady(mediaSource.sourceBuffers, controller.signal);
if (controller.signal.aborted) return;
if (mediaSource.readyState !== "open") return;
const maxBufferedEnd = getMaxBufferedEnd(mediaSource.sourceBuffers);
const duration = maxBufferedEnd > presentation.duration ? maxBufferedEnd : presentation.duration;
mediaSource.duration = duration;
};
writeWhenReady();
return () => controller.abort();
} }
}
});
}
const updateMediaSourceDuration = defineBehavior({
stateKeys: ["presentation"],
contextKeys: ["mediaSource"],
setup: updateMediaSourceDurationSetup
});
//#endregion
export { updateMediaSourceDuration };
//# sourceMappingURL=update-mediasource-duration.js.map