Disclosure first: I maintain the Rust crate ez-ffmpeg (no relation to the JavaScript “Ez FFmpeg” that once hit the HN front page). That is why the “who should not use it” section is the honest one.
What you type into a search box is never “how do I initialize an AVFormatContext” — it is “ffmpeg extract audio command.” You already know the command; what stops you in a Rust project is the mapping: which API does this command correspond to? For the last year this article would have been a hand-written lookup table — you find your row, you translate -c:v libx264 -crf 23 into builder calls one flag at a time. ez-ffmpeg 0.15’s cli feature does something less tedious and more interesting: paste the command string, and it either runs it in-process (from_cli) or translates it into compile-ready Rust (emit_rust_code).
The interesting part is not that it translates — it is the attitude with which it translates. It sorts every command into three classes: verified (safe to run), unverified (generates scaffolding code with a loud warning, and refuses to execute), and unrecognized (rejected on the spot with a token-anchored, typed error). It would rather refuse you clearly than “run, but come out subtly different from ffmpeg” — because that second failure mode is the one that destroys trust. This post is about that contract: how to use it, and why it is built this way.
from_cli: paste the command, run it in-process
Enable the cli feature (libav is still linked underneath, so FFmpeg 7.1–8.x must be installed):
[dependencies]
ez-ffmpeg = { version = "0.15", features = ["cli"] }
Enter fullscreen mode Exit fullscreen mode
A transcode command, pasted in and run — no subprocess:
use ez_ffmpeg::cli::from_cli;
fn main() -> Result<(), Box<dyn std::error::Error>> {
from_cli("ffmpeg -i input.mp4 -c:v libx264 -crf 28 -preset veryfast -c:a aac -y output.mp4")?
.start()?
.wait()?;
Ok(())
}
Enter fullscreen mode Exit fullscreen mode
I ran exactly this against a real audio+video clip: from_cli parses, classifies, builds the pipeline, and output.mp4 lands — ffprobe confirms H.264 video plus AAC audio, duration intact. The whole chain runs in your own process: no Command::new("ffmpeg"), no stderr scraping. The string form handles quotes and backslash escapes by POSIX rules; to avoid quoting entirely, pass argv directly with from_cli_args(&["-i", "input.mp4", ...]) — zero quoting ambiguity, the shape ffmpeg.wasm also chose.
One prerequisite up front: execution currently requires an FFmpeg 7.1 runtime. Link any other version (including 8.x) and it fails with UnverifiedRuntimeProfile before any I/O — deliberately, for reasons the “gates” section below covers.
emit_rust_code: or translate it to code
Sometimes you do not want it to run the command for you — you want the builder code, to extend and maintain. Same command:
use ez_ffmpeg::cli::emit_rust_code;
fn main() {
let code = emit_rust_code(
"ffmpeg -i in.mkv -c:v libx264 -crf 23 -preset fast -c:a aac -y out.mp4"
).unwrap();
println!("{code}");
}
Enter fullscreen mode Exit fullscreen mode
What prints is a complete, compile-ready program (real output — pinned byte-for-byte by the repository’s tests):
// Generated from an ffmpeg command by the ez-ffmpeg CLI-compat emitter.
// command: ffmpeg -i in.mkv -c:v libx264 -crf 23 -preset fast -c:a aac -y out.mp4
// dialect: ffmpeg 7.1 command line; manifest: r4; crate: ez-ffmpeg 0.15.0; cargo features: none required
// status: verified shape V1 (H.264/AAC transcode (crf + preset)) — verified by the manifest-driven semantic golden suite (oracle: Transcode) against the ffmpeg CLI; ...
use ez_ffmpeg::{FfmpegContext, Output};
fn main() -> Result<(), Box<dyn std::error::Error>> {
FfmpegContext::builder()
.input("in.mkv")
.output(
Output::from("out.mp4")
.set_video_codec("libx264") // -c:v libx264
.set_audio_codec("aac") // -c:a aac
.set_video_codec_opt("crf", "23") // -crf 23
.set_video_codec_opt("preset", "fast") // -preset fast
)
.build()?
.start()?
.wait()?;
Ok(())
}
Enter fullscreen mode Exit fullscreen mode
Read the header comments: the original command, the dialect version (ffmpeg 7.1), the manifest revision, the crate version, and the line that matters most — this shape’s verification status. This one is labeled verified shape V1: it passed a semantic golden suite that compares stream identity, codecs, dimensions, durations, and playlist topology against the actual ffmpeg CLI. The translation is not “looks about right”; it is “reconciled against the oracle.”
The contract: classify everything, approximate nothing
The design premise is stated plainly in the module docs: broad CLI compatibility is a non-goal. The ffmpeg CLI is ~14k lines of option machinery whose semantics shift with every release; chasing it wholesale produces the “runs, but subtly different” failures that destroy trust. So it does the opposite — every argv token must classify against a versioned compatibility manifest, or the entire command is rejected with a token-anchored typed diagnostic. Nothing is dropped, nothing is guessed. Three outcomes:
① Verified shape → runs. V1 above. Six verified shapes today (V1–V6): H.264/AAC transcode, re-encoded clip, audio extract, single-frame thumbnail, scaled transcode, single-rendition VOD HLS — each backed by a semantic golden. Six sounds small; what the number means is that every one of them is reconciled. Widening the subset costs one golden lane per shape, and the manifest grows release by release — breadth is bought with verification, never with approximation.
② Unverified shape → translated, never executed. For example, a transcode that names codecs but omits crf/preset. emit_rust_code still hands you code — but the header flips to a loud warning (real output):
// status: UNVERIFIED SCAFFOLDING — manifest entry U19 (audio+video codec selection only).
// This shape has no semantic golden. The code below compiles against the
// ez-ffmpeg builder API, but its behavior has NOT been checked against the
// ffmpeg CLI and must not be treated as a faithful translation. Review every
// call before use; in-process execution (from_cli / from_cli_args) refuses
// this shape.
Enter fullscreen mode Exit fullscreen mode
And from_cli refuses to run it — the error’s first line is command shape is not verified for execution, followed by the parsed options. It gives you scaffolding as a starting point, but says plainly: “I have not reconciled this one — review it yourself.”
③ Unrecognized → rejected on the spot, anchored to the token. A command with an option outside the subset — say ffmpeg -i in.mp4 -bogus 1 -y out.mp4:
unsupported option `-bogus` (token #2, output #0)
this option is not in the CLI-compat subset
Enter fullscreen mode Exit fullscreen mode
Not “ignore that option and run anyway” — the whole command is rejected, and it tells you which token broke it.
A few honest gates
Beyond the three-way sort, a handful of “refuse rather than fudge” decisions say the most about this layer’s character:
-
-yis mandatory. Omit it and you getmissing mandatory `-y`. The reasoning is concrete: without-ythe ffmpeg CLI prompts before overwriting, and this library always creates/truncates output — it cannot reproduce that prompt, so it requires you to make the overwrite explicit. -
No shell emulation. The string form does POSIX word-splitting only. A pipe in the command? A tokenize error anchored to the exact byte:
shell operator; pipes, redirects, command lists and subshells are not ffmpeg options— rejected, never emulated. It translates ffmpeg, not your shell. -
Strict AVOptions. CLI-initiated pipelines run in strict mode: an option no component consumed fails the whole command (fftools
check_avoptionsparity), instead of the default builder path’s warning. “That parameter silently did nothing” is not something you can miss here. -
Runtime-profile gate. Execution additionally requires a verified runtime profile of the linked FFmpeg (currently 7.1 only; 8.1 joins once its version-matched golden lane passes). Anything else fails with
UnverifiedRuntimeProfilebefore any I/O.
The command layout is fixed too: exactly one -i input and one output path, in canonical order; the -/stdin/stdout pseudo-paths are excluded — pipe I/O is process wiring, not part of the in-process subset.
Beyond the subset: the builder is the full surface
from_cli / emit_rust_code are an on-ramp, not the whole road. The subset covers the most common shapes; the full surface is the builder API itself. And migrating from the CLI to the builder needs only three conversion rules:
-
Seconds become microseconds:
-ss 10→set_start_time_us(10_000_000)(the_ussuffix is the reminder). -
Option names work verbatim, minus the dash:
-crf 23→set_video_codec_opt("crf", "23"),-movflags +faststart→set_format_opt("movflags", "faststart")— straight into FFmpeg’s AVOption system; whatever name the CLI accepts, this accepts. -
Filter strings copy over unchanged: the string after
-vfdrops into.filter_desc(...)character for character.
A watermark, for instance — -filter_complex "overlay=10:10" is multiple .input() calls mapping to [0]/[1], filter string copied as-is:
FfmpegContext::builder()
.input("input.mp4").input("logo.png")
.filter_desc("[0:v][1:v]overlay=10:10")
.output("watermarked.mp4")
.build()?.start()?.wait()?;
Enter fullscreen mode Exit fullscreen mode
The full mapping of ~50 CLI options and patterns (with each one’s implementation and known gaps) lives in the CLI-to-API mapping on docs.rs. Emitted scaffolding plus that table is the complete “any common command → maintainable Rust” path.
Who should not use it
- One-off jobs: use the CLI. Transcode one file, try one filter, verify one parameter — a terminal command is the shortest path; a Rust program is the detour. This layer is for commands you run repeatedly in a program, or want to grow into Rust code.
-
The full power of ffmpeg:
from_clicannot give it. It is a deliberately narrow trusted subset — six verified shapes, a 7.1 runtime. Two-pass encoding, complex-filter_complex, hardware acceleration, device capture are outside the subset and rejected outright. For the full CLI surface, shell out, or write the builder directly. -
Three honest facts: the compile/link-libav pain is untouched by this layer;
cliis an optional feature you must enable; and the CLI-compat layer is young — the manifest grows release by release.
Closing
Paste the command and it either runs it for you or hands you the code — but it never pretends to have translated something it did not verify. A tool that will say “I have not reconciled this one; review it yourself” is more trustworthy than one that runs anything. Hand-mapping CLI to API is now only needed outside the subset.
The cli feature is exercised in the repo’s examples/cli_emitted_* (the pinned translations of all six verified shapes) and the CLI-to-API mapping on docs.rs. The crate lives at github.com/YeautyYE/ez-ffmpeg.
An open question: which ffmpeg command do you most want to paste straight into Rust and run? Drop it in the comments — and if I’ve gotten anything above wrong, tell me and I’ll fix it.
답글 남기기