From 5697e4adc36c2804eb26392d3f1d27c77dd026f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Mon, 1 Jun 2026 16:44:49 -0400 Subject: [PATCH] fix(cli): pre-flight FFmpeg check and propagate render failure stage (#1149) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(cli): pre-flight FFmpeg check and propagate failed_stage on render errors Add an early FFmpeg availability check in renderLocal() so users get a clear error message before the render starts instead of a cryptic ENOENT mid-render. Also thread job.failedStage through handleRenderError into the render_error telemetry event so we can attribute failures to a specific pipeline stage. * fix(cli): consolidate FFmpeg pre-flight into renderLocal() Remove the duplicate findFFmpeg() check from run() — renderLocal() already validates FFmpeg availability before starting. Single source of truth. --- packages/cli/src/commands/render.ts | 35 +++++++++++++++++------------ 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/commands/render.ts b/packages/cli/src/commands/render.ts index d7de1c6c7..4be88de16 100644 --- a/packages/cli/src/commands/render.ts +++ b/packages/cli/src/commands/render.ts @@ -51,6 +51,7 @@ import { VERSION } from "../version.js"; import { isDevMode } from "../utils/env.js"; import { buildDockerRunArgs } from "../utils/dockerRunArgs.js"; import { normalizeErrorMessage } from "../utils/errorMessage.js"; +import { findFFmpeg, getFFmpegInstallHint } from "../browser/ffmpeg.js"; import type { RenderJob } from "@hyperframes/producer"; import { normalizeResolutionFlag, @@ -434,19 +435,6 @@ export default defineCommand({ console.log(""); } - // ── Check FFmpeg for local renders ─────────────────────────────────── - if (!useDocker) { - const { findFFmpeg, getFFmpegInstallHint } = await import("../browser/ffmpeg.js"); - if (!findFFmpeg()) { - errorBox( - "FFmpeg not found", - "Rendering requires FFmpeg for video encoding.", - `Install: ${getFFmpegInstallHint()}`, - ); - process.exit(1); - } - } - // ── Ensure browser for local renders ──────────────────────────────── let browserPath: string | undefined; if (!useDocker) { @@ -786,6 +774,16 @@ export async function renderLocal( options: RenderOptions, ): Promise { const producer = await loadProducer(); + + if (!findFFmpeg()) { + errorBox( + "FFmpeg not found", + "FFmpeg is required to encode video. The render cannot proceed without it.", + getFFmpegInstallHint(), + ); + process.exit(1); + } + const startTime = Date.now(); // Pass the resolved browser path to the producer via env var so @@ -822,7 +820,14 @@ export async function renderLocal( try { await producer.executeRenderJob(job, projectDir, outputPath, onProgress); } catch (error: unknown) { - handleRenderError(error, options, startTime, false, "Try --docker for containerized rendering"); + handleRenderError( + error, + options, + startTime, + false, + "Try --docker for containerized rendering", + job.failedStage, + ); } const elapsed = Date.now() - startTime; @@ -868,6 +873,7 @@ function handleRenderError( startTime: number, docker: boolean, hint: string, + failedStage?: string, ): never { const message = normalizeErrorMessage(error); trackRenderError({ @@ -878,6 +884,7 @@ function handleRenderError( gpu: options.gpu, elapsedMs: Date.now() - startTime, errorMessage: message, + failedStage, ...getMemorySnapshot(), }); errorBox("Render failed", message, hint);