feat(spf): relocate non-zero-PTS sources to a 0-based timeline

Assets whose A/V encode starts at a non-zero native PTS (Mux instant clips,
Apple bipbop @10s) are relocated onto a 0-based presentation timeline via
`SourceBuffer.timestampOffset`, so buffer / model / `currentTime` / `seekable`
stay 0-based and the adapter is untouched.

- `establishStartMediaTime` reactor establishes each track's media-timeline
  origin once per source (the DOM-free half), running an injected
  `deriveStartMediaTime` seam over discovered container data.
- Default `deriveSharedMinStartMediaTime`: relocate every track by the `min`
  across selected A/V origins — preserves real A/V skew and keeps every DTS >= 0.
  Origins below NEAR_ZERO_ORIGIN_THRESHOLD (1s) are left native (ordinary
  ~0-PTS VOD isn't perturbed).
- `relocation-steps` message pipelines discover the origin (mdhd/tfdt head-peek)
  and stamp `timestampOffset`; text cues rebase by X-TIMESTAMP-MAP − origin.
- Wired into both the default and audio-only HLS engines.

See internal/design/spf/presentation-timeline-model.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2026-07-14 12:00:15 -07:00
co-authored by Claude Opus 4.8
parent c0cad09fd5
commit 2733cba052
13 changed files with 1143 additions and 83 deletions
@@ -17,6 +17,13 @@ export type AppendSegmentMeta = Pick<Segment, 'id' | 'startTime' | 'duration'> &
trackId: Track['id'];
/** Declared track bandwidth in bps (from playlist BANDWIDTH attribute). */
trackBandwidth?: number;
/**
* Non-zero-PTS relocation: when present, applied as `SourceBuffer.timestampOffset`
* before this append so native PTS is relocated onto a 0-based presentation
* timeline. A relocating composition stamps it (constant per source) onto each
* media segment's meta; the apply is idempotent-guarded. Absent = no relocation.
*/
timestampOffset?: number;
};
export type { AppendData };
@@ -151,6 +158,12 @@ function appendSegmentTask(
});
}
// Relocation: set the offset before the coded frames are appended. The
// SerialRunner guarantees the buffer is idle here, so the assignment is safe.
// Guarded so re-stamping the (constant) offset on later appends is a no-op.
if (meta.timestampOffset != null && sourceBuffer.timestampOffset !== meta.timestampOffset) {
sourceBuffer.timestampOffset = meta.timestampOffset;
}
await appendSegment(sourceBuffer, message.data, taskSignal);
// No abort check here: the physical SourceBuffer has been modified, so
// the model must be updated to match regardless of signal state.