diff --git a/src/components/nodes/AudioInputNode.tsx b/src/components/nodes/AudioInputNode.tsx index f6f4d7b0..90882bb6 100644 --- a/src/components/nodes/AudioInputNode.tsx +++ b/src/components/nodes/AudioInputNode.tsx @@ -45,14 +45,23 @@ export function AudioInputNode({ id, data, selected }: NodeProps { + const handleEnded = () => { setIsPlaying(false); setCurrentTime(0); - }); - - audio.addEventListener("timeupdate", () => { + }; + const handleTimeUpdate = () => { setCurrentTime(audio.currentTime); - }); + }; + + audio.addEventListener("ended", handleEnded); + audio.addEventListener("timeupdate", handleTimeUpdate); + + return () => { + audio.removeEventListener("ended", handleEnded); + audio.removeEventListener("timeupdate", handleTimeUpdate); + audio.pause(); + audioRef.current = null; + }; } return () => { diff --git a/src/hooks/useAudioMixing.ts b/src/hooks/useAudioMixing.ts index 3a916077..b2ab336c 100644 --- a/src/hooks/useAudioMixing.ts +++ b/src/hooks/useAudioMixing.ts @@ -45,35 +45,51 @@ async function decodeWithMediaBunny( ): Promise { onProgress?.({ message: 'Reading audio tracks...', progress: 20 }); - const blobSource = new BlobSource(audioBlob); - const input = new Input({ source: blobSource, formats: ALL_FORMATS }); + let blobSource: BlobSource | undefined; + let input: Input | undefined; + let sink: AudioBufferSink | undefined; - const audioTrack = await input.getPrimaryAudioTrack(); - if (!audioTrack) { - throw new Error('No audio tracks found in file'); - } + try { + blobSource = new BlobSource(audioBlob); + input = new Input({ source: blobSource, formats: ALL_FORMATS }); - const decodable = await audioTrack.canDecode(); - if (!decodable) { - throw new Error('Audio codec not supported by this browser'); - } + const audioTrack = await input.getPrimaryAudioTrack(); + if (!audioTrack) { + throw new Error('No audio tracks found in file'); + } - const sink = new AudioBufferSink(audioTrack); - const audioDuration = await input.computeDuration(); + const decodable = await audioTrack.canDecode(); + if (!decodable) { + throw new Error('Audio codec not supported by this browser'); + } + + sink = new AudioBufferSink(audioTrack); + const audioDuration = await input.computeDuration(); - onProgress?.({ message: 'Decoding audio...', progress: 30 }); - const decodedBuffers: AudioBuffer[] = []; - for await (const wrappedBuffer of sink.buffers(0, audioDuration)) { - if (wrappedBuffer?.buffer) { - decodedBuffers.push(wrappedBuffer.buffer); + onProgress?.({ message: 'Decoding audio...', progress: 30 }); + const decodedBuffers: AudioBuffer[] = []; + for await (const wrappedBuffer of sink.buffers(0, audioDuration)) { + if (wrappedBuffer?.buffer) { + decodedBuffers.push(wrappedBuffer.buffer); + } } - } - if (decodedBuffers.length === 0) { - throw new Error('Failed to decode audio'); - } + if (decodedBuffers.length === 0) { + throw new Error('Failed to decode audio'); + } - return decodedBuffers; + return decodedBuffers; + } finally { + if (sink && typeof sink.close === 'function') { + try { await sink.close(); } catch (e) { console.warn('Failed to close sink:', e); } + } + if (input && typeof input.close === 'function') { + try { await input.close(); } catch (e) { console.warn('Failed to close input:', e); } + } + if (blobSource && typeof blobSource.close === 'function') { + try { await blobSource.close(); } catch (e) { console.warn('Failed to close blobSource:', e); } + } + } } /** diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts index 84e48143..ad0d8bdd 100644 --- a/src/store/workflowStore.ts +++ b/src/store/workflowStore.ts @@ -304,17 +304,22 @@ function trackSaveGeneration( async function waitForPendingImageSyncs(timeout: number = 60000): Promise { if (pendingImageSyncs.size === 0) return; + let timeoutId: ReturnType; const timeoutPromise = new Promise((resolve) => { - setTimeout(() => { + timeoutId = setTimeout(() => { console.warn(`Pending image syncs timed out after ${timeout}ms, continuing with save`); resolve(); }, timeout); }); - await Promise.race([ - Promise.all(pendingImageSyncs.values()), - timeoutPromise, - ]); + try { + await Promise.race([ + Promise.all(pendingImageSyncs.values()), + timeoutPromise, + ]); + } finally { + clearTimeout(timeoutId!); + } } // Concurrency settings diff --git a/src/utils/deduplicatedFetch.ts b/src/utils/deduplicatedFetch.ts index c5f70995..cac9c994 100644 --- a/src/utils/deduplicatedFetch.ts +++ b/src/utils/deduplicatedFetch.ts @@ -19,6 +19,9 @@ const responseCache = new Map pending cleanup timeout IDs (for clearFetchCache cleanup) +const pendingTimeouts = new Map>(); + /** * Generate a cache key from URL and headers */ @@ -105,9 +108,11 @@ export async function deduplicatedFetch( .finally(() => { // Clean up in-flight request after a short delay // (allows concurrent calls that started just after to still benefit) - setTimeout(() => { + const timeoutId = setTimeout(() => { inFlightRequests.delete(cacheKey); + pendingTimeouts.delete(cacheKey); }, 50); + pendingTimeouts.set(cacheKey, timeoutId); }); // Store the in-flight request @@ -123,4 +128,8 @@ export async function deduplicatedFetch( export function clearFetchCache(): void { responseCache.clear(); inFlightRequests.clear(); + for (const timeoutId of pendingTimeouts.values()) { + clearTimeout(timeoutId); + } + pendingTimeouts.clear(); }