From b820064b77d3a22d70a6bbf6fe6adada76551289 Mon Sep 17 00:00:00 2001 From: Christian Pillsbury Date: Fri, 10 Jul 2026 07:20:43 -0700 Subject: [PATCH] wip(spf): per-type relocation keying + non-0th-segment origin fix - Key mediaContainerData by track *type* (video/audio), not track id: one init+media pair per type, ABR rungs share the origin. relocationMessagePipelines -> relocationPipelinesFor(type); the engines pass per-type. - #4 fix: record the discovered segment's 0-based startTime and derive the origin as baseMediaDecodeTime/timescale - segmentStartTime, so relocation is correct when the first loaded segment isn't the 0th (non-zero initial currentTime, live/DVR). Adds segmentStartTime to MediaContainerData. - Rename derivePerTrackStartMediaTime -> derivePerTypeStartMediaTime; stampTracks applies each type's origin to every track of that type. - Add establish-start-media-time derive unit test. Unit-verified (segmentStartTime=100 still yields origin 60) and end-to-end sandbox-verified: start-at-300 buffers around the playhead (0-based); default start-at-0 A/V + audio-only remain 0-based. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...spf-non-zero-pts-timestamp-offset-spike.md | 19 +-- .../design/spf/presentation-timeline-model.md | 48 +++---- packages/spf/src/media/types/index.ts | 18 ++- .../behaviors/dom/relocation-steps.ts | 124 ++++++++++-------- .../behaviors/establish-start-media-time.ts | 38 ++++-- .../tests/establish-start-media-time.test.ts | 44 +++++++ .../playback/engines/hls/engine-audio-only.ts | 8 +- .../spf/src/playback/engines/hls/engine.ts | 11 +- .../spf/src/playback/engines/hls/index.ts | 2 +- 9 files changed, 195 insertions(+), 117 deletions(-) create mode 100644 packages/spf/src/playback/behaviors/tests/establish-start-media-time.test.ts diff --git a/.claude/plans/spf-non-zero-pts-timestamp-offset-spike.md b/.claude/plans/spf-non-zero-pts-timestamp-offset-spike.md index b3ef907b..4928c684 100644 --- a/.claude/plans/spf-non-zero-pts-timestamp-offset-spike.md +++ b/.claude/plans/spf-non-zero-pts-timestamp-offset-spike.md @@ -43,20 +43,24 @@ your detailed pass; **DEFERRED** = later/open. (config messagePipelines) + DOM split; deleted `relocation.ts`/`origin-discoverer.ts` — `05a0a6452` - audio-only relocation wiring (reactor + `audioMessagePipelines` baked into `engine-audio-only`) — audio 0-based sandbox-verified +- per-type keying (#3) + non-0th-segment origin `bmdt/ts − segmentStartTime` (#4) + + `derivePerTypeStartMediaTime` unit test — unit + end-to-end (start-at-300) verified ### Architecture (as landed) -- **Steps** (`behaviors/dom/relocation-steps.ts`, `relocationMessagePipelines`): a - plain config array. `discover` (`readInitTimescale`/`readSegmentOrigin`) writes - `state.mediaContainerData`; `stampStartMediaTime` reads that track's origin back - and sets `timestampOffset`. Steps read composition `state` from call-time `deps` - (no closures/context); one map serves both track types (key by segment trackId). +- **Steps** (`behaviors/dom/relocation-steps.ts`, `relocationPipelinesFor(type)`): a + plain config array, one per track type. `discover` (`readInitTimescale`/ + `readSegmentOrigin`) writes `state.mediaContainerData[type]` (`timescale`, + `baseMediaDecodeTime`, `segmentStartTime`); `stampStartMediaTime` reads that type's + origin (`bmdt/ts − segmentStartTime`) back and sets `timestampOffset`. Steps read + composition `state` from call-time `deps` (no closures/context). ABR rungs of a + type share the entry. - **Reactor** (`behaviors/establish-start-media-time.ts`, DOM-free): 3 states — `inactive` (clears the slot per source) / `monitoring` (derive effect = injected `deriveStartMediaTime` seam → writes `Track.startMediaTime`, the consume) / `established` (derive disabled, sticky). Owns `mediaContainerData` on `state`; selection **optional/defensive** (composes across video-only/audio-only/both). - `StepDeps` carries the composition `{state,context,config}` (opaque conduit). - Engines bake `*MessagePipelines = relocationMessagePipelines` + the reactor + + Engines bake `*MessagePipelines = relocationPipelinesFor(type)` + the reactor + `mediaContainerData` slot + `deriveStartMediaTime` config, all comment-marked. ### REVIEW — your detailed pass @@ -81,9 +85,6 @@ your detailed pass; **DEFERRED** = later/open. with comment markers; revisit a tree-shakeable opt-in + measure Tier-0 bundle. ### DEFERRED / open (see the doc's Open questions) -- **Per-type keying + non-0th-segment origin (#3/#4)** — next up. `mediaContainerData` - keyed by track *type* (ABR rungs share); `startMediaTime = bmdt/ts − segment.startTime` - so non-zero starts (initial currentTime ≠ 0, live/DVR) relocate correctly. - Tier 2 shared-`min` (a `deriveStartMediaTime` swap) + barrier-liveness bound. - Honest-`startMediaTime`-everywhere convergence + live-anchor dedup. - Suspected pre-existing: seek-into-evicted-back-buffer. diff --git a/internal/design/spf/presentation-timeline-model.md b/internal/design/spf/presentation-timeline-model.md index 320cd476..e56fd4c4 100644 --- a/internal/design/spf/presentation-timeline-model.md +++ b/internal/design/spf/presentation-timeline-model.md @@ -158,34 +158,36 @@ signals are **optional/defensive**, so the one reactor composes across video-onl audio-only / both. Per-source freshness is structural (the `inactive` transition clears the slot), not a hand-rolled reset. -- **Discover** (per-track step). A head-peek step reads the fetched byte stream — - the init (`mdhd` timescale) and the first media segment (`tfdt` - baseMediaDecodeTime) — and writes the raw values into `mediaContainerData`, a - `Signal>`. It's a - **two-source** read across two appends, but the slot *is* the shared state (init - writes `timescale`, the first segment writes `baseMediaDecodeTime`), so the two +- **Discover** (per-type steps). Head-peek steps read the init (`mdhd` timescale) + and the first media segment (`tfdt` baseMediaDecodeTime, plus that segment's + 0-based `startTime`) and write them into `mediaContainerData`, a + `Signal>` + **keyed by track type** (`'video'`/`'audio'`) — one init+media pair per type + suffices, and ABR rungs of a type share the entry. It's a **two-source** read + across two appends, but the slot *is* the shared state (init writes `timescale`, + the first media segment writes `baseMediaDecodeTime`+`segmentStartTime`), so the steps are independent — no shared closure, no self-discrimination. Writes are **synchronous** RMW of disjoint keys (the sync-merge invariant keeps this clear - of the #1746 hazard; an async RMW of the slot would reintroduce it). Runs on the - *fetched* stream, so transport stays pure `fetchBytes` — discovery is a content - concern. `mediaContainerData` is named for what it holds (raw parsed container - values) rather than the one thing derived from it, so other box data can land - there later without a rename. + of the #1746 hazard). Runs on the *fetched* stream, so transport stays pure + `fetchBytes`. `segmentStartTime` is recorded because the origin is + `baseMediaDecodeTime/timescale − segmentStartTime` — the first *loaded* segment + isn't necessarily the 0th (non-zero initial `currentTime`, live/DVR). - **Derive** (reactor effect). Watches `mediaContainerData` (+ selection for - Tier 2) and, via the injected **`deriveStartMediaTime`** seam, writes the - settled per-track `startMediaTime` onto the `Track`s. The seam is **pure** — - `(mediaContainerData, ctx) => Record`, where - `undefined` means "not ready yet" (the *when*) and the number is the *how* — and - the effect is the sole writer of the field. It's the **only tier knob**: Tier 1 - writes each track its own (`baseMediaDecodeTime / timescale`); Tier 2 writes the - `min` across the selected A/V origins onto every track. No Presentation-level - field is needed — Tier 2 just denormalizes the min across the per-track slots. + Tier 2) and, via the injected **`deriveStartMediaTime`** seam, writes the settled + per-type `startMediaTime` onto the `Track`s (each type's value stamped on every + track of that type). The seam is **pure** — + `(mediaContainerData, ctx) => Record` — `undefined` + means "not ready yet"; the effect is the sole writer of the field. It's the + **only tier knob**: Tier 1 gives each type its own + (`baseMediaDecodeTime/timescale − segmentStartTime`); Tier 2 writes the `min` + across the selected A/V origins to every type. No Presentation-level field is + needed — Tier 2 just denormalizes the min across the per-type entries. - **Apply** (per-track step). A stamp step relocates via `timestampOffset = startTime − startMediaTime`, which the SourceBufferActor applies to - `SourceBuffer.timestampOffset`. **Tier 1** reads the track's *own* discovered - origin straight from `mediaContainerData` — populated by the discover step - earlier in the same pipeline, so it's synchronous, independent of the - derive/model, and robust to `established` + late tracks. When no complete origin + `SourceBuffer.timestampOffset`. **Tier 1** reads the type's *own* discovered + origin (`bmdt/ts − segmentStartTime`) straight from `mediaContainerData` — + populated by the discover step earlier in the same pipeline, so it's synchronous, + independent of the derive/model, and robust to `established` + late tracks. When no complete origin is found (TS / containerless / 0-PTS) it leaves the append native (offset 0). Reading the *reduced* `Track.startMediaTime` instead — tier-agnostic apply, needed for Tier 2's shared `min` — is a deferred follow-up (it's where an abortable diff --git a/packages/spf/src/media/types/index.ts b/packages/spf/src/media/types/index.ts index a69fc79d..3ac2b014 100644 --- a/packages/spf/src/media/types/index.ts +++ b/packages/spf/src/media/types/index.ts @@ -168,16 +168,22 @@ export type Track = Ham & }; /** - * Raw per-track values parsed from the media container, accumulated as they're - * discovered across appends (`mdhd` timescale from the init, `tfdt` - * baseMediaDecodeTime from the first media segment) — hence both optional. The - * transient input the `establishStartMediaTime` reactor reduces into - * `Track.startMediaTime`. Named for what it holds (container data), not the one - * thing currently derived from it, so other box values can join later. + * Per-track-type origin-establishment data, accumulated across appends (`mdhd` + * timescale from the init, `tfdt` baseMediaDecodeTime from the first media + * segment) — hence optional. The transient input the `establishStartMediaTime` + * reactor reduces into `Track.startMediaTime`. + * + * `segmentStartTime` is the 0-based presentation start of the segment + * `baseMediaDecodeTime` was read from — *not* a container value (it's the playlist + * position), but co-located because the origin is `baseMediaDecodeTime/timescale − + * segmentStartTime`: the first *loaded* segment isn't necessarily the 0th (a + * non-zero initial `currentTime`, or live/DVR), so the decode time alone isn't the + * stream origin. */ export interface MediaContainerData { timescale?: number; baseMediaDecodeTime?: number; + segmentStartTime?: number; } /** diff --git a/packages/spf/src/playback/behaviors/dom/relocation-steps.ts b/packages/spf/src/playback/behaviors/dom/relocation-steps.ts index ef0e4389..d0f22339 100644 --- a/packages/spf/src/playback/behaviors/dom/relocation-steps.ts +++ b/packages/spf/src/playback/behaviors/dom/relocation-steps.ts @@ -34,62 +34,78 @@ function containerSlot(deps: StepDeps): ContainerSlot { return (deps.state as unknown as StateSignals).mediaContainerData; } -/** Synchronous RMW of the per-track entry — disjoint keys across producers, so no lost update. */ -function writeContainer(slot: ContainerSlot, trackId: string, patch: Partial): void { - update(slot, (current) => ({ ...current, [trackId]: { ...current?.[trackId], ...patch } })); +/** Synchronous RMW of the per-type entry — disjoint keys across producers, so no lost update. */ +function writeContainer(slot: ContainerSlot, trackType: string, patch: Partial): void { + update(slot, (current) => ({ ...current, [trackType]: { ...current?.[trackType], ...patch } })); } -/** Init pipeline step: head-peek the `mdhd` timescale into `state.mediaContainerData[trackId]`. */ -const readInitTimescale: LoadStep = async (frame, _signal, deps) => { - const { op } = frame; - if (op.type !== 'append-init' || !frame.data) return; - const slot = containerSlot(deps); - const { trackId } = op.meta; - if (peek(slot)?.[trackId]?.timescale !== undefined) return; // already have it - frame.data = await peekHead(frame.data, (bytes) => { - const timescale = readFirstMediaTimescale(bytes); - if (timescale === undefined) return false; - writeContainer(slot, trackId, { timescale }); - return true; - }); -}; - -/** Media-segment pipeline step: head-peek the `tfdt` baseMediaDecodeTime into `state.mediaContainerData[trackId]`. */ -const readSegmentOrigin: LoadStep = async (frame, _signal, deps) => { - const { op } = frame; - if (op.type !== 'append-segment' || !frame.data) return; - const slot = containerSlot(deps); - const { trackId } = op.meta; - if (peek(slot)?.[trackId]?.baseMediaDecodeTime !== undefined) return; // established - frame.data = await peekHead(frame.data, (bytes) => { - const baseMediaDecodeTime = readFirstBaseMediaDecodeTime(bytes); - if (baseMediaDecodeTime === undefined) return false; - writeContainer(slot, trackId, { baseMediaDecodeTime }); - return true; - }); -}; - /** - * Media-segment stamp step. Tier 1: relocate by the track's *own* discovered origin, - * read straight from `mediaContainerData` (populated by `readSegmentOrigin` earlier - * in this same pipeline, so it's available synchronously). If no complete origin was - * found — TS / containerless / a 0-PTS source — leave the append native (offset 0). + * Relocation pipelines for one track type — a plain config `messagePipelines`. + * Keyed by **track type** (`'video'` / `'audio'`), so ABR rungs of a type share the + * origin (discover skips once the type's value is present). The steps read/write + * `state.mediaContainerData[trackType]` via their call-time `deps`. */ -const stampStartMediaTime: LoadStep = (frame, _signal, deps) => { - const { op } = frame; - if (op.type !== 'append-segment') return; - const data = peek(containerSlot(deps))?.[op.meta.trackId]; - if (data?.timescale === undefined || data.baseMediaDecodeTime === undefined) return; - // startTime is 0-based, so the relocating offset is −startMediaTime. - frame.meta = { ...(frame.meta ?? op.meta), timestampOffset: -(data.baseMediaDecodeTime / data.timescale) }; -}; +export function relocationPipelinesFor(trackType: 'video' | 'audio'): MessagePipelines { + /** Init step: head-peek the `mdhd` timescale into `mediaContainerData[trackType]`. */ + const readInitTimescale: LoadStep = async (frame, _signal, deps) => { + const { op } = frame; + if (op.type !== 'append-init' || !frame.data) return; + const slot = containerSlot(deps); + if (peek(slot)?.[trackType]?.timescale !== undefined) return; // already have it (any rung of this type) + frame.data = await peekHead(frame.data, (bytes) => { + const timescale = readFirstMediaTimescale(bytes); + if (timescale === undefined) return false; + writeContainer(slot, trackType, { timescale }); + return true; + }); + }; -/** - * Relocation pipelines — a plain config `messagePipelines`. The same map serves - * every track type: the steps key by the segment's own `op.meta.trackId`. - */ -export const relocationMessagePipelines: MessagePipelines = () => ({ - remove: [dispatchStep], - 'append-init': [fetchStep, readInitTimescale, dispatchStep], - 'append-segment': [fetchStep, readSegmentOrigin, stampStartMediaTime, dispatchStep], -}); + /** + * Media-segment step: head-peek the `tfdt` baseMediaDecodeTime, recording the + * segment's 0-based `startTime` with it — the origin is `bmdt/ts − segmentStartTime`, + * so the first *loaded* segment need not be the 0th. + */ + const readSegmentOrigin: LoadStep = async (frame, _signal, deps) => { + const { op } = frame; + if (op.type !== 'append-segment' || !frame.data) return; + const slot = containerSlot(deps); + if (peek(slot)?.[trackType]?.baseMediaDecodeTime !== undefined) return; // established + const segmentStartTime = op.meta.startTime; + frame.data = await peekHead(frame.data, (bytes) => { + const baseMediaDecodeTime = readFirstBaseMediaDecodeTime(bytes); + if (baseMediaDecodeTime === undefined) return false; + writeContainer(slot, trackType, { baseMediaDecodeTime, segmentStartTime }); + return true; + }); + }; + + /** + * Stamp step. Tier 1: relocate by this type's *own* discovered origin, read + * straight from `mediaContainerData` (populated by `readSegmentOrigin` earlier in + * this pipeline, so it's synchronous). If no complete origin was found — TS / + * containerless / a 0-PTS source — leave the append native (offset 0). + */ + const stampStartMediaTime: LoadStep = (frame, _signal, deps) => { + const { op } = frame; + if (op.type !== 'append-segment') return; + const data = peek(containerSlot(deps))?.[trackType]; + if ( + data?.timescale === undefined || + data.baseMediaDecodeTime === undefined || + data.segmentStartTime === undefined + ) { + return; + } + // startMediaTime = baseMediaDecodeTime/timescale − segmentStartTime; offset = −startMediaTime. + frame.meta = { + ...(frame.meta ?? op.meta), + timestampOffset: data.segmentStartTime - data.baseMediaDecodeTime / data.timescale, + }; + }; + + return () => ({ + remove: [dispatchStep], + 'append-init': [fetchStep, readInitTimescale, dispatchStep], + 'append-segment': [fetchStep, readSegmentOrigin, stampStartMediaTime, dispatchStep], + }); +} diff --git a/packages/spf/src/playback/behaviors/establish-start-media-time.ts b/packages/spf/src/playback/behaviors/establish-start-media-time.ts index 64a42da6..f73c988f 100644 --- a/packages/spf/src/playback/behaviors/establish-start-media-time.ts +++ b/packages/spf/src/playback/behaviors/establish-start-media-time.ts @@ -33,9 +33,11 @@ import { findTrackById } from '../../media/utils/tracks'; export interface EstablishStartMediaTimeState { presentation?: MaybeResolvedPresentation; /** - * Transient per-track container data (keyed by track id), accumulated by the - * discover steps across appends. Reset per source. Never the model — the churn - * stays here; only the settled `startMediaTime` reaches `Track`. + * Transient origin-establishment data, keyed by **track type** (`'video'` / + * `'audio'`) — per spec one init+media pair per type suffices, and ABR rungs of + * a type share the origin. Filled by the discover steps across appends, reset + * per source. Never the model — the churn stays here; only the settled + * `startMediaTime` reaches `Track`. */ mediaContainerData?: Record; selectedVideoTrackId?: string; @@ -48,27 +50,35 @@ export interface DeriveStartMediaTimeContext { } /** - * Reduce the discovered container data into each track's `startMediaTime`. - * `undefined` for a track means "not ready yet". Pure and injected — the single - * point of tier variation: Tier 1 is per-track own; a Tier-2 variant returns the - * shared `min` across the selected A/V origins for every track. + * Reduce the discovered container data (keyed by track type) into each type's + * `startMediaTime`. `undefined` means "not ready yet". Pure and injected — the + * single point of tier variation: Tier 1 is per-type own; a Tier-2 variant returns + * the shared `min` across the selected A/V origins for every type. */ export type DeriveStartMediaTime = ( containerData: Record, ctx: DeriveStartMediaTimeContext ) => Record; -/** Tier 1 default — each track relocates by its own origin (`baseMediaDecodeTime ÷ timescale`). */ -export const derivePerTrackStartMediaTime: DeriveStartMediaTime = (containerData) => { +/** + * Tier 1 default — each type relocates by its own origin: + * `startMediaTime = baseMediaDecodeTime/timescale − segmentStartTime` (the + * `segmentStartTime` term makes it the stream origin even when the first loaded + * segment isn't the 0th). + */ +export const derivePerTypeStartMediaTime: DeriveStartMediaTime = (containerData) => { const out: Record = {}; - for (const [id, { timescale, baseMediaDecodeTime }] of Object.entries(containerData)) { - out[id] = timescale != null && baseMediaDecodeTime != null ? baseMediaDecodeTime / timescale : undefined; + for (const [type, { timescale, baseMediaDecodeTime, segmentStartTime }] of Object.entries(containerData)) { + out[type] = + timescale != null && baseMediaDecodeTime != null && segmentStartTime != null + ? baseMediaDecodeTime / timescale - segmentStartTime + : undefined; } return out; }; export interface EstablishStartMediaTimeConfig { - /** The reduce seam (tier knob). Defaults to {@link derivePerTrackStartMediaTime}. */ + /** The reduce seam (tier knob). Defaults to {@link derivePerTypeStartMediaTime}. */ deriveStartMediaTime?: DeriveStartMediaTime; } @@ -84,7 +94,7 @@ function stampTracks(presentation: Presentation, startMediaTimes: Record ({ ...switchingSet, tracks: switchingSet.tracks.map((track) => { - const startMediaTime = startMediaTimes[track.id]; + const startMediaTime = startMediaTimes[track.type]; if (startMediaTime === undefined || track.startMediaTime === startMediaTime) return track; changed = true; return { ...track, startMediaTime }; @@ -112,7 +122,7 @@ function establishStartMediaTimeSetup({ state, config = {}, }: EstablishStartMediaTimeDeps): Reactor { - const derive = config.deriveStartMediaTime ?? derivePerTrackStartMediaTime; + const derive = config.deriveStartMediaTime ?? derivePerTypeStartMediaTime; const selectionContext = (): DeriveStartMediaTimeContext => ({ selectedVideoTrackId: state.selectedVideoTrackId?.get(), diff --git a/packages/spf/src/playback/behaviors/tests/establish-start-media-time.test.ts b/packages/spf/src/playback/behaviors/tests/establish-start-media-time.test.ts new file mode 100644 index 00000000..dfee1ac7 --- /dev/null +++ b/packages/spf/src/playback/behaviors/tests/establish-start-media-time.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; +import { derivePerTypeStartMediaTime } from '../establish-start-media-time'; + +describe('derivePerTypeStartMediaTime', () => { + it('derives the origin as baseMediaDecodeTime/timescale − segmentStartTime (0th segment)', () => { + // Mux asset_start_time=60: origin ≈ 60s. First loaded segment is the 0th (startTime 0). + expect( + derivePerTypeStartMediaTime( + { video: { timescale: 90000, baseMediaDecodeTime: 90000 * 60, segmentStartTime: 0 } }, + {} + ) + ).toEqual({ video: 60 }); + }); + + it('subtracts a non-zero segmentStartTime so it yields the stream origin, not the loaded segment', () => { + // Same source, but the first *loaded* segment starts at presentation 100 (native ≈160s) — + // e.g. a non-zero initial currentTime. The origin must still be 60, not 160. + expect( + derivePerTypeStartMediaTime( + { video: { timescale: 90000, baseMediaDecodeTime: 90000 * 160, segmentStartTime: 100 } }, + {} + ) + ).toEqual({ video: 60 }); + }); + + it('is undefined for a type until timescale + baseMediaDecodeTime + segmentStartTime are all present', () => { + expect(derivePerTypeStartMediaTime({ video: { timescale: 90000 } }, {})).toEqual({ video: undefined }); + expect(derivePerTypeStartMediaTime({ audio: { baseMediaDecodeTime: 100, segmentStartTime: 0 } }, {})).toEqual({ + audio: undefined, + }); + }); + + it('resolves each track type independently (preserving real A/V skew)', () => { + expect( + derivePerTypeStartMediaTime( + { + video: { timescale: 90000, baseMediaDecodeTime: 90000 * 60, segmentStartTime: 0 }, + audio: { timescale: 48000, baseMediaDecodeTime: 48000 * 59.956, segmentStartTime: 0 }, + }, + {} + ) + ).toEqual({ video: 60, audio: 59.956 }); + }); +}); diff --git a/packages/spf/src/playback/engines/hls/engine-audio-only.ts b/packages/spf/src/playback/engines/hls/engine-audio-only.ts index 7658a4c3..0125c9b4 100644 --- a/packages/spf/src/playback/engines/hls/engine-audio-only.ts +++ b/packages/spf/src/playback/engines/hls/engine-audio-only.ts @@ -21,7 +21,7 @@ import { import { deriveCdnPriority } from '../../behaviors/derive-cdn-priority'; import { endOfStream } from '../../behaviors/dom/end-of-stream'; import { loadAudioSegments } from '../../behaviors/dom/load-segments'; -import { relocationMessagePipelines } from '../../behaviors/dom/relocation-steps'; +import { relocationPipelinesFor } from '../../behaviors/dom/relocation-steps'; import { setupAudioBufferActors } from '../../behaviors/dom/setup-buffer-actors'; import { setupMediaSource } from '../../behaviors/dom/setup-mediasource'; import { trackCurrentTime } from '../../behaviors/dom/track-current-time'; @@ -181,9 +181,9 @@ export function createHlsAudioOnlyEngine( canPlayTrack: config.canPlayTrack ?? canPlayTrack, resolveDuration: config.resolveDuration ?? getResolvedSelectedTrackDuration, parsePresentation: config.parsePresentation ?? parseMultivariantPlaylist, - // Non-zero-PTS relocation (spike): pair the audio loader with the relocation - // steps `establishStartMediaTime` derives from. Remove with the composed reactor. - audioMessagePipelines: relocationMessagePipelines, + // Non-zero-PTS relocation (spike): pair the audio loader with the per-type + // relocation steps `establishStartMediaTime` derives from. Remove with the reactor. + audioMessagePipelines: relocationPipelinesFor('audio'), }; return createComposition( diff --git a/packages/spf/src/playback/engines/hls/engine.ts b/packages/spf/src/playback/engines/hls/engine.ts index d2e4cf62..d11f3e74 100644 --- a/packages/spf/src/playback/engines/hls/engine.ts +++ b/packages/spf/src/playback/engines/hls/engine.ts @@ -43,7 +43,7 @@ import { 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 { relocationMessagePipelines } from '../../behaviors/dom/relocation-steps'; +import { 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'; @@ -360,11 +360,10 @@ export function createSimpleHlsEngine( addSubtitlesTracksToMedia: config.addSubtitlesTracksToMedia ?? addSubtitlesTracksToMedia, getShowingSubtitlesTrackFromMedia: config.getShowingSubtitlesTrackFromMedia ?? getShowingSubtitlesTrackFromMedia, removeAllSubtitlesTracksFromMedia: config.removeAllSubtitlesTracksFromMedia ?? removeAllSubtitlesTracksFromMedia, - // Non-zero-PTS relocation (spike): the discover/stamp steps `establishStartMediaTime` - // pairs with, given to both buffer loaders (the steps key by the segment's own track). - // Remove these two lines with the composed reactor to drop relocation. - videoMessagePipelines: relocationMessagePipelines, - audioMessagePipelines: relocationMessagePipelines, + // 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( diff --git a/packages/spf/src/playback/engines/hls/index.ts b/packages/spf/src/playback/engines/hls/index.ts index e2049684..d89fecd5 100644 --- a/packages/spf/src/playback/engines/hls/index.ts +++ b/packages/spf/src/playback/engines/hls/index.ts @@ -7,7 +7,7 @@ export { getMediaPlaylistMetadata } from '../../../media/types'; // consumer swapping the tier policy via `config.deriveStartMediaTime`. export { type DeriveStartMediaTime, - derivePerTrackStartMediaTime, + derivePerTypeStartMediaTime, } from '../../behaviors/establish-start-media-time'; export type { SimpleHlsMediaAPI, SimpleHlsMediaProps } from './adapter'; export { SimpleHlsMediaElement, SimpleHlsMediaMixin, simpleHlsMediaDefaultProps } from './adapter';