mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
fix(render): diagnose unlaunchable Windows FFmpeg (#2430)
This commit is contained in:
@@ -4,6 +4,13 @@ import { parseToolVersion, runEnvironmentChecks } from "./preflight.js";
|
||||
import * as manager from "./manager.js";
|
||||
import * as linuxDeps from "./linuxDeps.js";
|
||||
|
||||
const execFileSync = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("node:child_process", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("node:child_process")>()),
|
||||
execFileSync,
|
||||
}));
|
||||
|
||||
describe("runEnvironmentChecks", () => {
|
||||
const originalFfmpegPath = process.env.HYPERFRAMES_FFMPEG_PATH;
|
||||
const originalFfprobePath = process.env.HYPERFRAMES_FFPROBE_PATH;
|
||||
@@ -11,6 +18,7 @@ describe("runEnvironmentChecks", () => {
|
||||
beforeEach(() => {
|
||||
process.env.HYPERFRAMES_FFMPEG_PATH = process.execPath;
|
||||
process.env.HYPERFRAMES_FFPROBE_PATH = process.execPath;
|
||||
execFileSync.mockReturnValue("ffmpeg version 7.1.1\n");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -57,6 +65,29 @@ describe("runEnvironmentChecks", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks rendering when the selected FFmpeg binary cannot launch", async () => {
|
||||
execFileSync.mockImplementation((binaryPath: string) => {
|
||||
if (binaryPath !== process.env.HYPERFRAMES_FFMPEG_PATH) return "ffprobe version 7.1.1\n";
|
||||
throw Object.assign(new Error("Command failed with exit code 3221225781"), {
|
||||
status: 3221225781,
|
||||
});
|
||||
});
|
||||
|
||||
const result = await runEnvironmentChecks();
|
||||
const ffmpeg = result.outcomes.find((outcome) => outcome.name === "FFmpeg");
|
||||
|
||||
expect(ffmpeg).toMatchObject({
|
||||
ok: false,
|
||||
level: "error",
|
||||
title: "FFmpeg cannot start",
|
||||
path: process.execPath,
|
||||
});
|
||||
expect(ffmpeg?.detail).toContain(process.execPath);
|
||||
expect(ffmpeg?.detail).toContain("3221225781");
|
||||
expect(ffmpeg?.hint).toContain("working 64-bit FFmpeg build");
|
||||
expect(result.ffmpegPath).toBeUndefined();
|
||||
});
|
||||
|
||||
it("validates an explicit browser path without needing browser discovery", async () => {
|
||||
const result = await runEnvironmentChecks({
|
||||
includeBrowser: true,
|
||||
|
||||
@@ -55,15 +55,23 @@ function configuredMissingDetail(envName: string): string | undefined {
|
||||
return `Configured path does not exist: ${envName}="${configured}"`;
|
||||
}
|
||||
|
||||
function readToolVersion(binaryPath: string): string {
|
||||
type ToolVersionResult = { ok: true; detail: string } | { ok: false; detail: string };
|
||||
|
||||
function readToolVersion(binaryPath: string): ToolVersionResult {
|
||||
try {
|
||||
const raw =
|
||||
execFileSync(binaryPath, ["-version"], { encoding: "utf-8", timeout: 5000 }).split("\n")[0] ??
|
||||
"";
|
||||
const version = parseToolVersion(raw);
|
||||
return version ? `${version} at ${binaryPath}` : binaryPath;
|
||||
} catch {
|
||||
return binaryPath;
|
||||
return { ok: true, detail: version ? `${version} at ${binaryPath}` : binaryPath };
|
||||
} catch (error) {
|
||||
const status =
|
||||
typeof error === "object" && error !== null && "status" in error ? error.status : undefined;
|
||||
const exitDetail = typeof status === "number" ? ` (exit code ${status})` : "";
|
||||
return {
|
||||
ok: false,
|
||||
detail: `Failed to run "${binaryPath}" -version${exitDetail}.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +90,19 @@ function checkFFmpeg(): EnvironmentCheckOutcome {
|
||||
|
||||
const path = findFFmpeg();
|
||||
if (path) {
|
||||
return { name: "FFmpeg", ok: true, level: "ok", detail: readToolVersion(path), path };
|
||||
const version = readToolVersion(path);
|
||||
if (!version.ok) {
|
||||
return {
|
||||
name: "FFmpeg",
|
||||
ok: false,
|
||||
level: "error",
|
||||
title: "FFmpeg cannot start",
|
||||
detail: version.detail,
|
||||
hint: "Install a working 64-bit FFmpeg build with all required runtime DLLs.",
|
||||
path,
|
||||
};
|
||||
}
|
||||
return { name: "FFmpeg", ok: true, level: "ok", detail: version.detail, path };
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -110,7 +130,8 @@ function checkFFprobe(): EnvironmentCheckOutcome {
|
||||
|
||||
const path = findFFprobe();
|
||||
if (path) {
|
||||
return { name: "FFprobe", ok: true, level: "ok", detail: readToolVersion(path), path };
|
||||
const version = readToolVersion(path);
|
||||
return { name: "FFprobe", ok: true, level: "ok", detail: version.detail, path };
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -270,7 +291,7 @@ export async function runEnvironmentChecks(
|
||||
|
||||
return {
|
||||
outcomes,
|
||||
...(ffmpeg.path ? { ffmpegPath: ffmpeg.path } : {}),
|
||||
...(ffmpeg.ok && ffmpeg.path ? { ffmpegPath: ffmpeg.path } : {}),
|
||||
...(ffprobe.path ? { ffprobePath: ffprobe.path } : {}),
|
||||
...(browser ? { browser } : {}),
|
||||
};
|
||||
|
||||
@@ -6,9 +6,12 @@ import { formatFfmpegError } from "./runFfmpeg.js";
|
||||
|
||||
describe("formatFfmpegError", () => {
|
||||
const originalPlatform = process.platform;
|
||||
const originalFfmpegPath = process.env.HYPERFRAMES_FFMPEG_PATH;
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true });
|
||||
if (originalFfmpegPath === undefined) delete process.env.HYPERFRAMES_FFMPEG_PATH;
|
||||
else process.env.HYPERFRAMES_FFMPEG_PATH = originalFfmpegPath;
|
||||
});
|
||||
|
||||
it("reports exit code alone when stderr is empty", () => {
|
||||
@@ -57,6 +60,20 @@ describe("formatFfmpegError", () => {
|
||||
|
||||
expect(formatFfmpegError(3221225595, "")).toContain("wrong architecture");
|
||||
});
|
||||
|
||||
it.each([3221225781, -1073741515])(
|
||||
"maps Windows DLL-not-found exit code %s to selected-path guidance",
|
||||
(exitCode) => {
|
||||
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
||||
process.env.HYPERFRAMES_FFMPEG_PATH = "/tools/ffmpeg.exe";
|
||||
|
||||
const message = formatFfmpegError(exitCode, "");
|
||||
|
||||
expect(message).toContain("0xC0000135 (STATUS_DLL_NOT_FOUND)");
|
||||
expect(message).toContain("/tools/ffmpeg.exe");
|
||||
expect(message).toContain("working 64-bit Windows FFmpeg build");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
function createSpawnSpy() {
|
||||
|
||||
@@ -29,6 +29,14 @@ const DEFAULT_STDERR_TAIL_LINES = 15;
|
||||
|
||||
function formatWindowsFfmpegExit(exitCode: number | null): string | undefined {
|
||||
if (process.platform !== "win32" || exitCode === null) return undefined;
|
||||
if (exitCode === 3221225781 || exitCode === -1073741515) {
|
||||
const ffmpegPath = getFfmpegBinary();
|
||||
return (
|
||||
`[FFmpeg] Windows could not start "${ffmpegPath}": ` +
|
||||
"0xC0000135 (STATUS_DLL_NOT_FOUND). A required DLL could not be loaded. " +
|
||||
"Install a working 64-bit Windows FFmpeg build with all required runtime DLLs."
|
||||
);
|
||||
}
|
||||
if (exitCode === 3221225595 || exitCode === -1073741701) {
|
||||
return (
|
||||
"[FFmpeg] Windows could not start ffmpeg.exe (STATUS_INVALID_IMAGE_FORMAT). " +
|
||||
|
||||
Reference in New Issue
Block a user