Files
2026-05-26 22:46:00 +02:00

60 lines
1.9 KiB
TypeScript

import "server-only";
import { spawn } from "node:child_process";
const cache = new Map<string, number>();
const PROBE_TIMEOUT_MS = 10_000;
/**
* Probe a video file's duration in seconds via ffprobe. Cached per-path
* for the lifetime of the process — files don't change duration on us.
* Returns null if ffprobe fails or returns garbage.
*
* Caps the probe at PROBE_TIMEOUT_MS and ties to an optional AbortSignal
* so a hung ffprobe (network mount, weird codec, dead disk) can't leave
* the request awaiting forever or zombie the subprocess.
*/
export async function probeDuration(abs: string, signal?: AbortSignal): Promise<number | null> {
const cached = cache.get(abs);
if (cached !== undefined) return cached;
return new Promise((resolve) => {
const proc = spawn("ffprobe", [
"-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
abs,
]);
let out = "";
let settled = false;
const settle = (n: number | null) => {
if (settled) return;
settled = true;
if (timeoutId) clearTimeout(timeoutId);
if (signal && abortHandler) signal.removeEventListener("abort", abortHandler);
if (n != null && Number.isFinite(n) && n > 0) {
cache.set(abs, n);
resolve(n);
} else {
resolve(null);
}
};
const kill = () => {
try { proc.kill("SIGKILL"); } catch { /* ignore */ }
settle(null);
};
const timeoutId = setTimeout(kill, PROBE_TIMEOUT_MS);
const abortHandler = signal ? () => kill() : null;
if (signal && abortHandler) {
if (signal.aborted) { kill(); return; }
signal.addEventListener("abort", abortHandler, { once: true });
}
proc.stdout?.on("data", (d) => { out += d.toString(); });
proc.on("close", () => settle(Number(out.trim())));
proc.on("error", () => settle(null));
});
}