From 2b42ff48efde40725fb6e338dbb078f78955d32e Mon Sep 17 00:00:00 2001 From: Christian Pillsbury Date: Wed, 24 Jun 2026 13:55:50 -0700 Subject: [PATCH] refactor(spf): remove the superseded per-track anchor primitives anchorTrackToBufferedSegment and anchorTrackToSequenceOrigin implemented the per-track pinning that the shared presentation anchor replaces; they have no remaining callers. Drop them and their tests, repoint buffered-anchor's doc link to presentationAnchorFromBuffer, and remove the now-moot bridge test in presentation-anchor.test.ts. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../media/anchor-track-to-buffered-segment.ts | 52 ----------- .../media/anchor-track-to-sequence-origin.ts | 65 ------------- packages/spf/src/media/buffered-anchor.ts | 5 +- .../anchor-track-to-buffered-segment.test.ts | 67 -------------- .../anchor-track-to-sequence-origin.test.ts | 92 ------------------- .../media/tests/presentation-anchor.test.ts | 13 --- 6 files changed, 3 insertions(+), 291 deletions(-) delete mode 100644 packages/spf/src/media/anchor-track-to-buffered-segment.ts delete mode 100644 packages/spf/src/media/anchor-track-to-sequence-origin.ts delete mode 100644 packages/spf/src/media/tests/anchor-track-to-buffered-segment.test.ts delete mode 100644 packages/spf/src/media/tests/anchor-track-to-sequence-origin.test.ts diff --git a/packages/spf/src/media/anchor-track-to-buffered-segment.ts b/packages/spf/src/media/anchor-track-to-buffered-segment.ts deleted file mode 100644 index 77b022e2..00000000 --- a/packages/spf/src/media/anchor-track-to-buffered-segment.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { Track } from './types'; - -/** - * Re-origin a track's timeline onto the buffer's (native-PTS) timeline, using a - * segment whose *actual* buffered position is known as ground truth. - * - * `anchorTrackToSequenceOrigin` positions the timeline from the manifest alone - * (an `averageDuration × sequence` estimate); this is the authoritative - * correction that supersedes it once real data exists. Given a segment present - * in the track (`segmentId`) and where it actually landed in the SourceBuffer - * (`actualStart`, from `mediaElement.buffered`), the offset is - * `actualStart − segment.startTime` (expected) and the whole track shifts by it — - * so the model's coordinates coincide with the buffer's. Per the - * no-mid-stream-discontinuity assumption the offset is constant, so pinning from - * one known segment re-origins the entire window. - * - * Segment `startDate` (PDT) is intrinsic wall clock and stays put; only timeline - * positions move. `Track.startDate` (the wall clock at timeline 0) shifts with - * the origin. - * - * No-op (returns the same track) when the segment isn't present or the offset is - * zero (already aligned) — so callers can apply it unconditionally each reload. - */ -export function anchorTrackToBufferedSegment( - track: Tracks, - segmentId: string, - actualStart: number -): Tracks { - const segment = track.segments.find((s) => s.id === segmentId); - if (!segment) return track; - - const shift = actualStart - segment.startTime; - if (shift === 0) return track; - - // `Track.startDate` is the wall clock at timeline 0; shifting positions by - // `+shift` moves timeline 0 to an earlier instant, so it adjusts by `−shift`. - // Equivalently, from the pinned segment's intrinsic PDT: `startDate − - // actualStart` (both forms agree along a linear timeline). - const startDate = - track.startDate !== undefined - ? track.startDate - shift - : segment.startDate !== undefined - ? segment.startDate - actualStart - : undefined; - - return { - ...track, - startTime: track.startTime + shift, - ...(startDate === undefined ? {} : { startDate }), - segments: track.segments.map((s) => ({ ...s, startTime: s.startTime + shift })), - }; -} diff --git a/packages/spf/src/media/anchor-track-to-sequence-origin.ts b/packages/spf/src/media/anchor-track-to-sequence-origin.ts deleted file mode 100644 index 5b57fdd4..00000000 --- a/packages/spf/src/media/anchor-track-to-sequence-origin.ts +++ /dev/null @@ -1,65 +0,0 @@ -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. - */ - presumedStartSequence?: number; -} - -/** - * Re-origin a track's timeline to an estimated stream start (the segment at - * `presumedStartSequence`, 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 − presumedStartSequence) × averageDuration`; present segments keep - * their actual relative spacing, only the offset to the unseen origin is - * estimated. - * - * ROUGH and provisional: assumes `presumedStartSequence` 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 `startDate`. - */ -export function anchorTrackToSequenceOrigin( - track: Tracks, - { presumedStartSequence = 0 }: AnchorToSequenceOriginOptions = {} -): Tracks { - const { segments } = track; - const anchorIndex = segments.findIndex((segment) => !isUndefined(segment.startDate)); - const anchor = segments[anchorIndex]; - if (!anchor || isUndefined(anchor.startDate)) { - 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 - presumedStartSequence) * averageDuration; - const shift = originOffset - anchor.startTime; - if (shift === 0) { - return track; - } - - return { - ...track, - startTime: track.startTime + shift, - startDate: anchor.startDate - originOffset, - segments: segments.map((segment) => ({ ...segment, startTime: segment.startTime + shift })), - }; -} diff --git a/packages/spf/src/media/buffered-anchor.ts b/packages/spf/src/media/buffered-anchor.ts index c866cf2a..a4159ba5 100644 --- a/packages/spf/src/media/buffered-anchor.ts +++ b/packages/spf/src/media/buffered-anchor.ts @@ -16,8 +16,9 @@ export interface BufferedAnchor { * ranges (`mediaElement.buffered`, exposed DOM-free by the buffer actor). The * latest appended segment sits at the buffer's leading edge, so its actual start * is `maxBufferedEnd − duration`. Pairing its id with that start lets - * {@link anchorTrackToBufferedSegment} re-origin the whole track onto the buffer - * (the constant offset means one anchor pins the window). + * {@link presentationAnchorFromBuffer} derive the shared presentation anchor + * that positions every track (the constant offset means one anchor pins the + * window). * * Returns `undefined` before anything is buffered — callers fall back to the * sequence estimate until then. diff --git a/packages/spf/src/media/tests/anchor-track-to-buffered-segment.test.ts b/packages/spf/src/media/tests/anchor-track-to-buffered-segment.test.ts deleted file mode 100644 index 826d9a28..00000000 --- a/packages/spf/src/media/tests/anchor-track-to-buffered-segment.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { anchorTrackToBufferedSegment } from '../anchor-track-to-buffered-segment'; -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 ? {} : { startDate: s.pdt }), - }) - ), - metadata: { - [MEDIA_PLAYLIST_METADATA_KEY]: { mediaSequence, targetDuration: 5, endList: false }, - }, - }; -} - -describe('anchorTrackToBufferedSegment', () => { - it('re-origins the whole track so the named segment lands at its actual buffered start', () => { - // Estimate placed segment-85 at model startTime 340; the SourceBuffer actually - // holds it at native PTS 370 — a +30 correction the buffer (ground truth) wins. - const track = makeTrack(85, [ - { startTime: 340, duration: 4, pdt: 1000 }, - { startTime: 344, duration: 4, pdt: 1004 }, - ]); - - const pinned = anchorTrackToBufferedSegment(track, 'segment-85', 370); - - expect(pinned.segments.map((s) => s.startTime)).toEqual([370, 374]); - expect(pinned.startTime).toBe(370); - // startDate (wall clock at timeline 0) tracks the shift: PDT(seg) − newStart = 1000 − 370. - expect(pinned.startDate).toBe(630); - // Per-segment PDT is intrinsic — unchanged by the re-origin. - expect(pinned.segments.map((s) => s.startDate)).toEqual([1000, 1004]); - }); - - it('corrects a backward drift too (estimate ahead of the buffer)', () => { - const track = makeTrack(85, [{ startTime: 340, duration: 4, pdt: 1000 }]); - const pinned = anchorTrackToBufferedSegment(track, 'segment-85', 320); - expect(pinned.startTime).toBe(320); - expect(pinned.segments[0]?.startTime).toBe(320); - }); - - it('is idempotent when the segment already sits at the buffered start (offset 0)', () => { - const track = makeTrack(85, [{ startTime: 340, duration: 4, pdt: 1000 }]); - expect(anchorTrackToBufferedSegment(track, 'segment-85', 340)).toBe(track); - }); - - it('no-ops (returns the same track) when the segment is not present', () => { - const track = makeTrack(85, [{ startTime: 340, duration: 4, pdt: 1000 }]); - expect(anchorTrackToBufferedSegment(track, 'segment-999', 500)).toBe(track); - }); -}); diff --git a/packages/spf/src/media/tests/anchor-track-to-sequence-origin.test.ts b/packages/spf/src/media/tests/anchor-track-to-sequence-origin.test.ts deleted file mode 100644 index 4eb7bd9f..00000000 --- a/packages/spf/src/media/tests/anchor-track-to-sequence-origin.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -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 ? {} : { startDate: 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 presumedStartSequence (no shift when it equals the window start)', () => { - const track = makeTrack(85, [{ startTime: 0, duration: 4, pdt: 1000 }]); - // presumedStartSequence = 85 → originOffset 0 → already at origin → unchanged identity. - expect(anchorTrackToSequenceOrigin(track, { presumedStartSequence: 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 startDate', () => { - 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); - }); -}); diff --git a/packages/spf/src/media/tests/presentation-anchor.test.ts b/packages/spf/src/media/tests/presentation-anchor.test.ts index c266aad6..7711ec41 100644 --- a/packages/spf/src/media/tests/presentation-anchor.test.ts +++ b/packages/spf/src/media/tests/presentation-anchor.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from 'vitest'; -import { anchorTrackToBufferedSegment } from '../anchor-track-to-buffered-segment'; import { positionTrackToAnchor, presentationAnchorEstimate, @@ -90,15 +89,3 @@ describe('positionTrackToAnchor', () => { 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)); - }); -});