mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user