mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +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:
@@ -65,7 +65,7 @@ import { VERSION } from "../version.js";
|
||||
import { isDevMode } from "../utils/env.js";
|
||||
import { buildDockerRunArgs, resolveDockerPlatform } from "../utils/dockerRunArgs.js";
|
||||
import { normalizeErrorMessage } from "../utils/errorMessage.js";
|
||||
import { findFFmpeg, getFFmpegInstallHint } from "../browser/ffmpeg.js";
|
||||
import { runEnvironmentChecks } from "../browser/preflight.js";
|
||||
import type { ProducerLogger, RenderJob } from "@hyperframes/producer";
|
||||
import {
|
||||
normalizeResolutionFlag,
|
||||
@@ -967,30 +967,42 @@ export async function renderLocal(
|
||||
outputPath: string,
|
||||
options: RenderOptions,
|
||||
): Promise<void> {
|
||||
const producer = await loadProducer();
|
||||
|
||||
if (!findFFmpeg()) {
|
||||
errorBox(
|
||||
"FFmpeg not found",
|
||||
"FFmpeg is required to encode video. The render cannot proceed without it.",
|
||||
getFFmpegInstallHint(),
|
||||
);
|
||||
const preflight = await runEnvironmentChecks({
|
||||
projectDir,
|
||||
browserPath: options.browserPath,
|
||||
includeBrowser: true,
|
||||
includeDisk: true,
|
||||
includeWindowsUnc: true,
|
||||
});
|
||||
const failedChecks = preflight.outcomes.filter((outcome) => !outcome.ok);
|
||||
if (failedChecks.length > 0) {
|
||||
for (const check of failedChecks) {
|
||||
errorBox(check.title ?? `${check.name} check failed`, check.detail, check.hint);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
if (!options.quiet) {
|
||||
for (const outcome of preflight.outcomes) {
|
||||
if (outcome.level === "warn") {
|
||||
console.warn(c.warn(` ${outcome.name}: ${outcome.detail}`));
|
||||
if (outcome.hint) console.warn(c.dim(` ${outcome.hint}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (preflight.ffmpegPath) process.env.HYPERFRAMES_FFMPEG_PATH = preflight.ffmpegPath;
|
||||
if (preflight.ffprobePath) process.env.HYPERFRAMES_FFPROBE_PATH = preflight.ffprobePath;
|
||||
if (preflight.browser?.executablePath && !process.env.PRODUCER_HEADLESS_SHELL_PATH) {
|
||||
process.env.PRODUCER_HEADLESS_SHELL_PATH = preflight.browser.executablePath;
|
||||
}
|
||||
|
||||
const producer = await loadProducer();
|
||||
|
||||
const startTime = Date.now();
|
||||
const logger = createRenderTelemetryLogger(
|
||||
producer.createConsoleLogger?.("info") ?? createNoopProducerLogger(),
|
||||
);
|
||||
|
||||
// Pass the resolved browser path to the producer via env var so
|
||||
// resolveConfig() picks it up. This bridges the CLI's ensureBrowser()
|
||||
// (which knows about system Chrome on macOS) with the engine's
|
||||
// acquireBrowser() (which only checks the puppeteer cache).
|
||||
if (options.browserPath && !process.env.PRODUCER_HEADLESS_SHELL_PATH) {
|
||||
process.env.PRODUCER_HEADLESS_SHELL_PATH = options.browserPath;
|
||||
}
|
||||
|
||||
const job = producer.createRenderJob({
|
||||
fps: options.fps,
|
||||
quality: options.quality,
|
||||
|
||||
Reference in New Issue
Block a user