refactor(spf): hold one shared presentation anchor for all live tracks

Convert anchor-live-tracks from N per-track buffer pins to a single
shared (media-time <-> PDT) anchor applied to every selected track —
video, audio, and now text. A two-state reactor (unanchored -> anchored)
positions from the manifest estimate until a selected A/V track has
SourceBuffer ground truth, then establishes the shared anchor once
(first track to buffer wins) and positions each track onto it by PDT,
leaving it to the parser's carry-forward thereafter.

The resolveBufferedAnchor seam now takes the standard (track, deps)
setup arguments instead of closing over engine scope; the HLS engine's
implementation lives in its own generic module (resolve-buffered-anchor)
so a future audio-only-live engine can reuse it. anchorLiveTracks is now
a makeAnchorLiveTracks<Context>() factory mirroring makeShareSignals.

Realizes internal/decisions/live-presentation-anchor.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2026-06-25 10:00:45 -07:00
co-authored by Claude Opus 4.8
parent 3a9d1faca0
commit 7cde2ea28d
6 changed files with 432 additions and 179 deletions
+11 -6
View File
@@ -99,12 +99,17 @@ This promotes open question **[4] sync anchor** in
## Verification
Not yet implemented. The implementation converts `anchor-live-tracks` into a
reactor (`unanchored → anchored`; a single entry establishes the shared anchor
once an A/V track has buffer ground truth, then positions all selected tracks)
— tracked separately. Verification will cover: one A/V pin placing audio + text
by PDT; first-track-wins; pre-pin estimate → buffer-pin upgrade; pin-once (no
re-pin across reloads); inert when no PDT / no resolved track.
Implemented. `anchor-live-tracks` is a two-state reactor (`unanchored →
anchored`): `unanchored` positions every selected track from the manifest
estimate; entering `anchored` establishes the shared anchor once from the first
selected A/V track with buffer ground truth, then positions all selected tracks
(incl. text) onto it. The old per-track pin primitives
(`anchorTrackToBufferedSegment` / `anchorTrackToSequenceOrigin`) are removed.
Unit-covered (`anchor-live-tracks.test.ts`): one A/V pin placing audio + text by
PDT; first-track-wins (video preferred); pre-pin estimate → buffer-pin upgrade;
pin-once (no re-pin across reloads); inert when no PDT / no resolved track. Live
end-to-end (a real stream with subtitles) is not yet smoke-tested.
## See also
@@ -137,7 +137,7 @@ realized. What remains is forward-looking:
| `liveWindowFromState` / `getLiveEdge` *(primitives)* | `playback/primitives/live-window.ts` | The state-reading call sites the live behaviors use. `liveWindowFromState` picks the timeline-bearing track — `selectedVideoTrackId ?? selectedAudioTrackId` (video positions both A/V; audio-only falls back to audio) — and calls `liveWindowFor`. `getLiveEdge({state,config})` adds the target playhead position (`liveEdgeStart = end − live latency`, clamped to start), bundling window geometry with the format-specific `config.resolveLiveLatency` policy so the behavior consumes one edge. Reads signals lazily (call inside an effect). |
| `syncLiveSeekableRange` | `behaviors/dom/sync-live-seekable-range.ts` | Consume `liveWindowFromState`; `setLiveSeekableRange(start, end)` reactively on each window slide, including while paused. Duration is owned solely by `updateMediaSourceDuration`. Composed before `seekToLiveEdge`. |
| `seekToLiveEdge` | `behaviors/dom/seek-to-live-edge.ts` | A reactor (`inactive ↔ live`) consuming `getLiveEdge`. `live` `entry` does the one-time seek to `liveEdgeStart`; `live` `effects` runs the window-exit guard (window-update re-fire + `play` listener). Format-neutral — the live latency comes from the injected `resolveLiveLatency` seam, never read here. The `mediaSource`-open precondition orders the entry seek after `sync-live-seekable-range` declares the range, so the seek lands in-window. |
| `anchorLiveTracks` | `behaviors/anchor-live-tracks.ts` | Pin live track timelines to the SourceBuffer's native-PTS ground truth (first appended segment) or manifest estimate; re-pin per reload as the window slides |
| `anchorLiveTracks` | `behaviors/anchor-live-tracks.ts` | A reactor (`unanchored → anchored`) holding **one** shared presentation anchor for all selected tracks (video, audio, *and* text). `unanchored` positions from the manifest estimate; entering `anchored` establishes the anchor once from the first selected A/V track's SourceBuffer ground truth (first-track-wins) and positions each track onto it by PDT, then leaves it to the parser's carry-forward (pin-once surfaces drift). See [live-presentation-anchor](../../../decisions/live-presentation-anchor.md). |
| `resolveVideoTrack` / `resolveAudioTrack` / `resolveTextTrack` | `behaviors/resolve-track.ts` | Own the reload loop via `RecurringRunner`; reschedule defaults to `mediaPlaylistReloadDelay`; per-type independent |
| `calculatePresentationDuration` | `behaviors/calculate-presentation-duration.ts` | Populate `presentation.duration` via the config resolver (`Infinity` for unended live) |
| `updateMediaSourceDuration` | `behaviors/dom/update-mediasource-duration.ts` | Propagate `presentation.duration` to `mediaSource.duration` once per MediaSource (uniform across variants) |
@@ -183,8 +183,9 @@ unconditionally (`anchorLiveTracks`, `calculatePresentationDuration`,
- `media/hls/tests/parse-media-playlist.test.ts` — `Infinity` for unended
live; `endList` on `#EXT-X-ENDLIST`; finite for `PLAYLIST-TYPE:VOD`; PDT
capture + carry-forward.
- `behaviors/tests/anchor-live-tracks.test.ts` — pin to buffer ground truth;
PDT carry-forward; sequence-origin bootstrap.
- `behaviors/tests/anchor-live-tracks.test.ts` — estimate bootstrap; one A/V
pin placing audio + text by PDT; first-track-wins; estimate → buffer-pin
upgrade; pin-once across reloads.
- `behaviors/tests/resolve-track.test.ts` — live reload re-resolves; stops on
finite duration; source-change abort.
- `behaviors/dom/tests/seek-to-live-edge.test.ts` — seeks to `liveEdgeStart`