From 89645f160e60abb9339f6c854a3ecda502046e9c Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Tue, 26 May 2026 16:21:00 +0000 Subject: [PATCH] fix(filmstrip): seeked event never fires at t=0; add per-frame seek timeout Two bugs: 1. Frame 0 sets currentTime=0 but probe starts at t=0 after onloadedmetadata, so 'seeked' never fires (no position change). Promise hangs until the 15s global timeout kills the whole build. Fix: when currentTime is already at target (within 0.05s), call done() immediately without waiting for seeked. 2. Seeks into unbuffered regions of large MP4s can stall indefinitely. Fix: 3s per-frame timeout captures the current decoded frame and moves on, so a slow/stalled seek doesn't block the remaining 27 frames. --- services/web-ui/public/screens-asset.jsx | 27 +++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/services/web-ui/public/screens-asset.jsx b/services/web-ui/public/screens-asset.jsx index 3a69d2f..2909101 100644 --- a/services/web-ui/public/screens-asset.jsx +++ b/services/web-ui/public/screens-asset.jsx @@ -135,6 +135,7 @@ function AssetDetail({ asset, onClose }) { const nextFrames = []; for (let i = 0; i < frameCount; i++) { const at = frameCount === 1 ? 0 : (probe.duration * i) / (frameCount - 1); + const target = Math.min(Math.max(at, 0), Math.max(0, probe.duration - 0.05)); await new Promise(function(resolve) { const done = function() { try { @@ -145,12 +146,28 @@ function AssetDetail({ asset, onClose }) { } resolve(); }; - const onSeeked = function() { - probe.removeEventListener('seeked', onSeeked); + // If already at the target position (frame 0 at t=0), 'seeked' will + // never fire because the browser sees no position change — call done() + // directly. Otherwise wait for the seeked event with a per-frame + // timeout so a stalled seek (unbuffered range) doesn't hang the strip. + if (Math.abs(probe.currentTime - target) < 0.05) { done(); - }; - probe.addEventListener('seeked', onSeeked); - probe.currentTime = Math.min(Math.max(at, 0), Math.max(0, probe.duration - 0.05)); + } else { + let frameTimer; + const onSeeked = function() { + clearTimeout(frameTimer); + probe.removeEventListener('seeked', onSeeked); + done(); + }; + probe.addEventListener('seeked', onSeeked); + probe.currentTime = target; + // 3s per-frame deadline — if seek stalls (e.g. unbuffered remote range), + // capture whatever frame is currently decoded and move on. + frameTimer = setTimeout(function() { + probe.removeEventListener('seeked', onSeeked); + done(); + }, 3000); + } }); if (cancelled) return; }