fix filmstrip: append probe to DOM, fix race condition, add 15s timeout

This commit is contained in:
Zac Gaetano 2026-05-25 11:26:02 -04:00
parent 7ea3a235da
commit 4c8c3b72bb

View file

@ -79,10 +79,6 @@ function AssetDetail({ asset, onClose }) {
}, [streamUrl, streamType]); }, [streamUrl, streamType]);
// Build filmstrip from real video frames. HLS streams use hls.js probe. // Build filmstrip from real video frames. HLS streams use hls.js probe.
// For MP4, we fetch the first 10 MB as a Blob to feed the probe video
// this works reliably even in headless browsers where <video> network loading
// often stalls, and ensures the moov atom (placed at the start by faststart)
// is already buffered when the probe element reads it.
React.useEffect(() => { React.useEffect(() => {
if (!streamUrl || totalMs <= 0) { if (!streamUrl || totalMs <= 0) {
setFilmFrames([]); setFilmFrames([]);
@ -92,23 +88,19 @@ function AssetDetail({ asset, onClose }) {
let cancelled = false; let cancelled = false;
const build = async function() { const build = async function() {
setFilmstripLoading(true); setFilmstripLoading(true);
let probe = null; const probe = document.createElement('video');
let blobUrl = null; probe.crossOrigin = 'anonymous';
probe.muted = true;
probe.playsInline = true;
probe.preload = 'auto';
probe.style.cssText = 'position:fixed;left:-9999px;top:-9999px;width:1px;height:1px;pointer-events:none';
document.body.appendChild(probe);
const timeout = setTimeout(function() { const timeout = setTimeout(function() {
if (blobUrl) URL.revokeObjectURL(blobUrl); if (probe.parentNode) probe.parentNode.removeChild(probe);
if (probe) probe.remove();
if (!cancelled) { setFilmFrames([]); setFilmstripLoading(false); } if (!cancelled) { setFilmFrames([]); setFilmstripLoading(false); }
cancelled = true; cancelled = true;
}, 15000); }, 15000);
try { try {
probe = document.createElement('video');
probe.crossOrigin = 'anonymous';
probe.muted = true;
probe.playsInline = true;
probe.preload = 'auto';
probe.style.cssText = 'position:fixed;left:-9999px;top:-9999px;width:1px;height:1px;pointer-events:none';
document.body.appendChild(probe);
if (streamType === 'hls') { if (streamType === 'hls') {
if (!window.Hls) throw new Error('hls.js not loaded'); if (!window.Hls) throw new Error('hls.js not loaded');
await new Promise(function(resolve, reject) { await new Promise(function(resolve, reject) {
@ -122,16 +114,10 @@ function AssetDetail({ asset, onClose }) {
hls.attachMedia(probe); hls.attachMedia(probe);
}); });
} else { } else {
// Pre-buffer the first 10 MB as a Blob proxies use faststart so
// the moov atom lives at the start, making this sufficient to parse
// the full duration and seek any frame.
const resp = await fetch(streamUrl, { headers: { Range: 'bytes=0-10485760' } });
const blob = await resp.blob();
blobUrl = URL.createObjectURL(blob);
await new Promise(function(resolve, reject) { await new Promise(function(resolve, reject) {
probe.onloadedmetadata = resolve; probe.onloadedmetadata = resolve;
probe.onerror = reject; probe.onerror = reject;
probe.src = blobUrl; probe.src = streamUrl;
}); });
} }
const frameCount = 28; const frameCount = 28;
@ -168,8 +154,7 @@ function AssetDetail({ asset, onClose }) {
if (!cancelled) setFilmFrames([]); if (!cancelled) setFilmFrames([]);
} finally { } finally {
clearTimeout(timeout); clearTimeout(timeout);
if (blobUrl) URL.revokeObjectURL(blobUrl); if (probe.parentNode) probe.parentNode.removeChild(probe);
if (probe) probe.remove();
if (!cancelled) setFilmstripLoading(false); if (!cancelled) setFilmstripLoading(false);
} }
}; };
@ -302,7 +287,6 @@ function AssetDetail({ asset, onClose }) {
setComments(function(c) { return [...c, _normalizeComment(row)]; }); setComments(function(c) { return [...c, _normalizeComment(row)]; });
}) })
.catch(function(e) { .catch(function(e) {
// Best-effort fallback so the user doesn't lose the typed comment.
window.alert('Could not post comment: ' + (e.message || 'unknown error')); window.alert('Could not post comment: ' + (e.message || 'unknown error'));
setNewComment(text); setNewComment(text);
}); });
@ -759,4 +743,4 @@ function avatarColor(initials) {
return 'linear-gradient(135deg, hsl(' + (h % 360) + ' 60% 50%), hsl(' + ((h + 60) % 360) + ' 60% 45%))'; 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 });