From dd394813b6c4b45f3c72662b60a18db48c819693 Mon Sep 17 00:00:00 2001 From: anirudhu-ui Date: Wed, 24 Jun 2026 22:51:34 +0530 Subject: [PATCH] Fix waveform OOM guard --- src/components/TrimControl.tsx | 11 +++++++- src/hooks/__tests__/useAudioWaveform.test.tsx | 26 +++++++++++++++++++ src/hooks/useAudioWaveform.ts | 17 +++++++++++- 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 src/hooks/__tests__/useAudioWaveform.test.tsx diff --git a/src/components/TrimControl.tsx b/src/components/TrimControl.tsx index d48bdd69..bdc1f61e 100644 --- a/src/components/TrimControl.tsx +++ b/src/components/TrimControl.tsx @@ -25,7 +25,11 @@ export default function TrimControl({ recipe, onChange, duration, file }: Props) recipe.trimStart.toString() ); - const { waveform, isLoading: waveformLoading } = useAudioWaveform(file); + const { + waveform, + isLoading: waveformLoading, + waveformError, + } = useAudioWaveform(file); const hasAudio = waveform.length > 0; useEffect(() => { @@ -317,6 +321,11 @@ export default function TrimControl({ recipe, onChange, duration, file }: Props) {formatDuration(duration)}

)} + {waveformError && ( +

+ {waveformError} +

+ )} {recipe.trimEnd !== null && recipe.trimEnd - recipe.trimStart < MIN_CLIP_DURATION && (

diff --git a/src/hooks/__tests__/useAudioWaveform.test.tsx b/src/hooks/__tests__/useAudioWaveform.test.tsx new file mode 100644 index 00000000..5064a9cc --- /dev/null +++ b/src/hooks/__tests__/useAudioWaveform.test.tsx @@ -0,0 +1,26 @@ +import { renderHook, waitFor } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { + MAX_WAVEFORM_FILE_SIZE_BYTES, + useAudioWaveform, +} from "../useAudioWaveform"; + +describe("useAudioWaveform", () => { + it("skips waveform decoding for files that are too large", async () => { + const file = new File(["video"], "large-video.mp4", { type: "video/mp4" }); + Object.defineProperty(file, "size", { + value: MAX_WAVEFORM_FILE_SIZE_BYTES + 1, + }); + const arrayBufferSpy = vi.spyOn(file, "arrayBuffer"); + + const { result } = renderHook(() => useAudioWaveform(file)); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.waveformError).toMatch(/larger than 50 MB/i); + }); + + expect(result.current.waveform).toEqual([]); + expect(arrayBufferSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/src/hooks/useAudioWaveform.ts b/src/hooks/useAudioWaveform.ts index 25ed57d5..13e07889 100644 --- a/src/hooks/useAudioWaveform.ts +++ b/src/hooks/useAudioWaveform.ts @@ -3,6 +3,9 @@ import { useEffect, useState } from "react"; const DEFAULT_BAR_COUNT = 96; +export const MAX_WAVEFORM_FILE_SIZE_BYTES = 50 * 1024 * 1024; +const LARGE_FILE_WAVEFORM_MESSAGE = + "Waveform preview is disabled for files larger than 50 MB."; type BrowserWindow = Window & typeof globalThis & { @@ -33,6 +36,7 @@ export function useAudioWaveform( ) { const [waveform, setWaveform] = useState([]); const [isLoading, setIsLoading] = useState(false); + const [waveformError, setWaveformError] = useState(null); useEffect(() => { let isCancelled = false; @@ -41,6 +45,14 @@ export function useAudioWaveform( async function extractWaveform() { if (!file) { setWaveform([]); + setWaveformError(null); + setIsLoading(false); + return; + } + + if (file.size > MAX_WAVEFORM_FILE_SIZE_BYTES) { + setWaveform([]); + setWaveformError(LARGE_FILE_WAVEFORM_MESSAGE); setIsLoading(false); return; } @@ -50,11 +62,13 @@ export function useAudioWaveform( if (!AudioContextCtor) { setWaveform([]); + setWaveformError(null); setIsLoading(false); return; } setIsLoading(true); + setWaveformError(null); try { audioContext = new AudioContextCtor(); @@ -70,6 +84,7 @@ export function useAudioWaveform( } catch { if (!isCancelled) { setWaveform([]); + setWaveformError("Unable to generate waveform preview for this file."); } } finally { await audioContext?.close(); @@ -86,5 +101,5 @@ export function useAudioWaveform( }; }, [barCount, file]); - return { waveform, isLoading }; + return { waveform, isLoading, waveformError }; }