mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
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:
co-authored by
Claude Opus 4.8
parent
a46a94e80b
commit
9b973b8e66
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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';
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user