refactor(spf): drop redundant live-hls and live-playlist-spike engines

Live playback was folded into createSimpleHlsEngine (VoD + live, one
composition), making the separate createLiveHlsEngine — which only wrapped
the same behaviors plus the hls engine itself — redundant, and leaving
live-playlist-spike an orphaned experiment with no exports or consumers.
Delete both, drop the ./live-hls package export and tsdown entry, and point
the sandbox live harness at createSimpleHlsEngine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2026-06-25 09:59:44 -07:00
co-authored by Claude Opus 4.8
parent a46a94e80b
commit 9b973b8e66
9 changed files with 6 additions and 399 deletions
@@ -17,7 +17,7 @@
</style>
</head>
<body>
<h1>Live HLS Engine — <code>createLiveHlsEngine</code></h1>
<h1>Live HLS Engine — <code>createSimpleHlsEngine</code></h1>
<video id="video" controls muted playsinline></video>
<div class="state" id="state"></div>
<h2>Log</h2>
@@ -1,9 +1,9 @@
/**
* Bare harness for the experimental SPF live HLS engine — wires
* `createLiveHlsEngine` to a raw <video> (no player/skin) and logs playback
* Bare harness for live HLS playback — wires `createSimpleHlsEngine` (which
* folds in live support) to a raw <video> (no player/skin) and logs playback
* state, to validate live CMAF/LL-HLS playback end-to-end.
*/
import { createLiveHlsEngine, type LiveHlsEngineSignals } from '@videojs/spf/live-hls';
import { createSimpleHlsEngine, type SimpleHlsEngineSignals } from '@videojs/spf/hls';
// Override per run with `?src=<m3u8 url>` (live test streams are ephemeral).
const SRC =
@@ -46,8 +46,8 @@ for (const ev of ['loadedmetadata', 'durationchange', 'canplay', 'playing', 'wai
}
video.addEventListener('error', () => log(`video error: ${video.error?.code}`, 'err'));
let signals: LiveHlsEngineSignals | undefined;
const engine = createLiveHlsEngine({
let signals: SimpleHlsEngineSignals | undefined;
const engine = createSimpleHlsEngine({
onSignalsReady: (refs) => {
signals = refs;
},
-5
View File
@@ -33,11 +33,6 @@
"development": "./dist/dev/hls.js",
"default": "./dist/default/hls.js"
},
"./live-hls": {
"types": "./dist/dev/live-hls.d.ts",
"development": "./dist/dev/live-hls.js",
"default": "./dist/default/live-hls.js"
},
"./background-video": {
"types": "./dist/dev/background-video.d.ts",
"development": "./dist/dev/background-video.js",
@@ -1,159 +0,0 @@
import type { Constructor, MixinReturn } from '@videojs/utils/types';
import type { Composition } from '../../../core/composition/create-composition';
import {
createLiveHlsEngine,
type LiveHlsEngineConfig,
type LiveHlsEngineContext,
type LiveHlsEngineSignals,
type LiveHlsEngineState,
} from './engine';
export interface LiveHlsMediaProps {
src: string;
preload: '' | 'none' | 'metadata' | 'auto';
}
export const liveHlsMediaDefaultProps: LiveHlsMediaProps = {
src: '',
preload: '',
};
export interface LiveHlsMediaAPI extends LiveHlsMediaProps {
readonly engine: Composition<LiveHlsEngineState, LiveHlsEngineContext>;
attach(mediaElement: HTMLMediaElement): void;
detach(): void;
destroy(): void;
play(): Promise<void>;
}
/**
* Adapter mixin for the live HLS engine — mirrors `SimpleHlsMediaMixin`,
* swapping in `createLiveHlsEngine`. Implements the WHATWG `src`/`preload`/
* `play()` contract so the live engine drops into anywhere a media element is
* expected. A fresh engine is created on each `src` assignment (full teardown
* of the prior source); the attached media element is preserved across changes.
*
* Distinct from the VoD adapter (rather than a shared parameterized mixin)
* while the live engine stabilizes.
*/
export function LiveHlsMediaMixin<Base extends Constructor<any>>(BaseClass: Base) {
class LiveHlsMediaImpl extends BaseClass {
#engine: Composition<LiveHlsEngineState, LiveHlsEngineContext>;
#config: LiveHlsEngineConfig;
#signals!: LiveHlsEngineSignals;
#preload: '' | 'none' | 'metadata' | 'auto' = liveHlsMediaDefaultProps.preload;
/** Pending loadstart listener from a deferred play() retry, if any. */
#loadstartListener: (() => void) | null = null;
constructor(...args: any[]) {
super(...args);
const { config } = args?.[0] ?? {};
this.#config = config;
this.#engine = this.#createEngine();
}
get engine(): Composition<LiveHlsEngineState, LiveHlsEngineContext> {
return this.#engine;
}
attach(mediaElement: HTMLMediaElement): void {
super.attach?.(mediaElement);
this.#signals.context.mediaElement.set(mediaElement);
}
detach(): void {
this.#cancelPendingPlay();
this.#signals.context.mediaElement.set(undefined);
super.detach?.();
}
destroy(): void {
this.#cancelPendingPlay();
this.#engine.destroy();
}
get preload(): '' | 'none' | 'metadata' | 'auto' {
return this.#preload;
}
set preload(value: '' | 'none' | 'metadata' | 'auto') {
this.#preload = value;
if (value) {
this.#signals.state.preload.set(value);
}
}
get src(): string {
return this.#signals.state.presentation.get()?.url ?? '';
}
set src(value: string) {
const prevMediaElement = this.#signals.context.mediaElement.get();
this.#cancelPendingPlay();
this.#engine.destroy();
this.#engine = this.#createEngine();
if (this.#preload) {
this.#signals.state.preload.set(this.#preload);
}
if (prevMediaElement) {
this.#signals.context.mediaElement.set(prevMediaElement);
}
if (value) {
this.#signals.state.presentation.set({ url: value });
}
}
play(): Promise<void> {
const mediaElement = this.#signals.context.mediaElement.get();
if (!mediaElement) {
return Promise.reject(new Error('LiveHlsMediaElement: no media element attached'));
}
// Signal play intent — enables loading even with preload="none".
this.#signals.state.loadActivated.set(true);
return mediaElement.play().catch((err: unknown) => {
// The rejection may be because MSE hasn't attached a blob URL yet. Wait
// for loadstart (src assigned by MSE setup) and retry once.
if (this.src) {
return new Promise<void>((resolve, reject) => {
const listener = () => {
this.#loadstartListener = null;
mediaElement.play().then(resolve, reject);
};
this.#loadstartListener = listener;
mediaElement.addEventListener('loadstart', listener, { once: true });
});
}
throw err;
});
}
#createEngine(): Composition<LiveHlsEngineState, LiveHlsEngineContext> {
return createLiveHlsEngine({
...this.#config,
onSignalsReady: (signals) => {
this.#signals = signals;
},
});
}
#cancelPendingPlay(): void {
if (!this.#loadstartListener) return;
const mediaElement = this.#signals.context.mediaElement.get();
mediaElement?.removeEventListener('loadstart', this.#loadstartListener);
this.#loadstartListener = null;
}
}
return LiveHlsMediaImpl as unknown as MixinReturn<Base, LiveHlsMediaAPI>;
}
/** Standalone live SPF media adapter with no base class. */
export class LiveHlsMediaElement extends LiveHlsMediaMixin(class {}) {}
@@ -1,139 +0,0 @@
/**
* Live HLS playback engine (experimental).
*
* Reuses the VoD HLS engine's MSE / segment-loading / ABR behaviors, swapping
* one-shot track resolution for the per-type live reload loop, adding the
* stream-origin timeline anchor, and defaulting `resolveDuration` to `Infinity`.
* Demuxed audio + video; text and discontinuity handling are out of scope for
* now. Built to validate live playback end-to-end against a real CMAF/LL-HLS
* stream — see [live-presentation-modeling.md](../../../../../internal/design/spf/live-presentation-modeling.md).
*
* Distinct engine (not a refactor of `createSimpleHlsEngine`) so the VoD path
* stays untouched while the live composition stabilizes.
*/
import { type Composition, createComposition } from '../../../core/composition/create-composition';
import { makeShareSignals } from '../../../core/composition/share-signals';
import { delayedReschedule } from '../../../core/tasks/delayed-reschedule';
import { canPlayTrack } from '../../../media/dom/capabilities';
import { parseMultivariantPlaylist } from '../../../media/hls/parse-multivariant';
import { mediaPlaylistReloadDelay } from '../../../media/hls/reload-policy';
import { anchorLiveTracks } from '../../behaviors/anchor-live-tracks';
import { calculatePresentationDuration } from '../../behaviors/calculate-presentation-duration';
import { deriveCdnPriority } from '../../behaviors/derive-cdn-priority';
import { endOfStream } from '../../behaviors/dom/end-of-stream';
import { loadAudioSegments, loadVideoSegments } from '../../behaviors/dom/load-segments';
import { seekToLiveEdge } from '../../behaviors/dom/seek-to-live-edge';
import { setupAudioBufferActors, setupVideoBufferActors } from '../../behaviors/dom/setup-buffer-actors';
import { setupMediaSource } from '../../behaviors/dom/setup-mediasource';
import { trackCurrentTime } from '../../behaviors/dom/track-current-time';
import { trackLoadTriggers } from '../../behaviors/dom/track-load-triggers';
import { updateMediaSourceDuration } from '../../behaviors/dom/update-mediasource-duration';
import { resolvePresentation } from '../../behaviors/resolve-presentation';
import { resolveAudioTrack, resolveVideoTrack } from '../../behaviors/resolve-track';
import { setupFailoverMonitor } from '../../behaviors/setup-failover-monitor';
import { syncPreload } from '../../behaviors/sync-preload';
import { switchAudioTrack, switchVideoTrack } from '../../behaviors/track-switching';
import type {
SimpleHlsEngineConfig,
SimpleHlsEngineContext,
SimpleHlsEngineSignals,
SimpleHlsEngineState,
} from '../hls/engine';
/** Config for the live HLS engine: the VoD config plus live-only options. */
export interface LiveHlsEngineConfig extends SimpleHlsEngineConfig {
/**
* Sequence number assumed to be the stream origin (time 0) for the
* timeline anchor. Default 0. See `anchorTrackToSequenceOrigin`.
*/
startSequence?: number;
}
export type LiveHlsEngineState = SimpleHlsEngineState;
export type LiveHlsEngineContext = SimpleHlsEngineContext;
export type LiveHlsEngineSignals = SimpleHlsEngineSignals;
const shareSignals = makeShareSignals<LiveHlsEngineState, LiveHlsEngineContext>([
'userVideoTrackSelection',
'userAudioTrackSelection',
]);
/**
* Create a live HLS playback engine.
*
* Drive it like the VoD engine: capture signals via `onSignalsReady`, set
* `context.mediaElement`, then `state.presentation = { url }`.
*/
export function createLiveHlsEngine(
config: LiveHlsEngineConfig = {}
): Composition<LiveHlsEngineState, LiveHlsEngineContext> {
const finalConfig = {
...config,
canPlayTrack: config.canPlayTrack ?? canPlayTrack,
parsePresentation: config.parsePresentation ?? parseMultivariantPlaylist,
// Live: duration is unbounded. `updateMediaSourceDuration` propagates
// Infinity to `mediaSource.duration` per the MSE spec.
resolveDuration: config.resolveDuration ?? (() => Number.POSITIVE_INFINITY),
startSequence: config.startSequence ?? 0,
// Reload the selected playlists via the loaders' RecurringRunner — the
// target-duration cadence, start-anchored + made awaitable by `delayedReschedule`.
reschedule: config.reschedule ?? delayedReschedule(mediaPlaylistReloadDelay),
};
return createComposition(
[
syncPreload,
trackLoadTriggers,
resolvePresentation,
deriveCdnPriority,
setupFailoverMonitor,
// Loader (category [1]): resolves the selected track and, via its
// RecurringRunner + `reschedule`, re-fetches it on a target-duration
// cadence until #EXT-X-ENDLIST, carrying the timeline forward.
resolveVideoTrack,
resolveAudioTrack,
// Anchor selected tracks' timelines to the estimated stream origin so
// segment.startTime ≈ native PTS (what the loader matches currentTime
// against). Downstream of reload, upstream of load.
anchorLiveTracks,
calculatePresentationDuration,
setupMediaSource,
updateMediaSourceDuration,
setupVideoBufferActors,
setupAudioBufferActors,
trackCurrentTime,
switchVideoTrack,
switchAudioTrack,
loadVideoSegments,
loadAudioSegments,
// Seek the playhead into the (native-PTS) buffered window once segments
// land, so playback can start.
seekToLiveEdge,
// No-op for unbounded live (no EXT-X-ENDLIST), composed for parity.
endOfStream,
shareSignals,
],
{
config: finalConfig,
initialState: {
bandwidthState: {
fastEstimate: 0,
fastTotalWeight: 0,
slowEstimate: 0,
slowTotalWeight: 0,
bytesSampled: 0,
},
},
}
);
}
@@ -1,9 +0,0 @@
export type { LiveHlsMediaAPI, LiveHlsMediaProps } from './adapter';
export { LiveHlsMediaElement, LiveHlsMediaMixin, liveHlsMediaDefaultProps } from './adapter';
export type {
LiveHlsEngineConfig,
LiveHlsEngineContext,
LiveHlsEngineSignals,
LiveHlsEngineState,
} from './engine';
export { createLiveHlsEngine } from './engine';
@@ -1,73 +0,0 @@
/**
* **POC SPIKE** — a composition that *just handles playlists*.
*
* The smallest engine that exercises the live foundation
* ([live-presentation-modeling.md](../../../../../internal/design/spf/live-presentation-modeling.md)):
* resolve the multivariant manifest, pick a video rendition, then reload that
* track's media playlist on a target-duration cadence, merging snapshots — no
* MSE, no SourceBuffers, no segment fetching, no DOM. Point it at a live stream
* and observe `state.presentation` evolve (segments append / roll off;
* duration / streamType / live edge derivable from the resolved track).
*
* Drive it via `onSignalsReady`: set `presentation = { url }`. Selection and
* reloading then run on their own.
*/
import {
type Composition,
type ContextSignals,
createComposition,
type StateSignals,
} from '../../../core/composition/create-composition';
import { makeShareSignals, type ShareSignalsConfig } from '../../../core/composition/share-signals';
import { delayedReschedule } from '../../../core/tasks/delayed-reschedule';
import { parseMultivariantPlaylist } from '../../../media/hls/parse-multivariant';
import { mediaPlaylistReloadDelay } from '../../../media/hls/reload-policy';
import { pickHighestResolutionVideoTrack, type TrackPicker } from '../../../media/primitives/select-tracks';
import type { MaybeResolvedPresentation } from '../../../media/types';
import { type ParsePresentation, resolvePresentation } from '../../behaviors/resolve-presentation';
import { resolveVideoTrack } from '../../behaviors/resolve-track';
import { type SelectVideoTrackConfig, selectVideoTrack } from '../../behaviors/select-tracks';
export interface LivePlaylistSpikeState {
presentation?: MaybeResolvedPresentation;
selectedVideoTrackId?: string;
preload?: 'auto' | 'metadata' | 'none';
loadActivated?: boolean;
}
export type LivePlaylistSpikeContext = Record<never, never>;
export type LivePlaylistSpikeSignals = {
state: StateSignals<LivePlaylistSpikeState>;
context: ContextSignals<LivePlaylistSpikeContext>;
};
export interface LivePlaylistSpikeConfig extends ShareSignalsConfig<LivePlaylistSpikeState, LivePlaylistSpikeContext> {
/** Video-track picker handed to `selectVideoTrack`. Default: max resolution. */
picker?: TrackPicker<SelectVideoTrackConfig>;
/** Multivariant parser. Defaults to the HLS multivariant-playlist parser. */
parsePresentation?: ParsePresentation;
}
const shareSignals = makeShareSignals<LivePlaylistSpikeState, LivePlaylistSpikeContext>();
export function createLivePlaylistSpikeEngine(
config: LivePlaylistSpikeConfig = {}
): Composition<LivePlaylistSpikeState, LivePlaylistSpikeContext> {
const finalConfig = {
...config,
picker: config.picker ?? pickHighestResolutionVideoTrack,
parsePresentation: config.parsePresentation ?? parseMultivariantPlaylist,
// Reload the resolved video playlist (the spike's whole point) via the
// loader's RecurringRunner — target-duration cadence, start-anchored + made
// awaitable by `delayedReschedule`.
reschedule: delayedReschedule(mediaPlaylistReloadDelay),
};
return createComposition([resolvePresentation, selectVideoTrack, resolveVideoTrack, shareSignals], {
config: finalConfig,
// Spike skips the preload gate — resolve as soon as a url is set.
initialState: { loadActivated: true },
});
}
@@ -1,7 +0,0 @@
export {
createLivePlaylistSpikeEngine,
type LivePlaylistSpikeConfig,
type LivePlaylistSpikeContext,
type LivePlaylistSpikeSignals,
type LivePlaylistSpikeState,
} from './engine';
-1
View File
@@ -8,7 +8,6 @@ const createConfig = (mode: PackageBuildMode): UserConfig => ({
index: 'src/index.ts',
dom: 'src/dom.ts',
hls: 'src/playback/engines/hls/index.ts',
'live-hls': 'src/playback/engines/live-hls/index.ts',
'background-video': 'src/playback/engines/background-video/index.ts',
},
});