fix(producer): an ffmpeg failure names how ffmpeg ended and keeps its last line (#5678)

* test: ffmpeg failures name how ffmpeg ended and keep its last line

* fix(producer): an ffmpeg failure names how ffmpeg ended and keeps its last line

* refactor(producer): say why in names, not comments
This commit is contained in:
Miguel Ángel
2026-10-16 01:38:20 +00:00
committed by GitHub
parent 8e17ee9300
commit c03532b285
4 changed files with 77 additions and 8 deletions
+43 -1
View File
@@ -2,7 +2,12 @@ import { EventEmitter } from "node:events";
import { resolve } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { formatFfmpegError, isExternalFfmpegInterruption, ffmpegStatsReader } from "./runFfmpeg.js";
import {
describeFfmpegFailure,
formatFfmpegError,
isExternalFfmpegInterruption,
ffmpegStatsReader,
} from "./runFfmpeg.js";
describe("isExternalFfmpegInterruption", () => {
const base = {
@@ -152,6 +157,43 @@ function createSpawnSpy() {
return { spawn, calls };
}
describe("describeFfmpegFailure", () => {
const stderr = "Output #0, ipod, to 'audio.m4a':\n Metadata:\n major_brand : M4A\n";
it("names the signal that ended ffmpeg when it left no exit code", () => {
const message = describeFfmpegFailure(
{ exitCode: null, signal: "SIGKILL", stderr, terminationReason: "exit" },
1000,
);
expect(message).toMatch(/^FFmpeg ended without an exit code \(SIGKILL\)\./);
expect(message).toContain("major_brand : M4A");
});
it("names a stop the caller asked for", () => {
const message = describeFfmpegFailure(
{ exitCode: null, signal: "SIGTERM", stderr, terminationReason: "abort" },
1000,
);
expect(message).toMatch(/^FFmpeg ended without an exit code \(abort\)\./);
});
it("leads with the timeout when the deadline ended it", () => {
const message = describeFfmpegFailure(
{ exitCode: null, signal: "SIGTERM", stderr, terminationReason: "deadline" },
1000,
);
expect(message).toMatch(/^FFmpeg timed out after 1000 ms/);
});
it("keeps an exit code's message as it was", () => {
const message = describeFfmpegFailure(
{ exitCode: 1, signal: null, stderr: "Invalid argument\n", terminationReason: "exit" },
1000,
);
expect(message).toBe(formatFfmpegError(1, "Invalid argument\n"));
});
});
describe("runFfmpeg binary resolution", () => {
const originalFfmpegPath = process.env.HYPERFRAMES_FFMPEG_PATH;
+13 -6
View File
@@ -140,15 +140,22 @@ export function formatFfmpegError(
/** formatFfmpegError, led by the timeout when this process was killed at its deadline. */
export function describeFfmpegFailure(
result: Pick<RunFfmpegResult, "exitCode" | "stderr" | "terminationReason">,
result: Pick<RunFfmpegResult, "exitCode" | "signal" | "stderr" | "terminationReason">,
timeoutMs: number,
): string {
const error = formatFfmpegError(result.exitCode, result.stderr);
if (result.terminationReason !== "deadline") return error;
return (
`FFmpeg timed out after ${timeoutMs} ms (ffmpegProcessTimeout; long renders can raise ` +
`FFMPEG_PROCESS_TIMEOUT_MS). ${error}`
);
if (result.terminationReason === "deadline") {
return (
`FFmpeg timed out after ${timeoutMs} ms (ffmpegProcessTimeout; long renders can raise ` +
`FFMPEG_PROCESS_TIMEOUT_MS). ${error}`
);
}
if (result.exitCode !== null) return error;
const signalOrRequestedStop =
result.terminationReason === "exit" ? result.signal : result.terminationReason;
return signalOrRequestedStop
? `FFmpeg ended without an exit code (${signalOrRequestedStop}). ${error}`
: error;
}
function spawnFfmpeg(args: string[]): ChildProcess {
@@ -334,6 +334,25 @@ describe("padOrTrimAudioToVideoFrameCount", () => {
expect(correctionArgs[correctionArgs.indexOf("-t") + 1]).toBe("3.000000");
});
it("keeps the end of a long correction failure, where ffmpeg says what went wrong", async () => {
const { input } = harness({
video: { frameCount: 90, fpsNum: 30, fpsDen: 1 },
audio: { durationSeconds: 3 },
});
const banner = Array.from({ length: 12 }, (_, i) => ` metadata line ${i}: value`).join("\n");
const cause = "Error while filtering: the real ffmpeg cause";
let runs = 0;
input.probeAudioTruePeakDbfs = async () => 1.5;
input.runFfmpeg = async () =>
++runs === 1 ? { success: true } : { success: false, error: `[FFmpeg] ${banner}\n${cause}` };
const result = await padOrTrimAudioToVideoFrameCount(input);
expect(result.success).toBe(false);
expect(result.error!.length).toBeGreaterThan(240);
expect(result.error).toContain(cause);
});
it("uses correction headroom to converge within three AAC passes", async () => {
const dir = mkdtempSync(join(tmpdir(), "hf-aac-convergence-"));
const corrections: number[] = [];
@@ -280,7 +280,8 @@ function sanitizeProbeFailure(reason: unknown, paths: readonly string[]): string
// turned a returned failure result into a rejected promise, which is a
// behaviour regression the cast introduced.
const message = reason instanceof Error ? reason.message : String(reason);
return redactTelemetryString(redactKnownPaths(message, paths));
const keepFfmpegsLastLineUncapped = Number.POSITIVE_INFINITY;
return redactTelemetryString(redactKnownPaths(message, paths), keepFfmpegsLastLineUncapped);
}
/**