fix: preserve shared FFmpeg resolver behavior

This commit is contained in:
Miguel Angel Simon Sierra
2026-07-16 19:10:04 -04:00
parent 88c049f525
commit 7980479083
3 changed files with 47 additions and 12 deletions
+32
View File
@@ -108,6 +108,38 @@ describe("findFfBinary", () => {
expect(findFfBinary("ffmpeg")).toBe(resolve("/opt/homebrew/bin/ffmpeg")); expect(findFfBinary("ffmpeg")).toBe(resolve("/opt/homebrew/bin/ffmpeg"));
}); });
it("falls back to the project-local .hyperframes bin", async () => {
delete process.env.HYPERFRAMES_FFMPEG_PATH;
process.env.PATH = "";
const projectBinary = resolve(
".hyperframes",
"bin",
process.platform === "win32" ? "ffmpeg.exe" : "ffmpeg",
);
vi.resetModules();
vi.doMock("node:child_process", () => {
const mocked = {
execFileSync: () => {
throw new Error("not found");
},
};
return { ...mocked, default: mocked };
});
vi.doMock("node:fs", () => {
const mocked = {
existsSync: (candidate: unknown) => candidate === projectBinary,
accessSync: () => {
throw new Error("not executable");
},
constants: { X_OK: 1 },
};
return { ...mocked, default: mocked };
});
const { findFfBinary } = await importFresh();
expect(findFfBinary("ffmpeg")).toBe(projectBinary);
});
it("returns undefined when the binary is nowhere, and caches the miss until cleared", async () => { it("returns undefined when the binary is nowhere, and caches the miss until cleared", async () => {
delete process.env.HYPERFRAMES_FFMPEG_PATH; delete process.env.HYPERFRAMES_FFMPEG_PATH;
Object.defineProperty(process, "platform", { value: "linux", configurable: true }); Object.defineProperty(process, "platform", { value: "linux", configurable: true });
+12 -4
View File
@@ -4,7 +4,7 @@ import { delimiter, join, resolve } from "node:path";
/** /**
* Shared FFmpeg/FFprobe binary resolution for every package that shells out * Shared FFmpeg/FFprobe binary resolution for every package that shells out
* to them (engine, cli, lint). Node-only: import via the * to them (engine, cli, lint, studio-server). Node-only: import via the
* `@hyperframes/parsers/ff-binaries` subpath, never from a browser bundle. * `@hyperframes/parsers/ff-binaries` subpath, never from a browser bundle.
*/ */
@@ -92,6 +92,12 @@ function findInCommonDirs(name: FfBinaryName): string | undefined {
return undefined; return undefined;
} }
function findInProjectLocalBin(name: FfBinaryName): string | undefined {
const extension = process.platform === "win32" ? ".exe" : "";
const candidate = resolve(".hyperframes", "bin", `${name}${extension}`);
return existsSync(candidate) ? candidate : undefined;
}
function lookupOnSystem(name: FfBinaryName): string | undefined { function lookupOnSystem(name: FfBinaryName): string | undefined {
if (pathLookupCache.has(name)) return pathLookupCache.get(name); if (pathLookupCache.has(name)) return pathLookupCache.get(name);
let found: string | undefined; let found: string | undefined;
@@ -106,6 +112,7 @@ function lookupOnSystem(name: FfBinaryName): string | undefined {
} catch { } catch {
found = scanPath(name); found = scanPath(name);
} }
found ??= findInProjectLocalBin(name);
found ??= findInCommonDirs(name); found ??= findInCommonDirs(name);
const resolved = found ? resolve(found) : undefined; const resolved = found ? resolve(found) : undefined;
pathLookupCache.set(name, resolved); pathLookupCache.set(name, resolved);
@@ -125,9 +132,10 @@ export interface FindFfBinaryOptions {
/** /**
* Resolve an FFmpeg-family binary: env override first, then `which`/`where`, * Resolve an FFmpeg-family binary: env override first, then `which`/`where`,
* then a manual PATH scan (covers Windows PATHEXT), then well-known Unix * then a manual PATH scan (covers Windows PATHEXT), a project-local
* install dirs. System lookups are cached per binary for the process * `.hyperframes/bin`, then well-known Unix install dirs. System lookups are
* lifetime; the env override is re-read on every call. * cached per binary for the process lifetime; the env override is re-read on
* every call.
*/ */
export function findFfBinary( export function findFfBinary(
name: FfBinaryName, name: FfBinaryName,
@@ -1,6 +1,7 @@
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import { existsSync, writeFileSync, mkdirSync } from "node:fs"; import { existsSync, writeFileSync, mkdirSync } from "node:fs";
import { join, resolve } from "node:path"; import { join } from "node:path";
import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
const SAMPLE_RATE = 4000; const SAMPLE_RATE = 4000;
const PEAK_COUNT = 4000; const PEAK_COUNT = 4000;
@@ -28,16 +29,10 @@ function computePeaks(floats: Float32Array, count: number): number[] {
return peaks.map((p) => p / maxPeak); return peaks.map((p) => p / maxPeak);
} }
function ffmpegBinary(): string {
const configured = process.env.HYPERFRAMES_FFMPEG_PATH?.trim();
if (configured) return resolve(configured);
return "ffmpeg";
}
export function decodeAudioPeaks(audioPath: string): Promise<number[]> { export function decodeAudioPeaks(audioPath: string): Promise<number[]> {
return new Promise((resolvePromise, reject) => { return new Promise((resolvePromise, reject) => {
const proc = spawn( const proc = spawn(
ffmpegBinary(), findFfBinary("ffmpeg") ?? "ffmpeg",
[ [
"-i", "-i",
audioPath, audioPath,