mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7cde2ea28d
commit
2b42ff48ef
@@ -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<Tracks extends Track>(
|
||||
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 })),
|
||||
};
|
||||
}
|
||||
@@ -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<Tracks extends Track>(
|
||||
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 })),
|
||||
};
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user