mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
305 lines
18 KiB
Markdown
305 lines
18 KiB
Markdown
---
|
|
status: draft
|
|
date: 2026-05-20
|
|
definition: technical
|
|
---
|
|
|
|
# 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](./live-stream-support.md),
|
|
[dvr-event-stream-support](./dvr-event-stream-support.md),
|
|
[ll-hls-support](./ll-hls-support.md)) for correct `currentTime` /
|
|
`seekable` semantics. **Implementation mechanism is an open
|
|
architectural question** — two viable approaches with different
|
|
trade-offs (see Open questions): apply the offset to the SourceBuffer
|
|
via `timestampOffset` (browser handles the translation; A/V sync risk
|
|
from per-buffer offset divergence) vs simulate the offset via adapter
|
|
+ math in behaviors/compositions (buffer holds original PTS;
|
|
translation happens at every consumer boundary; no A/V drift risk
|
|
but bigger translation surface).
|
|
|
|
A **Media-src feature** in the framing from
|
|
[clusters.md § Feature classification axes](./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:** not implemented in `createSimpleHlsEngine`. No
|
|
PTS-handling code in `packages/spf/src/` (no `timestampOffset`,
|
|
`initPTS`, or `EXT-X-DISCONTINUITY` references). Engine implicitly
|
|
assumes zero-based PTS — works for typical Mux Video VOD sources
|
|
(transcoded with PTS rebased to zero) but breaks for live + clips.
|
|
- **Definition depth:** technical — scope and SPF touchpoints
|
|
articulated against MSE `timestampOffset` semantics + HLS spec;
|
|
implementation specifics open. Source material: [SPF Epics Working
|
|
Doc — epic #6 Non-zero PTS / Instant Clip Support](https://www.notion.so/35f97a7f89d08123a13fecab1ca1cac4)
|
|
(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. Discontinuity-handling (mid-stream PTS jumps via
|
|
`EXT-X-DISCONTINUITY` or encoder restart) is carved out as a sister
|
|
candidate feature.
|
|
|
|
## Phases of complexity
|
|
|
|
Content phases by source-type. All three use the same underlying
|
|
mechanism (first-segment-PTS detection + `timestampOffset`
|
|
application); they differ in motivation, customer-data context, and
|
|
composition with other cluster A features.
|
|
|
|
| Phase | Source type | What | Notes |
|
|
|---|---|---|---|
|
|
| Non-zero-PTS VOD | Re-mastered or processed VOD where the manifest retains original PTS rather than zero-rebasing the source. Source has stable, non-zero PTS throughout. Often the result of clipping / transcoding pipelines that preserve PTS | Detect first-segment baseMediaDecodeTime from init segment (MP4 `tfdt` box, timescale-aware). Compute `presentationTimeOffset = -baseMediaDecodeTime / timescale`. Apply the offset via the chosen mechanism (see Open questions on mechanism choice). User-visible `currentTime` resolves to zero at source start | The baseline non-zero PTS case. Offset is one-time setup (detected once at first segment, stable during source lifetime). Mechanism choice (timestampOffset vs simulated translation) affects everything downstream but doesn't change the per-phase user-visible semantics |
|
|
| Instant clips (deliberate offset) | Sources where the consumer / Mux Video creates a clip by serving a manifest starting at a non-zero PTS (the clip's start position in the original media). User-visible `currentTime` should still start at zero | Same offset-detection mechanism as non-zero-PTS VOD. **Plus:** optionally surface the original PTS to the consumer for telemetry / "watch from minute N of source" use cases. Customer-data hooks: HLS `EXT-X-START` time-offset attribute (if present) or manifest-level clip metadata can hint at the offset | The deliberate-clip case. Engine treats it identically to non-zero-PTS VOD; the difference is that the offset is intentional and the value carries meaning customer-side (e.g., for Mux Data telemetry mapping the clipped-content currentTime back to original-media position) |
|
|
| Live streams with non-zero PTS | Live encoder PTS counts from broadcast / encoder start. First segment loaded by the engine has a large PTS (potentially hours/days). Sliding-window updates roll segments off the start as live progresses; the *seekable range* slides forward but the offset itself is stable within an encoder run | Same first-segment-PTS-detection mechanism. **Composes with:** [live-stream-support](./live-stream-support.md)'s `setLiveSeekableRange` — the live window's start/end are user-visible-time values (offset-corrected, however the mechanism translates). **Composes with:** [dvr-event-stream-support](./dvr-event-stream-support.md)'s growing-window — start = user-visible time of first retained segment; end = user-visible time of live edge. **Composes with:** [ll-hls-support](./ll-hls-support.md)'s partial-segment edge tracking — same offset applies | The live-specific case. Encoder restart (new encoder run on the same source URL) is one of the discontinuity scenarios carved out to the sister feature |
|
|
|
|
## What's in scope vs out of scope
|
|
|
|
**In scope:**
|
|
- All three source-type phases for HLS content with non-zero PTS
|
|
- Init-segment PTS extraction (MP4 `tfdt` parsing; HLS spec `EXT-X-PROGRAM-DATE-TIME` correlation when present)
|
|
- `SourceBuffer.timestampOffset` application before initial segment
|
|
append
|
|
- Engine-side `currentTime` / `seekable` mapping (engine reports
|
|
offset-corrected values throughout)
|
|
- `EXT-X-START` HLS spec attribute parsing for clip-start offset hints
|
|
- Composition with cluster A features (live, DVR, LL-HLS) — their
|
|
seekable-range writers consume the offset-corrected values
|
|
|
|
**Out of scope (separate Borderline-flavored candidate features):**
|
|
- **`[discontinuity-handling]`** *(new candidate, sibling of this
|
|
feature)* — mid-stream PTS jumps via `EXT-X-DISCONTINUITY` tags,
|
|
encoder restarts on the same source URL, 33-bit PTS rollover
|
|
(~26 hours of 90kHz timestamps). The *initial offset* is stable;
|
|
this feature handles that. The *changes mid-stream* are the sister
|
|
feature's concern. Both consume the same time-mapping primitive
|
|
internally.
|
|
- **Cluster B sub-cluster: 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 60-minute source via 10-minute clip"). Engine surfaces
|
|
the offset; adapter/customer renders the UI.
|
|
- Mux Data telemetry mapping of clipped currentTime → original-media
|
|
position. Service-side / adapter-side.
|
|
- `EXT-X-PROGRAM-DATE-TIME` wall-clock display (synced to original
|
|
broadcast time). Engine exposes the value; consumer UI renders
|
|
"broadcast wall clock" if needed.
|
|
|
|
## Likely cross-cutting impact
|
|
|
|
Things this feature probably forces decisions on, not just additions:
|
|
|
|
- **PTS-detection location: parser vs MSE-side.** Two shapes for
|
|
extracting baseMediaDecodeTime: (a) parse the init segment in the
|
|
engine (MP4 box parsing for `tfdt` + timescale) before passing to
|
|
SourceBuffer; (b) attach a parser to the streaming `fetchStream`
|
|
chunks that emits a PTS-detected event. Option (a) is simpler but
|
|
adds MP4-box-parsing code; option (b) leverages the streaming
|
|
pipeline. Lean: (a) — init segments are small and already buffered
|
|
before the segment-stream begins; a one-shot parser at append time
|
|
is the cleanest insertion point.
|
|
- **Mechanism choice: `timestampOffset` vs simulated translation
|
|
(load-bearing).** Two viable approaches with different trade-offs:
|
|
|
|
*(a) `SourceBuffer.timestampOffset`*: Set
|
|
`sourceBuffer.timestampOffset = -firstPTS` before appending the
|
|
first segment; browser shifts all timestamps; `currentTime` and
|
|
`seekable` are naturally offset-corrected. Sequences against
|
|
[mse-mms-pipeline](./mse-mms-pipeline.md)'s setup: setup →
|
|
setMediaKeys (if DRM) → addSourceBuffer →
|
|
detect-PTS-from-init-segment → set timestampOffset → append.
|
|
**Risks:** per-SourceBuffer write (video and audio buffers have
|
|
separate offset values); sub-millisecond detection-time precision
|
|
differences between video and audio init segments can cause A/V
|
|
drift that compounds. Mid-source offset changes (discontinuity)
|
|
require coordinated multi-buffer writes. Browser quirks during
|
|
append (Safari especially).
|
|
|
|
*(b) Simulated translation*: Don't set `timestampOffset`; buffer
|
|
holds original PTS. The `<video>` element's `currentTime` reads in
|
|
original PTS (large for non-zero-PTS sources). Engine + adapter
|
|
translate at every consumer boundary: customer-reading `currentTime`
|
|
→ subtract offset; customer seeking → add offset; `setLiveSeekableRange`
|
|
→ write original-PTS values to MSE, expose user-visible-time values
|
|
through engine state. **Risks:** translation layer at every
|
|
boundary; consumer-facing APIs must consistently use translated
|
|
values (any leak of raw `<video>.currentTime` shows wrong time);
|
|
bigger code surface than mechanism (a). **Benefits:** no A/V drift
|
|
risk (no per-buffer offset divergence); MSE buffer stays "pure";
|
|
discontinuity handling can change the translation offset without
|
|
any MSE coordination.
|
|
|
|
Both mechanisms preserve user-visible semantics; the choice is
|
|
engineering trade-off (browser quirks + A/V sync risk vs
|
|
translation surface area). Mechanism (b) is more SPF-natural —
|
|
behavior-driven translation at boundaries fits the composition
|
|
model; (a) is more browser-API-natural — uses what the spec
|
|
provides. See Open questions.
|
|
- **State slot for the offset.** New slot:
|
|
`presentationTimeOffset: { [trackType: 'video' | 'audio']: number }`
|
|
or similar. Per-type (video and audio can have independent PTS
|
|
origins; usually correlated but not guaranteed). Read by:
|
|
`setLiveSeekableRange` writer (live + DVR + LL-HLS), engine-side
|
|
`currentTime` consumers (telemetry, customer hooks), and any
|
|
cluster B sister feature.
|
|
- **`currentTime` and `seekable` semantics — depends on mechanism.**
|
|
Under (a) `timestampOffset`: browser does the translation; DOM-side
|
|
`currentTime` and `seekable` are naturally offset-corrected;
|
|
engine-side mirrors DOM-side. Under (b) simulated translation:
|
|
DOM-side reads original PTS; engine/adapter applies offset at
|
|
every read boundary; consumer-facing `state.currentTime` is the
|
|
offset-corrected value; `setLiveSeekableRange` writes original-PTS
|
|
values (so MSE knows what it's been fed) but engine state exposes
|
|
user-visible-time values. The mechanism choice is invisible to
|
|
consumers — both paths preserve user-visible-time semantics — but
|
|
the implementation surface differs substantially.
|
|
- **Live + encoder-restart scenario.** When a live encoder restarts
|
|
on the same source URL, the new PTS series starts at a different
|
|
value than the previous one. Without `EXT-X-DISCONTINUITY` tags,
|
|
this looks identical to a malformed stream; with them, it's a
|
|
proper discontinuity. The sister `[discontinuity-handling]`
|
|
candidate covers this. Boundary: this feature handles "single
|
|
stable PTS origin per source"; discontinuity handles "PTS origin
|
|
changes mid-source."
|
|
- **Init-segment PTS vs first-media-segment PTS.** The init segment
|
|
doesn't contain media samples (no `tfdt` per the spec strictly);
|
|
the first media segment carries the `tfdt`. PTS detection happens
|
|
on the **first media segment append**, not on init append. This
|
|
affects the offset-write sequencing: `timestampOffset` must be
|
|
set before the first media segment append, not before the init
|
|
segment append. Verify behavior.
|
|
|
|
## Open questions
|
|
|
|
- **Mechanism choice: `timestampOffset` vs simulated translation
|
|
(load-bearing).** Per the cross-cutting note. Both viable. (a)
|
|
uses the browser's MSE API directly but exposes A/V sync risk
|
|
from per-buffer offset divergence + browser-specific quirks.
|
|
(b) keeps the MSE buffer "pure" with original PTS and translates
|
|
at every consumer boundary; bigger translation surface but
|
|
eliminates the A/V drift risk and simplifies discontinuity
|
|
handling (sister feature). The choice constrains the entire
|
|
feature's implementation; resolving it requires empirical work
|
|
(test A/V sync drift on target browsers under mechanism (a)) and
|
|
scoping the translation surface under (b). No clear lean yet.
|
|
- **PTS-detection implementation: parser depth.** Full MP4 box
|
|
parser inside SPF vs targeted `tfdt`-only extractor vs depend on
|
|
an external `mp4box.js`-style library. Lean: targeted extractor
|
|
(small surface, only what's needed). Independent of mechanism
|
|
choice.
|
|
- **`EXT-X-PROGRAM-DATE-TIME` consumption.** HLS spec attribute
|
|
giving wall-clock time per segment. Useful for: (a) correlating
|
|
user-visible time with broadcast time; (b) cross-checking
|
|
PTS-detected offset (if `EXT-X-PROGRAM-DATE-TIME` is present,
|
|
verify the offset computation is consistent); (c) exposing to
|
|
consumer telemetry. Open whether to surface this through engine
|
|
state (new slot) or only via parser-output.
|
|
- **`EXT-X-START` parsing.** HLS spec attribute giving a desired
|
|
playback-start offset (typically a few seconds before live edge
|
|
for live, or arbitrary for VOD). Should the engine respect this
|
|
by setting initial `currentTime`, or expose to consumer? Lean:
|
|
expose; let consumer decide.
|
|
- **Per-type offset (video vs audio).** Theoretically independent;
|
|
usually correlated. Treat as `{ video: number, audio: number }`
|
|
or assume video offset applies to both? Empirical question;
|
|
start with per-type to be safe.
|
|
- **Discontinuity boundary semantics.** The discontinuity sister
|
|
feature handles mid-stream offset changes. The boundary between
|
|
this feature (stable offset) and that one (offset changes) is
|
|
drawn at "single PTS origin per source." A stream with one
|
|
`EXT-X-DISCONTINUITY` after segment 100 sits in the sister
|
|
feature's territory.
|
|
- **Telemetry / customer-data offset exposure.** Surface
|
|
`presentationTimeOffset` to consumers for telemetry (Mux Data,
|
|
custom analytics)? Lean: yes — read-only via the engine state
|
|
surface or a derived signal.
|
|
- **`currentTime` near zero for non-zero-PTS VOD.** After offset
|
|
applied, `currentTime` starts at zero. Some browsers may behave
|
|
unexpectedly when `seekable.start(0)` is exactly 0 (vs floating-
|
|
point slightly above zero). Worth empirical verification on
|
|
Safari, especially.
|
|
- **Composition with capability-probing.** When the browser doesn't
|
|
decode the codec, the engine fails before reaching this feature's
|
|
PTS handling. Boundary: capability-probing filters unsupported
|
|
variants upstream; this feature operates on the variants that
|
|
pass. No direct dependency.
|
|
|
|
## Related features
|
|
|
|
- **[live-stream-support](./live-stream-support.md)** *(consumer)* —
|
|
consumes this feature's offset for `setLiveSeekableRange` start /
|
|
end and for `currentTime` semantics during live playback. Cluster
|
|
A foundation that this feature is the cluster B foundation
|
|
prerequisite for.
|
|
- **[dvr-event-stream-support](./dvr-event-stream-support.md)**
|
|
*(consumer)* — same as live-stream-support but with growing-window
|
|
semantics; the offset still applies, the seekable start is
|
|
offset-corrected from segment 0.
|
|
- **[ll-hls-support](./ll-hls-support.md)** *(consumer)* — partial-
|
|
segment edge tracking + live-edge updates all use offset-corrected
|
|
values.
|
|
- **[mse-mms-pipeline](./mse-mms-pipeline.md)** — under mechanism
|
|
(a), `SourceBuffer.timestampOffset` is the MSE API consumed; the
|
|
segment-append sequence gains a PTS-detection + offset-write
|
|
stage. Under mechanism (b), MSE-side is unchanged; translation
|
|
lives in adapter / behavior boundary instead.
|
|
- **[buffer-management](./buffer-management.md)** — forward-buffer
|
|
planner operates on `currentTime` and segment time ranges; both
|
|
are offset-corrected naturally. Planner code doesn't change.
|
|
- **[presentation-modeling](../presentation-modeling.md)** — parser
|
|
may surface PTS-related HLS attributes (`EXT-X-START`,
|
|
`EXT-X-PROGRAM-DATE-TIME`) on the resolved presentation. Parser
|
|
extension for HLS metadata.
|
|
- **`[discontinuity-handling]`** *(new candidate, this session)* —
|
|
sister feature carved out of this one's scope. Mid-stream PTS
|
|
jumps via `EXT-X-DISCONTINUITY`, encoder restart, 33-bit PTS
|
|
rollover. Same underlying primitive (re-detect-and-apply offset),
|
|
different motivation (changes mid-source vs stable).
|
|
- **Cluster B Borderline content compensation sub-cluster:**
|
|
[pseudo-ended-detection](./pseudo-ended-detection.md),
|
|
[edit-list-compensation](./edit-list-compensation.md),
|
|
[buffer-stall-recovery](./buffer-stall-recovery.md) — all build on
|
|
this feature's time-mapping primitive but address different
|
|
content-defect concerns.
|
|
See [clusters.md § Time normalization](./clusters.md#time-normalization)'s
|
|
sub-cluster note.
|
|
|
|
## See also
|
|
|
|
- [clusters.md § Time normalization](./clusters.md#time-normalization)
|
|
— cluster B description; this feature is the foundation
|
|
- [clusters.md § Feature classification axes](./clusters.md#feature-classification-axes)
|
|
— Media-src feature framing
|
|
- [live-stream-support.md](./live-stream-support.md) — primary
|
|
consumer; cluster A foundation
|
|
- [mse-mms-pipeline.md](./mse-mms-pipeline.md) —
|
|
`SourceBuffer.timestampOffset` API surface
|
|
- [SPF Epics Working Doc](https://www.notion.so/35f97a7f89d08123a13fecab1ca1cac4)
|
|
— source material; epic #6 (Non-zero PTS / Instant Clip Support)
|
|
- [HLS Spec — `EXT-X-START`](https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-4.4.5.2)
|
|
and [`EXT-X-PROGRAM-DATE-TIME`](https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-4.4.4.6)
|
|
— HLS spec time-related metadata attributes
|
|
- [MSE Spec — `SourceBuffer.timestampOffset`](https://w3c.github.io/media-source/#dom-sourcebuffer-timestampoffset)
|
|
— the MSE API for shifting appended segment timestamps; one of the
|
|
two candidate mechanisms (A/V sync risk from per-buffer offset
|
|
divergence)
|
|
- [ISO BMFF — `tfdt` box (Track Fragment Decode Time)](https://www.iso.org/standard/68960.html)
|
|
— MP4 init/media segment PTS source
|