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

114 lines
5.7 KiB
JavaScript

import { computed, signal } from "../../../core/signals/primitives.js";
import { defineBehavior } from "../../../core/composition/create-composition.js";
import { createMachineReactor } from "../../../core/reactors/create-machine-reactor.js";
import { isResolvedPresentation } from "../../../media/types/index.js";
import { attachMediaSource, createMediaSource, waitForMediaSourceOpen } from "../../../media/dom/mse/mediasource-setup.js";
import { listen } from "@videojs/utils/dom";
//#region src/playback/behaviors/dom/setup-mediasource.ts
/**
* **Own the MediaSource lifecycle for the current source.** When a resolved
* presentation and a mediaElement are both in scope, creates a MediaSource,
* attaches it to the element, waits for `'open'`, and publishes it on
* `context.mediaSource`. On source change or behavior destroy, detaches the
* MediaSource and clears the slot so the next source starts fresh.
*
* Single-positive-state reactor (`'preconditions-unmet'` ↔ `'mediasource-attached'`):
* state derivation gates on `mediaElement + isResolvedPresentation`. Riding the
* resolver's resolved/unresolved lifecycle makes direct URL replacement
* structural — `resolvePresentation` routes the presentation back through
* unresolved on URL change, which drives this reactor through
* `'preconditions-unmet'` so the entry's state-exit cleanup detaches the old
* MediaSource before the new one is built.
*
* The entry resolves preconditions in sequence before publishing:
*
* 1. **Create + attach** — `createMediaSource` + `attachMediaSource` run
* synchronously on entry. The `detach` closure returned by
* `attachMediaSource` is captured for state-exit cleanup, so the cleanup
* is always bound to its setup even if the wait below is aborted.
* 2. **Wait for `'open'`** — `waitForMediaSourceOpen` defers until the first
* `sourceopen` event (or any readyState transition out of `'closed'`).
* 3. **Publish on `'open'`** — re-check `readyState === 'open'` after the
* await (covers `'ended'` / `'closed'` race) before writing to
* `context.mediaSource`. Downstream `setupVideoBufferActors` /
* `setupAudioBufferActors` call `addSourceBuffer` directly, which
* throws on non-open, so publish-only-when-open is the load-bearing
* contract.
*
* State-exit cleanup aborts the in-flight wait, detaches the MediaSource,
* and clears `context.mediaSource`. Order: abort first (prevents a late
* publish racing the slot clear), then detach, then clear.
*
* # Sourceclose recovery
*
* The behavior owns one **unclosed** MediaSource per source identity. The UA can
* close the attached MediaSource out from under the engine (Safari on an
* AirPlay handoff — see `setupAirPlay` — or a ManagedMediaSource evicted
* under memory pressure), and a closed MediaSource can never reopen. The
* `sourceclose` listener tears the attachment down synchronously; every
* teardown records a local close-fact, which holds the machine out until
* the fact is consumed — then the re-derive comes back in with a fresh
* MediaSource for the *same* source. Cause-agnostic. Consumption honors an
* observed `loadingSuspended` (attaching runs `element.load()` — new loading
* work, e.g. resource selection under an active AirPlay receiver), and
* happens only while the machine is out, so a suspension can never tear
* down an existing attachment.
*
* Sole writer of `context.mediaSource`; other MSE behaviors
* (`setupVideoBufferActors`, `setupAudioBufferActors`,
* `updateMediaSourceDuration`, `endOfStream`, `loadVideoSegments`) only
* read.
*/
function deriveState(presentation, mediaElement, mediaSourceClosed) {
if (!mediaElement || !isResolvedPresentation(presentation)) return "preconditions-unmet";
if (mediaSourceClosed) return "preconditions-unmet";
return "mediasource-attached";
}
function setupMediaSourceSetup({ state, context, config = {} }) {
const attach = config.attachMediaSource ?? attachMediaSource;
const loadingSuspended = state.loadingSuspended;
const mediaSourceClosed = signal(false);
const derivedStateSignal = computed(() => deriveState(state.presentation.get(), context.mediaElement.get(), mediaSourceClosed.get()));
return createMachineReactor({
initial: "preconditions-unmet",
monitor: () => derivedStateSignal.get(),
states: {
"preconditions-unmet": { effects: () => {
if (mediaSourceClosed.get() && !loadingSuspended?.get()) mediaSourceClosed.set(false);
} },
"mediasource-attached": { entry: () => {
const mediaElement = context.mediaElement.get();
const controller = new AbortController();
const mediaSource = createMediaSource({ preferManaged: true });
const { detach } = attach(mediaSource, mediaElement);
const teardown = () => {
mediaSourceClosed.set(true);
controller.abort();
context.mediaSource.set(void 0);
detach({ deferReset: true });
};
listen(mediaSource, "sourceclose", teardown, { signal: controller.signal });
const publishWhenOpen = async () => {
await waitForMediaSourceOpen(mediaSource, controller.signal);
if (controller.signal.aborted) return;
if (mediaSource.readyState !== "open") {
console.warn(`[setupMediaSource] MediaSource transitioned to '${mediaSource.readyState}' before first 'sourceopen' — slot left unpublished; recoverable on next source reset.`);
return;
}
context.mediaSource.set(mediaSource);
};
publishWhenOpen().catch((err) => console.error("[setupMediaSource] failed to publish MediaSource:", err));
return teardown;
} }
}
});
}
const setupMediaSource = defineBehavior({
stateKeys: ["presentation"],
contextKeys: ["mediaElement", "mediaSource"],
setup: setupMediaSourceSetup
});
//#endregion
export { setupMediaSource };
//# sourceMappingURL=setup-mediasource.js.map