From 400c02483890fd60e1b3e0c1869eaaa4015a4d12 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Sat, 10 Oct 2026 15:08:52 -0700 Subject: [PATCH 1/2] fix(parsers): find ffmpeg installed after a long-running preview started --- packages/parsers/src/ffBinaries.test.ts | 15 +++++++-------- packages/parsers/src/ffBinaries.ts | 16 +++++++++------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/parsers/src/ffBinaries.test.ts b/packages/parsers/src/ffBinaries.test.ts index dbb7e36c2ce..bb4f794a4c6 100644 --- a/packages/parsers/src/ffBinaries.test.ts +++ b/packages/parsers/src/ffBinaries.test.ts @@ -173,10 +173,11 @@ describe("findFfBinary", () => { expect(findFfBinary("ffmpeg")).toBe(projectBinary); }); - it("returns undefined when the binary is nowhere, and caches the miss until cleared", async () => { + it("finds a binary installed in a common dir after an earlier miss, then caches the hit", async () => { delete process.env.HYPERFRAMES_FFMPEG_PATH; Object.defineProperty(process, "platform", { value: "linux", configurable: true }); process.env.PATH = ""; + let installed = false; const execFileSync = vi.fn(() => { throw new Error("not found"); }); @@ -184,7 +185,7 @@ describe("findFfBinary", () => { vi.doMock("node:child_process", () => ({ execFileSync, default: { execFileSync } })); vi.doMock("node:fs", () => { const mocked = { - existsSync: () => false, + existsSync: (candidate: unknown) => installed && candidate === "/opt/homebrew/bin/ffmpeg", accessSync: () => { throw new Error("not executable"); }, @@ -192,14 +193,12 @@ describe("findFfBinary", () => { }; return { ...mocked, default: mocked }; }); - const { findFfBinary, clearFfBinaryLookupCache } = await importFresh(); - - expect(findFfBinary("ffmpeg")).toBeUndefined(); - expect(findFfBinary("ffmpeg")).toBeUndefined(); - expect(execFileSync).toHaveBeenCalledOnce(); + const { findFfBinary } = await importFresh(); - clearFfBinaryLookupCache(); expect(findFfBinary("ffmpeg")).toBeUndefined(); + installed = true; + expect(findFfBinary("ffmpeg")).toBe(resolve("/opt/homebrew/bin/ffmpeg")); + expect(findFfBinary("ffmpeg")).toBe(resolve("/opt/homebrew/bin/ffmpeg")); expect(execFileSync).toHaveBeenCalledTimes(2); }); }); diff --git a/packages/parsers/src/ffBinaries.ts b/packages/parsers/src/ffBinaries.ts index b9070af16bd..489e3f7d9e2 100644 --- a/packages/parsers/src/ffBinaries.ts +++ b/packages/parsers/src/ffBinaries.ts @@ -18,7 +18,7 @@ const ENV_BY_NAME: Record = { ffprobe: FFPROBE_PATH_ENV, }; -const pathLookupCache = new Map(); +const pathLookupCache = new Map(); function candidateFileName(candidate: string): string { return candidate.split(/[\\/]/).at(-1)?.toLowerCase() ?? candidate.toLowerCase(); @@ -102,7 +102,8 @@ function findInProjectLocalBin(name: FfBinaryName): string | undefined { } function lookupOnSystem(name: FfBinaryName): string | undefined { - if (pathLookupCache.has(name)) return pathLookupCache.get(name); + const cached = pathLookupCache.get(name); + if (cached) return cached; let found: string | undefined; if (process.platform === "win32") { // `where.exe` writes bytes in the active console code page, while Node @@ -125,7 +126,8 @@ function lookupOnSystem(name: FfBinaryName): string | undefined { } found ??= findInProjectLocalBin(name); found ??= findInCommonDirs(name); - const resolved = found ? resolve(found) : undefined; + if (!found) return undefined; + const resolved = resolve(found); pathLookupCache.set(name, resolved); return resolved; } @@ -144,10 +146,10 @@ export interface FindFfBinaryOptions { /** * Resolve an FFmpeg-family binary: env override first, then a native * current-directory/PATH scan on Windows or `which` plus PATH scan on Unix, - * then a project-local - * `.hyperframes/bin`, then well-known Unix install dirs. System lookups are - * cached per binary for the process lifetime; the env override is re-read on - * every call. + * then a project-local `.hyperframes/bin`, then well-known Unix install dirs. + * A found binary is cached for the process lifetime; a miss is not, so a + * long-running server sees an install made after it started. The env + * override is re-read on every call. */ export function findFfBinary( name: FfBinaryName, From ffa53eb55d1fc8d5e88094c575df8d7a69222bb7 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Sat, 10 Oct 2026 16:04:51 -0700 Subject: [PATCH 2/2] fix(parsers): remember a missing ffmpeg for a few seconds so per-file probes stay cheap --- packages/parsers/src/ffBinaries.test.ts | 11 +++++++++-- packages/parsers/src/ffBinaries.ts | 25 +++++++++++++++++++------ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/packages/parsers/src/ffBinaries.test.ts b/packages/parsers/src/ffBinaries.test.ts index bb4f794a4c6..3c2e9a42831 100644 --- a/packages/parsers/src/ffBinaries.test.ts +++ b/packages/parsers/src/ffBinaries.test.ts @@ -1,7 +1,7 @@ import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, onTestFinished, vi } from "vitest"; type FfBinariesModule = typeof import("./ffBinaries.js"); @@ -173,7 +173,11 @@ describe("findFfBinary", () => { expect(findFfBinary("ffmpeg")).toBe(projectBinary); }); - it("finds a binary installed in a common dir after an earlier miss, then caches the hit", async () => { + it("finds a binary installed in a common dir shortly after an earlier miss, then caches the hit", async () => { + vi.useFakeTimers(); + onTestFinished(() => { + vi.useRealTimers(); + }); delete process.env.HYPERFRAMES_FFMPEG_PATH; Object.defineProperty(process, "platform", { value: "linux", configurable: true }); process.env.PATH = ""; @@ -197,6 +201,9 @@ describe("findFfBinary", () => { expect(findFfBinary("ffmpeg")).toBeUndefined(); installed = true; + expect(findFfBinary("ffmpeg")).toBeUndefined(); + expect(execFileSync).toHaveBeenCalledTimes(1); + vi.advanceTimersByTime(5_000); expect(findFfBinary("ffmpeg")).toBe(resolve("/opt/homebrew/bin/ffmpeg")); expect(findFfBinary("ffmpeg")).toBe(resolve("/opt/homebrew/bin/ffmpeg")); expect(execFileSync).toHaveBeenCalledTimes(2); diff --git a/packages/parsers/src/ffBinaries.ts b/packages/parsers/src/ffBinaries.ts index 489e3f7d9e2..4b64d8f6dfd 100644 --- a/packages/parsers/src/ffBinaries.ts +++ b/packages/parsers/src/ffBinaries.ts @@ -19,6 +19,9 @@ const ENV_BY_NAME: Record = { }; const pathLookupCache = new Map(); +// A miss is trusted briefly: callers probe per media file, and an install still shows up within seconds. +const MISS_TTL_MS = 5_000; +const lastMissAt = new Map(); function candidateFileName(candidate: string): string { return candidate.split(/[\\/]/).at(-1)?.toLowerCase() ?? candidate.toLowerCase(); @@ -101,9 +104,7 @@ function findInProjectLocalBin(name: FfBinaryName): string | undefined { return existsSync(candidate) ? candidate : undefined; } -function lookupOnSystem(name: FfBinaryName): string | undefined { - const cached = pathLookupCache.get(name); - if (cached) return cached; +function searchSystem(name: FfBinaryName): string | undefined { let found: string | undefined; if (process.platform === "win32") { // `where.exe` writes bytes in the active console code page, while Node @@ -126,7 +127,18 @@ function lookupOnSystem(name: FfBinaryName): string | undefined { } found ??= findInProjectLocalBin(name); found ??= findInCommonDirs(name); - if (!found) return undefined; + return found; +} + +function lookupOnSystem(name: FfBinaryName): string | undefined { + const cached = pathLookupCache.get(name); + if (cached) return cached; + if (Date.now() - (lastMissAt.get(name) ?? -Infinity) < MISS_TTL_MS) return undefined; + const found = searchSystem(name); + if (!found) { + lastMissAt.set(name, Date.now()); + return undefined; + } const resolved = resolve(found); pathLookupCache.set(name, resolved); return resolved; @@ -147,8 +159,8 @@ export interface FindFfBinaryOptions { * Resolve an FFmpeg-family binary: env override first, then a native * current-directory/PATH scan on Windows or `which` plus PATH scan on Unix, * then a project-local `.hyperframes/bin`, then well-known Unix install dirs. - * A found binary is cached for the process lifetime; a miss is not, so a - * long-running server sees an install made after it started. The env + * A found binary is cached for the process lifetime; a miss only for a few + * seconds, so a long-running server sees an install made after it started. The env * override is re-read on every call. */ export function findFfBinary( @@ -166,4 +178,5 @@ export function findFfBinary( /** Test hook: drop cached system lookups so resolution can be re-exercised. */ export function clearFfBinaryLookupCache(): void { pathLookupCache.clear(); + lastMissAt.clear(); }