# The HLS Engine A reference walkthrough of how SPF composes a playback engine. The HLS engine in `@videojs/spf/hls` is one specific composition — manifest resolution, track selection, MSE setup, segment loading, end-of-stream — but the patterns it uses are how any SPF playback engine is built. This doc walks the composition stage-by-stage and calls out the patterns that recur. It assumes familiarity with HLS, MSE, and adaptive streaming. If you want to learn what a "switching set" is, look elsewhere. If you want to learn how SPF lets you turn those concepts into a composable, declarative engine, you're in the right place. For SPF primitives (signals, reactors, tasks, actors), see [fundamentals.md](./fundamentals.md). --- ## The engine at a glance `createSimpleHlsEngine` is a thin wrapper around `createComposition`. The full composition, lifted from `src/playback/engines/hls/engine.ts`: ```ts const shareSignals = makeShareSignals(); export function createSimpleHlsEngine( config: SimpleHlsEngineConfig = {} ): Composition { return createComposition( [ syncPreload, trackPlaybackInitiated, resolvePresentation, // Track selection (reads config for initial preferences) selectVideoTrack, selectAudioTrack, selectTextTrack, // Resolve selected tracks (fetch media playlists) resolveVideoTrack, resolveAudioTrack, resolveTextTrack, // Presentation duration calculatePresentationDuration, // MSE setup setupMediaSource, updateMediaSourceDuration, setupSourceBuffers, // Playback tracking trackCurrentTime, switchQuality, // Segment loading loadVideoSegments, loadAudioSegments, // End of stream coordination endOfStream, // Text tracks syncTextTracks, setupTextTrackActors, loadTextTrackSegments, // Hands writable signal refs to the consumer's onSignalsReady callback // so external code (the adapter, or any direct consumer) can drive the // engine. Placed last so other behaviors' setup has run by the time the // callback fires — initial state writes are visible to the consumer. shareSignals, ], { config, initialState } ); } ``` Read top to bottom, the engine tells a story: resolve a manifest, pick tracks, set up MSE, load segments, coordinate end-of-stream, render text tracks. Each line is a behavior — a small, focused unit of logic that owns one job. To build a different engine — fewer behaviors, different protocol, different platform — you change the list. Three things are doing the work here: 1. **`createComposition`** — the SPF primitive that wires the behaviors together. It owns the lifecycle, derives the state and context signal maps from each behavior's declared `stateKeys` / `contextKeys`, and gives each behavior access to the slots it asks for plus the engine's static `config`. 2. **The behaviors** — independent, type-specialized objects built with `defineBehavior`. Each declares which slots it reads and writes and contributes its body. No engine-side wrappers — `selectVideoTrack`, `loadAudioSegments`, etc. are imported directly from their behavior modules. 3. **`shareSignals`** — a generic passthrough behavior that hands the composition's writable signal refs to a consumer-supplied `config.onSignalsReady` callback at setup time. The canonical way to drive the engine from outside. The rest of this doc walks each stage. --- ## State, context, and config Every SPF composition is parameterized by three shapes: ```ts export interface SimpleHlsEngineState { /** * The presentation being played. A caller writes `{ url }`; * `resolvePresentation` parses the manifest and populates the rest. */ presentation?: MaybeResolvedPresentation; preload?: 'auto' | 'metadata' | 'none'; selectedVideoTrackId?: string; selectedAudioTrackId?: string; selectedTextTrackId?: string; bandwidthState?: BandwidthState; abrDisabled?: boolean; currentTime?: number; playbackInitiated?: boolean; mediaSourceReadyState?: MediaSource['readyState']; } export interface SimpleHlsEngineContext { mediaElement?: HTMLMediaElement; mediaSource?: MediaSource; videoBuffer?: SourceBuffer; audioBuffer?: SourceBuffer; videoBufferActor?: SourceBufferActor; audioBufferActor?: SourceBufferActor; textTracksActor?: TextTracksActor; textTrackSegmentLoaderActor?: TextTrackSegmentLoaderActor; } export interface SimpleHlsEngineConfig extends ShareSignalsConfig { initialBandwidth?: number; preferredAudioLanguage?: string; preferredSubtitleLanguage?: string; includeForcedTracks?: boolean; enableDefaultTrack?: boolean; } ``` The split: - **State** holds reactive playback data — the manifest, selected track ids, current time, bandwidth estimate. It flows through the composition over time. Each field is its own discrete signal that any behavior can read or (if the behavior typed the slot writable) write. - **Context** holds **resources** — values with identity and imperative interfaces, not just data. The `