Originally published at ffmpeg-micro.com.
You need video processing in your Rust app. Maybe you're building a media pipeline, handling user uploads, or generating thumbnails in a backend service. You search "ffmpeg rust" and find a few crates. All of them require FFmpeg compiled and installed on the machine running your code.
That works on your dev machine. It breaks when you deploy to a minimal Docker image, a serverless function, or a CI runner that doesn't have FFmpeg in PATH. There are three approaches in Rust, each with different tradeoffs.
Using std::process::Command to Call FFmpeg
The most direct option. Install FFmpeg on your system, then shell out:
use std::process::Command;
fn main() {
let output = Command::new("ffmpeg")
.args([
"-i", "input.mp4",
"-c:v", "libx264",
"-crf", "23",
"-preset", "medium",
"-c:a", "aac",
"-b:a", "128k",
"output.mp4",
])
.output()
.expect("Failed to execute ffmpeg");
if output.status.success() {
println!("Transcoding complete");
} else {
eprintln!("FFmpeg error: {}", String::from_utf8_lossy(&output.stderr));
}
}
This is what most Rust developers start with. It works, but you're responsible for everything: making sure FFmpeg is installed on every deploy target, parsing stderr for errors, and confirming the right codecs are compiled in. On a scratch or distroless Docker image, there's no FFmpeg at all. You'll need a multi-stage build or a fat base image that adds 80-200MB.
Using rusty_ffmpeg (FFI Bindings)
The rusty_ffmpeg crate provides Rust bindings to FFmpeg's C libraries via FFI. It has over 250,000 downloads on crates.io and gives you lower-level access than shelling out:
// Cargo.toml
// [dependencies]
// ffmpeg-sys-the-third = "2.1"
// Note: rusty_ffmpeg / ffmpeg-sys-the-third requires:
// - FFmpeg dev libraries installed (libavcodec, libavformat, etc.)
// - pkg-config or vcpkg to locate them
// - A C compiler (clang or gcc)
The FFI approach gives you direct access to FFmpeg's internal APIs for frame-level processing, custom filters, and codec control. But the build dependencies are significant. You need FFmpeg's development headers, a C toolchain, and pkg-config configured correctly. Cross-compilation (say, building on macOS for a Linux container) adds another layer of complexity. Every team member and CI server needs the same FFmpeg version with the same codec configuration.
For frame-by-frame processing or real-time video manipulation, FFI bindings are the right tool. For standard operations like transcoding, compression, or format conversion, they're more complexity than you need.
Processing Video via HTTP API (No FFmpeg Install)
If you don't want to deal with FFmpeg binaries, build dependencies, or server management, you can send video processing jobs to a cloud API. FFmpeg Micro gives you full FFmpeg capabilities through HTTP requests. From Rust, it's just an HTTP POST:
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Serialize)]
struct TranscodeRequest {
inputs: Vec<Input>,
#[serde(rename = "outputFormat")]
output_format: String,
preset: Option<Preset>,
}
#[derive(Serialize)]
struct Input {
url: String,
}
#[derive(Serialize)]
struct Preset {
quality: String,
resolution: String,
}
#[derive(Deserialize, Debug)]
struct TranscodeResponse {
id: String,
status: String,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let api_key = std::env::var("FFMPEG_MICRO_API_KEY")?;
let request = TranscodeRequest {
inputs: vec![Input {
url: "https://example.com/input.mp4".to_string(),
}],
output_format: "mp4".to_string(),
preset: Some(Preset {
quality: "high".to_string(),
resolution: "1080p".to_string(),
}),
};
let response = client
.post("https://api.ffmpeg-micro.com/v1/transcodes")
.header(AUTHORIZATION, format!("Bearer {}", api_key))
.header(CONTENT_TYPE, "application/json")
.json(&request)
.send()
.await?;
let transcode: TranscodeResponse = response.json().await?;
println!("Job created: {} (status: {})", transcode.id, transcode.status);
Ok(())
}
Three dependencies (reqwest, serde, tokio), zero FFmpeg installation, and it compiles on any target that supports TLS. The API handles scaling, codec support, and infrastructure. Your Rust binary stays lean.
Checking Job Status
After you create a transcode, poll for completion:
use std::time::Duration;
use tokio::time::sleep;
async fn wait_for_completion(
client: &reqwest::Client,
api_key: &str,
job_id: &str,
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
loop {
let response = client
.get(format!("https://api.ffmpeg-micro.com/v1/transcodes/{}", job_id))
.header(AUTHORIZATION, format!("Bearer {}", api_key))
.send()
.await?;
let job: serde_json::Value = response.json().await?;
let status = job["status"].as_str().unwrap_or("unknown");
match status {
"completed" => return Ok(job),
"failed" => return Err(format!("Job failed: {:?}", job["error"]).into()),
_ => {
println!("Status: {}. Waiting...", status);
sleep(Duration::from_secs(3)).await;
}
}
}
}
The outputUrl field in the completed response gives you a download link for the processed file.
When to Use Each Approach
| Approach | Best for | Drawback |
|---|---|---|
std::process::Command |
Quick scripts, local tools | FFmpeg must be installed everywhere |
rusty_ffmpeg (FFI) |
Frame-level processing, real-time video | Heavy build deps, C toolchain required |
Cloud API (reqwest) |
Production services, serverless, CI pipelines | Network latency, per-job cost |
If you're building a local CLI tool and FFmpeg is already installed, Command is fine. If you need frame-level manipulation or custom filters, FFI bindings are worth the setup cost. If you're building a production service and want your Rust binary to stay simple, an HTTP API removes the entire FFmpeg dependency chain.
Common Pitfalls
FFmpeg not in PATH: Your code works locally but fails in Docker. Always specify the full path in containers or use an API to avoid this entirely.
Version mismatches with FFI: rusty_ffmpeg builds against specific FFmpeg versions. If your CI has FFmpeg 6.x but you linked against 5.x headers, you'll get cryptic linker errors.
Blocking the async runtime: If you use std::process::Command inside a Tokio async context, spawn it on a blocking thread with tokio::task::spawn_blocking. Otherwise you'll stall the executor.
Missing codecs: A minimal FFmpeg build might not include libx265 or libvpx. When you control the install, you control the codec set. With a cloud API, all codecs are available by default.
FAQ
Can I use FFmpeg in Rust without installing it?
Yes. You can call a cloud FFmpeg API like FFmpeg Micro over HTTP from Rust using reqwest. No local FFmpeg binary or C libraries needed. Your Rust binary compiles and runs anywhere.
What's the difference between rusty_ffmpeg and calling FFmpeg as a subprocess?
rusty_ffmpeg gives you FFI bindings to FFmpeg's C libraries for frame-level access. Subprocess (std::process::Command) treats FFmpeg as a black box. FFI is more powerful but requires a C toolchain, dev headers, and careful version management.
Is there a performance penalty for using an HTTP API instead of local FFmpeg?
You add network round-trip time (typically under 200ms). The actual video processing runs on cloud infrastructure that auto-scales. For batch processing or production services, the API is usually faster end-to-end because you're not limited by your server's CPU.
What Rust crates do I need for the API approach?
reqwest for HTTP, serde and serde_json for JSON serialization, and tokio for async. Add them to your Cargo.toml:
cargo add reqwest serde serde_json tokio --features reqwest/json,serde/derive,tokio/full
Last verified: June 2026. All API examples tested against FFmpeg Micro v1.
Top comments (0)