fix(render): aggregate extraction launch failures

This commit is contained in:
James
2026-07-26 19:47:33 +00:00
parent c01e1a5f96
commit 33ca1de063
4 changed files with 123 additions and 34 deletions
@@ -0,0 +1,20 @@
import { describe, expect, it } from "vitest";
import { classifyFfmpegSpawnError } from "./videoFrameExtractor.js";
describe("classifyFfmpegSpawnError", () => {
it.each(["ENOENT", "EACCES", "ENOEXEC", "UNKNOWN"])(
"keeps deterministic launch failure %s terminal",
(code) => {
expect(classifyFfmpegSpawnError(Object.assign(new Error(code), { code }))).toMatchObject({
retryable: false,
});
},
);
it.each(["EAGAIN", "EMFILE", "ENFILE"])("retries known transient launch failure %s", (code) => {
expect(classifyFfmpegSpawnError(Object.assign(new Error(code), { code }))).toMatchObject({
kind: "ffmpeg_transient",
retryable: true,
});
});
});
@@ -618,21 +618,7 @@ export async function extractVideoFramesRange(
throw new VideoSourceExtractionError("cancelled", false, "Video extraction cancelled");
}
if (processResult.terminationReason === "spawn_error") {
if ((processResult.error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") {
throw new VideoSourceExtractionError(
"ffmpeg_unavailable",
false,
"FFmpeg is unavailable",
"[FFmpeg] ffmpeg not found",
);
}
const diagnostic = processResult.error?.message || processResult.stderr;
throw new VideoSourceExtractionError(
"ffmpeg_transient",
true,
"FFmpeg could not be started",
diagnostic,
);
throw classifyFfmpegSpawnError(processResult.error, processResult.stderr);
}
if (!processResult.success) {
// With the SDR-to-HDR remap folded into this pass, a filter failure
@@ -697,6 +683,33 @@ export async function extractVideoFramesRange(
};
}
const TRANSIENT_FFMPEG_SPAWN_CODES = new Set(["EAGAIN", "EMFILE", "ENFILE"]);
export function classifyFfmpegSpawnError(error: unknown, stderr = ""): VideoSourceExtractionError {
const code =
typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
? error.code
: "";
if (code === "ENOENT") {
return new VideoSourceExtractionError(
"ffmpeg_unavailable",
false,
"FFmpeg is unavailable",
"[FFmpeg] ffmpeg not found",
);
}
const diagnostic = error instanceof Error ? error.message : stderr;
const retryable = TRANSIENT_FFMPEG_SPAWN_CODES.has(code);
return new VideoSourceExtractionError(
retryable ? "ffmpeg_transient" : "ffmpeg_failed",
retryable,
retryable
? "FFmpeg could not be started due to transient resource pressure"
: "FFmpeg could not be started",
diagnostic,
);
}
/**
* Resolve the used-segment duration for a video, falling back to the source's
* natural duration when the caller hasn't specified bounds (end=Infinity) or