/** * The ffmpeg surface av-club exposes, as data — the same shape as the sharp vocabulary. * * Options are spelled the way ffmpeg spells them (`c:v`, `crf`, `pix_fmt`), so ffmpeg's own * documentation is the reference. What is NOT here is anything that names a file: no `-i`, no output * path, no protocol. See docs/SECURITY.md — that single restriction is what makes the rest of this * safe to open up. */ export type FfmpegParam = | { kind: "int"; min: number; max: number; note?: string } | { kind: "number"; min: number; max: number; note?: string } | { kind: "enum"; values: readonly string[]; note?: string } | { kind: "bitrate"; note?: string }; /** Options that go BEFORE `-i`: seeking and trimming, which are far cheaper applied to the input. */ export const INPUT_OPTIONS: Record = { ss: { kind: "number", min: 0, max: 86_400, note: "Start at this many seconds." }, t: { kind: "number", min: 0.001, max: 86_400, note: "Take this many seconds." }, to: { kind: "number", min: 0, max: 86_400, note: "Stop at this timestamp." }, }; /** * Output options. * * The encoder list is short on purpose: these are the codecs a browser plays. An encoder ffmpeg * happens to have compiled in is not automatically something worth publishing. */ export const OUTPUT_OPTIONS: Record = { "c:v": { kind: "enum", values: ["libx264", "libvpx-vp9", "libaom-av1", "copy"], note: "Video encoder. libx264 is the one every browser plays." }, "c:a": { kind: "enum", values: ["aac", "libopus", "copy"], note: "Audio encoder." }, crf: { kind: "int", min: 0, max: 63, note: "Quality. Lower is better and bigger; 23 is a sane default for x264." }, preset: { kind: "enum", values: ["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"], note: "Encoder effort. Slower is smaller at the same quality." }, tune: { kind: "enum", values: ["film", "animation", "grain", "stillimage", "fastdecode", "zerolatency"] }, "profile:v": { kind: "enum", values: ["baseline", "main", "high"], note: "`high` is fine everywhere current." }, level: { kind: "enum", values: ["3.0", "3.1", "4.0", "4.1", "4.2", "5.0", "5.1"] }, pix_fmt: { kind: "enum", values: ["yuv420p", "yuv422p", "yuv444p"], note: "yuv420p, or Safari and QuickTime will not play it." }, "b:v": { kind: "bitrate", note: "Target video bitrate, e.g. 2M." }, "b:a": { kind: "bitrate", note: "Target audio bitrate, e.g. 128k." }, maxrate: { kind: "bitrate" }, bufsize: { kind: "bitrate" }, r: { kind: "number", min: 1, max: 120, note: "Output frame rate." }, g: { kind: "int", min: 1, max: 600, note: "Keyframe interval." }, ar: { kind: "enum", values: ["22050", "44100", "48000"], note: "Audio sample rate." }, ac: { kind: "int", min: 1, max: 2 }, "frames:v": { kind: "int", min: 1, max: 10_000, note: "Stop after this many frames — 1 gives a poster." }, movflags: { kind: "enum", values: ["+faststart"], note: "Moves the index to the front so playback can start before the file has arrived. Always want this for web." }, an: { kind: "enum", values: ["1"], note: "Drop audio entirely." }, }; /** Output containers. `image2` is how a single frame comes out as a still. */ export const FORMATS: Record = { mp4: { extension: "mp4", describe: "H.264/AAC in MP4 — plays everywhere." }, webm: { extension: "webm", describe: "VP9/Opus. Smaller, but not on older Safari." }, mp3: { extension: "mp3", describe: "Audio only." }, image2: { extension: "jpg", describe: "A single frame, for poster images." }, }; /** * Filters, default-deny. * * ffmpeg's filter graph is a language, and most of it is arithmetic on pixels. A small, specific part * of it reads files or loads native code — `movie`, `subtitles`, `frei0r`, `ladspa` and relatives — * and those are not in this list, so they are refused. A filter nobody has classified is refused too: * a blocklist would silently admit whatever the next ffmpeg release adds. */ export const VIDEO_FILTERS: Record> = { scale: { w: { kind: "int", min: -2, max: 8192, note: "-2 keeps the aspect ratio and an even number of pixels, which h264 requires." }, h: { kind: "int", min: -2, max: 8192 }, }, crop: { w: { kind: "int", min: 1, max: 8192 }, h: { kind: "int", min: 1, max: 8192 }, x: { kind: "int", min: 0, max: 8192 }, y: { kind: "int", min: 0, max: 8192 }, }, fps: { fps: { kind: "number", min: 1, max: 120 } }, format: { pix_fmts: { kind: "enum", values: ["yuv420p", "yuv422p", "yuv444p", "rgb24"] } }, transpose: { dir: { kind: "enum", values: ["0", "1", "2", "3"] } }, hflip: {}, vflip: {}, eq: { brightness: { kind: "number", min: -1, max: 1 }, contrast: { kind: "number", min: -1000, max: 1000 }, saturation: { kind: "number", min: 0, max: 3 }, gamma: { kind: "number", min: 0.1, max: 10 }, }, unsharp: { luma_msize_x: { kind: "int", min: 3, max: 23 }, luma_msize_y: { kind: "int", min: 3, max: 23 }, luma_amount: { kind: "number", min: -2, max: 5 }, }, }; export const AUDIO_FILTERS: Record> = { volume: { volume: { kind: "number", min: 0, max: 10 } }, aresample: { osr: { kind: "enum", values: ["22050", "44100", "48000"] } }, loudnorm: { I: { kind: "number", min: -70, max: -5, note: "Target loudness in LUFS. -16 suits the web." }, TP: { kind: "number", min: -9, max: 0 }, }, }; /** * Named presets, so a caller does not have to know encoder settings to get a web-playable file. * * This is where the media knowledge lives — the whole reason av-club is a separate repo. A consumer * says what the media is FOR; what that means in flags is decided here and can be revised in one * place for every site that uses it. */ export const PRESETS: Record; filters?: { v?: { name: string; args: Record }[] } }> = { "web-1080": { describe: "H.264 up to 1080p. The safe default — plays in every browser.", format: "mp4", options: { "c:v": "libx264", crf: 23, preset: "medium", "profile:v": "high", pix_fmt: "yuv420p", "c:a": "aac", "b:a": "128k", movflags: "+faststart" }, filters: { v: [{ name: "scale", args: { w: -2, h: 1080 } }] }, }, "web-720": { describe: "H.264 up to 720p.", format: "mp4", options: { "c:v": "libx264", crf: 23, preset: "medium", "profile:v": "high", pix_fmt: "yuv420p", "c:a": "aac", "b:a": "128k", movflags: "+faststart" }, filters: { v: [{ name: "scale", args: { w: -2, h: 720 } }] }, }, "web-480": { describe: "H.264 up to 480p, for slow connections.", format: "mp4", options: { "c:v": "libx264", crf: 26, preset: "medium", "profile:v": "main", pix_fmt: "yuv420p", "c:a": "aac", "b:a": "96k", movflags: "+faststart" }, filters: { v: [{ name: "scale", args: { w: -2, h: 480 } }] }, }, poster: { describe: "One frame as a JPEG, for a video's poster image.", format: "image2", options: { "frames:v": 1 }, }, };