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; }