fix filmstrip: append probe to DOM, fix race condition, add 15s timeout
This commit is contained in:
parent
7ea3a235da
commit
4c8c3b72bb
1 changed files with 11 additions and 27 deletions
|
|
@ -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 });
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue