diff --git a/src/components/nodes/VideoStitchNode.tsx b/src/components/nodes/VideoStitchNode.tsx index accee880..c39a86d0 100644 --- a/src/components/nodes/VideoStitchNode.tsx +++ b/src/components/nodes/VideoStitchNode.tsx @@ -131,44 +131,66 @@ export function VideoStitchNode({ id, data, selected }: NodeProps>(new Map()); + // Fingerprint cache: edgeId -> last-20-chars of videoData, used to detect which clips changed + const thumbnailFingerprintsRef = useRef>(new Map()); // Extract thumbnails from connected videos useEffect(() => { let cancelled = false; + const cleanupVideo = (video: HTMLVideoElement) => { + video.onloadedmetadata = null; + video.onerror = null; + video.onseeked = null; + video.src = ""; + video.load(); + }; + const extractThumbnails = async () => { - thumbnailsRef.current = new Map(); const newThumbnails = new Map(); + const newFingerprints = new Map(); for (const clip of orderedClips) { if (cancelled) return; if (!clip.videoData) continue; - if (thumbnailsRef.current.has(clip.edgeId)) { + + const fingerprint = clip.videoData.slice(-20); + newFingerprints.set(clip.edgeId, fingerprint); + + // Reuse cached thumbnail if the video data hasn't changed + const cachedFingerprint = thumbnailFingerprintsRef.current.get(clip.edgeId); + if (cachedFingerprint === fingerprint && thumbnailsRef.current.has(clip.edgeId)) { newThumbnails.set(clip.edgeId, thumbnailsRef.current.get(clip.edgeId)!); continue; } + const video = document.createElement("video"); try { - const video = document.createElement("video"); video.src = clip.videoData; video.crossOrigin = "anonymous"; video.muted = true; + video.preload = "metadata"; await new Promise((resolve, reject) => { video.onloadedmetadata = () => resolve(); video.onerror = () => reject(new Error("Failed to load video")); }); - if (cancelled) return; + if (cancelled) { cleanupVideo(video); return; } const seekTime = video.duration * 0.25; video.currentTime = seekTime; - await new Promise((resolve) => { - video.onseeked = () => resolve(); - }); + await Promise.race([ + new Promise((resolve) => { + video.onseeked = () => resolve(); + }), + new Promise((_, reject) => + setTimeout(() => reject(new Error("Seek timeout")), 10_000) + ), + ]); - if (cancelled) return; + if (cancelled) { cleanupVideo(video); return; } const canvas = document.createElement("canvas"); const thumbWidth = 160; @@ -177,7 +199,7 @@ export function VideoStitchNode({ id, data, selected }: NodeProps