Files
v10/dist/dev/playback/behaviors/dom/load-segments.js
T

181 lines
6.4 KiB
JavaScript

import { computed, peek } from "../../../core/signals/primitives.js";
import { defineBehavior } from "../../../core/composition/create-composition.js";
import { createMachineReactor } from "../../../core/reactors/create-machine-reactor.js";
import { DEFAULT_FORWARD_BUFFER_CONFIG, segmentStartForTime } from "../../../media/buffer/forward-buffer.js";
import { findResolvedAudioTrack, findResolvedTextTrack, findResolvedVideoTrack } from "../../../media/utils/tracks.js";
import { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from "../../primitives/track-types.js";
//#region src/playback/behaviors/dom/load-segments.ts
/**
* **Per-type segment loading dispatch.** Per available track type (video /
* audio / text), reads the per-type segment-loader actor from context and
* sends typed `'load'` messages whenever a meaningful loading condition
* changes (selected track, current time crossing a segment boundary).
*
* Loader-actor lifecycle is owned upstream:
* - Video: `setupVideoBufferActors`
* - Audio: `setupAudioBufferActors`
* - Text: `setupTextTrackActors`
*
* This behavior is pure-consumer: it reads `context[loaderKey]` and
* dispatches typed messages via the variant's per-type `findResolvedTrack`
* resolver.
*
* # Load modes as reactor states
*
* Four states encode the load-gating policy directly:
*
* - `'preconditions-unmet'` — no loader actor in context, or the selected
* track hasn't resolved.
* - `'dormant'` — loading disabled by policy: an observed `loadingSuspended`
* (highest precedence) or `preload === 'none' && !loadActivated`. Nothing
* fires; already-queued loader work drains. Auto-resumes into the derived
* state when the policy lifts.
* - `'metadata-only'` — `!loadActivated && preload !== 'auto' && preload !== 'none'`.
* Fires an init-segment-only `load` message **once on entry**. The
* variant's loader actor decides what to do — v/a's actor fetches the
* init segment; text's actor no-ops (no init concept).
* - `'full-range'` — `loadActivated || preload === 'auto'`. Effect re-fires
* on selected-track change and on segment-boundary crossing.
*
* # Per-type parameterization (inference-driven)
*
* The helper is generic over `Track` — the resolved-track type. Each
* variant supplies its own `findResolvedTrack` resolver via config; TS
* infers `Track` from the resolver's return type. The loader signal's
* value type is constrained to `SegmentLoaderLike<Track>` — anything with
* a `send` method accepting the `Track`-parameterized message. Concrete
* actor types (`SegmentLoaderActor`, `TextTrackSegmentLoaderActor`)
* satisfy this via function-parameter contravariance: an actor whose
* `.send` accepts a wider track type is assignable to a slot expecting a
* narrower track type.
*
* No widening of actor message types is needed; no casts inside the
* helper. Per-variant wiring (right loader paired with right resolver)
* is enforced at the variant call site.
*/
/**
* Specialization helper. Generic over `Track` (inferred from
* `findResolvedTrack`'s return type). The loader value type is
* constrained to `SegmentLoaderLike<Track>` — concrete actor types
* (whose `.send` accepts a wider track union) satisfy this via
* function-parameter contravariance.
*
* Tracks that the helper handles must have a `segments` field — used by
* `segmentBoundarySignal` to compute the load-anchor boundary. Each
* variant's resolver narrows to the right resolved-track shape.
*/
function setupSegmentLoading({ state, context, config }) {
const { selectedKey, loaderKey, findResolvedTrack } = config;
const bufferDuration = config.forwardBuffer?.bufferDuration ?? DEFAULT_FORWARD_BUFFER_CONFIG.bufferDuration;
const selectedTrack = computed(() => findResolvedTrack(state.presentation.get(), state[selectedKey].get()));
const segmentBoundarySignal = computed(() => {
const track = selectedTrack.get();
if (!track) return void 0;
return segmentStartForTime(state.currentTime.get() ?? 0, track.segments);
});
const derivedStateSignal = computed(() => {
if (state.loadingSuspended?.get()) return "dormant";
if (!context[loaderKey].get() || !selectedTrack.get()) return "preconditions-unmet";
if (state.loadActivated.get() || state.preload.get() === "auto") return "full-range";
if (state.preload.get() === "none") return "dormant";
return "metadata-only";
});
return createMachineReactor({
initial: "preconditions-unmet",
monitor: () => derivedStateSignal.get(),
states: {
"preconditions-unmet": {},
dormant: {},
"metadata-only": { entry: () => {
const track = selectedTrack.get();
context[loaderKey].get().send({
type: "load",
track
});
} },
"full-range": { effects: () => {
const track = selectedTrack.get();
segmentBoundarySignal.get();
const currentTime = peek(state.currentTime) ?? 0;
peek(context[loaderKey]).send({
type: "load",
track,
range: {
start: currentTime,
end: currentTime + bufferDuration
}
});
} }
}
});
}
const VIDEO_SEGMENT_LOADING_CONFIG = {
...VIDEO_TYPE_CONFIG,
findResolvedTrack: findResolvedVideoTrack
};
const AUDIO_SEGMENT_LOADING_CONFIG = {
...AUDIO_TYPE_CONFIG,
findResolvedTrack: findResolvedAudioTrack
};
const TEXT_SEGMENT_LOADING_CONFIG = {
...TEXT_TYPE_CONFIG,
findResolvedTrack: findResolvedTextTrack
};
const loadVideoSegments = defineBehavior({
stateKeys: [
"presentation",
"preload",
"currentTime",
"loadActivated",
"selectedVideoTrackId"
],
contextKeys: ["videoSegmentLoaderActor"],
setup: ({ state, context, config = {} }) => setupSegmentLoading({
state,
context,
config: {
...VIDEO_SEGMENT_LOADING_CONFIG,
...config
}
})
});
const loadAudioSegments = defineBehavior({
stateKeys: [
"presentation",
"preload",
"currentTime",
"loadActivated",
"selectedAudioTrackId"
],
contextKeys: ["audioSegmentLoaderActor"],
setup: ({ state, context, config = {} }) => setupSegmentLoading({
state,
context,
config: {
...AUDIO_SEGMENT_LOADING_CONFIG,
...config
}
})
});
const loadTextTrackSegments = defineBehavior({
stateKeys: [
"presentation",
"preload",
"currentTime",
"loadActivated",
"selectedTextTrackId"
],
contextKeys: ["textTrackSegmentLoaderActor"],
setup: ({ state, context, config = {} }) => setupSegmentLoading({
state,
context,
config: {
...TEXT_SEGMENT_LOADING_CONFIG,
...config
}
})
});
//#endregion
export { loadAudioSegments, loadTextTrackSegments, loadVideoSegments };
//# sourceMappingURL=load-segments.js.map