Two decision docs for the live-HLS timeline work: - mse-timestamp-offset — native-PTS default for live; timestampOffset reserved for relocation (the manifest→buffer / encoded layer). - live-timeline-anchoring — align demuxed tracks and recover turnover via PDT, not sequence number (the manifest layer). Resolve open questions [4] (sync anchor) and turnover startTime recovery in live-presentation-modeling, pointing at the decisions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
status, date
| status | date |
|---|---|
| decided | 2026-06-12 |
MSE timeline derivation and timestampOffset usage
Decision
Place media on the MSE presentation timeline at its native PTS by default
— neither rewriting timestamps nor applying a timestampOffset for the common
case. Reach for timestampOffset only to relocate a region. Concretely:
segmentsmode, notsequencemode. The engine's own timeline model is the source of truth;segmentsmode keeps frame placement legible to that model.sequencemode delegates placement (and an implicittimestampOffsetrecomputation) to the user agent, which we don't want.- Default (continuous live): append at native PTS, offset 0. Declare the
seekable window with
MediaSource.setLiveSeekableRange()in that native timeline; seek into the window on load. No timeline remapping is required — live UIs are window-relative (DVR: differences againstseekable.start/seekable.end) or position-less (pure live edge), so the origin is irrelevant; absolute-time display comes from PDT separately. This is the simplest and safest path: large positive timestamps are precision-safe in JS doubles (microsecond-fine even at 10⁹ s) and sidestep the Chromium negative-DTS append failure entirely (timestamps never go negative). For a CMAF-first engine there is no remux step, so byte-level rewriting never even arises. - Reach for
timestampOffsetonly to relocate a region, set once before the first append of that region (afterabort()+ feeding a keyframe first), never per-segment. The relocation cases: instant-clip / "currentTimestarts at 0" product semantics; mid-stream discontinuities / encoder restart; and aligning tracks whose PTS epochs differ onto a common timeline. VHS's "offset only at boundaries" shape applies here; per-segment offsetting is the anti-pattern. - Derive any relocation offset empirically / by introspection, then
validate.
Read the first segment's
tfdtbaseMediaDecodeTime(timescale-aware) and confirm against read-backSourceBuffer.bufferedrather than assuming a fixed relationship between encoded media time and presentation time. - When you do offset, guard it to keep DTS ≥ 0 on Chromium. A negative DTS after offsetting is a hard append failure on Chrome/Chromium (not Firefox). Any relocation offset that could push B-frame DTS negative must be clamped or paired with a DTS rebase. (The native-PTS default avoids this by construction.)
- Audio sample-continuity across discontinuities is a separate, hard
requirement. Chrome paces the audio clock from decoded sample counts,
not from appended per-frame timestamps.
timestampOffsetanchors where a coded-frame-group starts; it does not make Chrome honor per-frame audio PTS mid-run. Across a discontinuity the audio must be made sample-continuous (or the gap handled explicitly), independent of the offset.
This resolves the mechanism fork in non-zero-pts-support and informs open question [4] in live-presentation-modeling.
Context
Live HLS does not need a zero-based presentation timeline — that's a VOD /
instant-clip requirement, not a live one. A non-seekable live UI renders no
position at all (a "LIVE" indicator, not a scrubber), and a DVR UI is entirely
window-relative — scrubber fill, "behind live", and window length are all
differences against seekable.start/seekable.end, so the timeline's origin
cancels out. Absolute-time display (time-of-day) comes from
EXT-X-PROGRAM-DATE-TIME, a separate media→wall-clock mapping orthogonal to
currentTime's origin. What live actually needs from the engine is narrower:
a consistent monotonic timeline in the buffer (native PTS satisfies this), a
declared seekable window via setLiveSeekableRange in whatever coordinates
were appended, a one-time seek into that window on load, optional PDT
wall-clock mapping, and A/V sync across the window and any discontinuities
(e.g. SSAI ad splices). The engine maintains its own timeline model separate
from MSE, so the question is how that model drives the SourceBuffer — and
whether to touch timestampOffset at all, given every production OSS web
engine uses it in some form.
A deep research pass (W3C MSE spec, byte-stream registry, Chromium/WebKit/ Gecko trackers, and the source of hls.js / shaka / VHS) plus targeted reading of specific Chromium and WebAudio/WebCodecs issues grounded the trade-offs. See Evidence below for citations.
Alternatives Considered
-
Byte-level timestamp rewriting (rewrite PTS/DTS in the segment before append — hls.js's historical path). Rejected: hls.js maintainers call it "costly and result in issues" and are migrating to
timestampOffset(hls.js #5715), with negative-DTS decode artifacts as a documented failure class (#5710). Walking away fromtimestampOffsetmeans walking into the path the industry is abandoning. -
Rebase to zero via
timestampOffset(buffercurrentTimebecomes ~0-based at source start; browser does the math). Considered as the default but not chosen for continuous live. It does not actually shrink the live translation surface: zero-at-encoder-start is still not zero-at-window- start, so DVR UIs compute window-relative time fromseekableeither way. And it gives up the native-PTS path's two free wins (negative-DTS immunity, no offset to maintain). Kept as the mechanism for the relocation cases (Decision §3), where 0-based product semantics or cross-epoch alignment are the actual requirement. -
sequencemode (Shaka's HLS default). Rejected: it still usestimestampOffsetunder the hood (UA sets it from the group-start timestamp), so it doesn't "avoid" the mechanism — it just hides placement from our model. We want the model authoritative and placement explicit. -
Per-segment
timestampOffset(Shaka's per-segment empirical offset). Rejected for our continuous-run case: VHS explicitly removed per-segment offset changes because they "produce bad behavior, especially around long-running live streams." Per-segment offsetting also fights Chrome's audio sample clock. We keep the offset stable within a run and re-anchor only at boundaries (the VHS shape).
Rationale
The OSS field converges on timestampOffset, but mostly because those engines
remux TS→fMP4 (hls.js) or stitch multi-period content (Shaka/DASH) and need to
relocate media as a matter of course. A CMAF-first live engine that holds its
own timeline model has no such standing need: appending at native PTS lets the
media place itself, and setLiveSeekableRange declares the window in the same
coordinates. That default is strictly safer — large positive timestamps are
precision-safe and can't trip the Chromium negative-DTS abort — and the failure
modes the field works around (long-running-live drift, A/V divergence,
negative-DTS) all trace to applying or changing an offset, which we now do
only when relocation genuinely requires it. The mechanism remains the right
tool for that narrow job; the decision is to stop reaching for it by default.
Evidence
Spec / mechanics:
- MSE coded-frame processing adds
timestampOffsetto both PTS and DTS;segmentsvssequenceplacement; coded-frame-group / need-random-access- point rules — https://www.w3.org/TR/media-source-2/, https://www.w3.org/TR/mse-byte-stream-format-mp2t/.
Engine prior art:
- hls.js abandoning byte rewriting for
timestampOffset— https://github.com/video-dev/hls.js/issues/5715, https://github.com/video-dev/hls.js/issues/5710. - Shaka per-segment empirical offset +
sequencemode for HLS — Shakamedia_source_engine.js(calculatedTimestampOffset = reference.startTime − realTimestamp); >150 ms audio compensation. - VHS sets offset only at discontinuity/rendition boundaries via a serialized queue; removed per-segment offsetting for long-running-live — https://github.com/videojs/http-streaming/blob/main/docs/a-walk-through-vhs.md.
Browser behavior:
- Negative-DTS-after-offset hard append failure on Chrome/Chromium, not
Firefox/legacy-Edge — https://github.com/google/shaka-player/issues/1108,
https://github.com/Dash-Industry-Forum/dash.js/issues/2265;
bFrameAdjustmentworkaround — https://github.com/shaka-project/shaka-player/pull/731. - Chrome paces the audio clock from decoded sample counts, not muxed timestamps ("chrome used to trust the muxed timestamps for pacing the media clock, but found that the numbers are often imprecise"); drift at SSAI ad-splice discontinuities; classified app-bug / WontFix — Chromium #41340529 (crbug 757799), hls.js #828. This is the dominant A/V-sync constraint for live.
- Edit-list (
edts/elst) handling diverges by browser and inverts by API: in MSE, Chrome appliesmedia_timestart-trim while Firefox/Edge don't (nzhang227/gapless_audio_mse); in Web AudiodecodeAudioData, Safari trims AAC priming while Chrome/Firefox/Edge don't (https://github.com/WebAudio/web-audio-api/issues/1091). The popular "Chrome ignores edts, Safari applies" framing is not supported. Own priming-trim explicitly viaappendWindow+timestampOffset(https://developer.chrome.com/blog/media-source-extensions-for-audio); encoder priming counts aren't reliably emitted upstream (https://github.com/w3c/webcodecs/issues/626).
Open follow-ups (verification, not blockers)
- Double-precision PTS drift is a non-issue at these magnitudes — a JS
double holds microsecond resolution out past 10⁹ s (~31 yr), and Chromium
uses integer microseconds internally. The "~26.5 h / 33-bit rollover" figure
is the MPEG-TS 33-bit PTS field wrapping, not a float-precision bound:
it's a TS-container concern (a mid-stream wrap is a discontinuity to handle),
irrelevant to CMAF/fMP4 (64-bit
tfdtbaseMediaDecodeTime). So the native-PTS default is precision-safe for CMAF; TS sources carry a wrap caveat that pushes them toward normalization. No primary source confirmed any double-precision degradation threshold — treat that framing as retired. - Chrome audio sample-pacing on current Chrome. The primary source is Chrome 60 / 2017 (WontFix-Obsolete 2022). It's a deliberate design choice corroborated by current hls.js audio-restamping, but verify on a current Chrome against a discontinuity stream before hardening the audio-continuity requirement.
See also
- non-zero-pts-support — the mechanism fork this decision resolves.
- live-presentation-modeling —
open question [4] (PDT →
timestampOffset, A/V sync). - mse-mms-pipeline — the SourceBuffer pipeline this offsetting plugs into.