fix(spf): load final segment when seeking to exact end (#1828) (#1852)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2026-07-28 07:51:49 -07:00
committed by GitHub
co-authored by Claude Opus 4.8
parent a3e673bd68
commit 12ae97ddba
2 changed files with 60 additions and 11 deletions
@@ -168,10 +168,25 @@ export function getSegmentsToLoad(
// Find segments to load:
// - Overlaps buffer window [currentTime, targetTime)
// - Not already buffered at that time position
const toLoad = segments.filter((seg) => {
// Segment must overlap the buffer window
const toLoad = segments.filter((seg, i) => {
const segmentEnd = seg.startTime + seg.duration;
const isInRange = seg.startTime < targetTime && segmentEnd > currentTime;
const isLast = i === segments.length - 1;
// Interior segments use strict `>` so a segment the playhead just finished
// (endTime === currentTime at a boundary) isn't reloaded. The terminal
// segment has no successor and no reachable position past it — the playhead
// is clamped to the presentation's range — so it needs no upper bound: it's
// loadable whenever the forward window reaches it. That resolves the
// exact-end seek (#1828) and the post-loop re-seek — where
// `MediaSource.duration`, clamped by `endOfStream()` to the true buffered
// end, has grown a hair past the model's EXTINF-derived end — with no
// dependency on any duration value.
//
// LIVE: for an un-ended live/DVR presentation `isLast` is the sliding edge
// segment; loading it eagerly here is benign today (forward-window-bounded,
// deduped by the buffered check), but the live effort should confirm the
// edge / end-of-stream interaction when it lands.
const overlapsPlayhead = isLast || segmentEnd > currentTime;
const isInRange = seg.startTime < targetTime && overlapsPlayhead;
// Must not have a segment buffered at this time position
const isNotBuffered = !bufferedStartTimes.has(seg.startTime);
@@ -279,15 +279,14 @@ describe('getSegmentsToLoad', () => {
expect(toLoad).toHaveLength(2); // Load both segments within 30s
});
it('should handle currentTime after all segments', () => {
it('past all segments, does not reload interior segments the playhead has passed', () => {
// A playhead past every segment is unreachable in practice (clamped to the
// presentation's range), but the rule stays well-defined: interior segments
// behind the playhead are NOT reloaded. The terminal segment has no
// successor, so it stays selectable — see the exact-end / overshoot cases.
const segments: Segment[] = [createSegment(0, 6), createSegment(6, 6)];
const bufferedSegments: Segment[] = [];
const currentTime = 100;
const toLoad = getSegmentsToLoad(segments, bufferedSegments, currentTime);
expect(toLoad).toHaveLength(0); // No segments in range
const toLoad = getSegmentsToLoad(segments, [], 100);
expect(toLoad.map((s) => s.id)).toEqual(['seg-6']); // only the terminal segment
});
it('should not load segments beyond target', () => {
@@ -401,4 +400,39 @@ describe('getSegmentsToLoad', () => {
expect(toLoad[0]?.id).toBe('seg-18');
});
});
describe('exact-end boundary (#1828)', () => {
it('loads the final segment when currentTime is exactly the total duration', () => {
// Regression for #1828: seeking to currentTime === duration must still
// select the last segment. A strict `endTime > currentTime` overlap test
// drops the final segment (whose endTime === duration), so it never loads
// and endOfStream() never fires → the seek stalls forever.
const segments = [createSegment(0, 6), createSegment(6, 6), createSegment(12, 6)] as const;
// Playhead dragged to the exact end (duration = 18), nothing buffered there.
const toLoad = getSegmentsToLoad(segments, [], 18);
expect(toLoad.map((s) => s.id)).toEqual(['seg-12']);
});
it('loads the final segment when currentTime slightly exceeds its model end', () => {
// Post-loop re-seek variant of #1828. After the first end, endOfStream()
// clamps MediaSource.duration to the true buffered end, which can run a
// hair past the model's EXTINF-derived last-segment end. A later seek to
// that grown duration lands just past the last segment's endTime — its
// real media still covers the position, so it must still be selected, or
// the loader goes idle and the seek stalls.
const segments = [createSegment(0, 6), createSegment(6, 6), createSegment(12, 6)] as const;
// Model end = 18; duration grew to 18.03, seek lands past seg-12's endTime.
const toLoad = getSegmentsToLoad(segments, [], 18.03);
expect(toLoad.map((s) => s.id)).toEqual(['seg-12']);
});
it('does not re-select a finished segment at a mid-stream boundary', () => {
// Guard: the fix is scoped to the final segment. At an interior boundary
// (currentTime === a non-last segment's endTime) the just-finished segment
// must NOT be reloaded — only the segments the playhead is entering.
const segments = [createSegment(0, 6), createSegment(6, 6), createSegment(12, 6)] as const;
const toLoad = getSegmentsToLoad(segments, [], 6);
expect(toLoad.map((s) => s.id)).toEqual(['seg-6', 'seg-12']);
});
});
});