feat(spf): add presentation-anchor primitives for the shared live anchor

Introduce media/presentation-anchor.ts realizing the single presentation-level
anchor decision (internal/decisions/live-presentation-anchor.md):
presentationAnchorFromBuffer / presentationAnchorEstimate derive the shared
(media-time ↔ PDT) anchor — buffer-pinned, or the pre-buffer estimate — and
positionTrackToAnchor re-origins any track onto it by its own per-segment PDT.
Tested to generalize anchorTrackToBufferedSegment. Consumed by the
anchor-live-tracks reactor conversion (next), which replaces the per-track
anchor primitives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2026-06-25 10:00:45 -07:00
co-authored by Claude Opus 4.8
parent f531e1055c
commit 3a9d1faca0
2 changed files with 202 additions and 0 deletions
@@ -0,0 +1,98 @@
import { isUndefined } from '@videojs/utils/predicate';
import { getMediaPlaylistMetadata, type Track } from './types';
/**
* The presentation's live timeline anchor: the wall-clock (PDT, epoch seconds)
* that corresponds to **media-time 0**. One such value is shared across every
* track — establish it from one track and any other track positions itself by
* its own per-segment PDT. See
* [live-presentation-anchor](../../../internal/decisions/live-presentation-anchor.md).
*
* It is exactly the `Track.startDate` concept (wall clock at the timeline
* origin), promoted from per-track to presentation-level.
*/
export type PresentationAnchor = number;
/**
* Derive the presentation anchor from a buffer pin — the authoritative source.
* Given a segment present in the track and where it *actually* landed in the
* SourceBuffer (`actualStart`, native PTS), the wall clock at media-time 0 is
* `segment.startDate actualStart` (both along the linear timeline). `undefined`
* when the segment isn't present or carries no PDT.
*/
export function presentationAnchorFromBuffer(
track: Track,
segmentId: string,
actualStart: number
): PresentationAnchor | undefined {
const segment = track.segments.find((s) => s.id === segmentId);
if (!segment || isUndefined(segment.startDate)) return undefined;
return segment.startDate - actualStart;
}
export interface PresentationAnchorEstimateOptions {
/**
* Sequence number assumed to be the stream's origin (time 0). Defaults to 0.
* See {@link presentationAnchorEstimate}.
*/
presumedStartSequence?: number;
}
/**
* Estimate the presentation anchor from the manifest alone — the pre-buffer
* bootstrap, superseded by {@link presentationAnchorFromBuffer} once ground
* truth exists. A mid-join live playlist omits earlier segments, so the unseen
* origin's distance is estimated from the observed segments' **average**
* duration (more reliable than the target duration, a spec ceiling). The wall
* clock at media-time 0 is the anchor segment's PDT minus its estimated
* stream-origin offset, `(sequence presumedStartSequence) × averageDuration`.
*
* ROUGH and provisional — assumes roughly-uniform durations and no
* discontinuities in the unseen past; error grows with the sequence gap.
* `undefined` when no segment carries PDT.
*
* TODO: the `mediaSequence` read is the remaining HLS coupling (a segment's
* ordinal-from-origin); neutralize it behind a format-neutral segment-position
* abstraction so DASH can supply its own (tracked separately).
*/
export function presentationAnchorEstimate(
track: Track,
{ presumedStartSequence = 0 }: PresentationAnchorEstimateOptions = {}
): PresentationAnchor | undefined {
const { segments } = track;
const anchorIndex = segments.findIndex((segment) => !isUndefined(segment.startDate));
const anchor = segments[anchorIndex];
if (!anchor || isUndefined(anchor.startDate)) return undefined;
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 - presumedStartSequence) * averageDuration;
return anchor.startDate - originOffset;
}
/**
* Re-origin a track's timeline so media-time 0 coincides with the shared
* presentation `anchor` (PDT at the origin). The track shifts by
* `track.startDate anchor` — its own per-segment PDT carries it onto the
* shared timeline, so one anchor positions every track without a second buffer
* read. (Under the no-inter-track-skew assumption all tracks share the PTS
* clock; see the decision doc.)
*
* Segment `startDate` (intrinsic PDT) stays put; only timeline positions move.
* No-op when the track has no PDT origin (`startDate` undefined) or is already
* on the anchor — so callers can apply it unconditionally.
*/
export function positionTrackToAnchor<Tracks extends Track>(track: Tracks, anchor: PresentationAnchor): Tracks {
if (isUndefined(track.startDate)) return track;
const shift = track.startDate - anchor;
if (shift === 0) return track;
return {
...track,
startTime: track.startTime + shift,
startDate: anchor,
segments: track.segments.map((segment) => ({ ...segment, startTime: segment.startTime + shift })),
};
}
@@ -0,0 +1,104 @@
import { describe, expect, it } from 'vitest';
import { anchorTrackToBufferedSegment } from '../anchor-track-to-buffered-segment';
import {
positionTrackToAnchor,
presentationAnchorEstimate,
presentationAnchorFromBuffer,
} from '../presentation-anchor';
import { MEDIA_PLAYLIST_METADATA_KEY, type Track } from '../types';
/** Minimal live track: a window starting at `startTime`, 2s segments, PDT from `startDate`. */
function track(
opts: {
startTime?: number;
startDate?: number | undefined;
mediaSequence?: number;
segmentStartDates?: (number | undefined)[];
} = {}
): Track {
const startTime = opts.startTime ?? 100;
const startDate = 'startDate' in opts ? opts.startDate : 1000;
return {
type: 'video',
id: 'v-1',
startTime,
startDate,
segments: [0, 2, 4, 6, 8].map((offset, i) => ({
id: `segment-${(opts.mediaSequence ?? 50) + i}`,
url: `${(opts.mediaSequence ?? 50) + i}.m4s`,
duration: 2,
startTime: startTime + offset,
// Each segment's PDT = the track origin (startDate) + the segment's own
// media-time (startTime + offset), so track.startDate stays the PDT at time 0.
startDate: opts.segmentStartDates
? opts.segmentStartDates[i]
: startDate === undefined
? undefined
: startDate + startTime + offset,
})),
metadata: {
[MEDIA_PLAYLIST_METADATA_KEY]: { mediaSequence: opts.mediaSequence ?? 50, targetDuration: 2, endList: false },
},
} as unknown as Track;
}
describe('presentationAnchorFromBuffer', () => {
it('is the pinned segment PDT minus where it actually landed (PDT at media-time 0)', () => {
// segment-50 has PDT 1100 and actually landed at native PTS 480 → anchor 620.
expect(presentationAnchorFromBuffer(track(), 'segment-50', 480)).toBe(620);
});
it('is undefined when the segment is absent or carries no PDT', () => {
expect(presentationAnchorFromBuffer(track(), 'missing', 480)).toBeUndefined();
expect(
presentationAnchorFromBuffer(track({ segmentStartDates: [undefined, 1, 2, 3, 4] }), 'segment-50', 480)
).toBeUndefined();
});
});
describe('presentationAnchorEstimate', () => {
it('estimates PDT at media-time 0 from sequence × average duration', () => {
// anchor segment-50 PDT 1100; originOffset = (50 0) × 2 = 100 → anchor 1000.
expect(presentationAnchorEstimate(track())).toBe(1000);
});
it('honors presumedStartSequence', () => {
// originOffset = (50 10) × 2 = 80 → 1100 80 = 1020.
expect(presentationAnchorEstimate(track(), { presumedStartSequence: 10 })).toBe(1020);
});
it('is undefined when no segment carries PDT', () => {
expect(presentationAnchorEstimate(track({ startDate: undefined }))).toBeUndefined();
});
});
describe('positionTrackToAnchor', () => {
it('shifts the track so startDate coincides with the anchor', () => {
// track.startDate 1000, anchor 900 → shift +100 (startTime 100 → 200).
const positioned = positionTrackToAnchor(track(), 900);
expect(positioned.startDate).toBe(900);
expect(positioned.startTime).toBe(200);
expect(positioned.segments.map((s) => s.startTime)).toEqual([200, 202, 204, 206, 208]);
// Segment PDTs are intrinsic — they don't move.
expect(positioned.segments.map((s) => s.startDate)).toEqual([1100, 1102, 1104, 1106, 1108]);
});
it('is a no-op when already on the anchor or the track has no PDT origin', () => {
const t = track();
expect(positionTrackToAnchor(t, 1000)).toBe(t); // startDate already 1000
const noPdt = track({ startDate: undefined });
expect(positionTrackToAnchor(noPdt, 900)).toBe(noPdt);
});
});
describe('generalizes anchorTrackToBufferedSegment', () => {
it('positioning to the buffer-derived anchor equals the per-track buffer pin', () => {
const t = track();
const anchor = presentationAnchorFromBuffer(t, 'segment-50', 480)!;
const viaAnchor = positionTrackToAnchor(t, anchor);
const viaPin = anchorTrackToBufferedSegment(t, 'segment-50', 480);
expect(viaAnchor.startTime).toBe(viaPin.startTime);
expect(viaAnchor.startDate).toBe(viaPin.startDate);
expect(viaAnchor.segments.map((s) => s.startTime)).toEqual(viaPin.segments.map((s) => s.startTime));
});
});