refactor(media): trim unused ffmpeg wrapper options

Binary paths, the ffmpeg version, the probe's container name and a
configurable stall timeout had no caller; detection is cached per process,
so every helper looks the binaries up itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
This commit is contained in:
Claude 2026-10-03 13:51:44 +00:00
parent 44702e3770
commit f14d91d24a
No known key found for this signature in database
3 changed files with 6 additions and 15 deletions

View File

@ -60,7 +60,6 @@ describe("probe parsing", () => {
); );
expect(probe).toEqual({ expect(probe).toEqual({
durationMs: 12_345, durationMs: 12_345,
formatName: "mov,mp4,m4a,3gp,3g2,mj2",
video: { video: {
codec: "h264", codec: "h264",
width: 1080, width: 1080,

View File

@ -13,12 +13,10 @@ import { delimiter, join } from "node:path";
export interface FfmpegInfo { export interface FfmpegInfo {
ffmpegPath: string; ffmpegPath: string;
ffprobePath: string; ffprobePath: string;
version: string;
} }
export interface MediaProbe { export interface MediaProbe {
durationMs: number; durationMs: number;
formatName: string;
/** Width and height as displayed: swapped when the stream is rotated by 90 or 270 degrees. */ /** Width and height as displayed: swapped when the stream is rotated by 90 or 270 degrees. */
video?: { video?: {
codec: string; codec: string;
@ -50,8 +48,6 @@ export interface RunFfmpegOptions {
maxOutputBytes?: number; maxOutputBytes?: number;
/** Files removed when the run fails or is aborted. */ /** Files removed when the run fails or is aborted. */
outputs?: readonly string[]; outputs?: readonly string[];
/** Kills ffmpeg when it reports no progress for this long. */
stallTimeoutMs?: number;
} }
export class FfmpegError extends Error { export class FfmpegError extends Error {
@ -65,7 +61,8 @@ export class FfmpegError extends Error {
} }
} }
const DEFAULT_STALL_TIMEOUT_MS = 2 * 60_000; /** ffmpeg reports progress twice a second while it works; silence this long means a hang. */
const STALL_TIMEOUT_MS = 2 * 60_000;
const PROBE_TIMEOUT_MS = 60_000; const PROBE_TIMEOUT_MS = 60_000;
const STDERR_TAIL_BYTES = 8 * 1024; const STDERR_TAIL_BYTES = 8 * 1024;
const KILL_GRACE_MS = 5_000; const KILL_GRACE_MS = 5_000;
@ -101,9 +98,7 @@ async function locateFfmpeg(): Promise<FfmpegInfo | null> {
capture(ffmpegPath, ["-hide_banner", "-version"], { timeoutMs: 10_000 }), capture(ffmpegPath, ["-hide_banner", "-version"], { timeoutMs: 10_000 }),
capture(ffprobePath, ["-hide_banner", "-version"], { timeoutMs: 10_000 }), capture(ffprobePath, ["-hide_banner", "-version"], { timeoutMs: 10_000 }),
]); ]);
if (ffmpeg.code !== 0 || ffprobe.code !== 0) return null; return ffmpeg.code === 0 && ffprobe.code === 0 ? { ffmpegPath, ffprobePath } : null;
const version = /version (\S+)/.exec(ffmpeg.stdout)?.[1] ?? "unknown";
return { ffmpegPath, ffprobePath, version };
} catch { } catch {
return null; return null;
} }
@ -129,7 +124,7 @@ async function findExecutable(name: string): Promise<string | null> {
return null; return null;
} }
export async function requireFfmpeg(): Promise<FfmpegInfo> { async function requireFfmpeg(): Promise<FfmpegInfo> {
const info = await detectFfmpeg(); const info = await detectFfmpeg();
if (!info) throw new Error("ffmpeg and ffprobe are not installed on this server."); if (!info) throw new Error("ffmpeg and ffprobe are not installed on this server.");
return info; return info;
@ -223,7 +218,7 @@ interface ProbeStream {
/** Parses `ffprobe -print_format json -show_format -show_streams` output. */ /** Parses `ffprobe -print_format json -show_format -show_streams` output. */
export function parseProbeOutput(json: string): MediaProbe { export function parseProbeOutput(json: string): MediaProbe {
const data = JSON.parse(json) as { const data = JSON.parse(json) as {
format?: { duration?: string; format_name?: string }; format?: { duration?: string };
streams?: ProbeStream[]; streams?: ProbeStream[];
}; };
const streams = data.streams ?? []; const streams = data.streams ?? [];
@ -237,7 +232,6 @@ export function parseProbeOutput(json: string): MediaProbe {
Math.max(0, ...streams.map((stream) => positive(Number(stream.duration)) ?? 0)); Math.max(0, ...streams.map((stream) => positive(Number(stream.duration)) ?? 0));
return { return {
durationMs: Math.round(seconds * 1000), durationMs: Math.round(seconds * 1000),
formatName: data.format?.format_name ?? "",
...(video?.width && video.height ...(video?.width && video.height
? { video: probeVideo(video, video.width, video.height) } ? { video: probeVideo(video, video.width, video.height) }
: {}), : {}),
@ -385,12 +379,11 @@ function spawnFfmpeg(
setTimeout(() => finish(error), KILL_GRACE_MS).unref?.(); setTimeout(() => finish(error), KILL_GRACE_MS).unref?.();
}; };
const abort = () => kill(signal?.reason ?? new Error("aborted")); const abort = () => kill(signal?.reason ?? new Error("aborted"));
const stallTimeoutMs = options.stallTimeoutMs ?? DEFAULT_STALL_TIMEOUT_MS;
const armStallTimer = () => { const armStallTimer = () => {
clearTimeout(stallTimer); clearTimeout(stallTimer);
stallTimer = setTimeout( stallTimer = setTimeout(
() => kill(new FfmpegError("ffmpeg stopped making progress.", null, tail(stderr))), () => kill(new FfmpegError("ffmpeg stopped making progress.", null, tail(stderr))),
stallTimeoutMs, STALL_TIMEOUT_MS,
); );
stallTimer.unref?.(); stallTimer.unref?.();
}; };

View File

@ -13,7 +13,6 @@ function probe(
const { durationMs = 10_000, hasAudio = true, ...video } = overrides; const { durationMs = 10_000, hasAudio = true, ...video } = overrides;
return { return {
durationMs, durationMs,
formatName: "mov,mp4,m4a,3gp,3g2,mj2",
video: { video: {
codec: "h264", codec: "h264",
width: 1920, width: 1920,