Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
# `binary`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module owns ffmpeg-binary discovery for `@suaveplan/audio-codec-ffmpeg-native`.
It implements a fixed four-step precedence chain — caller-supplied provider,
explicit absolute path, dynamically imported `@ffmpeg-installer/ffmpeg`
optional dependency, then PATH-based discovery — and memoises successful
resolutions by options-object identity so that high-traffic decode/encode
pipelines pay the resolution cost exactly once per shared options bundle.
The resolver throws `AudioCodecFfmpegNativeError("AUDIO_CODEC_FFMPEG_NATIVE.BINARY_MISSING")`
when every step fails, which lets the codec surface an actionable error to the
caller instead of waiting for a downstream `ENOENT` from `child_process.spawn`.
## Features
- Strict precedence: provider → explicit path → installer → PATH lookup.
- Promise-returning provider supported (sync providers also accepted).
- Dynamic `import("@ffmpeg-installer/ffmpeg")` so the optional dependency is
never a hard requirement at module load.
- Test-injectable `installerLoader` and `pathLookup` hooks decouple resolution
from real filesystem state.
- `BINARY_MISSING` error code on full-chain failure with no leaked stack from
the underlying optional-dep failure.
- WeakMap-keyed memoisation that automatically GCs entries when the caller
drops their options reference.
## Usage: Basic
```ts
import { resolveBinary } from "@suaveplan/audio-codec-ffmpeg-native/binary/binary";
const { path, source } = await resolveBinary();
console.log(`Using ffmpeg at ${path} via ${source}`);
```
## Usage: Advanced
```ts
import { resolveBinary } from "@suaveplan/audio-codec-ffmpeg-native/binary/binary";
const opts = {
binaryProvider: async () => process.env.FFMPEG_PATH ?? "",
binaryPath: "/usr/local/bin/ffmpeg",
installerLoader: async () => ({ path: "/opt/ffmpeg" }),
pathLookup: () => "/usr/bin/ffmpeg",
} as const;
const first = await resolveBinary(opts);
const second = await resolveBinary(opts); // memoised — no re-resolution.
```
## API Reference
- `resolveBinary(opts?): Promise<ResolvedBinary>` — returns `{ path, source }`.
- `BinaryResolverOptions` — `binaryProvider`, `binaryPath`, `installerLoader`,
`pathLookup` (all optional; first match wins per the precedence chain).
- `ResolvedBinary.source` — `"provider" | "explicit" | "installer" | "path"`.
## Implementation Notes
The four-step chain is hard-coded — there is no plugin slot — because deviation
in precedence would silently change which binary actually runs on production
servers. Empty strings are treated as "miss" so callers can write `() => ""`
in a provider to opt into the next strategy without throwing.
The installer is imported via dynamic `import()` because static imports would
make `@ffmpeg-installer/ffmpeg` a hard dependency rather than the optional one
the spec requires. Any failure inside the installer load (missing module,
platform mismatch, exec-bit stripped) is swallowed and the chain advances —
the only surfaced error is `BINARY_MISSING` after the entire chain misses.
Memoisation uses a `WeakMap` keyed by the options object reference; callers
who want forced re-resolution pass a fresh options bundle.

View File

@@ -0,0 +1,157 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { AudioCodecFfmpegNativeError } from "../error-codes/error-codes.js";
import { extractInstallerPath, resolveBinary } from "./binary.js";
describe("binary/resolveBinary", () => {
it("uses binaryProvider when it returns non-empty", {
timeout: 5000,
}, async () => {
const res = await resolveBinary({
binaryProvider: () => "/from/provider",
installerLoader: async () => ({ path: "/from/installer" }),
pathLookup: () => "/from/path",
});
expect(res).toEqual({ path: "/from/provider", source: "provider" });
});
it("supports async binaryProvider", { timeout: 5000 }, async () => {
const res = await resolveBinary({
binaryProvider: async () => "/async/provider",
installerLoader: async () => null,
});
expect(res).toEqual({ path: "/async/provider", source: "provider" });
});
it("falls through provider empty string to explicit binaryPath", {
timeout: 5000,
}, async () => {
const res = await resolveBinary({
binaryProvider: () => "",
binaryPath: "/explicit",
installerLoader: async () => null,
});
expect(res).toEqual({ path: "/explicit", source: "explicit" });
});
it("uses installer when provider+explicit absent", {
timeout: 5000,
}, async () => {
const res = await resolveBinary({
installerLoader: async () => ({ path: "/installer" }),
});
expect(res).toEqual({ path: "/installer", source: "installer" });
});
it("falls through installer throw to pathLookup", {
timeout: 5000,
}, async () => {
const res = await resolveBinary({
installerLoader: async () => {
throw new Error("missing");
},
pathLookup: () => "/from/path",
});
expect(res).toEqual({ path: "/from/path", source: "path" });
});
it("falls through installer null to pathLookup", {
timeout: 5000,
}, async () => {
const res = await resolveBinary({
installerLoader: async () => null,
pathLookup: () => "/from/path",
});
expect(res).toEqual({ path: "/from/path", source: "path" });
});
it("falls through installer empty path to pathLookup", {
timeout: 5000,
}, async () => {
const res = await resolveBinary({
installerLoader: async () => ({ path: "" }),
pathLookup: () => "/from/path",
});
expect(res).toEqual({ path: "/from/path", source: "path" });
});
it("falls through empty pathLookup to BINARY_MISSING", {
timeout: 5000,
}, async () => {
await expect(
resolveBinary({
installerLoader: async () => null,
pathLookup: () => undefined,
})
).rejects.toBeInstanceOf(AudioCodecFfmpegNativeError);
});
it("throws BINARY_MISSING when every step misses", {
timeout: 5000,
}, async () => {
await expect(
resolveBinary({
installerLoader: async () => null,
})
).rejects.toMatchObject({
code: "AUDIO_CODEC_FFMPEG_NATIVE.BINARY_MISSING",
});
});
it("memoises by options reference", { timeout: 5000 }, async () => {
let calls = 0;
const opts = {
binaryProvider: () => {
calls += 1;
return "/cached";
},
};
await resolveBinary(opts);
await resolveBinary(opts);
await resolveBinary(opts);
expect(calls).toBe(1);
});
it("default installer loader resolves real @ffmpeg-installer/ffmpeg", {
timeout: 5000,
}, async () => {
// Real @ffmpeg-installer/ffmpeg IS installed via catalog:audio in
// this monorepo, so the default loader returns its bundled path.
const res = await resolveBinary({});
expect(res.source === "installer" || res.source === "path").toBe(true);
expect(res.path.length).toBeGreaterThan(0);
});
it("resolveBinary supports no-argument invocation", {
timeout: 5000,
}, async () => {
// Covers the `opts: BinaryResolverOptions = {}` default-argument
// branch — without it we fall back to the default installer.
const res = await resolveBinary();
expect(res.path.length).toBeGreaterThan(0);
});
});
describe("binary/extractInstallerPath", () => {
it("returns { path } from default-exported shape", {
timeout: 5000,
}, () => {
expect(extractInstallerPath({ default: { path: "/d" } })).toEqual({
path: "/d",
});
});
it("falls back to named `path` when default has no `path`", {
timeout: 5000,
}, () => {
expect(extractInstallerPath({ path: "/named" })).toEqual({
path: "/named",
});
});
it("returns null when neither default nor named path is present", {
timeout: 5000,
}, () => {
expect(extractInstallerPath({})).toBeNull();
expect(extractInstallerPath({ default: {} })).toBeNull();
});
});

View File

@@ -0,0 +1,95 @@
import {
AUDIO_CODEC_FFMPEG_NATIVE_CODES,
AudioCodecFfmpegNativeError,
} from "../error-codes/error-codes.js";
import type { BinaryResolverOptions, ResolvedBinary } from "./binary.types.js";
const cache = new WeakMap<BinaryResolverOptions, ResolvedBinary>();
/**
* Resolve the ffmpeg binary path using the 4-step precedence order documented
* on `BinaryResolverOptions`. Throws `BINARY_MISSING` if every strategy fails.
*
* The result is memoised per `BinaryResolverOptions` reference — pass the same
* options object to benefit from caching across many decode/encode calls.
*/
export async function resolveBinary(
opts: BinaryResolverOptions = {}
): Promise<ResolvedBinary> {
const hit = cache.get(opts);
if (hit !== undefined) return hit;
const resolved = await tryResolve(opts);
if (resolved === null) {
throw new AudioCodecFfmpegNativeError(
AUDIO_CODEC_FFMPEG_NATIVE_CODES.BINARY_MISSING,
"ffmpeg-native.binary.missing",
{}
);
}
cache.set(opts, resolved);
return resolved;
}
async function tryResolve(
opts: BinaryResolverOptions
): Promise<ResolvedBinary | null> {
if (opts.binaryProvider !== undefined) {
const path = await Promise.resolve(opts.binaryProvider());
if (isNonEmpty(path)) return { path, source: "provider" };
}
if (isNonEmpty(opts.binaryPath)) {
return { path: opts.binaryPath, source: "explicit" };
}
const installerPath = await tryInstaller(opts.installerLoader);
if (installerPath !== null) {
return { path: installerPath, source: "installer" };
}
if (opts.pathLookup !== undefined) {
const path = opts.pathLookup();
if (isNonEmpty(path)) return { path, source: "path" };
}
return null;
}
function isNonEmpty(value: string | undefined): value is string {
return typeof value === "string" && value.length > 0;
}
async function tryInstaller(
loader: BinaryResolverOptions["installerLoader"]
): Promise<string | null> {
const load = loader ?? defaultInstallerLoader;
try {
const mod = await load();
if (mod !== null && isNonEmpty(mod.path)) return mod.path;
} catch {
// Optional dependency not installed; fall through.
}
return null;
}
async function defaultInstallerLoader(): Promise<{
readonly path: string;
} | null> {
const mod = (await import("@ffmpeg-installer/ffmpeg")) as {
readonly default?: { readonly path?: string };
readonly path?: string;
};
return extractInstallerPath(mod);
}
/**
* Extract a usable `path` from a `@ffmpeg-installer/ffmpeg` module export
* shape. Handles both default-exported and named-exported `path` strings and
* returns `null` when neither is present. Exposed for unit-test coverage of
* the empty-export branch which production traffic cannot reach (the installed
* module always exports a path on supported platforms).
*/
export function extractInstallerPath(mod: {
readonly default?: { readonly path?: string };
readonly path?: string;
}): { readonly path: string } | null {
const path = mod.default?.path ?? mod.path;
return path !== undefined ? { path } : null;
}

View File

@@ -0,0 +1,30 @@
/**
* Options accepted by `resolveBinary`. Resolution order is:
*
* 1. `binaryProvider()` — caller-supplied factory (highest precedence).
* 2. `binaryPath` — explicit absolute path.
* 3. Dynamic `@ffmpeg-installer/ffmpeg` import.
* 4. PATH-based discovery via `node:child_process` `which`/`where`.
*
* The first non-empty result that is also `executable` wins.
*/
export interface BinaryResolverOptions {
readonly binaryPath?: string;
readonly binaryProvider?: () => string | Promise<string>;
/**
* Injected to make the PATH fallback testable. Defaults to spawning
* `which ffmpeg` (POSIX) — left undefined disables PATH discovery, which
* is the safest server-tier default.
*/
readonly pathLookup?: () => string | undefined;
/**
* Injected installer loader. Defaults to dynamic import of the optional
* `@ffmpeg-installer/ffmpeg` dependency.
*/
readonly installerLoader?: () => Promise<{ readonly path: string } | null>;
}
export interface ResolvedBinary {
readonly path: string;
readonly source: "provider" | "explicit" | "installer" | "path";
}

View File

@@ -0,0 +1,2 @@
export { resolveBinary } from "./binary.js";
export type { BinaryResolverOptions, ResolvedBinary } from "./binary.types.js";