mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
91 lines
4.5 KiB
JavaScript
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
|