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 };
}