mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
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:
co-authored by
Claude Opus 4.8
parent
bfe12eedf9
commit
f531e1055c
@@ -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.
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user