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

Opt-in relocateTimestampOrigin on the HLS engine. The video loader reads
each track's decode-time origin (tfdt baseMediaDecodeTime / mdhd timescale)
from the init + first media segment, then relocates the buffer to 0-based
via SourceBuffer.timestampOffset = -origin, so currentTime/seekable/duration
stay 0-based with no adapter translation. Off by default (zero-PTS VOD pays
nothing).

The established offset is published on the segment-loader snapshot so the
text path can rebase cues onto the same timeline: cueFinal = cueNative +
timestampOffset, where cueNative applies the X-TIMESTAMP-MAP correction
(Apple) or the absolute cue time (map-less Mux, which can't self-derive the
origin and reads the A/V-established one).

Checkpoint before the composition-driven refactor.
This commit is contained in:
Christian Pillsbury
2026-07-08 10:13:20 -07:00
parent 555f9fdacc
commit 6a20bade9b
6 changed files with 208 additions and 18 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 (spike): when present, set as
* `SourceBuffer.timestampOffset` before this append so native PTS is relocated
* onto a 0-based presentation timeline. Constant per source; the loader
* includes it once (on the first media segment). Absent = no relocation.
*/
timestampOffset?: number;
};
export type { AppendData };
@@ -151,6 +158,12 @@ function appendSegmentTask(
});
}
// Relocation (non-zero-PTS spike): set the offset before the coded frames
// are appended. The SerialRunner guarantees the buffer is idle here, so the
// assignment is safe; it's constant per source (loader sends it once).
if (meta.timestampOffset != null) {
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.