feat(spf): HLS engine composition walkthrough + doc-driven cleanups (#1512)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2026-05-05 12:07:26 -07:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 17d44a5d32
commit 0cfd3bb395
103 changed files with 2509 additions and 1277 deletions
@@ -0,0 +1,230 @@
import { type Composition, createComposition } from '../../../core/composition/create-composition';
import type { Signal } from '../../../core/signals/primitives';
import type { BandwidthState } from '../../../media/abr/bandwidth-estimator';
import { resolveVttSegment } from '../../../media/dom/text/resolve-vtt-segment';
import type { MaybeResolvedPresentation } from '../../../media/types';
import type { SourceBufferActor } from '../../actors/dom/source-buffer';
import type { TextTracksActor } from '../../actors/dom/text-tracks';
import type { TextTrackSegmentLoaderActor } from '../../actors/text-track-segment-loader';
import { calculatePresentationDuration } from '../../behaviors/calculate-presentation-duration';
import { endOfStream } from '../../behaviors/dom/end-of-stream';
import { loadSegments } from '../../behaviors/dom/load-segments';
import { setupMediaSource } from '../../behaviors/dom/setup-mediasource';
import { setupSourceBuffers } from '../../behaviors/dom/setup-sourcebuffer';
import { setupTextTrackActors as _setupTextTrackActors } from '../../behaviors/dom/setup-text-track-actors';
import { syncTextTracks } from '../../behaviors/dom/sync-text-tracks';
import { trackCurrentTime } from '../../behaviors/dom/track-current-time';
import { trackPlaybackInitiated } from '../../behaviors/dom/track-playback-initiated';
import { updateDuration } from '../../behaviors/dom/update-duration';
import { loadTextTrackCues } from '../../behaviors/load-text-track-cues';
import { switchQuality as _switchQuality } from '../../behaviors/quality-switching';
import { resolvePresentation } from '../../behaviors/resolve-presentation';
import { resolveTrack } from '../../behaviors/resolve-track';
import {
selectAudioTrack as _selectAudioTrack,
selectTextTrack as _selectTextTrack,
selectVideoTrack as _selectVideoTrack,
} from '../../behaviors/select-tracks';
import { syncPreloadAttribute } from '../../behaviors/sync-preload-attribute';
// ============================================================================
// HLS Engine State & Owners
// ============================================================================
/**
* State shape for the HLS playback engine.
*
* This is the union of all state required by the behaviors composed into
* the HLS engine. Each behavior declares its own state interface; this
* type satisfies all of them.
*/
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'];
}
/**
* Owners shape for the HLS playback engine.
*
* Platform objects and actor references managed by HLS behaviors.
*/
export interface SimpleHlsEngineOwners {
mediaElement?: HTMLMediaElement | undefined;
mediaSource?: MediaSource;
videoBuffer?: SourceBuffer;
audioBuffer?: SourceBuffer;
videoBufferActor?: SourceBufferActor;
audioBufferActor?: SourceBufferActor;
textTracksActor?: TextTracksActor;
segmentLoaderActor?: TextTrackSegmentLoaderActor;
}
/**
* Configuration for the HLS playback engine.
*
* Each option is consumed by the appropriate behavior — the engine itself
* has no config beyond what its behaviors read.
*/
export interface SimpleHlsEngineConfig {
initialBandwidth?: number;
preferredAudioLanguage?: string;
preferredSubtitleLanguage?: string;
includeForcedTracks?: boolean;
enableDefaultTrack?: boolean;
}
/** Shorthand for the deps shape used by HLS engine behaviors. */
type Deps = {
state: Signal<SimpleHlsEngineState>;
owners: Signal<SimpleHlsEngineOwners>;
config: SimpleHlsEngineConfig;
};
// ============================================================================
// Thin media-type wrappers
//
// Behaviors parameterized by media type get thin wrappers that close over
// the type value, so the engine composition reads as a flat list of
// behaviors without inline config.
// ============================================================================
const loadVideoSegments = (deps: Deps) => loadSegments(deps, { type: 'video' });
const loadAudioSegments = (deps: Deps) => loadSegments(deps, { type: 'audio' });
const resolveVideoTrack = (deps: Deps) => resolveTrack(deps, { type: 'video' as const });
const resolveAudioTrack = (deps: Deps) => resolveTrack(deps, { type: 'audio' as const });
const resolveTextTrack = (deps: Deps) => resolveTrack(deps, { type: 'text' as const });
const setupTextTrackActors = ({ owners }: Deps) =>
_setupTextTrackActors({ owners, config: { resolveTextTrackSegment: resolveVttSegment } });
// ============================================================================
// Config-aware behavior wrappers
//
// Behaviors that read from engine config get wrappers that thread the
// relevant config fields into the behavior's own config parameter.
// ============================================================================
const selectVideoTrack = ({ config, ...deps }: Deps) =>
_selectVideoTrack(deps, {
type: 'video',
...(config.initialBandwidth !== undefined && { initialBandwidth: config.initialBandwidth }),
});
const selectAudioTrack = ({ config, ...deps }: Deps) =>
_selectAudioTrack(deps, {
type: 'audio',
...(config.preferredAudioLanguage !== undefined && {
preferredAudioLanguage: config.preferredAudioLanguage,
}),
});
const selectTextTrack = ({ config, ...deps }: Deps) =>
_selectTextTrack(deps, {
type: 'text',
...(config.preferredSubtitleLanguage !== undefined && {
preferredSubtitleLanguage: config.preferredSubtitleLanguage,
}),
...(config.includeForcedTracks !== undefined && { includeForcedTracks: config.includeForcedTracks }),
...(config.enableDefaultTrack !== undefined && { enableDefaultTrack: config.enableDefaultTrack }),
});
const switchQuality = ({ config, ...deps }: Deps) =>
_switchQuality(deps, config.initialBandwidth !== undefined ? { defaultBandwidth: config.initialBandwidth } : {});
// ============================================================================
// HLS Playback Engine
// ============================================================================
/**
* Create an HLS playback engine.
*
* Composes SPF behaviors into a reactive pipeline for HLS playback over MSE:
* manifest resolution, track selection, ABR, segment loading, and
* end-of-stream coordination.
*
* @example
* ```ts
* const engine = createSimpleHlsEngine({
* initialBandwidth: 2_000_000,
* preferredAudioLanguage: 'en',
* });
*
* engine.owners.set({ ...engine.owners.get(), mediaElement: videoEl });
* engine.state.set({ ...engine.state.get(), presentation: { url: 'https://example.com/stream.m3u8' } });
*
* videoEl.play();
*
* await engine.destroy();
* ```
*/
export function createSimpleHlsEngine(
config: SimpleHlsEngineConfig = {}
): Composition<SimpleHlsEngineState, SimpleHlsEngineOwners> {
return createComposition<SimpleHlsEngineState, SimpleHlsEngineOwners, SimpleHlsEngineConfig>(
[
syncPreloadAttribute,
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,
updateDuration,
setupSourceBuffers,
// Playback tracking
trackCurrentTime,
switchQuality,
// Segment loading
loadVideoSegments,
loadAudioSegments,
// End of stream coordination
endOfStream,
// Text tracks
syncTextTracks,
setupTextTrackActors,
loadTextTrackCues,
],
{
config,
initialState: {
bandwidthState: {
fastEstimate: 0,
fastTotalWeight: 0,
slowEstimate: 0,
slowTotalWeight: 0,
bytesSampled: 0,
},
},
initialOwners: {},
}
);
}