Files
v10/internal/design/spf/features/non-zero-pts-support.md
T

18 KiB
Raw Blame History

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 VODSourceBuffer.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 createSimpleHlsEngine and createHlsAudioOnlyEngine on feat/spf-non-zero-pts-relocation (draft PR #1847). The establishStartMediaTime reactor + the relocation-pipelines primitive discover each track's decode-time origin (tfdt/mdhd, matched by track_id) and relocate via SourceBuffer.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 currentTime semantics.

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 via EXT-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-TIME consumption — the HLS start-offset and per-segment wall-clock attributes. Lean: parse + expose; let the consumer decide initial currentTime / 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) vs findMediaTrack/readBaseMediaDecodeTime (track-id-matched, for muxed containers with an extra clcp/caption track). media/mp4/box.ts — the minimal box parser.
  • media/text/parse-vtt-timestamp-map.ts — scrapes the WebVTT X-TIMESTAMP-MAP header (the one line the native <track> parser discards); resolve-vtt-metadata.ts — DOM-free cue metadata resolution.

State slots:

  • startMediaTimeper-track on the CMAF-HAM Track (a peer of startTime and startDate), not a parallel slot. Sole writer is the reactor's derive effect. timestampOffset is derived (startTime startMediaTime), never stored. The write shares presentation'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 touches presentation; cleared per source by the reactor's inactive entry.

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-min barrier, 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 leading clcp traf'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.tsX-TIMESTAMP-MAP scrape.
    • 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-based currentTime/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: timestampOffset vs simulated translation. Resolved. timestampOffset relocation 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 full mp4box.js-style dependency — split into presumptive vs track-id readers for tree-shaking.
  • Per-type offset (video vs audio). Resolved. mediaContainerData is keyed by track type; the default derive reduces the min across selected A/V origins (preserves skew, keeps every DTS ≥ 0).
  • currentTime near zero for non-zero-PTS VOD. Resolved. NEAR_ZERO_ORIGIN_THRESHOLD snaps sub-second and negative origins to 0 (native timeline, no offset set); cross-browser smoke confirmed no exact-zero seekable.start(0) misbehavior on the target browsers.
  • Text-only sources. No A/V tfdt to establish from — but the X-TIMESTAMP-MAP MPEGTS is a media-timeline reference, so text could self-establish. Deferrable special path.
  • EXT-X-START / EXT-X-PROGRAM-DATE-TIME consumption 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-min awaitDefined forever) and the first-segment hold's interaction with preempt/replan are tracked in presentation-timeline-model.md § Open questions.
  • live-stream-support (consumer) — the media↔wall-clock sibling; consumes offset-corrected currentTime/seekable during 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-pipelineSourceBuffer.timestampOffset is the MSE API consumed; the segment-append sequence gains a discover + offset-write stage (loader message-pipeline steps, applied by the SourceBufferActor).
  • buffer-management — the forward-buffer planner operates on currentTime and segment ranges; both stay 0-based, so the planner is unchanged.
  • presentation-modeling — the data model startMediaTime extends (peer of startTime/startDate); the parser is where EXT-X-START/EXT-X-PROGRAM-DATE-TIME would 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