mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
Reinstate text-cue relocation (dropped with relocation.ts) and, in the same pass, rearchitect the text-segment loader onto the v/a loader's composed-step model so relocation is a pipeline step, not a resolver wrapper. Text loader gains TextFrame / TextLoadStep / TextStepDeps / TextMessagePipelines mirroring segment-loader, with base resolveCuesStep + dispatchCuesStep (default: resolveCues -> dispatchCues). makeLoadTask now just runs the pipeline. The resolver reverts to a pure (url) => cues host primitive (the text analog of fetchBytes) -- composition-awareness moves entirely into the injected step, so composition state threads via compositionDeps into TextStepDeps. Relocation supplies relocatingTextPipelines (resolveWithMetadata -> relocateCues -> dispatchCues): cues + X-TIMESTAMP-MAP fetched in parallel, cues shifted by (mapCorrection - startMediaTime) using the primary A/V track's startMediaTime (video ?? audio, awaited). The engine bakes textMessagePipelines, mirroring video/audioMessagePipelines. Base steps are generic arrow consts (TextStepDeps is contravariant in C via textTracksActor.send). Sandbox-verified 0-based against the Mux asset_start_time=60 clip: active cue brackets the 0-based playhead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
497 lines
23 KiB
TypeScript
497 lines
23 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 { delayedReschedule } from '../../../core/tasks/delayed-reschedule';
|
|
import type { Reschedule } from '../../../core/tasks/task';
|
|
import type { QualityConfig } from '../../../media/abr/quality-selection';
|
|
import type { BackBufferConfig } from '../../../media/buffer/back-buffer';
|
|
import type { ForwardBufferConfig } from '../../../media/buffer/forward-buffer';
|
|
import { canPlayTrack } from '../../../media/dom/capabilities';
|
|
import { resolveVttSegment } from '../../../media/dom/text/resolve-vtt-segment';
|
|
import {
|
|
addSubtitlesTracksToMedia,
|
|
getShowingSubtitlesTrackFromMedia,
|
|
removeAllSubtitlesTracksFromMedia,
|
|
} from '../../../media/dom/text/text-track-slots';
|
|
import { parseMultivariantPlaylist } from '../../../media/hls/parse-multivariant';
|
|
import { mediaPlaylistReloadDelay, resolveLiveLatency } from '../../../media/hls/reload-policy';
|
|
import type {
|
|
AudioTrack,
|
|
CanPlayTrack,
|
|
MaybeResolvedPresentation,
|
|
MediaContainerData,
|
|
ResolvedTrack,
|
|
TextTrack,
|
|
VideoTrack,
|
|
} from '../../../media/types';
|
|
import type { GetCdnId } from '../../../media/utils/cdn';
|
|
import { getResolvedSelectedTrackDuration } from '../../../media/utils/track-selection';
|
|
import type { BandwidthConfig, BandwidthState } from '../../../network/bandwidth-estimator';
|
|
import type { SegmentLoaderActor } from '../../actors/dom/segment-loader';
|
|
import type { SourceBufferActor } from '../../actors/dom/source-buffer';
|
|
import type { TextTracksActor } from '../../actors/dom/text-tracks';
|
|
import type { TextTrackSegmentLoaderActor, TextTrackSegmentResolver } from '../../actors/text-track-segment-loader';
|
|
import { makeAnchorPresentationTimeline } from '../../behaviors/anchor-presentation-timeline';
|
|
import {
|
|
calculatePresentationDuration,
|
|
type PresentationDurationResolver,
|
|
} from '../../behaviors/calculate-presentation-duration';
|
|
import { deriveCdnPriority } from '../../behaviors/derive-cdn-priority';
|
|
import { endOfStream } from '../../behaviors/dom/end-of-stream';
|
|
import { loadAudioSegments, loadTextTrackSegments, loadVideoSegments } from '../../behaviors/dom/load-segments';
|
|
import { relocatingTextPipelines, relocationPipelinesFor } from '../../behaviors/dom/relocation-steps';
|
|
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 { setupTextTrackActors } from '../../behaviors/dom/setup-text-track-actors';
|
|
import { syncLiveSeekableRange } from '../../behaviors/dom/sync-live-seekable-range';
|
|
import { syncTextTracks } from '../../behaviors/dom/sync-text-tracks';
|
|
import { trackCurrentTime } from '../../behaviors/dom/track-current-time';
|
|
import { trackLoadTriggers } from '../../behaviors/dom/track-load-triggers';
|
|
import { updateMediaSourceDuration } from '../../behaviors/dom/update-mediasource-duration';
|
|
// Non-zero-PTS relocation (spike): remove this import, the composed reactor, the
|
|
// `video/audio/textMessagePipelines` finalConfig entries, the `mediaContainerData`
|
|
// state slot, and the `deriveStartMediaTime` config field to drop relocation entirely
|
|
// (text then falls back to the plain `resolveVttSegment` resolver).
|
|
import { type DeriveStartMediaTime, establishStartMediaTime } from '../../behaviors/establish-start-media-time';
|
|
import { type ParsePresentation, resolvePresentation } from '../../behaviors/resolve-presentation';
|
|
import { resolveAudioTrack, resolveTextTrack, resolveVideoTrack } from '../../behaviors/resolve-track';
|
|
import { type FailoverMonitorConfig, setupFailoverMonitor } from '../../behaviors/setup-failover-monitor';
|
|
import { syncPreload } from '../../behaviors/sync-preload';
|
|
import { switchAudioTrack, switchTextTrack, switchVideoTrack } from '../../behaviors/track-switching';
|
|
import { resolveBufferedAnchor } from './resolve-buffered-anchor';
|
|
|
|
// ============================================================================
|
|
// HLS Engine State & Context
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 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;
|
|
// Non-zero-PTS relocation (spike): transient per-track container data owned by
|
|
// `establishStartMediaTime`. Remove with the composed reactor.
|
|
mediaContainerData?: Record<string, MediaContainerData>;
|
|
userVideoTrackSelection?: Partial<VideoTrack>;
|
|
/**
|
|
* Consumer-driven constraint narrowing the audio candidate set. Sibling
|
|
* of `userVideoTrackSelection`. Partial-track shape — `{ language: 'es' }`,
|
|
* `{ id: 'audio-en' }`, etc. `selectAudioTrack` reads this and re-picks
|
|
* when it changes. Multi-language-audio Tier 2 programmatic-write path.
|
|
*/
|
|
userAudioTrackSelection?: Partial<AudioTrack>;
|
|
/**
|
|
* Consumer-driven *intent* for text selection, resolved into
|
|
* `selectedTextTrackId` by `switchTextTrack`. A language-based partial
|
|
* (`{ language: 'es' }`) selects captions, `'off'` disables them, and absence
|
|
* means auto (the engine's `preferredSubtitleLanguage` / DEFAULT-track policy).
|
|
* Also the write path for the DOM caption UI (via `syncTextTracks`); unlike the
|
|
* resolved id it persists across source changes (sticky preference).
|
|
*/
|
|
userTextTrackSelection?: Partial<TextTrack> | 'off';
|
|
/**
|
|
* The CDNs the source is served from (track-URL origins), in manifest
|
|
* priority order — most-preferred first (mirrors HLS content steering's
|
|
* `PATHWAY-PRIORITY`). Owned by `deriveCdnPriority`, read by
|
|
* `track-switching`'s `preferActiveCdn` scope, which narrows to the
|
|
* highest-priority CDN with surviving tracks so video / audio / text stay on
|
|
* one host. Only meaningful for redundant-stream sources; a single-CDN source
|
|
* has one entry.
|
|
*/
|
|
cdnPriority?: string[];
|
|
/**
|
|
* CDN ids (origins) currently in failover cooldown — written by the CDN
|
|
* monitor when a host fails too often, read by `track-switching`'s
|
|
* `excludeFailedCdns` hard constraint, which prunes their tracks so the
|
|
* active-CDN scope falls to the next CDN in `cdnPriority`. Empty / absent
|
|
* means all CDNs are eligible.
|
|
*/
|
|
failedCdns?: string[];
|
|
currentTime?: number;
|
|
loadActivated?: boolean;
|
|
/**
|
|
* The shared presentation timeline anchor (wall clock at media-time 0), published by
|
|
* `anchorPresentationTimeline` once the buffer pin lands. `seekToLiveEdge` gates its
|
|
* live-edge seek on it. Absent for VoD / until the first segment buffers.
|
|
*/
|
|
presentationAnchor?: number;
|
|
}
|
|
|
|
/**
|
|
* Context shape for the HLS playback engine.
|
|
*
|
|
* Platform objects and actor references managed by HLS behaviors.
|
|
*/
|
|
export interface SimpleHlsEngineContext {
|
|
mediaElement?: HTMLMediaElement | undefined;
|
|
mediaSource?: MediaSource;
|
|
videoBufferActor?: SourceBufferActor;
|
|
audioBufferActor?: SourceBufferActor;
|
|
videoSegmentLoaderActor?: SegmentLoaderActor;
|
|
audioSegmentLoaderActor?: SegmentLoaderActor;
|
|
textTracksActor?: TextTracksActor;
|
|
textTrackSegmentLoaderActor?: TextTrackSegmentLoaderActor;
|
|
}
|
|
|
|
/**
|
|
* 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 SimpleHlsEngineSignals = {
|
|
state: StateSignals<SimpleHlsEngineState>;
|
|
context: ContextSignals<SimpleHlsEngineContext>;
|
|
};
|
|
|
|
/**
|
|
* 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 extends ShareSignalsConfig<SimpleHlsEngineState, SimpleHlsEngineContext> {
|
|
/**
|
|
* Bandwidth estimate in bps to use before enough samples have been
|
|
* collected. Default: `DEFAULT_INITIAL_BANDWIDTH` (5 Mbps).
|
|
*/
|
|
initialBandwidth?: number;
|
|
/**
|
|
* Codec capability probe injected into `track-switching`'s
|
|
* `excludeUnplayableTracks` constraint — drops renditions the environment
|
|
* can't decode before selection. Defaults to the `MediaSource.isTypeSupported`
|
|
* -backed `canPlayTrack`; supply your own to override (e.g. force-exclude a
|
|
* codec).
|
|
*/
|
|
canPlayTrack?: CanPlayTrack;
|
|
preferredAudioLanguage?: string;
|
|
preferredSubtitleLanguage?: string;
|
|
includeForcedTracks?: boolean;
|
|
enableDefaultTrack?: boolean;
|
|
/**
|
|
* Resolver that turns a text-track segment fetch into VTT cues.
|
|
* Defaults to the DOM-bound `resolveVttSegment` resolver, which uses an
|
|
* offscreen `<track>` element to parse WebVTT.
|
|
*/
|
|
resolveTextTrackSegment?: TextTrackSegmentResolver<VTTCue>;
|
|
/**
|
|
* Resolver for `presentation.duration`. Defaults to
|
|
* `getResolvedSelectedTrackDuration` (the first resolved selected track's
|
|
* `duration`), which handles both VoD and live: the parser sets
|
|
* `Track.duration` to a finite EXTINF sum for a complete playlist and to
|
|
* `Number.POSITIVE_INFINITY` while it can still grow (live). Downstream
|
|
* `updateMediaSourceDuration` propagates the value to `mediaSource.duration`
|
|
* per the MSE spec. Override to force a value.
|
|
*/
|
|
resolveDuration?: PresentationDurationResolver;
|
|
/**
|
|
* Sequence number assumed to be the stream origin (time 0) for the live
|
|
* timeline anchor (`anchorPresentationTimeline`). Default 0. Only meaningful for live
|
|
* sources; ignored for VoD (the anchor is a no-op without `#EXT-X-PROGRAM-DATE-TIME`).
|
|
*/
|
|
presumedStartSequence?: number;
|
|
/**
|
|
* Live media-playlist re-run policy for the resolve* loaders' `RecurringRunner`:
|
|
* returns a promise that resolves when the playlist should reload, or `null` to
|
|
* stop. Defaults to `mediaPlaylistReloadDelay` (target-duration cadence, half on
|
|
* an unchanged window, stop on `#EXT-X-ENDLIST`) composed with a cancellable
|
|
* `sleep`. Inert for VoD (a complete playlist stops it after the first resolve).
|
|
* Override to tune live reload timing.
|
|
*/
|
|
reschedule?: Reschedule<ResolvedTrack>;
|
|
/**
|
|
* Manifest parser handed to `resolvePresentation`. Defaults to the HLS
|
|
* multivariant-playlist parser; supply your own for alternate format
|
|
* support without forking the engine.
|
|
*/
|
|
parsePresentation?: ParsePresentation;
|
|
/**
|
|
* Allocate SPF-owned text-track slots on the media element. Defaults to
|
|
* the standard `<track>`-element implementation in
|
|
* `media/dom/text/text-track-slots`.
|
|
*/
|
|
addSubtitlesTracksToMedia?: typeof addSubtitlesTracksToMedia;
|
|
/**
|
|
* Return the SPF-owned subtitle/caption `TextTrack` currently in showing
|
|
* mode. Defaults to the standard selector-based implementation in
|
|
* `media/dom/text/text-track-slots`.
|
|
*/
|
|
getShowingSubtitlesTrackFromMedia?: typeof getShowingSubtitlesTrackFromMedia;
|
|
/**
|
|
* Evict all SPF-owned text-track slots from the media element. Defaults to
|
|
* the standard selector-based implementation in
|
|
* `media/dom/text/text-track-slots`.
|
|
*/
|
|
removeAllSubtitlesTracksFromMedia?: typeof removeAllSubtitlesTracksFromMedia;
|
|
/**
|
|
* Forward-buffer tuning. `bufferDuration` controls how far ahead of the
|
|
* playhead segments are loaded (and where forward-flush kicks in).
|
|
* Defaults: see `DEFAULT_FORWARD_BUFFER_CONFIG` (30 seconds). Threaded to
|
|
* segment-loader actors (v/a + text) at construction time and to
|
|
* `loadXSegments` dispatchers for the load-message range.
|
|
*/
|
|
forwardBuffer?: Partial<ForwardBufferConfig>;
|
|
/**
|
|
* Back-buffer tuning. `keepSegments` controls how many segments stay
|
|
* behind the playhead before eviction. Defaults: see
|
|
* `DEFAULT_BACK_BUFFER_CONFIG` (2 segments). Threaded to the v/a
|
|
* segment-loader actor only (text tracks don't use back-buffer eviction).
|
|
*/
|
|
backBuffer?: Partial<BackBufferConfig>;
|
|
/**
|
|
* Bandwidth-estimator tuning. Overrides any field of `BandwidthConfig`
|
|
* (`fastHalfLife`, `slowHalfLife`, `minTotalBytes`, `minBytes`,
|
|
* `minDuration`). `bandwidth.minTotalBytes` supersedes the flat
|
|
* `minTotalBytes` field above. Defaults: see `DEFAULT_BANDWIDTH_CONFIG`.
|
|
*/
|
|
bandwidth?: Partial<BandwidthConfig>;
|
|
/**
|
|
* Quality-selection tuning. `safetyMargin` is the bandwidth-headroom
|
|
* multiplier used by `selectQuality`; `upgradeMargin` is the hysteresis
|
|
* ratio gating ABR upgrades. Defaults: `DEFAULT_QUALITY_CONFIG` (0.85 / 1.15).
|
|
*/
|
|
quality?: Partial<QualityConfig>;
|
|
/**
|
|
* Multi-CDN failover monitor tuning. `cooldownMs` is how long a CDN stays
|
|
* excluded after a failed fetch trips it. Defaults:
|
|
* `DEFAULT_FAILOVER_MONITOR_CONFIG` (300s). Only meaningful for redundant-stream
|
|
* sources.
|
|
*/
|
|
failover?: Partial<FailoverMonitorConfig>;
|
|
/**
|
|
* How to derive a CDN grouping key from a track URL — used to build
|
|
* `cdnPriority`, to record the failover trip in `failedCdns`, and by the
|
|
* track-switching CDN scope + failover constraint. One function, read by all of
|
|
* them, so the keys stay comparable. Defaults to the URL origin; override to
|
|
* key on something else (e.g. Mux's `cdn=` query param).
|
|
*/
|
|
getCdnId?: GetCdnId;
|
|
/**
|
|
* Non-zero-PTS relocation (spike): the reduce seam consumed by the
|
|
* `establishStartMediaTime` reactor. Defaults to per-track own origin (Tier 1);
|
|
* a Tier-2 variant returns the shared `min` across selected A/V. Relocation is
|
|
* composed into the standard engine below — see the marked block — so this only
|
|
* needs setting to swap the tier policy. See
|
|
* `internal/design/spf/presentation-timeline-model.md`.
|
|
*/
|
|
deriveStartMediaTime?: DeriveStartMediaTime;
|
|
}
|
|
|
|
// ============================================================================
|
|
// HLS Playback Engine
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Generic `shareSignals` instantiated against the HLS engine's full state
|
|
* and context — captures composition signal refs into the consumer's
|
|
* `onSignalsReady` callback at setup time, and materializes input slots that no
|
|
* composed behavior produces: `user*TrackSelection` (track-switching only reads
|
|
* them). `failedCdns` is owned by `setupFailoverMonitor`, so it's already
|
|
* materialized and reachable on the `onSignalsReady` refs without being listed
|
|
* here.
|
|
*/
|
|
const shareSignals = makeShareSignals<SimpleHlsEngineState, SimpleHlsEngineContext>([
|
|
'userVideoTrackSelection',
|
|
'userAudioTrackSelection',
|
|
'userTextTrackSelection',
|
|
]);
|
|
|
|
/**
|
|
* 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
|
|
* let signals: SimpleHlsEngineSignals;
|
|
* const engine = createSimpleHlsEngine({
|
|
* initialBandwidth: 2_000_000,
|
|
* preferredAudioLanguage: 'en',
|
|
* onSignalsReady: (refs) => {
|
|
* signals = refs;
|
|
* },
|
|
* });
|
|
*
|
|
* signals.context.mediaElement.set(videoEl);
|
|
* signals.state.presentation.set({ url: 'https://example.com/stream.m3u8' });
|
|
*
|
|
* videoEl.play();
|
|
*
|
|
* await engine.destroy();
|
|
* ```
|
|
*/
|
|
export function createSimpleHlsEngine(
|
|
config: SimpleHlsEngineConfig = {}
|
|
): Composition<SimpleHlsEngineState, SimpleHlsEngineContext> {
|
|
const finalConfig = {
|
|
...config,
|
|
resolveBufferedAnchor,
|
|
// Format-neutral live-latency seam for `seekToLiveEdge` — the HLS resolver
|
|
// (HOLD-BACK); a DASH engine would inject `suggestedPresentationDelay`.
|
|
resolveLiveLatency,
|
|
canPlayTrack: config.canPlayTrack ?? canPlayTrack,
|
|
// The resolve* loaders' RecurringRunner re-runs on this `reschedule`: the pure
|
|
// target-duration cadence, start-anchored + made awaitable by `delayedReschedule`.
|
|
// Inert for VoD (the cadence returns null once a playlist is complete), so it
|
|
// composes always.
|
|
reschedule: config.reschedule ?? delayedReschedule(mediaPlaylistReloadDelay),
|
|
resolveTextTrackSegment: config.resolveTextTrackSegment ?? resolveVttSegment,
|
|
// Non-zero-PTS relocation (spike): the text pipeline rebases cues onto the
|
|
// relocated 0-based timeline. Remove `textMessagePipelines` to drop text relocation.
|
|
textMessagePipelines: relocatingTextPipelines,
|
|
resolveDuration: config.resolveDuration ?? getResolvedSelectedTrackDuration,
|
|
parsePresentation: config.parsePresentation ?? parseMultivariantPlaylist,
|
|
addSubtitlesTracksToMedia: config.addSubtitlesTracksToMedia ?? addSubtitlesTracksToMedia,
|
|
getShowingSubtitlesTrackFromMedia: config.getShowingSubtitlesTrackFromMedia ?? getShowingSubtitlesTrackFromMedia,
|
|
removeAllSubtitlesTracksFromMedia: config.removeAllSubtitlesTracksFromMedia ?? removeAllSubtitlesTracksFromMedia,
|
|
// Non-zero-PTS relocation (spike): the per-type discover/stamp steps
|
|
// `establishStartMediaTime` pairs with. Remove these two lines with the reactor.
|
|
videoMessagePipelines: relocationPipelinesFor('video'),
|
|
audioMessagePipelines: relocationPipelinesFor('audio'),
|
|
};
|
|
|
|
const composition = createComposition(
|
|
[
|
|
syncPreload,
|
|
trackLoadTriggers,
|
|
resolvePresentation,
|
|
|
|
// Session-level CDN priority for redundant-stream sources. Owns
|
|
// `cdnPriority`; `track-switching`'s preferActiveCdn scope reads it so
|
|
// every type stays on one CDN. No-op for single-CDN sources.
|
|
//
|
|
// Placed before switch* so `cdnPriority` is set before the first pick —
|
|
// but this ordering is only *mildly* load-bearing, not required for
|
|
// correctness. Selection is reactive: a late `cdnPriority` re-fires the
|
|
// pick and converges on the same result (see the late-arrival test in
|
|
// track-switching.test.ts). Order affects only a transient, and only for
|
|
// an *asymmetric* manifest (a type listing a non-primary CDN first):
|
|
// composing this after switch* would let that type fire one wasted
|
|
// media-playlist fetch to the wrong CDN before correcting. Symmetric
|
|
// redundant streams (the norm) never hit it — the first-listed CDN is
|
|
// already the primary we'd pick anyway.
|
|
deriveCdnPriority,
|
|
|
|
// CDN failover cooldown: owns the expiry half of failover — watches
|
|
// `failedCdns` (tripped directly by track resolution on a failed
|
|
// media-playlist fetch) and removes each CDN once its cooldown lapses.
|
|
setupFailoverMonitor,
|
|
|
|
// Resolve selected tracks (fetch media playlists). Composed before the
|
|
// switch* slot owners; selection is reactive, so a resolve* re-fires once
|
|
// its switch* sets the id (same convergence for all three types). Also the
|
|
// live loader: its `RecurringRunner` re-resolves on the playlist's
|
|
// target-duration cadence (`reschedule` in config) until `#EXT-X-ENDLIST`.
|
|
// Inert for VoD (a complete playlist stops the policy after one resolve).
|
|
resolveVideoTrack,
|
|
resolveAudioTrack,
|
|
resolveTextTrack,
|
|
|
|
// Re-base selected live tracks' timelines onto the shared presentation
|
|
// anchor (estimate, then buffer ground truth). No-op for VoD (no PDT).
|
|
makeAnchorPresentationTimeline<SimpleHlsEngineContext>(),
|
|
|
|
// Presentation duration (finite for complete playlists, Infinity for live)
|
|
calculatePresentationDuration,
|
|
|
|
// MSE setup. Video cluster is registered first so that, when both
|
|
// per-type variants flip to `'buffer-ready'` on the shared gate's
|
|
// monitor evaluation, `addSourceBuffer(video)` runs before
|
|
// `addSourceBuffer(audio)` — see the Firefox `mozHasAudio` invariant
|
|
// in setup-buffer-actors.ts.
|
|
setupMediaSource,
|
|
updateMediaSourceDuration,
|
|
|
|
// ── Non-zero-PTS relocation (spike) ──────────────────────────────────
|
|
// Establishes per-track `startMediaTime` and publishes the relocating
|
|
// segment-loader pipelines to context. MUST precede `setup*BufferActors`
|
|
// so the pipelines are published before the loaders read them. Remove this
|
|
// one line (+ the import, the `mediaContainerData`/`*MessagePipelines`
|
|
// slots including `textMessagePipelines`, and the `deriveStartMediaTime`
|
|
// config) to drop relocation and test the Tier-0 baseline / bundle size.
|
|
establishStartMediaTime,
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
setupVideoBufferActors,
|
|
setupAudioBufferActors,
|
|
|
|
// Playback tracking
|
|
trackCurrentTime,
|
|
switchVideoTrack,
|
|
switchAudioTrack,
|
|
// Mid-stream audio-buffer flush on language switch is handled in
|
|
// `segment-loader`'s `planTasks` (predicate: language differs from
|
|
// the previously-buffered track) — not in switchAudioTrack itself.
|
|
|
|
// Text selection: resolves `userTextTrackSelection` intent (incl. 'off',
|
|
// or the configured preferred-language / DEFAULT-track policy) against the
|
|
// failed-CDN-pruned, active-CDN-scoped text renditions. Optional selection
|
|
// (captions are opt-in), so it can resolve to none.
|
|
switchTextTrack,
|
|
|
|
// Segment loading
|
|
loadVideoSegments,
|
|
loadAudioSegments,
|
|
|
|
// Live: declare the seekable window, then seek the playhead to the live
|
|
// edge + keep it in-window. No-op for complete playlists (VoD / ended).
|
|
// Order matters: the seekable range must be declared (syncLiveSeekableRange)
|
|
// before seekToLiveEdge moves the playhead into it (a seek outside
|
|
// `seekable` is clamped).
|
|
syncLiveSeekableRange,
|
|
seekToLiveEdge,
|
|
|
|
// End of stream coordination
|
|
endOfStream,
|
|
|
|
// Text tracks
|
|
syncTextTracks,
|
|
setupTextTrackActors,
|
|
loadTextTrackSegments,
|
|
|
|
// Behavior whose sole purpose is to use a callback to allow for signal writing from the outside (e.g. an adapter)
|
|
// NOTE: While not required, adding at the end since behaviors are setup in order, so this increases the likelihood
|
|
// that initial signal setup will have occurred before shareSignals' callback is invoked. (CJP)
|
|
shareSignals,
|
|
],
|
|
{
|
|
config: finalConfig,
|
|
// Seed bandwidthState so switchVideoTrack fires on initial subscribe
|
|
// with the `initialBandwidth` fallback rather than waiting for the
|
|
// first chunk. The empty sample buffer means `getBandwidthEstimate`
|
|
// returns the configured initial bandwidth until real samples land.
|
|
initialState: {
|
|
bandwidthState: {
|
|
fastEstimate: 0,
|
|
fastTotalWeight: 0,
|
|
slowEstimate: 0,
|
|
slowTotalWeight: 0,
|
|
bytesSampled: 0,
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
return composition;
|
|
}
|