wip(spf): wire non-zero-PTS relocation into the audio-only engine

Compose establishStartMediaTime + bake audioMessagePipelines + the
mediaContainerData slot + deriveStartMediaTime config into
createHlsAudioOnlyEngine, comment-marked, mirroring the default engine. The
reactor's optional/defensive selection composes with only audio in scope.

Audio relocates to a 0-based timeline — sandbox-verified on the Mux
asset_start_time=60 clip via the html-simple-hls-audio-only harness (audio
buffered/seekable 0-based, duration 540).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2026-07-09 14:31:15 -07:00
co-authored by Claude Opus 4.8
parent 05a0a6452d
commit 0b99687db4
2 changed files with 43 additions and 26 deletions
@@ -39,33 +39,28 @@ your detailed pass; **DEFERRED** = later/open.
- spike relocation (boolean/inline approach, since reworked) — `6a20bade9`
- VTT `X-TIMESTAMP-MAP` parse — `7b0d8bbdc`
- messagePipelines step model (loader step pipelines) — `041cecca6`
- relocation as `establishStartMediaTime` reactor (DOM-free) + `relocation-steps`
(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
### Uncommitted — KEEP (reactor + config-pipelines; typecheck/tests/lint/build green, A/V sandbox-verified 0-based)
- `behaviors/dom/establish-start-media-time.ts`**new**. Two pieces:
- **Steps** (`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 their call-time `deps` — no closures, no
context. One map serves both track types (steps key by the segment's trackId).
- **Reactor** (`establishStartMediaTime`): 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**.
- `primitives/head-peek.ts`**new** generic eager head-peek (replaces the old
self-discriminating `origin-discoverer`).
- `media/types/index.ts``Track.startMediaTime` + `MediaContainerData`.
- `actors/dom/segment-loader.ts``StepDeps` widened with the composition
`{ state, context, config }` (opaque conduit; loader never reads them).
- `behaviors/dom/setup-buffer-actors.ts` — reads relocation pipelines from **config**
(`video/audioMessagePipelines`), threads composition deps into the loader. No context slots.
- `engines/hls/engine.ts` — composes the reactor before `setup*BufferActors`;
bakes `video/audioMessagePipelines = relocationMessagePipelines` + `mediaContainerData`
slot + `deriveStartMediaTime` config. All comment-marked for easy removal.
- `engines/hls/index.ts` — exports `DeriveStartMediaTime` + `derivePerTrackStartMediaTime`.
- **Deleted:** `engines/hls/relocation.ts`, `primitives/origin-discoverer.ts`.
### 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).
- **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 +
`mediaContainerData` slot + `deriveStartMediaTime` config, all comment-marked.
### REVIEW — your detailed pass
- The reactor + steps end-to-end (KEEP surface above).
- The reactor + steps end-to-end (architecture above).
- **`StepDeps` typing:** steps get composition `{state,context,config}` typed loose
(`AnySlotMap`); relocation asserts its slots via `containerSlot(deps)` (one cast,
since it legitimately knows the composition provides them). OK, or want it tighter?
@@ -86,8 +81,10 @@ 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.
- Audio-only composition wiring (reactor baked into the standard engine only).
- Honest-`startMediaTime`-everywhere convergence + live-anchor dedup.
- Suspected pre-existing: seek-into-evicted-back-buffer.
@@ -9,7 +9,7 @@ import type { BackBufferConfig } from '../../../media/buffer/back-buffer';
import type { ForwardBufferConfig } from '../../../media/buffer/forward-buffer';
import { canPlayTrack } from '../../../media/dom/capabilities';
import { parseMultivariantPlaylist } from '../../../media/hls/parse-multivariant';
import type { AudioTrack, CanPlayTrack, MaybeResolvedPresentation } from '../../../media/types';
import type { AudioTrack, CanPlayTrack, MaybeResolvedPresentation, MediaContainerData } from '../../../media/types';
import type { GetCdnId } from '../../../media/utils/cdn';
import { getResolvedSelectedTrackDuration } from '../../../media/utils/track-selection';
import type { SegmentLoaderActor } from '../../actors/dom/segment-loader';
@@ -21,11 +21,16 @@ 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 { setupAudioBufferActors } 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';
// Non-zero-PTS relocation (spike): remove this import, the composed reactor, the
// `audioMessagePipelines` finalConfig entry, the `mediaContainerData` state slot,
// and the `deriveStartMediaTime` config field to drop relocation from audio-only.
import { type DeriveStartMediaTime, establishStartMediaTime } from '../../behaviors/establish-start-media-time';
import { type ParsePresentation, resolvePresentation } from '../../behaviors/resolve-presentation';
import { resolveAudioTrack } from '../../behaviors/resolve-track';
import { type FailoverMonitorConfig, setupFailoverMonitor } from '../../behaviors/setup-failover-monitor';
@@ -47,6 +52,9 @@ export interface SimpleHlsAudioOnlyEngineState {
presentation?: MaybeResolvedPresentation;
preload?: 'auto' | 'metadata' | 'none';
selectedAudioTrackId?: string;
// Non-zero-PTS relocation (spike): transient per-track container data owned by
// `establishStartMediaTime`. Remove with the composed reactor.
mediaContainerData?: Record<string, MediaContainerData>;
/**
* Consumer-driven constraint narrowing the audio candidate set. Sibling
* of `userVideoTrackSelection` in the default engine. Partial-track
@@ -119,6 +127,8 @@ export interface SimpleHlsAudioOnlyEngineConfig
* Defaults to the URL origin; override to key on e.g. Mux's `cdn=` param.
*/
getCdnId?: GetCdnId;
/** Non-zero-PTS relocation (spike): the reduce seam (tier knob); defaults to per-track own. */
deriveStartMediaTime?: DeriveStartMediaTime;
}
// ============================================================================
@@ -171,6 +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,
};
return createComposition(
@@ -210,6 +223,13 @@ export function createHlsAudioOnlyEngine(
// so the Firefox `mozHasAudio` registration ordering is moot here.
setupMediaSource,
updateMediaSourceDuration,
// Non-zero-PTS relocation (spike): establishes per-track startMediaTime;
// MUST precede setupAudioBufferActors. Remove this line + the import + the
// finalConfig/state entries to drop relocation. (Selection is optional in the
// reactor, so it works with only audio in scope.)
establishStartMediaTime,
setupAudioBufferActors,
// Playback tracking