Files
v10/packages/spf/src/playback/engines/background-video/engine.ts
T

181 lines
6.9 KiB
TypeScript

import {
type Composition,
type ContextSignals,
createComposition,
type StateSignals,
} from '../../../core/composition/create-composition';
import { makeShareSignals, type ShareSignalsConfig } from '../../../core/composition/share-signals';
import { parseMultivariantPlaylist } from '../../../media/hls/parse-multivariant';
import { pickHighestResolutionVideoTrack, type TrackPicker } from '../../../media/primitives/select-tracks';
import type { MaybeResolvedPresentation } from '../../../media/types';
import { getResolvedSelectedTrackDuration } from '../../../media/utils/track-selection';
import type { SegmentLoaderActor } from '../../actors/dom/segment-loader';
import type { SourceBufferActor } from '../../actors/dom/source-buffer';
import { calculatePresentationDuration } from '../../behaviors/calculate-presentation-duration';
import { endOfStream } from '../../behaviors/dom/end-of-stream';
import { loadVideoSegments } from '../../behaviors/dom/load-segments';
import { setupVideoBufferActors } from '../../behaviors/dom/setup-buffer-actors';
import { setupMediaSource } from '../../behaviors/dom/setup-mediasource';
import { trackCurrentTime } from '../../behaviors/dom/track-current-time';
import { updateMediaSourceDuration } from '../../behaviors/dom/update-mediasource-duration';
import { type ParsePresentation, resolvePresentation } from '../../behaviors/resolve-presentation';
import { resolveVideoTrack } from '../../behaviors/resolve-track';
import { type SelectVideoTrackConfig, selectVideoTrack } from '../../behaviors/select-tracks';
// ============================================================================
// Background-video engine state & context
// ============================================================================
/**
* State shape for the background-video playback engine.
*
* Narrower than `SimpleHlsEngineState`: audio/text track slots are absent
* because their selection/resolution behaviors are subtracted. `bandwidthState`
* is present because `setupVideoBufferActors` declares it and `loadVideoSegments`
* samples into it (wasted work in this variant — a Phase 3 alt-impl will skip
* sampling).
*/
export interface BackgroundVideoEngineState {
/**
* The presentation being played. A caller writes `{ url }`;
* `resolvePresentation` parses the manifest and populates the rest.
*/
presentation?: MaybeResolvedPresentation;
preload?: 'auto' | 'metadata' | 'none';
selectedVideoTrackId?: string;
loadActivated?: boolean;
}
/**
* Context shape for the background-video engine.
*/
export interface BackgroundVideoEngineContext {
mediaElement?: HTMLMediaElement | undefined;
mediaSource?: MediaSource;
videoBufferActor?: SourceBufferActor;
videoSegmentLoaderActor?: SegmentLoaderActor;
}
/**
* The composition signal refs handed to `onSignalsReady` callers — the
* canonical way to drive the engine externally (writes) or observe its
* state (reads) without touching `composition.state` / `composition.context`
* directly.
*/
export type BackgroundVideoEngineSignals = {
state: StateSignals<BackgroundVideoEngineState>;
context: ContextSignals<BackgroundVideoEngineContext>;
};
/**
* Configuration for the background-video engine.
*
* Each option is consumed by the appropriate behavior — the engine itself
* has no config beyond what its behaviors read. Compared to
* `SimpleHlsEngineConfig`, audio/text/ABR/bandwidth/quality knobs are
* dropped: the variant subtracts the behaviors that read them.
*/
export interface BackgroundVideoEngineConfig
extends ShareSignalsConfig<BackgroundVideoEngineState, BackgroundVideoEngineContext> {
/**
* Track picker handed to `selectVideoTrack`. Default:
* `pickHighestResolutionVideoTrack` — picks the highest-resolution variant on
* presentation resolve and pins it for the session. Override for
* mobile-aware or content-aware caps.
*
* Adapters (e.g. `BackgroundVideoMediaElement`) install their own
* picker; this default applies when the engine is constructed directly.
*/
picker?: TrackPicker<SelectVideoTrackConfig>;
/**
* Manifest parser handed to `resolvePresentation`. Defaults to the HLS
* multivariant-playlist parser.
*/
parsePresentation?: ParsePresentation;
}
// ============================================================================
// Background-video playback engine
// ============================================================================
const shareSignals = makeShareSignals<BackgroundVideoEngineState, BackgroundVideoEngineContext>();
/**
* Create a background-video playback engine.
*
* Subtractive composition over the HLS engine baseline:
* audio-side, text-side, ABR-driven, preload-monitoring, and play/seek
* load-trigger behaviors are removed. `selectVideoTrack` (with a
* max-resolution picker by default) replaces `switchVideoQuality`, pinning
* a single rendition for the session. The initial state seeds
* `loadActivated: true` so the composition behaves as if preload has
* already been activated — appropriate for ambient / hero / GIF-replacement
* surfaces that should start loading the moment a src is set.
*
* Native `loop` / `muted` / `autoplay` are adapter concerns and live on
* `BackgroundVideoMediaElement` rather than the engine.
*
* @example
* ```ts
* let signals: BackgroundVideoEngineSignals;
* const engine = createBackgroundVideoEngine({
* onSignalsReady: (refs) => {
* signals = refs;
* },
* });
*
* signals.context.mediaElement.set(videoEl);
* signals.state.presentation.set({ url: 'https://example.com/stream.m3u8' });
*
* await engine.destroy();
* ```
*/
export function createBackgroundVideoEngine(
config: BackgroundVideoEngineConfig = {}
): Composition<BackgroundVideoEngineState, BackgroundVideoEngineContext> {
const finalConfig = {
...config,
picker: config.picker ?? pickHighestResolutionVideoTrack,
parsePresentation: config.parsePresentation ?? parseMultivariantPlaylist,
resolveDuration: getResolvedSelectedTrackDuration,
};
return createComposition(
[
resolvePresentation,
// Presentation duration
calculatePresentationDuration,
// Track selection - pinned single-rendition pick on presentation resolve.
selectVideoTrack,
// Resolve selected video track (fetch its media playlist)
resolveVideoTrack,
// Segment loading — video-only.
loadVideoSegments,
// MSE setup — video-only.
setupMediaSource,
updateMediaSourceDuration,
setupVideoBufferActors,
// Playback tracking
trackCurrentTime,
// End of stream coordination
endOfStream,
// Behavior whose sole purpose is to expose signal refs via a callback
// (e.g. to an adapter). Listed last so initial signal setup has run
// before the callback fires.
shareSignals,
],
{
config: finalConfig,
initialState: {
// Note: Set to true until we add preload configuration
loadActivated: true,
},
}
);
}