mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 15:20:13 +00:00
refactor(parsers): single shared FFmpeg/FFprobe binary resolver
The cli, engine, and lint packages each carried their own copy of the ffmpeg/ffprobe lookup, annotated fallow-ignore code-duplication, and the copies had drifted: the engine copy handled Windows PATHEXT and executed which/where without a shell but lacked the Homebrew-dirs fallback for GUI-spawned processes; the cli copy had the opposite. One resolver in @hyperframes/parsers (the dependency-graph bottom) now carries the union of both hardenings, and all three packages delegate to it. Every consumer gets strictly more robust resolution; env-override semantics per call site are preserved via configuredMustExist.
This commit is contained in:
@@ -1,99 +1,17 @@
|
||||
// fallow-ignore-file code-duplication
|
||||
import { execFileSync } from "child_process";
|
||||
import { accessSync, constants, existsSync } from "fs";
|
||||
import { delimiter, join, resolve } from "path";
|
||||
import { existsSync } from "fs";
|
||||
import { FFMPEG_PATH_ENV, FFPROBE_PATH_ENV, findFfBinary } from "@hyperframes/parsers/ff-binaries";
|
||||
|
||||
export const FFMPEG_PATH_ENV = "HYPERFRAMES_FFMPEG_PATH";
|
||||
export const FFPROBE_PATH_ENV = "HYPERFRAMES_FFPROBE_PATH";
|
||||
|
||||
const pathCache = new Map<string, string | undefined>();
|
||||
|
||||
function candidateFileName(candidate: string): string {
|
||||
return candidate.split(/[\\/]/).at(-1)?.toLowerCase() ?? candidate.toLowerCase();
|
||||
}
|
||||
|
||||
function chooseBestPathCandidate(
|
||||
name: "ffmpeg" | "ffprobe",
|
||||
candidates: readonly string[],
|
||||
): string | undefined {
|
||||
const normalized = candidates.map((candidate) => candidate.trim()).filter(Boolean);
|
||||
return (
|
||||
normalized.find((candidate) => candidateFileName(candidate) === `${name}.exe`) ??
|
||||
normalized.find((candidate) => candidateFileName(candidate) === name) ??
|
||||
normalized.find((candidate) => !candidateFileName(candidate).match(/\.(cmd|bat)$/i)) ??
|
||||
normalized[0]
|
||||
);
|
||||
}
|
||||
|
||||
function scanPath(name: "ffmpeg" | "ffprobe"): string | undefined {
|
||||
const pathValue = process.env.PATH;
|
||||
if (!pathValue) return undefined;
|
||||
|
||||
const extensions =
|
||||
process.platform === "win32"
|
||||
? [
|
||||
".exe",
|
||||
...new Set(
|
||||
(process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
|
||||
.split(";")
|
||||
.map((ext) => ext.trim().toLowerCase())
|
||||
.filter(Boolean),
|
||||
),
|
||||
"",
|
||||
]
|
||||
: [""];
|
||||
const candidates: string[] = [];
|
||||
for (const dir of pathValue.split(delimiter)) {
|
||||
if (!dir) continue;
|
||||
for (const ext of extensions) {
|
||||
const candidate = join(dir, `${name}${ext}`);
|
||||
if (isExecutablePathCandidate(candidate)) candidates.push(candidate);
|
||||
}
|
||||
}
|
||||
return chooseBestPathCandidate(name, candidates);
|
||||
}
|
||||
|
||||
function isExecutablePathCandidate(candidate: string): boolean {
|
||||
if (process.platform === "win32") return existsSync(candidate);
|
||||
try {
|
||||
accessSync(candidate, constants.X_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function findOnPath(name: "ffmpeg" | "ffprobe"): string | undefined {
|
||||
if (pathCache.has(name)) return pathCache.get(name);
|
||||
let found: string | undefined;
|
||||
try {
|
||||
const command = process.platform === "win32" ? "where" : "which";
|
||||
const output = execFileSync(command, [name], {
|
||||
encoding: "utf-8",
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
timeout: 5000,
|
||||
});
|
||||
found = chooseBestPathCandidate(name, output.split(/\r?\n/));
|
||||
} catch {
|
||||
found = scanPath(name);
|
||||
}
|
||||
const resolved = found ? resolve(found) : undefined;
|
||||
pathCache.set(name, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function getConfiguredBinary(envName: string, binaryName: "ffmpeg" | "ffprobe"): string {
|
||||
const configured = process.env[envName]?.trim();
|
||||
if (configured) return resolve(configured);
|
||||
return findOnPath(binaryName) ?? binaryName;
|
||||
}
|
||||
export { FFMPEG_PATH_ENV, FFPROBE_PATH_ENV };
|
||||
|
||||
// The engine hands spawn a bare binary name as the last resort so the spawn
|
||||
// error names what the user must install; a configured-but-missing override
|
||||
// is surfaced separately by assertConfiguredFfmpegBinariesExist below.
|
||||
export function getFfmpegBinary(): string {
|
||||
return getConfiguredBinary(FFMPEG_PATH_ENV, "ffmpeg");
|
||||
return findFfBinary("ffmpeg") ?? "ffmpeg";
|
||||
}
|
||||
|
||||
export function getFfprobeBinary(): string {
|
||||
return getConfiguredBinary(FFPROBE_PATH_ENV, "ffprobe");
|
||||
return findFfBinary("ffprobe") ?? "ffprobe";
|
||||
}
|
||||
|
||||
export function assertConfiguredFfmpegBinariesExist(): void {
|
||||
|
||||
Reference in New Issue
Block a user