From bbdb2e94197b1aa93ce7022f4667ac218d65e880 Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Mon, 22 Jun 2026 10:52:36 +0100 Subject: [PATCH] fix(ui): hold must-watch seek lock on progressive (non-HLS) video The lock clamped currentTime only during the seeking event. HLS defers the forward seek until the fragment loads, so that clamp caught it; but for progressive MP4 the forward seek takes effect immediately and the mid-seek write is ignored, so playback leaked past the watched point (snackbar showed, no snap-back). Keep the immediate seeking-time clamp (HLS relies on it to avoid buffering ahead) and add an authoritative re-clamp in seeked: if the playhead still landed past the limit, snap back via a backward seek into buffered content, which holds. Snackbar now fires once per blocked gesture and only when a clamp actually happens. --- .../media-player/players/plyr-player.svelte | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/custom/media-player/players/plyr-player.svelte b/packages/ui/src/custom/media-player/players/plyr-player.svelte index b409a8fa..b438594c 100644 --- a/packages/ui/src/custom/media-player/players/plyr-player.svelte +++ b/packages/ui/src/custom/media-player/players/plyr-player.svelte @@ -84,7 +84,8 @@ let playedSeconds = 0; let lastValidTime = element.currentTime; let isSeeking = false; - let hasBlockedThisSeek = false; + let isReclamping = false; + let seekBlockNotified = false; let lastHeartbeatAt = 0; const seekTolerance = 0.5; const heartbeatIntervalMs = 15_000; @@ -105,18 +106,50 @@ playedSeconds = 0; }; + const isAheadOfLimit = () => player.currentTime > furthestSeconds + seekTolerance; + + const notifySeekBlocked = () => { + if (seekBlockNotified) return; + + seekBlockNotified = true; + policy.onSeekBlocked?.(); + }; + + // Immediate clamp. For HLS this short-circuits the forward fragment fetch + // so the player never buffers ahead. For progressive MP4 the write is + // often ignored mid-seek, so `onSeeked` re-asserts it once settled. const onSeeking = () => { isSeeking = true; - if (!hasBlockedThisSeek && player.currentTime > furthestSeconds + seekTolerance) { - hasBlockedThisSeek = true; + if (isReclamping) return; + + if (isAheadOfLimit()) { + notifySeekBlocked(); player.currentTime = furthestSeconds; - policy.onSeekBlocked?.(); } }; const onSeeked = () => { + // Consume the `seeked` produced by our own clamp write, then settle. + if (isReclamping) { + isReclamping = false; + isSeeking = false; + seekBlockNotified = false; + lastValidTime = player.currentTime; + return; + } + + // Authoritative enforcement: if the seek landed past the limit (the + // mid-seek clamp didn't hold, e.g. progressive MP4), snap back now — + // a backward seek into buffered content, which holds reliably. + if (isAheadOfLimit()) { + isReclamping = true; + notifySeekBlocked(); + player.currentTime = furthestSeconds; + return; + } + isSeeking = false; - hasBlockedThisSeek = false; + seekBlockNotified = false; lastValidTime = player.currentTime; };