18 KiB
status, date, definition
| status | date | definition |
|---|---|---|
| implemented | 2026-07-16 | sketched |
Non-zero PTS support
Time-mapping primitive for sources where media-segment PTS
(Presentation Timestamps) don't start at zero. Three motivating
source types — re-mastered VOD, deliberate instant clips, and live
streams — all share the same underlying problem: detect the first
segment's PTS, then make user-visible currentTime start at zero.
The cluster B foundation; consumed by every cluster A feature
(live-stream-support,
dvr-event-stream-support,
ll-hls-support) for correct currentTime /
seekable semantics.
Mechanism: resolved and implemented for VOD — SourceBuffer.timestampOffset
relocation. The offset is applied per SourceBuffer (timestampOffset = −startMediaTime) so the buffer, the model's Track.startTime, and
currentTime all stay 0-based and the adapter is untouched; text cues get the
same shift as arithmetic. The A/V-sync risk from per-buffer offset divergence is
handled by relocating every track by one shared min origin across the selected
A/V tracks, which keeps every DTS ≥ 0 and preserves real skew. The coordinate
model and the discover→derive→apply architecture are documented in full in
presentation-timeline-model.md; the mechanism
decision (native-PTS default, relocation for the 0-based cases) in
../../../decisions/spf/mse-timestamp-offset.md.
(The alternative — simulated translation in the adapter — was the parked approach
on feat/spf-non-zero-pts.)
A Media-src feature in the framing from
clusters.md § Feature classification axes:
without it, live and instant-clip sources play with wrong
currentTime (a live stream's playhead jumps to hours/days into the
session because PTS is large; an instant clip's playhead starts at
the original media's PTS rather than 0).
Combines Notion epic #6 ("Non-zero PTS / Instant Clip Support") scope: non-zero PTS and instant clips share the same time-mapping primitive, distinct motivations.
Status
- Composition: implemented for VOD in
createSimpleHlsEngineandcreateHlsAudioOnlyEngineonfeat/spf-non-zero-pts-relocation(draft PR #1847). TheestablishStartMediaTimereactor + therelocation-pipelinesprimitive discover each track's decode-time origin (tfdt/mdhd, matched bytrack_id) and relocate viaSourceBuffer.timestampOffset = −startMediaTime. Relocation is composition-time opt-in — the always-present loader/buffer actors carry no relocation vocabulary, and a zero-PTS composition imports none of this code (see presentation-timeline-model.md § Branch-free always-present actors). - Live is parked on
feat/spf-non-zero-pts; the live edge has its own wall-clock anchor (anchorPresentationTimeline), the media↔wall-clock sibling of this feature's media↔presentation relocation. Mid-stream PTS jumps remain the sister[discontinuity-handling]feature's scope. - Definition depth: sketched — implemented and code-grounded for VOD. Source material: SPF Epics Working Doc — epic #6 Non-zero PTS / Instant Clip Support (cluster B, Media-src, eng L, validation M).
- Foundational for cluster B — first cluster B doc. Consumed by
every cluster A feature (live, DVR, LL-HLS) for correct
currentTimesemantics.
Phases of complexity
Content phases by source-type. All three use the same underlying
mechanism (first-segment-PTS detection + timestampOffset relocation);
they differ in motivation, customer-data context, and composition with
other cluster A features.
| Phase | Source type | What | Status |
|---|---|---|---|
| Non-zero-PTS VOD | Re-mastered or processed VOD where the manifest retains original PTS rather than zero-rebasing the source. Stable, non-zero PTS throughout — often the output of clipping / transcoding pipelines that preserve PTS | Detect first-segment baseMediaDecodeTime (tfdt) + timescale (mdhd) from the init + first media segment, matched by track_id. Relocate via SourceBuffer.timestampOffset = −startMediaTime; currentTime resolves to zero at source start. Offset is one-time setup (established once per source, sticky) |
Implemented. The baseline case |
| Instant clips (deliberate offset) | Sources where Mux Video creates a clip by serving a manifest starting at a non-zero PTS (the clip's start position in the original media). currentTime should still start at zero |
Engine treats it identically to non-zero-PTS VOD — same detect + relocate. The clip-specific extra (surfacing the original PTS for telemetry / "watch from minute N") is an adapter-layer concern, not engine | Implemented (relocation); original-PTS surfacing is out of scope (adapter layer) |
| Live streams with non-zero PTS | Live encoder PTS counts from broadcast / encoder start; the first loaded segment has a large PTS. The sliding window rolls segments off the start; the seekable range slides forward but the origin is stable within an encoder run | Live uses the wall-clock anchorPresentationTimeline (media↔wall-clock edge) rather than the VOD media↔presentation relocation; composes with live-stream-support's setLiveSeekableRange, dvr-event-stream-support's growing-window, and ll-hls-support's partial-segment edge tracking |
Parked on feat/spf-non-zero-pts |
What's not implemented
Extension boundaries — each is a separate candidate feature or a parked phase:
- Live non-zero-PTS relocation — parked on
feat/spf-non-zero-pts. Live has its own wall-clock anchor today; converging live and VOD onto one establishment unit (both write per-track coordinate base values) is the flagged eventual dedup — see presentation-timeline-model.md § Consume. [discontinuity-handling](sister candidate) — mid-stream PTS jumps viaEXT-X-DISCONTINUITY, encoder restart on the same source URL, 33-bit PTS rollover (~26h of 90 kHz ticks). This feature handles a single stable PTS origin per source; the sister handles the origin changing mid-source. Both consume the same time-mapping primitive; the meta-per-append offset shape already generalizes to per-period re-base (see presentation-timeline-model.md § Offset applied via append meta).EXT-X-START/EXT-X-PROGRAM-DATE-TIMEconsumption — the HLS start-offset and per-segment wall-clock attributes. Lean: parse + expose; let the consumer decide initialcurrentTime/ wall-clock display. PDT is already the live anchor's input; VOD consumption of either is unbuilt.- Cluster B Borderline content compensation —
[pseudo-ended-detection],[edit-list-compensation],[buffer-stall-recovery]. All build on this feature's primitive but address different content-defect concerns.
Out of scope (different architectural layer):
- Customer-facing display of original-media position (e.g. "watching minute 3 of a 60-minute source via a 10-minute clip"). Engine relocates; adapter/consumer renders the UI.
- Mux Data telemetry mapping of clip
currentTime→ original-media position. Service-side / adapter-side.
Implementation surface
Composition: packages/spf/src/playback/engines/hls/engine.ts (and
engine-audio-only.ts) — the relocation wiring is comment-marked as one removable
block (reactor + deriveStartMediaTime seam + the video/audio/text
message-pipeline config), so it adds/drops as a unit for bundle / back-compat
testing.
Behaviors:
| Behavior | File | Responsibility |
|---|---|---|
establishStartMediaTime |
playback/behaviors/establish-start-media-time.ts |
Per-source reactor (inactive/monitoring/established). Owns the transient mediaContainerData slot (cleared per source); runs the injected deriveStartMediaTime seam and stamps the settled per-track startMediaTime onto the model — sole writer, establish-once sticky. VOD sibling of anchorPresentationTimeline |
recoverEndStall |
playback/behaviors/dom/recover-end-stall.ts |
Recovers the Chrome end-of-stream freeze on skewed A/V: on waiting with the MediaSource ended and the playhead within endStallNudgeWindow of the reachable buffered end, nudges currentTime to duration to force native ended. Needed because shared-min relocation preserves A/V skew |
Primitives (playback/primitives/, DOM-free):
| Primitive | File | Role |
|---|---|---|
deriveStartMediaTime seam + defaults |
derive-start-media-time.ts (type/context), establish-start-media-time.ts (deriveSharedMinStartMediaTime default, derivePerTypeStartMediaTime opt-out) |
The one coordination knob: (mediaContainerData, ctx) => per-type startMediaTime. Default reduces the min across selected A/V origins |
relocationPipelinesFor(type, derive) |
relocation-pipelines.ts |
The loader messagePipelines — discover (track_id + mdhd timescale, then that track's tfdt baseMediaDecodeTime) → stamp (timestampOffset = −startMediaTime, awaitDefined holdback, liveness-guarded) |
relocatingTextPipelines() |
relocation-pipelines.ts |
Text-loader pipeline: resolve metadata → shift cues by mapCorrection − startMediaTime (mapCorrection = X-TIMESTAMP-MAP mpegts/90000 − local) → dispatch. Origin is awaited — the primary selected A/V startMediaTime, so an early cue load can't resolve to 0 and shift every cue by the full mapCorrection (mirrors the A/V stamp step); a genuinely text-only source (no A/V tracks) relocates by 0 |
Media primitives (DOM-free, no core/):
media/mp4/timestamp-origin.ts— two tree-shakeable box readers:readFirstMediaTimescale/readFirstBaseMediaDecodeTime(presumptive, first box) vsfindMediaTrack/readBaseMediaDecodeTime(track-id-matched, for muxed containers with an extraclcp/caption track).media/mp4/box.ts— the minimal box parser.media/text/parse-vtt-timestamp-map.ts— scrapes the WebVTTX-TIMESTAMP-MAPheader (the one line the native<track>parser discards);resolve-vtt-metadata.ts— DOM-free cue metadata resolution.
State slots:
startMediaTime— per-track on the CMAF-HAMTrack(a peer ofstartTimeandstartDate), not a parallel slot. Sole writer is the reactor's derive effect.timestampOffsetis derived (startTime − startMediaTime), never stored. The write sharespresentation's existing multi-writer situation (#1746), accepted rather than worked around.mediaContainerData— transient, keyed by track type ({ trackId?, timescale?, baseMediaDecodeTime?, segmentStartTime? }). Holds the partial per-append discover churn so it never touchespresentation; cleared per source by the reactor'sinactiveentry.
Config surface
{
// Coordination seam (the one tier knob). Default deriveSharedMinStartMediaTime
// (min across selected A/V origins); derivePerTypeStartMediaTime is the
// barrier-free per-type opt-out for known-aligned A/V.
deriveStartMediaTime?: DeriveStartMediaTime;
// recoverEndStall proximity window (seconds); default 0.2. How close the
// playhead must sit to the reachable buffered end for a `waiting` to count as
// the end-of-stream freeze.
endStallNudgeWindow?: number;
}
NEAR_ZERO_ORIGIN_THRESHOLD (1s, in establish-start-media-time.ts) is a fixed
policy, not config: origins below it (incl. negatives) snap to 0, leaving the
source on its native ~0-based timeline (no timestampOffset set). Relocation
targets intentional large origins (instant clips, bipbop @10s), not the small
encode origin ordinary VOD carries (audio priming, first-frame CTS, edit lists).
Verification
- Unit tests:
playback/behaviors/tests/establish-start-media-time.test.ts— reactor lifecycle, the derive seam (shared-minbarrier, per-type opt-out, threshold snapping).playback/primitives/tests/relocation-pipelines.test.ts— discover/stamp steps; the track-id match resolves the media track's origin under a caption-first muxing (10s, not the leadingclcptraf's 50s); cue-origin resolution (waits for the selected A/V origin instead of shifting cues by the full map correction; text-only source relocates by 0).media/mp4/tests/{timestamp-origin,box}.test.ts— box parsing + both parser variants.media/text/tests/parse-vtt-timestamp-map.test.ts—X-TIMESTAMP-MAPscrape.media/dom/mse/tests/duration.test.ts,behaviors/dom/tests/recover-end-stall.test.ts— reachable buffered-end + the skewed-A/V EOS nudge.
- Cross-browser smoke (chromium/firefox/webkit × Mux clip / Apple bipbop / Mux
full, incl.
?engine=audio): 0-basedcurrentTime/seekable/buffered, per-SourceBuffer relocation with A/V skew preserved (Apple ~44ms, Mux clip ~1ms), text-cue alignment, ended+loop. The~0.043-origin full-length source is left un-relocated (below threshold). Sandbox harness is local/git-ignored, not in-repo.
Open questions
Mechanism choice:Resolved.timestampOffsetvs simulated translation.timestampOffsetrelocation for VOD; see the opening and ../../../decisions/spf/mse-timestamp-offset.md.PTS-detection implementation: parser depth.Resolved. A targeted extractor (media/mp4/timestamp-origin.ts), not a fullmp4box.js-style dependency — split into presumptive vs track-id readers for tree-shaking.Per-type offset (video vs audio).Resolved.mediaContainerDatais keyed by track type; the default derive reduces theminacross selected A/V origins (preserves skew, keeps every DTS ≥ 0).Resolved.currentTimenear zero for non-zero-PTS VOD.NEAR_ZERO_ORIGIN_THRESHOLDsnaps sub-second and negative origins to0(native timeline, no offset set); cross-browser smoke confirmed no exact-zeroseekable.start(0)misbehavior on the target browsers.- Text-only sources. No A/V
tfdtto establish from — but theX-TIMESTAMP-MAPMPEGTSis a media-timeline reference, so text could self-establish. Deferrable special path. EXT-X-START/EXT-X-PROGRAM-DATE-TIMEconsumption for VOD. Parse + expose vs act on. Lean: expose, let the consumer decide.- Telemetry / customer-data offset exposure. Surface the origin (or derived offset) to consumers for Mux Data / analytics? Lean: yes, read-only via engine state.
- Discontinuity boundary semantics. The boundary between this feature (stable
origin) and
[discontinuity-handling](origin changes mid-source) is drawn at "single PTS origin per source." - Mechanism-level questions — the barrier-liveness bound (audio absent/errored
must not block the shared-
minawaitDefinedforever) and the first-segment hold's interaction with preempt/replan are tracked in presentation-timeline-model.md § Open questions.
Related features
- live-stream-support (consumer) — the
media↔wall-clock sibling; consumes offset-corrected
currentTime/seekableduring live. Cluster A foundation this feature is the cluster B prerequisite for. - dvr-event-stream-support (consumer) — growing-window seekable start is offset-corrected from segment 0.
- ll-hls-support (consumer) — partial-segment edge tracking uses offset-corrected values.
- mse-mms-pipeline —
SourceBuffer.timestampOffsetis the MSE API consumed; the segment-append sequence gains a discover + offset-write stage (loader message-pipeline steps, applied by theSourceBufferActor). - buffer-management — the forward-buffer planner
operates on
currentTimeand segment ranges; both stay 0-based, so the planner is unchanged. - presentation-modeling — the data model
startMediaTimeextends (peer ofstartTime/startDate); the parser is whereEXT-X-START/EXT-X-PROGRAM-DATE-TIMEwould surface. [discontinuity-handling](sister candidate) — mid-stream PTS jumps; same primitive, different motivation (changes mid-source vs stable).- Cluster B Borderline content compensation: pseudo-ended-detection, edit-list-compensation, buffer-stall-recovery — build on this feature's primitive. See clusters.md § Time normalization.
See also
- presentation-timeline-model.md — the architectural deep-dive: the three-timeline coordinate model, the discover→derive→apply reactor architecture, the capability axes, and the key decisions. This feature doc frames; that doc explains the mechanism.
- ../../../decisions/spf/mse-timestamp-offset.md — the mechanism decision (native-PTS default; relocation for the 0-based cases).
- clusters.md § Time normalization — cluster B description; this feature is the foundation.
- mse-mms-pipeline.md —
SourceBuffer.timestampOffsetAPI surface. - SPF Epics Working Doc — source material; epic #6 (Non-zero PTS / Instant Clip Support).
- HLS Spec —
EXT-X-STARTandEXT-X-PROGRAM-DATE-TIME. - ISO BMFF —
tfdtbox — MP4 media-segment decode-time origin.