mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
refactor(spf): rename anchorLiveTracks to anchorPresentationTimeline
The behavior's mechanism — pin one track from buffer ground truth, derive one shared offset, stamp every track onto it — is format-neutral; only the offset source (PDT) is live-specific. Rename it and its published signal (liveAnchor → presentationAnchor, matching the PresentationAnchor type it holds) so the name reflects the general role, and note the non-zero-PTS reuse path through the existing resolveBufferedAnchor seam. No behavior change. 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
6bbbc23961
commit
dbddbf67bb
@@ -29,7 +29,7 @@ media-playlist parser places their segments on the shared timeline at first
|
||||
resolve (`placeOnAnchor`). So a track selected later — an ABR rung, another audio
|
||||
language, late captions — resolves already anchored, with no per-track
|
||||
positioning pass. The established anchor is also published as a presentation-level
|
||||
`liveAnchor`, which `seekToLiveEdge` gates its live-edge seek on (see below).
|
||||
`presentationAnchor`, which `seekToLiveEdge` gates its live-edge seek on (see below).
|
||||
|
||||
No pre-buffer estimate. Until ground truth exists a track rides its raw parser
|
||||
timeline — a valid timeline for fetching the first segments (the same segments
|
||||
@@ -37,7 +37,7 @@ are fetched either way), so the loader bootstraps the buffer, and thus the pin,
|
||||
without it. The one thing the estimate originally covered: `seekToLiveEdge` must
|
||||
not seek before the pin, or it targets the raw window and the pin's later shift
|
||||
strands the playhead off-window (confirmed by smoke test). We address that by
|
||||
**gating** that seek on the published anchor (`liveAnchor`, below) rather than
|
||||
**gating** that seek on the published anchor (`presentationAnchor`, below) rather than
|
||||
bootstrapping it off a manifest estimate — the estimate's ~27 s turnover drift
|
||||
made it an unreliable seek target anyway. An earlier design kept the estimate as
|
||||
bootstrap; it's dropped in favor of the gate.
|
||||
@@ -60,7 +60,7 @@ single shared anchor.
|
||||
[live-timeline-anchoring](./live-timeline-anchoring.md) settled the anchor
|
||||
*source* — PDT, the universal wall clock shared across renditions — but left
|
||||
open how that anchor drives a shared presentation timeline. Today's
|
||||
`anchor-live-tracks` pins each selected A/V track to its own buffer
|
||||
`anchor-presentation-timeline` pins each selected A/V track to its own buffer
|
||||
independently, and doesn't anchor text at all.
|
||||
|
||||
Two facts make a single shared anchor sufficient — and per-track pinning
|
||||
@@ -84,7 +84,7 @@ it does not require returning to per-track pinning.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- **Per-track buffer pins (today's `anchor-live-tracks`).** Pin each selected
|
||||
- **Per-track buffer pins (today's `anchor-presentation-timeline`).** Pin each selected
|
||||
A/V track to its own buffered segment. Rejected: under the no-skew assumption
|
||||
the pins necessarily *agree*, so the extra pins are redundant restatement;
|
||||
and the approach structurally **cannot anchor text**, which has no
|
||||
@@ -115,7 +115,7 @@ This promotes open question **[4] sync anchor** in
|
||||
|
||||
## Verification
|
||||
|
||||
Implemented. `anchor-live-tracks` is a reactor that, on first buffer ground truth
|
||||
Implemented. `anchor-presentation-timeline` is a reactor that, on first buffer ground truth
|
||||
(entry to `anchored`), establishes the shared anchor once and stamps it onto
|
||||
every track via `positionAllTracksToAnchor` — resolved tracks shifted, shells
|
||||
given `startDate` for the parser's `placeOnAnchor` to honor at first resolve. No
|
||||
@@ -127,7 +127,7 @@ Unit-covered: the parser honors a pre-applied anchor (`parse-media-playlist.test
|
||||
preserving (`presentation-anchor.test.ts`); the behavior establishes from the
|
||||
buffered video track and stamps all tracks, first-track-wins, establishes once
|
||||
across reloads, and is inert without buffer truth or a selected track
|
||||
(`anchor-live-tracks.test.ts`); `seekToLiveEdge` holds its seek until `liveAnchor`
|
||||
(`anchor-presentation-timeline.test.ts`); `seekToLiveEdge` holds its seek until `presentationAnchor`
|
||||
is published (`seek-to-live-edge.test.ts`). Live A/V init smoke-tested on an
|
||||
ephemeral Mux LL-HLS stream: with no estimate, the seek is gated until the pin
|
||||
lands, then fires once to the live edge — clean startup, no stranding (an
|
||||
@@ -148,4 +148,4 @@ race.
|
||||
- [live-presentation-modeling](../design/spf/live-presentation-modeling.md) —
|
||||
open question [4], promoted to presentation-level here.
|
||||
- [live-stream-support](../design/spf/features/live-stream-support.md) — the
|
||||
`anchor-live-tracks` behavior that implements this.
|
||||
`anchor-presentation-timeline` behavior that implements this.
|
||||
|
||||
@@ -136,8 +136,8 @@ realized. What remains is forward-looking:
|
||||
| `liveWindowFor` *(pure helper)* | `media/live-window.ts` | Derive the live window `{start,end}` from the track with the given id (type-agnostic via `findTrackById`), or `null` (VOD/ended/unresolved). Purely geometric — no delivery-format metadata. Centralizes all inertness so consumers don't re-derive the window. |
|
||||
| `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. Also gated on `liveAnchor` (published by `anchorLiveTracks`): the seek waits until the timeline is buffer-anchored, so it targets the final native-PTS window rather than the raw pre-anchor one (which the pin's later shift would strand). |
|
||||
| `anchorLiveTracks` | `behaviors/anchor-live-tracks.ts` | A reactor that establishes **one** shared presentation anchor once per source — on first buffer ground truth, first-track-wins — and stamps it onto every track via `positionAllTracksToAnchor`: resolved tracks shift onto it, not-yet-resolved shells get `startDate` for the parser's `placeOnAnchor` to honor at first resolve. Covers video, audio, and text; any track selected later (ABR / audio language / late captions) resolves already anchored. No pre-buffer estimate. See [live-presentation-anchor](../../../decisions/live-presentation-anchor.md). |
|
||||
| `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. Also gated on `presentationAnchor` (published by `anchorPresentationTimeline`): the seek waits until the timeline is buffer-anchored, so it targets the final native-PTS window rather than the raw pre-anchor one (which the pin's later shift would strand). |
|
||||
| `anchorPresentationTimeline` | `behaviors/anchor-presentation-timeline.ts` | A reactor that establishes **one** shared presentation anchor once per source — on first buffer ground truth, first-track-wins — and stamps it onto every track via `positionAllTracksToAnchor`: resolved tracks shift onto it, not-yet-resolved shells get `startDate` for the parser's `placeOnAnchor` to honor at first resolve. Covers video, audio, and text; any track selected later (ABR / audio language / late captions) resolves already anchored. No pre-buffer estimate. 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) |
|
||||
@@ -158,7 +158,7 @@ Live edge and `liveEdgeStart` are **derived** (via `getLiveEdge`) from
|
||||
`track.segments` + the injected latency, not state slots.
|
||||
|
||||
**Engine composition:** `engines/hls/engine.ts` composes the live behaviors
|
||||
unconditionally (`anchorLiveTracks`, `calculatePresentationDuration`,
|
||||
unconditionally (`anchorPresentationTimeline`, `calculatePresentationDuration`,
|
||||
`updateMediaSourceDuration`, the `resolveXTrack` family with
|
||||
`reschedule: delayedReschedule(mediaPlaylistReloadDelay)`, `seekToLiveEdge`,
|
||||
`endOfStream`); VOD inertness comes from finite-duration guards, not a branch.
|
||||
@@ -183,7 +183,7 @@ 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` — establishes from the buffered
|
||||
- `behaviors/tests/anchor-presentation-timeline.test.ts` — establishes from the buffered
|
||||
video track and stamps all tracks (incl. an unresolved text shell);
|
||||
first-track-wins; establishes once across reloads; inert without buffer truth.
|
||||
- `behaviors/tests/resolve-track.test.ts` — live reload re-resolves; stops on
|
||||
@@ -271,7 +271,7 @@ covers the guard logic deterministically.
|
||||
- [clusters.md § Composition vs Policy vs middle pattern](./clusters.md#composition-vs-policy-vs-middle-pattern)
|
||||
— the edge-only composition framing.
|
||||
- [live-timeline-anchoring](../../../decisions/live-timeline-anchoring.md) — PDT
|
||||
anchor that places the sliding-window timeline `anchorLiveTracks` consumes.
|
||||
anchor that places the sliding-window timeline `anchorPresentationTimeline` consumes.
|
||||
- [mse-timestamp-offset](../../../decisions/mse-timestamp-offset.md) — native-PTS
|
||||
default, `setLiveSeekableRange` in native coords, one-time seek into the
|
||||
window on load.
|
||||
|
||||
@@ -338,7 +338,7 @@ prematurely fix.
|
||||
anticipated split: **fetch scheduling is per-track** — each `resolveXTrack`
|
||||
owns a `RecurringRunner` paced by its own `TARGETDURATION` — while
|
||||
cross-track **timeline reconciliation** stays separate (the parser carries
|
||||
the timeline forward per fetch; `anchorLiveTracks` aligns renditions by PDT).
|
||||
the timeline forward per fetch; `anchorPresentationTimeline` aligns renditions by PDT).
|
||||
So fetch scheduling is independent without the timeline reconciliation being
|
||||
forced independent, as this question anticipated.
|
||||
- **[4] How captured PDT feeds the A/V-sync anchor — DECIDED.** Anchor *source*
|
||||
|
||||
Reference in New Issue
Block a user