fix(spf): pin the buffer anchor to the trailing edge to avoid a mid-append off-by-one

bufferedAnchorFor paired the newest appended segment with maxBufferedEnd, but
appendSegment streams one appendBuffer per chunk, so the newest segment's bytes
are only partway into `buffered` mid-append — mis-reading its native start by up
to a full segment, skewing the derived anchor ~2s and shifting the whole model
timeline. seg0 went negative on a window anchored at the origin, so
setLiveSeekableRange threw every reload and back-seek stalled in the gap. Pin to
the trailing edge instead (earliest settled segment + minBufferedStart), which
only moves on eviction, and exclude partial (still-appending) segments — the
actor already flags them. Intermittent and live-only; exposed by DVR/EVENT
growing windows, masked at the live edge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2026-06-25 10:00:46 -07:00
co-authored by Claude Opus 4.8
parent dbddbf67bb
commit 7c99f266a6
2 changed files with 67 additions and 27 deletions
+34 -17
View File
@@ -8,28 +8,45 @@ export interface BufferedAnchor {
}
/**
* Correlate the model against the buffer to find a pin anchor: the segment at the
* buffer's leading edge and where it *actually* sits in native PTS.
* Correlate the model against the buffer to find a pin anchor: a fully-buffered
* segment and where it *actually* sits in native PTS.
*
* `appendedSegments` are the segments known to be in the SourceBuffer (model
* coordinates — `meta.startTime`); `bufferedRanges` are the real native-PTS
* 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 presentationAnchorFromBuffer} derive the shared presentation anchor
* that positions every track (the constant offset means one anchor pins the
* window).
* `appendedSegments` are the segments recorded as appended (model coordinates —
* `meta.startTime`), each flagged `partial` while its streaming append is still
* in progress; `bufferedRanges` are the real native-PTS ranges
* (`mediaElement.buffered`, exposed DOM-free by the buffer actor).
*
* Returns `undefined` before anything is buffered — callers fall back to the
* sequence estimate until then.
* Pins to the buffer's **trailing edge**: the earliest fully-appended segment
* sits at the start of the earliest buffered range, so its actual native start is
* `minBufferedStart`. The trailing edge — not the leading edge — because a
* streaming append grows the leading edge one chunk at a time (one `appendBuffer`
* per chunk; see `appendSegment`), so the newest segment's bytes are only partway
* into `buffered`; pairing it with `maxBufferedEnd duration` mis-reads its start
* by up to a full segment, which then shifts the whole derived timeline (observed
* as a ~2 s offset that turns `seg0` negative on a window anchored at the origin).
* The trailing edge only moves on eviction, which hasn't happened when the anchor
* is first established. Partial (still-appending) segments are excluded outright —
* their bytes are not fully in `buffered`.
*
* Pairing the chosen segment's id with that start lets
* {@link presentationAnchorFromBuffer} derive the shared presentation anchor that
* positions every track (the constant offset means any in-window segment pins the
* whole window).
*
* Returns `undefined` before any segment is fully buffered.
*/
export function bufferedAnchorFor(
appendedSegments: readonly Pick<Segment, 'id' | 'startTime' | 'duration'>[],
appendedSegments: readonly (Pick<Segment, 'id' | 'startTime' | 'duration'> & { partial?: boolean })[],
bufferedRanges: readonly { readonly start: number; readonly end: number }[]
): BufferedAnchor | undefined {
if (appendedSegments.length === 0 || bufferedRanges.length === 0) return undefined;
if (bufferedRanges.length === 0) return undefined;
const latest = appendedSegments.reduce((a, b) => (b.startTime > a.startTime ? b : a));
const maxBufferedEnd = Math.max(...bufferedRanges.map((r) => r.end));
return { segmentId: latest.id, actualStart: maxBufferedEnd - latest.duration };
// Only fully-appended segments are reliable ground truth; a partial segment's
// bytes are only partway into `buffered`.
const settled = appendedSegments.filter((segment) => !segment.partial);
if (settled.length === 0) return undefined;
const earliest = settled.reduce((a, b) => (b.startTime < a.startTime ? b : a));
const minBufferedStart = Math.min(...bufferedRanges.map((r) => r.start));
return { segmentId: earliest.id, actualStart: minBufferedStart };
}
@@ -2,9 +2,11 @@ import { describe, expect, it } from 'vitest';
import { bufferedAnchorFor } from '../buffered-anchor';
describe('bufferedAnchorFor', () => {
it('pairs the leading-edge segment with its actual native-PTS start', () => {
// Model says the latest segment sits at 110 (+10s long); the buffer's real
// leading edge is 550 → it actually starts at 540 (a +430 native-PTS offset).
it('pairs the trailing-edge (earliest) segment with its actual native-PTS start', () => {
// Model says the earliest segment sits at 100; the buffer's real trailing edge
// is 530 → it actually starts at 530 (a +430 native-PTS offset). The same
// presentation anchor results whichever in-window segment is used, so pinning
// to the stable trailing edge is equivalent when the buffer is consistent.
const anchor = bufferedAnchorFor(
[
{ id: 's1', startTime: 100, duration: 10 },
@@ -12,22 +14,43 @@ describe('bufferedAnchorFor', () => {
],
[{ start: 530, end: 550 }]
);
expect(anchor).toEqual({ segmentId: 's2', actualStart: 540 });
expect(anchor).toEqual({ segmentId: 's1', actualStart: 530 });
});
it('uses the max end across discontiguous ranges', () => {
it('excludes a partial (still-appending) segment so an in-flight leading edge cannot skew the pin', () => {
// s1 is mid-stream-append: its bytes are only partway into `buffered` (the
// range reaches 3, i.e. s0 fully + 1s of s1). Pinning to the leading edge here
// would mis-read s1's start as 3 2 = 1 (off by ~a segment); the trailing-edge
// pin reads s0 at the stable buffer start (0).
const anchor = bufferedAnchorFor(
[{ id: 's2', startTime: 110, duration: 10 }],
[
{ start: 530, end: 545 },
{ start: 548, end: 552 },
{ id: 's0', startTime: 0, duration: 2 },
{ id: 's1', startTime: 2, duration: 2, partial: true },
],
[{ start: 0, end: 3 }]
);
expect(anchor).toEqual({ segmentId: 's0', actualStart: 0 });
});
it('uses the earliest range start across discontiguous ranges', () => {
const anchor = bufferedAnchorFor(
[{ id: 's0', startTime: 0, duration: 10 }],
[
{ start: 5, end: 15 },
{ start: 20, end: 30 },
]
);
expect(anchor?.actualStart).toBe(542); // 552 10
expect(anchor?.actualStart).toBe(5);
});
it('returns undefined before anything is buffered', () => {
it('returns undefined before any segment is fully buffered', () => {
// No buffered ranges yet.
expect(bufferedAnchorFor([{ id: 's1', startTime: 0, duration: 10 }], [])).toBeUndefined();
// No appended segments.
expect(bufferedAnchorFor([], [{ start: 0, end: 10 }])).toBeUndefined();
// Only a partial segment — not yet reliable ground truth.
expect(
bufferedAnchorFor([{ id: 's0', startTime: 0, duration: 2, partial: true }], [{ start: 0, end: 1 }])
).toBeUndefined();
});
});