mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
fix(spf): rescue a playhead stranded by an out-of-window seek
The window-exit guard in seekToLiveEdge bailed whenever `mediaElement.seeking` was true, to avoid fighting an in-flight DVR scrub-back. But a seek whose target sits behind the window start can never complete — the data has slid out of the window and been evicted — so `seeking` stays true forever and the playhead strands permanently, the very stall the guard exists to rescue. Drop the `seeking` bail. The `currentTime < windowStart` test is itself the precise discriminator: an in-window scrub-back lands at `currentTime >= windowStart` and never trips it, so only a stuck out-of-window seek is repositioned to the live edge. Verified live (Mux sliding-window LL-HLS): repeated seeks to the window start now snap back to the edge within ~1.5s instead of stalling, while mid-window DVR scrub-back still holds. 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
6888b83abe
commit
88edc78cb2
@@ -17,8 +17,10 @@
|
||||
* `entry`, it fires once per entry into `live`; a source change exits to
|
||||
* `inactive`, so the next source re-seeks (no closure latch to reset).
|
||||
*
|
||||
* Window-exit guard: while playing (not paused, not mid-seek), reposition to the
|
||||
* live edge when the playhead has fallen behind the window start. Two triggers:
|
||||
* Window-exit guard: while playing (not paused), reposition to the live edge
|
||||
* when the playhead has fallen behind the window start — including when a seek
|
||||
* to a now-evicted position has stranded the playhead (such a seek can never
|
||||
* settle, so we rescue rather than wait on it). Two triggers:
|
||||
* the **window-update re-fire** (the guard reads the live edge, so each reload /
|
||||
* slide re-runs it — this catches a stall, where `timeupdate` stops but the
|
||||
* playlist keeps reloading) and a **`play` listener** for immediate reactivity on
|
||||
@@ -171,8 +173,16 @@ function seekToLiveEdgeSetup({
|
||||
const { start: windowStart, liveEdgeStart } = getLiveEdge({ state, config })!;
|
||||
const mediaElement = peek(context.mediaElement)!;
|
||||
const reposition = () => {
|
||||
// Don't yank a paused viewer or fight an in-flight seek (DVR scrub-back).
|
||||
if (mediaElement.paused || mediaElement.seeking) return;
|
||||
// Don't yank a paused viewer. We deliberately DON'T bail on
|
||||
// `mediaElement.seeking`: a seek whose target sits behind the window
|
||||
// start can never complete (the data has slid out of the window and
|
||||
// been evicted), so `seeking` would stay true forever and strand the
|
||||
// playhead — the very stall this guard exists to rescue. The
|
||||
// `currentTime < windowStart` test is itself the precise
|
||||
// discriminator: an in-window DVR scrub-back lands at
|
||||
// `currentTime >= windowStart` and never trips it, so only a stuck
|
||||
// out-of-window seek is repositioned.
|
||||
if (mediaElement.paused) return;
|
||||
if (mediaElement.currentTime < windowStart - REPOSITION_TOLERANCE) {
|
||||
mediaElement.currentTime = liveEdgeStart;
|
||||
}
|
||||
|
||||
@@ -300,17 +300,35 @@ describe('seekToLiveEdge', () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('does not reposition while a seek is in progress; repositions once it settles', () => {
|
||||
it('does not yank an in-flight seek to an in-window position (DVR scrub-back)', () => {
|
||||
const { el, cleanup } = started();
|
||||
el.paused = false;
|
||||
el.currentTime = 90;
|
||||
el.currentTime = 102; // scrubbing back to a valid position within [100, 110]
|
||||
el.seeking = true;
|
||||
el.dispatchEvent(new Event('play'));
|
||||
expect(el.currentTime).toBe(90); // seek in flight → untouched
|
||||
// In-window → never trips the `< windowStart` guard, so the seek proceeds
|
||||
// untouched. The guard discriminates on position, not the `seeking` flag.
|
||||
expect(el.currentTime).toBe(102);
|
||||
cleanup();
|
||||
});
|
||||
|
||||
el.seeking = false;
|
||||
el.dispatchEvent(new Event('play'));
|
||||
expect(el.currentTime).toBe(104);
|
||||
it('rescues a seek stranded behind the window start, even while still seeking', async () => {
|
||||
const { el, state, cleanup } = started();
|
||||
el.paused = false;
|
||||
// A scrub toward the back of the window lands on data that has since slid
|
||||
// out and been evicted: the seek hangs (readyState drops, `seeking` stays
|
||||
// true) and `currentTime` is frozen behind the window start.
|
||||
el.seeking = true;
|
||||
el.readyState = 1; // HAVE_METADATA — stalled, no data at the target
|
||||
el.currentTime = 90;
|
||||
|
||||
// The window-update re-fire (a playlist reload — `timeupdate` is silent
|
||||
// during the stall) catches it: 90 < new windowStart 200. Previously the
|
||||
// `seeking` bail blocked this and the playhead stranded permanently.
|
||||
state.presentation.set(makePresentation(200, 100));
|
||||
await flush();
|
||||
|
||||
expect(el.currentTime).toBe(204); // snapped to the new live edge despite `seeking`
|
||||
cleanup();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user