Skip to content

Commit f9da60d

Browse files
vanceingallsclaude
andcommitted
fix(studio-server): reject a peak map whose decode exits nonzero
A decode that emits some samples and then fails no longer resolves as a complete map, so /peaks cannot cache a truncated clipping map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c38638d commit f9da60d

2 files changed

Lines changed: 34 additions & 1 deletion

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { chmodSync, mkdtempSync, writeFileSync } from "node:fs";
2+
import { tmpdir } from "node:os";
3+
import { join } from "node:path";
4+
import { describe, expect, it, vi } from "vitest";
5+
6+
// Stand-in binaries: ffprobe reports a stereo stream, ffmpeg writes samples and then fails.
7+
const fakeBin = vi.hoisted(() => ({ dir: "" }));
8+
vi.mock("@hyperframes/parsers/ff-binaries", () => ({
9+
findFfBinary: (name: string) => join(fakeBin.dir, name),
10+
}));
11+
12+
import { decodePeakMap } from "./peakMap";
13+
14+
describe("decodePeakMap, decode failure", () => {
15+
it.skipIf(process.platform === "win32")(
16+
"rejects partial output when ffmpeg exits nonzero",
17+
async () => {
18+
fakeBin.dir = mkdtempSync(join(tmpdir(), "hf-fake-ff-"));
19+
const script = (name: string, body: string) => {
20+
const file = join(fakeBin.dir, name);
21+
writeFileSync(file, `#!/bin/sh\n${body}\n`);
22+
chmodSync(file, 0o755);
23+
};
24+
script("ffprobe", "echo 2");
25+
script("ffmpeg", "head -c 38400 /dev/zero; exit 1");
26+
await expect(decodePeakMap("truncated.mov")).rejects.toThrow(/exit 1/);
27+
},
28+
);
29+
});

‎packages/studio-server/src/helpers/peakMap.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,12 @@ export async function decodePeakMap(mediaPath: string): Promise<number[]> {
107107
const acc = createPeakAccumulator(PEAK_SAMPLE_RATE, PEAK_BIN_SECONDS, channels);
108108
proc.stdout?.on("data", (chunk: Buffer) => accumulatePeaks(acc, chunk));
109109
proc.on("close", (code) => {
110+
if (code !== 0) {
111+
reject(new Error(`ffmpeg failed decoding audio (exit ${code})`));
112+
return;
113+
}
110114
if (acc.frame === 0) {
111-
reject(new Error(`ffmpeg produced no audio samples (exit ${code})`));
115+
reject(new Error("ffmpeg produced no audio samples"));
112116
return;
113117
}
114118
resolvePromise(finishPeaks(acc));

0 commit comments

Comments
 (0)