mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
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:
co-authored by
Claude Opus 4.8
parent
dbddbf67bb
commit
7c99f266a6
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user