diff --git a/src/components/ThumbnailStrip.tsx b/src/components/ThumbnailStrip.tsx index f5094722..e5d6810e 100644 --- a/src/components/ThumbnailStrip.tsx +++ b/src/components/ThumbnailStrip.tsx @@ -45,6 +45,10 @@ export default function ThumbnailStrip({ const cancelThumbnailRun = useCallback(() => { lastRunIdRef.current += 1; + if (offscreenVideoRef.current) { + offscreenVideoRef.current.src = ""; + offscreenVideoRef.current.load(); + } }, []); const generateThumbnails = useCallback(async () => { @@ -97,8 +101,15 @@ export default function ThumbnailStrip({ const time = times[i] ?? 0; await new Promise((resolve) => { - const onSeeked = async () => { + let timeoutId: number | undefined; + + const cleanup = () => { video.removeEventListener("seeked", onSeeked); + if (timeoutId) window.clearTimeout(timeoutId); + }; + + const onSeeked = async () => { + cleanup(); if (lastRunIdRef.current !== runId) { resolve(); @@ -127,6 +138,13 @@ export default function ThumbnailStrip({ setProgress(Math.round(((i + 1) / times.length) * 100)); resolve(); }; + + // Setup timeout fallback (2 seconds max per seek to prevent hang) + timeoutId = window.setTimeout(() => { + cleanup(); + resolve(); + }, 2000); + video.addEventListener("seeked", onSeeked); video.currentTime = time; }); diff --git a/src/hooks/useAudioWaveform.ts b/src/hooks/useAudioWaveform.ts index 25ed57d5..a46520d2 100644 --- a/src/hooks/useAudioWaveform.ts +++ b/src/hooks/useAudioWaveform.ts @@ -45,6 +45,13 @@ export function useAudioWaveform( return; } + // Skip decoding for files larger than 100MB to prevent browser OOM crash + if (file.size > 100 * 1024 * 1024) { + setWaveform([]); + setIsLoading(false); + return; + } + const AudioContextCtor = window.AudioContext || (window as BrowserWindow).webkitAudioContext;