export const DEMUXERS = ["mov", "matroska", "mpegts", "wav", "mp3", "ogg", "flac", "image2", "png_pipe", "jpeg_pipe", "rawvideo", "s16le"] as const; export type Demuxer = typeof DEMUXERS[number]; export interface MediaCommand { readonly binary: "ffmpeg" | "ffprobe"; readonly argv: readonly string[] } const issued = new WeakSet(); function command(binary: MediaCommand["binary"], argv: string[]): MediaCommand { const result = Object.freeze({ binary, argv: Object.freeze(argv) }); issued.add(result); return result; } export function assertMediaCommand(value: MediaCommand): void { if (!issued.has(value)) throw new Error("MediaCommandBuilder command required"); } export function assertAssetId(id: string): void { if (!/^(?:[0-9a-f]{64}|[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/.test(id)) throw new Error("invalid server asset id"); } function demuxer(value: string): void { if (!(DEMUXERS as readonly string[]).includes(value)) throw new Error("demuxer denied"); } export function assetPath(id: string): string { assertAssetId(id); return `file:/upload/${id}`; } const FILTERS = new Set(["scale", "pad", "setsar", "crop", "color", "split", "null", "overlay", "xstack", "format", "anullsrc", "aformat", "volume", "amix", "asplit", "noise", "fps", "aresample", "asetpts", "setpts"]); function validateFilters(text: string): void { if (text.length > 65536 || /(?:movie|amovie|subtitles|drawtext|zmq|azmq)\s*=|https?:|file:|concat:/i.test(text)) throw new Error("filter IO denied"); let quote = false, escaped = false, start = 0; const validate = (part: string): void => { const name = /^(?:\[[a-zA-Z0-9_:]+\])*([a-z][a-z0-9_]*)(?:=|\[|$)/.exec(part)?.[1]; if (!name || !FILTERS.has(name)) throw new Error("filter IO denied"); }; for (let i = 0; i < text.length; i++) { const c = text[i]; if (escaped) { escaped = false; continue; } if (c === "\\") { escaped = true; continue; } if (c === "'") quote = !quote; if (!quote && (c === "," || c === ";")) { validate(text.slice(start, i)); start = i + 1; } } if (quote || escaped) throw new Error("filter IO denied"); validate(text.slice(start)); } /** No original filename, arbitrary URL, shell string or extra-arguments escape hatch. */ export class MediaCommandBuilder { probe(assetId: string, format: Demuxer): MediaCommand { demuxer(format); return command("ffprobe", ["-hide_banner", "-v", "error", "-protocol_whitelist", "file", "-f", format, "-show_entries", "format=duration,size,format_name:stream=index,codec_type,codec_name,width,height,sample_rate,channels", "-of", "json", "-i", assetPath(assetId)]); } transcode(assetId: string, format: Demuxer, outputId: string, dimensions: { width: number; height: number }): MediaCommand { demuxer(format); assertAssetId(outputId); for (const v of [dimensions.width, dimensions.height]) { if (!Number.isSafeInteger(v) || v < 2 || v > 4096 || v % 2) throw new Error("invalid dimensions"); } return command("ffmpeg", ["-hide_banner", "-nostdin", "-loglevel", "error", "-threads", "2", "-filter_threads", "1", "-protocol_whitelist", "file", "-f", format, "-i", assetPath(assetId), "-vf", `scale=${dimensions.width}:${dimensions.height}`, "-c:v", "libx264", "-preset", "veryfast", "-c:a", "aac", "-f", "mp4", "-y", `file:/scratch/${outputId}.mp4`]); } /** Adapter ONLY for argv from the worker's existing typed graph builders. */ supervisor(binary: MediaCommand["binary"], args: readonly string[], scratch: string): MediaCommand { if (binary !== "ffmpeg") throw new Error("use the typed probe wrapper"); const flags = new Set(["-hide_banner", "-nostdin", "-re", "-y", "-an", "-vn", "-shortest"]); const values = new Set(["-loglevel", "-threads", "-threads:v", "-filter_threads", "-filter_complex_threads", "-thread_queue_size", "-probesize", "-analyzeduration", "-f", "-pix_fmt", "-s", "-framerate", "-ar", "-ac", "-fflags", "-i", "-vf", "-af", "-filter_complex", "-map", "-t", "-c", "-c:v", "-c:a", "-preset", "-tune", "-profile:v", "-level:v", "-b:v", "-b:a", "-maxrate", "-bufsize", "-g", "-keyint_min", "-sc_threshold", "-fps_mode", "-flush_packets", "-flags:v", "-flags:a", "-frames:v", "-q:v", "-progress", "-output_ts_offset", "-muxdelay", "-muxpreload", "-loop", "-mpegts_flags", "-hls_time", "-hls_list_size", "-hls_segment_type", "-hls_fmp4_init_filename", "-hls_flags", "-hls_segment_filename", "-protocol_whitelist"]); const argv: string[] = []; let format: string | undefined; const mapPath = (value: string): string => { const raw = value.startsWith("file:") ? value.slice(5) : value; if (!raw.startsWith(`${scratch}/`) || raw.split("/").includes("..")) throw new Error("media path denied"); const suffix = raw.slice(scratch.length); if (!/^\/[A-Za-z0-9_./%\-]+$/.test(suffix)) throw new Error("media path denied"); return `file:/scratch${suffix}`; }; for (let i = 0; i < args.length; i++) { const key = args[i]!; if (flags.has(key)) { argv.push(key); continue; } if (!key.startsWith("-") || key === "-") { if (key === "-" && format === "null") argv.push("pipe:1"); else if (format === "tee" && /^\[f=(?:rawvideo|s16le):onfail=ignore\]pipe:\d{1,3}\|\[f=null\]-$/.test(key)) argv.push(key); else argv.push(/^pipe:\d{1,3}$/.test(key) ? key : mapPath(key)); format = undefined; continue; } if (!values.has(key)) throw new Error("media option denied"); let value = args[++i]; if (value === undefined || /[\u0000-\u001f\u007f]/.test(value)) throw new Error("media option denied"); if (key === "-protocol_whitelist") continue; // Replaced per input below. if (key === "-f") format = value; if (["-vf", "-af", "-filter_complex"].includes(key)) validateFilters(value); if (key === "-i") { if (/^pipe:\d{1,3}$/.test(value)) { if (!format) { format = "mpegts"; argv.push("-f", format); } demuxer(format); argv.push("-protocol_whitelist", "pipe"); } else if (format === "lavfi") { if (!/^(?:testsrc2|smptehdbars|mandelbrot|rgbtestsrc|sine|color|anullsrc)=/.test(value) || /[\/;]|(?:movie|http|file|concat):/i.test(value)) throw new Error("synthetic input denied"); argv.push("-protocol_whitelist", "file"); } else { const path = mapPath(value); if (!/\/[0-9a-f]{64}\.png$/.test(path)) throw new Error("asset input denied"); argv.push("-protocol_whitelist", "file", "-f", "png_pipe"); value = path; } format = undefined; } else if (key === "-hls_segment_filename") { argv.push(key, mapPath(value)); continue; } else if (key === "-progress" && !/^pipe:\d{1,3}$/.test(value)) throw new Error("progress endpoint denied"); else if (key === "-hls_fmp4_init_filename" && value !== "init.mp4") throw new Error("init filename denied"); else if (!["-i", "-vf", "-af", "-filter_complex", "-framerate"].includes(key) && /(?:https?|rtmp|srt|tcp|udp|file|concat):|\//i.test(value)) throw new Error("media protocol denied"); argv.push(key, value); } return command(binary, argv); } }