Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/components/TrimControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -317,6 +321,11 @@ export default function TrimControl({ recipe, onChange, duration, file }: Props)
{formatDuration(duration)}
</p>
)}
{waveformError && (
<p className="text-[10px] text-[var(--muted)] font-heading">
{waveformError}
</p>
)}
{recipe.trimEnd !== null &&
recipe.trimEnd - recipe.trimStart < MIN_CLIP_DURATION && (
<p className="text-[10px] text-[var(--error)] font-heading">
Expand Down
26 changes: 26 additions & 0 deletions src/hooks/__tests__/useAudioWaveform.test.tsx
Original file line number Diff line number Diff line change
@@ -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();
});
});
17 changes: 16 additions & 1 deletion src/hooks/useAudioWaveform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand Down Expand Up @@ -33,6 +36,7 @@ export function useAudioWaveform(
) {
const [waveform, setWaveform] = useState<number[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [waveformError, setWaveformError] = useState<string | null>(null);

useEffect(() => {
let isCancelled = false;
Expand All @@ -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;
}
Expand All @@ -50,11 +62,13 @@ export function useAudioWaveform(

if (!AudioContextCtor) {
setWaveform([]);
setWaveformError(null);
setIsLoading(false);
return;
}

setIsLoading(true);
setWaveformError(null);

try {
audioContext = new AudioContextCtor();
Expand All @@ -70,6 +84,7 @@ export function useAudioWaveform(
} catch {
if (!isCancelled) {
setWaveform([]);
setWaveformError("Unable to generate waveform preview for this file.");
}
} finally {
await audioContext?.close();
Expand All @@ -86,5 +101,5 @@ export function useAudioWaveform(
};
}, [barCount, file]);

return { waveform, isLoading };
return { waveform, isLoading, waveformError };
}