docs(spf): decide the single presentation-level live timeline anchor

Capture the decision to hold one rolling (media-time ↔ PDT) anchor — learned
from the first A/V track to obtain SourceBuffer ground truth and applied to all
selected tracks (incl. text) by per-segment PDT — replacing today's per-track
buffer pins, which are redundant under the no-skew assumption and can't anchor
text (no SourceBuffer). New decision doc live-presentation-anchor.md; resolves
the deferred single-rolling-anchor question in live-timeline-anchoring and
promotes [4] in live-presentation-modeling to presentation-level.

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 bfe12eedf9
commit f531e1055c
3 changed files with 149 additions and 18 deletions
@@ -0,0 +1,121 @@
---
status: decided
date: 2026-06-24
---
# Single presentation-level live timeline anchor
## Decision
Hold **one** rolling presentation-level anchor — a `(media-time ↔ PDT)`
correspondence — and apply it to **every** selected track, rather than pinning
each track to its own buffer.
The anchor is **learned from whichever selected audio/video track first has
SourceBuffer ground truth**: the buffer pin (`resolveBufferedAnchor`) reports a
buffered segment's actual native-PTS start `M₀`, paired with that segment's PDT
`P₀`. From that one pair, any track's segment is placed by its own PDT:
```
segment.startTime = M₀ + (segment.programDateTime P₀)
```
Established **once** per source (pin-once — it surfaces drift rather than
masking it; the parser's PDT carry-forward maintains the timeline across
reloads). Before any A/V track has buffer ground truth, the manifest estimate
(the existing bootstrap) supplies an *estimated* anchor of the same shape, which
the buffer pin later upgrades.
This anchor covers **text tracks too** — they have no SourceBuffer to pin, so
the shared anchor is the *only* way to place them. WebVTT cues are assumed
already time-aligned (the analog of assuming encoded A/V samples are aligned);
what we align is the text track's **segment** timeline, by PDT, exactly as for
A/V. (That cues are sparse/overlapping — a cue may begin or end outside its
segment's nominal bounds — is a text *loader/eviction* concern, not an
anchoring one.)
Scope: this fixes the **single rolling anchor vs. per-segment measured times**
question deferred by [live-timeline-anchoring](./live-timeline-anchoring.md),
and realizes the *manifest → buffer* half of that decision's Boundary as a
single shared anchor.
## Context
[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
independently, and doesn't anchor text at all.
Two facts make a single shared anchor sufficient — and per-track pinning
redundant:
1. **MSE plays all SourceBuffers on one element timeline.** There is a single
`currentTime`; audio and video are coerced onto one native-PTS coordinate.
2. **Native-PTS default** ([mse-timestamp-offset](./mse-timestamp-offset.md)) —
no `timestampOffset`, so every track shares the encoder's PTS clock.
Under (1) + (2) there is no inter-track skew, so one track's `(media-time ↔
PDT)` pair describes all tracks; further pins would only restate it. And text —
which has no buffer to pin — can be placed *only* by a shared anchor.
**Assumption committed:** no inter-track PTS skew. This is the existing
live-model assumption (native-PTS default); per-track skew correction remains
deferred to
[non-zero-pts-support](../design/spf/features/non-zero-pts-support.md). If skew
ever needs handling it layers a per-track offset on top of the shared anchor —
it does not require returning to per-track pinning.
## Alternatives Considered
- **Per-track buffer pins (today's `anchor-live-tracks`).** 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
SourceBuffer — keeping it would force a separate, divergent text path for the
same concern.
- **Per-track PDT estimate only (no buffer truth).** Place every track from the
manifest estimate (`sequence × avgDuration` + PDT) and never pin. Rejected:
drops the authoritative correction — the estimate is duration-variance-prone
(it produced an observed ~27 s drift at window turnover), and the buffer pin
is what makes the timeline exact. The estimate is the right *bootstrap*, not
the final answer.
## Rationale
PDT is already the cross-track anchor source; this decision commits to **one**
anchor instead of N. The win is uniformity: audio, video, and text all position
by the same `(media-time ↔ PDT)` rule, so the behavior has one code path, text
stops being a special case, and "first track to buffer wins" makes the anchor
available as early as possible. Pinning once (not per reload) keeps the
drift-surfacing property: if a track's PDT disagrees with the established
anchor, it shows up as a visible desync rather than being silently re-corrected
every reload.
This promotes open question **[4] sync anchor** in
[live-presentation-modeling](../design/spf/live-presentation-modeling.md) from
"per-segment / per-track" to "presentation-level," as that doc anticipated.
## 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.
## See also
- [live-timeline-anchoring](./live-timeline-anchoring.md) — the anchor *source*
(PDT); this decision resolves its deferred "single rolling anchor vs.
per-segment measured times" question.
- [mse-timestamp-offset](./mse-timestamp-offset.md) — native-PTS default; the
manifest → buffer offset half this anchor realizes.
- [non-zero-pts-support](../design/spf/features/non-zero-pts-support.md) — where
per-track skew correction lands if the no-skew assumption ever breaks.
- [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.
+10 -5
View File
@@ -19,11 +19,13 @@ seconds) used for two things:
`targetDuration × offset`.
Scope of this decision: the **anchor source** is PDT. The parser *surfaces*
`Segment.programDateTime` (landed; see Verification). How that anchor drives a
shared presentation timeline — the cross-track adjuster / per-track
`presentationTimeOffset` derivation, and whether the model holds a single
rolling anchor vs. per-segment measured times — is downstream and not fixed
here.
`Segment.programDateTime` (landed; see Verification). Whether the model holds a
single rolling anchor vs. per-segment measured times is **fixed in
[live-presentation-anchor](./live-presentation-anchor.md)** — a single
presentation-level `(media-time ↔ PDT)` anchor, learned from the first A/V
track to buffer and applied to all tracks (incl. text) by PDT. The remaining
per-track `presentationTimeOffset` derivation for the skew case is downstream
and not fixed here.
This resolves open questions **[4] sync anchor** and **turnover `startTime`
recovery** in
@@ -86,6 +88,9 @@ cross-track adjuster are not yet implemented.
## See also
- [live-presentation-anchor](./live-presentation-anchor.md) — fixes how this
anchor drives a single shared presentation timeline (the deferred
single-rolling-anchor question).
- [mse-timestamp-offset](./mse-timestamp-offset.md) — the buffer-layer half;
the manifest→buffer offset this decision's anchor feeds into.
- [live-presentation-modeling](../design/spf/live-presentation-modeling.md) —
@@ -197,10 +197,13 @@ count); a parts-only LL-HLS update is invisible to it, which
`EXT-X-PROGRAM-DATE-TIME` is captured per-segment in the snapshot. It is
the **wall-clock anchor common across separate renditions** — the spec's
designated mechanism for keeping demuxed audio and video aligned. The
model's job here is to *capture* PDT; the presentation-level anchor
derivation and *how it drives per-track `timestampOffset`* is the
deferred A/V-sync decision (see [Open questions](#open-questions) and
[non-zero-pts-support](./features/non-zero-pts-support.md)). PDT alone
model's job here is to *capture* PDT; the **presentation-level anchor
derivation is now decided** — a single `(media-time ↔ PDT)` anchor learned
from the first A/V track to buffer and applied to all tracks (incl. text) by
PDT (see [live-presentation-anchor](../../decisions/live-presentation-anchor.md)).
Only *how that anchor drives per-track `timestampOffset` in the skew case*
remains the deferred A/V-sync decision (see [Open questions](#open-questions)
and [non-zero-pts-support](./features/non-zero-pts-support.md)). PDT alone
suffices as a sync anchor under the no-mid-stream-discontinuity
assumption this foundation makes; `EXT-X-DISCONTINUITY(-SEQUENCE)` is
deferred (it is not live-exclusive — ad-stitched VOD has it too — and is
@@ -338,15 +341,17 @@ prematurely fix.
the timeline forward per fetch; `anchorLiveTracks` 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).**
Resolved in [live-timeline-anchoring](../../decisions/live-timeline-anchoring.md):
align demuxed audio/video by equal PDT (same real instant), not by
sequence number or per-track zeroing. The parser now surfaces
`Segment.programDateTime`. Still open downstream: how that anchor drives a
shared presentation timeline (the cross-track adjuster / per-track
`presentationTimeOffset`), and the separate manifest→buffer offset
([mse-timestamp-offset](../../decisions/mse-timestamp-offset.md)). Couples
back to discontinuity handling when that lands.
- **[4] How captured PDT feeds the A/V-sync anchor — DECIDED.** Anchor *source*
resolved in [live-timeline-anchoring](../../decisions/live-timeline-anchoring.md)
(align demuxed audio/video by equal PDT, not sequence number or per-track
zeroing); the parser surfaces `Segment.programDateTime`. Anchor *structure*
resolved in [live-presentation-anchor](../../decisions/live-presentation-anchor.md):
a single presentation-level `(media-time ↔ PDT)` anchor, learned from the first
A/V track to buffer and applied to all tracks (incl. text) by PDT. Still open
downstream: the per-track `presentationTimeOffset` derivation for the *skew*
case, and the separate manifest→buffer offset
([mse-timestamp-offset](../../decisions/mse-timestamp-offset.md)). Couples back
to discontinuity handling when that lands.
- **Parser output shape — resurrect `MediaPlaylistInfo` or hang fields on
`Track`?** The snapshot/merge model wants a faithful per-fetch
representation; today's parser merges into `Track`. This is the parser