import { spawn } from "node:child_process"; const FFMPEG = process.env.FFMPEG_PATH ?? "ffmpeg"; const FFPROBE = process.env.FFPROBE_PATH ?? "ffprobe"; /** Run ffmpeg with the given args; rejects with the tail of stderr on non-zero exit. */ export function runFfmpeg(args: string[]): Promise { return new Promise((resolve, reject) => { const proc = spawn(FFMPEG, args, { stdio: ["ignore", "ignore", "pipe"] }); let stderr = ""; proc.stderr.on("data", (d) => { stderr += d.toString(); }); proc.on("error", (err) => reject(new Error(`Failed to spawn ffmpeg (${FFMPEG}): ${err.message}`)) ); proc.on("close", (code) => code === 0 ? resolve() : reject(new Error(`ffmpeg exited ${code}: ${stderr.slice(-600)}`)) ); }); } /** Probe an audio file's duration in whole seconds, or null if ffprobe is unavailable. */ export function ffprobeDuration(file: string): Promise { return new Promise((resolve) => { const proc = spawn(FFPROBE, [ "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", file, ]); let out = ""; proc.stdout.on("data", (d) => { out += d.toString(); }); proc.on("error", () => resolve(null)); proc.on("close", () => { const n = parseFloat(out.trim()); resolve(Number.isFinite(n) ? Math.round(n) : null); }); }); }