feat(spf): estimate stream-origin startDate from media sequence

Add anchorTrackToSequenceOrigin: re-bases a track's timeline to an estimated
stream start (the segment at startSequence, default 0) so startTime reads as
elapsed-since-stream-start and startDate becomes the wall clock at that origin.
The unseen earlier segments' duration is estimated from the observed segments'
average (more reliable than EXT-X-TARGETDURATION, a spec ceiling); present
segments keep their actual spacing. Rough and provisional — refined later from
the buffer; per-track.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2026-06-25 09:58:45 -07:00
co-authored by Claude Opus 4.8
parent bf8e87d237
commit 13996a654d
2 changed files with 157 additions and 0 deletions
@@ -0,0 +1,65 @@
import { isUndefined } from '@videojs/utils/predicate';
import { getMediaPlaylistMetadata, type Track } from './types';
export interface AnchorToSequenceOriginOptions {
/**
* Sequence number assumed to be the stream's origin (time 0). Defaults to 0
* — the spec default when `EXT-X-MEDIA-SEQUENCE` is absent, and the common
* encoder convention. Override when the true origin sequence is known.
*/
startSequence?: number;
}
/**
* Re-origin a track's timeline to an estimated stream start (the segment at
* `startSequence`, default 0), so `startTime` reads as elapsed-since-stream-start
* and `startDate` becomes the wall clock at that origin — the stream-absolute
* convention, from the manifest alone.
*
* A mid-join live playlist omits the earlier segments, so their total duration
* is estimated from the observed segments' **average duration** — more reliable
* than `EXT-X-TARGETDURATION` (a spec ceiling that systematically
* over-estimates). The origin offset of the first PDT-bearing segment is
* `(its sequence startSequence) × averageDuration`; present segments keep
* their actual relative spacing, only the offset to the unseen origin is
* estimated.
*
* ROUGH and provisional: assumes `startSequence` is the true origin (often but
* not always correct — configurable), roughly uniform durations, and no
* discontinuities in the unseen past; error grows with the sequence gap.
* Refined later from the buffer (`buffered`/`tfdt`), which is authoritative.
*
* Per-track: each track estimates independently, so two tracks' results can
* differ by their accumulated average-duration difference (e.g. AAC audio vs
* video). Exact cross-track A/V alignment comes from `alignTrackTimelines`
* (PDT) and ultimately the buffer, not from these estimates.
*
* No-op when there are no segments or none carries `programDateTime`.
*/
export function anchorTrackToSequenceOrigin<Tracks extends Track>(
track: Tracks,
{ startSequence = 0 }: AnchorToSequenceOriginOptions = {}
): Tracks {
const { segments } = track;
const anchorIndex = segments.findIndex((segment) => !isUndefined(segment.programDateTime));
const anchor = segments[anchorIndex];
if (!anchor || isUndefined(anchor.programDateTime)) {
return track;
}
const mediaSequence = getMediaPlaylistMetadata(track)?.mediaSequence ?? 0;
const anchorSequence = mediaSequence + anchorIndex;
const averageDuration = segments.reduce((sum, segment) => sum + segment.duration, 0) / segments.length;
const originOffset = (anchorSequence - startSequence) * averageDuration;
const shift = originOffset - anchor.startTime;
if (shift === 0) {
return track;
}
return {
...track,
startTime: track.startTime + shift,
startDate: anchor.programDateTime - originOffset,
segments: segments.map((segment) => ({ ...segment, startTime: segment.startTime + shift })),
};
}
@@ -0,0 +1,92 @@
import { describe, expect, it } from 'vitest';
import { anchorTrackToSequenceOrigin } from '../anchor-track-to-sequence-origin';
import { MEDIA_PLAYLIST_METADATA_KEY, type Segment, type Track } from '../types';
function makeTrack(
mediaSequence: number,
segments: Array<{ startTime: number; duration: number; pdt?: number }>
): Track {
return {
type: 'video',
id: 'track',
url: 'https://example.com/playlist.m3u8',
mimeType: 'video/mp4',
bandwidth: 0,
duration: Number.POSITIVE_INFINITY,
startTime: segments[0]?.startTime ?? 0,
segments: segments.map(
(s, i): Segment => ({
id: `segment-${mediaSequence + i}`,
url: `${mediaSequence + i}.m4s`,
duration: s.duration,
startTime: s.startTime,
...(s.pdt === undefined ? {} : { programDateTime: s.pdt }),
})
),
metadata: {
[MEDIA_PLAYLIST_METADATA_KEY]: { mediaSequence, targetDuration: 5, endList: false },
},
};
}
describe('anchorTrackToSequenceOrigin', () => {
it('re-bases startTime to elapsed-since-origin and startDate to the seq-0 wall clock', () => {
// Mid-join window starting at sequence 85, 4s segments, join-relative startTimes.
const track = makeTrack(85, [
{ startTime: 0, duration: 4, pdt: 1000 },
{ startTime: 4, duration: 4, pdt: 1004 },
]);
const anchored = anchorTrackToSequenceOrigin(track);
// origin offset = (85 0) × 4 = 340; first segment moves from 0 → 340.
expect(anchored.segments.map((s) => s.startTime)).toEqual([340, 344]);
expect(anchored.startTime).toBe(340);
// startDate = PDT(first) originOffset = 1000 340 = 660 (wall clock at seq 0).
expect(anchored.startDate).toBe(660);
});
it('uses observed average duration, not EXT-X-TARGETDURATION', () => {
// avg of [3,5,4] = 4; targetDuration is 5 — the estimate must use 4.
const track = makeTrack(10, [
{ startTime: 0, duration: 3, pdt: 1000 },
{ startTime: 3, duration: 5, pdt: 1003 },
{ startTime: 8, duration: 4, pdt: 1008 },
]);
const anchored = anchorTrackToSequenceOrigin(track);
// originOffset = (10 0) × 4 = 40 (not 10 × 5 = 50).
expect(anchored.segments[0]?.startTime).toBe(40);
});
it('honors a configured startSequence (no shift when it equals the window start)', () => {
const track = makeTrack(85, [{ startTime: 0, duration: 4, pdt: 1000 }]);
// startSequence = 85 → originOffset 0 → already at origin → unchanged identity.
expect(anchorTrackToSequenceOrigin(track, { startSequence: 85 })).toBe(track);
});
it('preserves present segments actual spacing (only the origin offset is estimated)', () => {
const track = makeTrack(10, [
{ startTime: 0, duration: 1.9, pdt: 1000 },
{ startTime: 1.9, duration: 2.1, pdt: 1001.9 },
]);
const anchored = anchorTrackToSequenceOrigin(track);
// Inter-segment gap stays the real 1.9s; both shift by the same offset.
const [a, b] = anchored.segments;
expect((b?.startTime ?? 0) - (a?.startTime ?? 0)).toBeCloseTo(1.9, 6);
});
it('is a no-op when no segment carries programDateTime', () => {
const track = makeTrack(85, [
{ startTime: 0, duration: 4 },
{ startTime: 4, duration: 4 },
]);
expect(anchorTrackToSequenceOrigin(track)).toBe(track);
});
it('is a no-op for an empty track', () => {
const track = makeTrack(0, []);
expect(anchorTrackToSequenceOrigin(track)).toBe(track);
});
});