Files
hyperframes/packages/cli/src/browser/ffmpeg.test.ts
T
Miguel Ángel e3ca89e9ac fix(cli): discover project-local ffmpeg binaries (#2464)
* fix(cli): discover project-local ffmpeg binaries

* style(cli): format project-local ffmpeg discovery

* style(media-use): format resolver script

* chore(skills): refresh bundled manifest
2026-07-15 00:37:18 -04:00

107 lines
3.9 KiB
TypeScript

import { execFileSync, execSync } from "node:child_process";
import { existsSync } from "node:fs";
import { resolve } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("node:child_process", () => ({ execFileSync: vi.fn(), execSync: vi.fn() }));
vi.mock("node:fs", () => ({ existsSync: vi.fn() }));
const mockExec = vi.mocked(execSync);
const mockExecFile = vi.mocked(execFileSync);
const mockExists = vi.mocked(existsSync);
// The common-dir fallback list is platform-gated (empty on win32), so pin the
// platform to a POSIX value to keep the test deterministic on Windows CI.
const originalPlatform = process.platform;
beforeEach(() => {
Object.defineProperty(process, "platform", { value: "linux", configurable: true });
vi.resetModules();
});
afterEach(() => {
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true });
vi.clearAllMocks();
delete process.env.HYPERFRAMES_FFMPEG_PATH;
delete process.env.HYPERFRAMES_FFPROBE_PATH;
});
describe("findFFmpeg", () => {
it("prefers the real Windows exe when where lists a cmd shim first", async () => {
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
mockExec.mockReturnValue("C:\\tools\\ffmpeg.cmd\r\nC:\\tools\\ffmpeg.exe\r\n");
const { findFFmpeg } = await import("./ffmpeg.js");
expect(findFFmpeg()).toBe(resolve("C:\\tools\\ffmpeg.exe"));
});
it("falls back to a common install dir when `which` fails (GUI-launched PATH)", async () => {
// Simulate a process whose PATH lacks /opt/homebrew/bin: `which ffmpeg` throws.
mockExec.mockImplementation(() => {
throw new Error("which: no ffmpeg in PATH");
});
mockExists.mockImplementation((p) => p === "/opt/homebrew/bin/ffmpeg");
const { findFFmpeg } = await import("./ffmpeg.js");
expect(findFFmpeg()).toBe("/opt/homebrew/bin/ffmpeg");
});
it("returns undefined when ffmpeg is on neither PATH nor a common dir", async () => {
mockExec.mockImplementation(() => {
throw new Error("not found");
});
mockExists.mockReturnValue(false);
const { findFFmpeg } = await import("./ffmpeg.js");
expect(findFFmpeg()).toBeUndefined();
});
it("finds project-local FFmpeg binaries when they are not on PATH", async () => {
mockExec.mockImplementation(() => {
throw new Error("not found");
});
const localFFmpeg = resolve(".hyperframes", "bin", "ffmpeg");
const localFFprobe = resolve(".hyperframes", "bin", "ffprobe");
mockExists.mockImplementation((path) => path === localFFmpeg || path === localFFprobe);
const { findFFmpeg, findFFprobe } = await import("./ffmpeg.js");
expect(findFFmpeg()).toBe(localFFmpeg);
expect(findFFprobe()).toBe(localFFprobe);
});
});
describe("resolveH264EncoderMode", () => {
it("falls back to VideoToolbox when libx264 is absent", async () => {
const { resolveH264EncoderMode } = await import("./ffmpeg.js");
const encoders = `
V....D h264_videotoolbox VideoToolbox H.264 Encoder
`;
expect(resolveH264EncoderMode(encoders, false)).toBe("gpu");
});
it("does not treat a compiled Linux hardware encoder as usable", async () => {
const { resolveH264EncoderMode } = await import("./ffmpeg.js");
const encoders = `
V....D h264_vaapi H.264/AVC (VAAPI)
`;
expect(() => resolveH264EncoderMode(encoders, false)).toThrow(
"neither libx264 nor VideoToolbox",
);
});
it("inspects the configured FFmpeg binary", async () => {
mockExecFile.mockReturnValue(
" V....D h264_videotoolbox VideoToolbox H.264 Encoder\n" as never,
);
const { detectH264EncoderMode } = await import("./ffmpeg.js");
expect(detectH264EncoderMode("/custom/ffmpeg", false)).toBe("gpu");
expect(mockExecFile).toHaveBeenCalledWith(
"/custom/ffmpeg",
["-hide_banner", "-encoders"],
expect.objectContaining({ encoding: "utf-8" }),
);
});
});