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