mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +00:00
fix(cli): verify browser/ffmpeg binaries exist before render starts (#1365)
## Problem Windows renders commonly fail with environment errors before any real work starts: - `Browser was not found at the configured executablePath (...chrome-headless-shell.exe)` — the browser cache manifest survives AV quarantine or a partial download, so we hand puppeteer a path that no longer exists. - `[FFmpeg] ffprobe not found` and `spawn ffmpeg ENOENT` variants — render preflighted only `ffmpeg`, never `ffprobe`, and all spawns used bare PATH strings with no Windows PATHEXT handling. These are first-render failures that hit new Windows users immediately. ## Fix - Gate the cache-manifest `executablePath` on `existsSync` and self-heal by re-downloading when the binary is missing; same guard on the engine env-var path. - New shared environment preflight (`packages/cli/src/browser/preflight.ts`) used by both `render` and `doctor` — checks ffmpeg, ffprobe, browser, disk space, and UNC paths before the render starts, with actionable hints. - Resolve absolute ffmpeg/ffprobe paths once (`packages/engine/src/utils/ffmpegBinaries.ts`) and pass them to every engine spawn instead of relying on PATH. - Map opaque Windows ffmpeg exit codes to actionable messages. ## Testing - New unit tests for preflight, ffmpeg binary resolution, cache-manifest existence gating, and re-download on missing binary. - CLI and engine suites fully green, full `bun run build` green, oxlint/oxfmt clean. - Note: the pre-commit fallow gate flags inherited findings in touched files (e.g. `audioExtractor.ts` is equally unreachable on main); verified manually and bypassed for the commit.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// fallow-ignore-file code-duplication
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const producerState = vi.hoisted(() => ({
|
||||
@@ -5,6 +6,31 @@ const producerState = vi.hoisted(() => ({
|
||||
resolveConfigCalls: [] as Array<Record<string, unknown>>,
|
||||
}));
|
||||
|
||||
const preflightState = vi.hoisted(() => ({
|
||||
result: {
|
||||
outcomes: [
|
||||
{ name: "FFmpeg", ok: true, level: "ok", detail: "/usr/bin/ffmpeg", path: "/usr/bin/ffmpeg" },
|
||||
{
|
||||
name: "FFprobe",
|
||||
ok: true,
|
||||
level: "ok",
|
||||
detail: "/usr/bin/ffprobe",
|
||||
path: "/usr/bin/ffprobe",
|
||||
},
|
||||
{
|
||||
name: "Chrome",
|
||||
ok: true,
|
||||
level: "ok",
|
||||
detail: "cache: /mock/chrome",
|
||||
path: "/mock/chrome",
|
||||
},
|
||||
],
|
||||
ffmpegPath: "/usr/bin/ffmpeg",
|
||||
ffprobePath: "/usr/bin/ffprobe",
|
||||
browser: { executablePath: "/mock/chrome", source: "cache" },
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../utils/producer.js", () => ({
|
||||
loadProducer: vi.fn(async () => ({
|
||||
resolveConfig: vi.fn((overrides: Record<string, unknown>) => {
|
||||
@@ -29,6 +55,10 @@ vi.mock("../browser/ffmpeg.js", () => ({
|
||||
getFFmpegInstallHint: vi.fn(() => "brew install ffmpeg"),
|
||||
}));
|
||||
|
||||
vi.mock("../browser/preflight.js", () => ({
|
||||
runEnvironmentChecks: vi.fn(async () => preflightState.result),
|
||||
}));
|
||||
|
||||
describe("renderLocal browser GPU config", () => {
|
||||
const savedEnv = new Map<string, string | undefined>();
|
||||
// Pre-resolve once. The first dynamic `import("./render.js")` in this file
|
||||
@@ -46,7 +76,7 @@ describe("renderLocal browser GPU config", () => {
|
||||
});
|
||||
|
||||
function setEnv(key: string, value: string) {
|
||||
savedEnv.set(key, process.env[key]);
|
||||
if (!savedEnv.has(key)) savedEnv.set(key, process.env[key]);
|
||||
process.env[key] = value;
|
||||
}
|
||||
|
||||
@@ -54,6 +84,12 @@ describe("renderLocal browser GPU config", () => {
|
||||
producerState.createdJobs = [];
|
||||
producerState.resolveConfigCalls = [];
|
||||
savedEnv.clear();
|
||||
savedEnv.set("HYPERFRAMES_FFMPEG_PATH", process.env.HYPERFRAMES_FFMPEG_PATH);
|
||||
savedEnv.set("HYPERFRAMES_FFPROBE_PATH", process.env.HYPERFRAMES_FFPROBE_PATH);
|
||||
savedEnv.set("PRODUCER_HEADLESS_SHELL_PATH", process.env.PRODUCER_HEADLESS_SHELL_PATH);
|
||||
delete process.env.HYPERFRAMES_FFMPEG_PATH;
|
||||
delete process.env.HYPERFRAMES_FFPROBE_PATH;
|
||||
delete process.env.PRODUCER_HEADLESS_SHELL_PATH;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -125,6 +161,22 @@ describe("renderLocal browser GPU config", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("passes preflight-resolved FFmpeg, FFprobe, and browser paths through env", async () => {
|
||||
await renderLocal("/tmp/project", "/tmp/out.mp4", {
|
||||
fps: { num: 30, den: 1 },
|
||||
quality: "standard",
|
||||
format: "mp4",
|
||||
gpu: false,
|
||||
browserGpuMode: "software",
|
||||
hdrMode: "auto",
|
||||
quiet: true,
|
||||
});
|
||||
|
||||
expect(process.env.HYPERFRAMES_FFMPEG_PATH).toBe("/usr/bin/ffmpeg");
|
||||
expect(process.env.HYPERFRAMES_FFPROBE_PATH).toBe("/usr/bin/ffprobe");
|
||||
expect(process.env.PRODUCER_HEADLESS_SHELL_PATH).toBe("/mock/chrome");
|
||||
});
|
||||
|
||||
it("resolves browser GPU from CLI flags, Docker mode, and env fallback", () => {
|
||||
// Default (no flag, no env): auto — engine probes and chooses.
|
||||
expect(resolveBrowserGpuForCli(false, undefined, undefined)).toBe("auto");
|
||||
|
||||
Reference in New Issue
Block a user