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:
parent
44702e3770
commit
f14d91d24a
@ -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,
|
||||||
|
|||||||
@ -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?.();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user