Files
hyperframes/packages/cli/src/browser/ffmpeg.ts
T
Miguel Ángel ff25058c5e fix(studio): resolve ffmpeg outside PATH so render doesn't 503 (#1536)
The render pre-flight check shells out to `which ffmpeg`, which only
searches the server process's PATH. When Studio is launched from a
GUI/Dock/launchd context that PATH lacks /opt/homebrew/bin, so `which`
fails even when ffmpeg is installed — and POST /render returns 503
"FFmpeg not found".

Fall back to probing well-known install dirs (Homebrew on Apple Silicon
and Intel, plus system/Linux locations) when the PATH lookup fails.

Also drop the [kf:static]/[kf:runtime] keyframe diagnostics that were
spamming the Studio console in prod, and fix two unrelated CI breakages
the branch inherited: a Windows-sensitive ffmpeg test (pin platform) and
a stale player test mock missing onRuntimeReady.
2026-06-17 10:21:24 -04:00

73 lines
2.3 KiB
TypeScript

// fallow-ignore-file code-duplication
import { execSync } from "node:child_process";
import { existsSync } from "node:fs";
import { resolve } from "node:path";
export const FFMPEG_PATH_ENV = "HYPERFRAMES_FFMPEG_PATH";
export const FFPROBE_PATH_ENV = "HYPERFRAMES_FFPROBE_PATH";
function findOnPath(name: "ffmpeg" | "ffprobe"): string | undefined {
try {
const cmd = process.platform === "win32" ? `where ${name}` : `which ${name}`;
const output = execSync(cmd, {
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
timeout: 5000,
});
const first = output
.split(/\r?\n/)
.map((s) => s.trim())
.find(Boolean);
return first ? resolve(first) : undefined;
} catch {
return undefined;
}
}
// GUI/Dock/launchd-spawned processes on macOS don't inherit the shell PATH, so
// `which ffmpeg` fails even when ffmpeg is installed via Homebrew. Probe the
// well-known install dirs as a fallback. (No-op on Windows, where `where` and
// installer-added PATH entries cover it.)
const COMMON_BIN_DIRS =
process.platform === "win32"
? []
: ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/snap/bin"];
function findInCommonDirs(name: "ffmpeg" | "ffprobe"): string | undefined {
for (const dir of COMMON_BIN_DIRS) {
const candidate = `${dir}/${name}`;
if (existsSync(candidate)) return candidate;
}
return undefined;
}
function findConfiguredBinary(
envName: string,
binaryName: "ffmpeg" | "ffprobe",
): string | undefined {
const configured = process.env[envName]?.trim();
if (configured) return existsSync(configured) ? resolve(configured) : undefined;
return findOnPath(binaryName) ?? findInCommonDirs(binaryName);
}
export function findFFmpeg(): string | undefined {
return findConfiguredBinary(FFMPEG_PATH_ENV, "ffmpeg");
}
export function findFFprobe(): string | undefined {
return findConfiguredBinary(FFPROBE_PATH_ENV, "ffprobe");
}
export function getFFmpegInstallHint(): string {
switch (process.platform) {
case "darwin":
return "brew install ffmpeg";
case "linux":
return "sudo apt install ffmpeg";
case "win32":
return "Download the 64-bit Windows build from https://ffmpeg.org/download.html#build-windows and add its bin/ directory to PATH.";
default:
return "https://ffmpeg.org/download.html";
}
}