diff --git a/services/web-ui/public/screens-asset.jsx b/services/web-ui/public/screens-asset.jsx index 126b020..5ebe270 100644 --- a/services/web-ui/public/screens-asset.jsx +++ b/services/web-ui/public/screens-asset.jsx @@ -78,9 +78,9 @@ function AssetDetail({ asset, onClose }) { return function() { hls.destroy(); }; }, [streamUrl, streamType]); - // Build filmstrip from real video when browser-playable media exists. + // Build filmstrip from real video frames. HLS streams use hls.js probe. React.useEffect(() => { - if (!streamUrl || totalMs <= 0 || streamType === 'hls') { + if (!streamUrl || totalMs <= 0) { setFilmFrames([]); setFilmstripLoading(false); return; @@ -94,11 +94,25 @@ function AssetDetail({ asset, onClose }) { probe.muted = true; probe.playsInline = true; probe.preload = 'auto'; - probe.src = streamUrl; - await new Promise(function(resolve, reject) { - probe.onloadedmetadata = resolve; - probe.onerror = reject; - }); + if (streamType === 'hls') { + if (!window.Hls) throw new Error('hls.js not loaded'); + await new Promise(function(resolve, reject) { + const hls = new window.Hls(); + hls.on(window.Hls.Events.MANIFEST_PARSED, function() { + probe.oncanplay = function() { probe.oncanplay = null; resolve(); }; + probe.onerror = reject; + }); + hls.on(window.Hls.Events.ERROR, function(ev, data) { reject(data); }); + hls.loadSource(streamUrl); + hls.attachMedia(probe); + }); + } else { + probe.src = streamUrl; + await new Promise(function(resolve, reject) { + probe.onloadedmetadata = resolve; + probe.onerror = reject; + }); + } const frameCount = 28; const width = 160; const height = 90; @@ -721,4 +735,4 @@ function avatarColor(initials) { return 'linear-gradient(135deg, hsl(' + (h % 360) + ' 60% 50%), hsl(' + ((h + 60) % 360) + ' 60% 45%))'; } -Object.assign(window, { AssetDetail, msToTimecode, parseDuration, avatarColor }); +Object.assign(window, { AssetDetail, msToTimecode, parseDuration, avatarColor }); \ No newline at end of file