import "server-only"; import { spawn } from "node:child_process"; const cache = new Map(); 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 { 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)); }); }