mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 16:42:27 +00:00
fix(render): aggregate extraction launch failures
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user