Quickstart Guide
This guide walks you through building a complete video transcoding application using libffmpeg. You’ll learn how to transcode videos with real-time progress monitoring and graceful cancellation.
Make sure you’ve completed the Installation guide before starting this tutorial.
What You’ll Build
By the end of this guide, you’ll have a command-line application that:
Transcodes video files using H.264 (libx264) and AAC audio
Displays real-time progress with percentage, fps, bitrate, and speed
Supports graceful cancellation via Ctrl+C
Extracts video duration using ffprobe
Uses structured progress parsing from ffmpeg output
Step-by-Step Tutorial
Create a new Rust project
cargo new video-transcoder
cd video-transcoder
Add dependencies to Cargo.toml
[ dependencies ]
libffmpeg = { git = "https://github.com/charliethomson/libffmpeg" }
tokio = { version = "1.47" , features = [ "rt" , "process" , "sync" , "time" , "macros" ] }
tokio-util = { version = "0.7" }
anyhow = "1"
clap = { version = "4" , features = [ "derive" ] }
humansize = "2"
humantime = "2"
Don’t forget to configure tracing support: mkdir -p .cargo
curl -o .cargo/config.toml https://raw.githubusercontent.com/charliethomson/libffmpeg/refs/heads/main/.cargo/config.toml
Write the main application
Create src/main.rs with a complete transcoding application: use std :: { path :: PathBuf , time :: Duration };
use clap :: Parser ;
use libffmpeg :: ffmpeg :: ffmpeg;
use tokio_util :: { future :: FutureExt , sync :: CancellationToken };
#[derive( Debug , Parser )]
struct Args {
/// Input video file path
#[arg(short, long)]
input : PathBuf ,
/// Output video file path
#[arg(short, long)]
output : PathBuf ,
/// Optional duration limit in seconds
#[arg(short, long)]
time : Option < u64 >,
}
#[tokio :: main]
async fn main () -> anyhow :: Result <()> {
let args = Args :: parse ();
// Create cancellation tokens
let root_token = CancellationToken :: new ();
let transcode_token = root_token . child_token ();
let exit_token = root_token . child_token ();
// Setup Ctrl+C handler (requires libsignal crate)
// For this quickstart, we'll handle cancellation manually
tokio :: spawn ({
let root_token = root_token . clone ();
async move {
tokio :: signal :: ctrl_c () . await . ok ();
println! ( " \n Received Ctrl+C, cancelling..." );
root_token . cancel ();
}
});
// Get video duration using ffprobe
println! ( "Analyzing input file..." );
let video_duration = libffmpeg :: util :: get_duration (
& args . input,
root_token . child_token ()
) . await ? ;
let total = args . time . unwrap_or ( video_duration . as_secs ()) as f64 ;
println! ( "Duration: {:.2}s \n " , total );
// Create command monitor for stdout/stderr streaming
let monitor = libffmpeg :: libcmd :: CommandMonitor :: with_capacity ( 100 );
// Spawn progress monitoring task
let monitor_fut = {
let mut client = monitor . client . clone ();
let exit_token = exit_token . clone ();
tokio :: spawn ( async move {
let mut progress = libffmpeg :: ffmpeg :: progress :: PartialProgress :: default ();
while let Some ( Some ( delivery )) =
client . recv () . with_cancellation_token ( & exit_token ) . await
{
match delivery {
libffmpeg :: libcmd :: CommandMonitorMessage :: Stdout { line } => {
// Try to parse as progress update
if ! progress . with_line ( & line ) {
println! ( "[stdout] {}" , line );
continue ;
}
// Print structured progress
if let Some ( update ) = progress . finish () {
let percent = 100.0 * update . out_time . as_secs_f64 () / total ;
println! (
"[Progress] {:.1}% | {} / {} | {} @ {:.0}fps ({:.1}x realtime)" ,
percent ,
humantime :: format_duration ( update . out_time),
humantime :: format_duration ( Duration :: from_secs_f64 ( total )),
humansize :: format_size_i (
update . bitrate,
humansize :: DECIMAL . suffix ( "ps" )
),
update . fps,
update . speed,
);
}
}
libffmpeg :: libcmd :: CommandMonitorMessage :: Stderr { line } => {
eprintln! ( "[stderr] {}" , line );
}
}
}
})
};
// Run ffmpeg with monitoring
println! ( "Starting transcode... \n " );
let result = ffmpeg ( transcode_token , & monitor . server, | cmd | {
cmd . arg ( "-i" ) . arg ( & args . input);
cmd . arg ( "-t" ) . arg ( total . to_string ());
cmd . arg ( "-c:v" ) . arg ( "libx264" );
cmd . arg ( "-preset" ) . arg ( "fast" );
cmd . arg ( "-crf" ) . arg ( "23" );
cmd . arg ( "-c:a" ) . arg ( "aac" );
cmd . arg ( "-b:a" ) . arg ( "128k" );
cmd . arg ( "-progress" ) . arg ( "pipe:1" );
cmd . arg ( "-y" );
cmd . arg ( & args . output);
}) . await ? ;
// Stop the monitor
exit_token . cancel ();
if let Err ( e ) = monitor_fut . await {
eprintln! ( "Failed to wait for monitor: {}" , e );
}
// Check result
println! ( " \n " );
if result . exit_code
. as_ref ()
. map ( | exit | exit . success)
. unwrap_or_default ()
{
println! ( "✓ Transcoding completed successfully!" );
println! ( "Output saved to: {}" , args . output . display ());
} else {
println! ( "✗ Transcoding failed" );
if let Some ( code ) = result . exit_code {
println! ( "Exit code: {:?}" , code . code);
}
}
Ok (())
}
Run the transcoder
# Basic usage
cargo run -- --input input.mp4 --output output.mp4
# Limit to first 30 seconds
cargo run -- --input input.mp4 --output output.mp4 --time 30
Expected output: Analyzing input file...
Duration: 120.50s
Starting transcode...
[Progress] 5.2% | 6s 271ms / 2m 0s 500ms | 2.1 Mps @ 30fps (1.0x realtime)
[Progress] 12.8% | 15s 440ms / 2m 0s 500ms | 2.3 Mps @ 30fps (1.1x realtime)
[Progress] 25.1% | 30s 200ms / 2m 0s 500ms | 2.2 Mps @ 30fps (1.0x realtime)
...
[Progress] 100.0% | 2m 0s 500ms / 2m 0s 500ms | 2.1 Mps @ 30fps (1.0x realtime)
✓ Transcoding completed successfully!
Output saved to: output.mp4
Understanding the Code
Let’s break down the key components:
Binary Discovery and Duration Extraction
let video_duration = libffmpeg :: util :: get_duration (
& args . input,
root_token . child_token ()
) . await ? ;
The get_duration function:
Automatically discovers ffprobe binary (via LIBFFMPEG_FFPROBE_PATH or PATH)
Runs: ffprobe -v quiet -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 input.mp4
Parses the output as f64 seconds
Returns std::time::Duration
Command Monitor Setup
let monitor = libffmpeg :: libcmd :: CommandMonitor :: with_capacity ( 100 );
The CommandMonitor provides:
Server — Attached to ffmpeg process for stdout/stderr streaming
Client — Receives messages from the server in your application
Capacity — Channel buffer size (100 messages)
Progress Parsing
let mut progress = libffmpeg :: ffmpeg :: progress :: PartialProgress :: default ();
if ! progress . with_line ( & line ) {
// Line was not a progress key=value pair
println! ( "[stdout] {}" , line );
continue ;
}
if let Some ( update ) = progress . finish () {
// Complete progress block received
println! ( "Frame: {}, Speed: {}x" , update . frame, update . speed);
}
PartialProgress accumulates lines until a complete progress block is received:
with_line(&str) -> bool — Returns true if line was recognized
finish() -> Option<Progress> — Returns complete progress when progress=continue or progress=end is received
The Progress struct contains:
frame: usize — Frames processed
fps: f64 — Current FPS
bitrate: isize — Bitrate in bytes/second
total_size: usize — Output size in bytes
out_time: Duration — Elapsed output time
speed: f64 — Realtime multiplier (2.0 = 2x speed)
FFmpeg Execution
let result = ffmpeg ( transcode_token , & monitor . server, | cmd | {
cmd . arg ( "-i" ) . arg ( & args . input);
cmd . arg ( "-progress" ) . arg ( "pipe:1" ); // Enable progress output to stdout
cmd . arg ( "-y" ); // Overwrite output file
cmd . arg ( & args . output);
}) . await ? ;
The ffmpeg function:
Signature : ffmpeg(CancellationToken, &CommandMonitorServer, Prepare) -> Result<CommandExit, FfmpegError>
Discovers binary via LIBFFMPEG_FFMPEG_PATH or PATH
Streams output through the monitor
Cancellable via the token
Returns CommandExit with exit code and captured lines
Next Examples
Basic Transcode (No Monitoring)
Use ffmpeg_slim when you don’t need progress:
use libffmpeg :: ffmpeg :: ffmpeg_slim;
use tokio_util :: sync :: CancellationToken ;
let token = CancellationToken :: new ();
let result = ffmpeg_slim ( token , | cmd | {
cmd . arg ( "-i" ) . arg ( "input.mp4" )
. arg ( "-c:v" ) . arg ( "libx265" )
. arg ( "-preset" ) . arg ( "medium" )
. arg ( "-crf" ) . arg ( "28" )
. arg ( "output.mp4" );
}) . await ? ;
if result . exit_code . map ( | e | e . success) . unwrap_or ( false ) {
println! ( "Success!" );
}
Graceful Shutdown
Use ffmpeg_graceful to allow ffmpeg to finalize output on cancellation:
use libffmpeg :: ffmpeg :: ffmpeg_graceful;
use libffmpeg :: libcmd :: CommandMonitor ;
use tokio_util :: sync :: CancellationToken ;
let monitor = CommandMonitor :: with_capacity ( 100 );
let token = CancellationToken :: new ();
// On cancellation, sends 'q' to stdin, waits 5 seconds, then SIGKILL
let result = ffmpeg_graceful (
token ,
& monitor . client,
& monitor . server,
| cmd | {
cmd . arg ( "-i" ) . arg ( "input.mp4" )
. arg ( "-c:v" ) . arg ( "libx264" )
. arg ( "output.mp4" );
}
) . await ? ;
Graceful shutdown sends q to ffmpeg’s stdin and waits up to 5 seconds. If ffmpeg doesn’t exit, it’s killed with SIGKILL.
Use ffprobe directly for custom queries:
use libffmpeg :: ffprobe :: ffprobe;
use tokio_util :: sync :: CancellationToken ;
let token = CancellationToken :: new ();
let result = ffprobe ( token , | cmd | {
cmd . arg ( "-v" ) . arg ( "quiet" )
. arg ( "-print_format" ) . arg ( "json" )
. arg ( "-show_format" )
. arg ( "-show_streams" )
. arg ( "input.mp4" );
}) . await ? ;
// Parse JSON from stdout_lines
let json = result . stdout_lines . join ( " \n " );
println! ( "{}" , json );
Common Patterns
Timeout for Long Operations
use tokio :: time :: {timeout, Duration };
let token = CancellationToken :: new ();
let result = timeout (
Duration :: from_secs ( 300 ), // 5 minute timeout
ffmpeg_slim ( token . clone (), | cmd | {
cmd . arg ( "-i" ) . arg ( "input.mp4" )
. arg ( "output.mp4" );
})
) . await ?? ;
Multiple Concurrent Transcodes
use futures :: future :: try_join_all;
let tasks : Vec < _ > = inputs . iter () . map ( | input | {
let token = CancellationToken :: new ();
async move {
ffmpeg_slim ( token , | cmd | {
cmd . arg ( "-i" ) . arg ( input )
. arg ( format! ( "{}.transcoded.mp4" , input ));
}) . await
}
}) . collect ();
let results = try_join_all ( tasks ) . await ? ;
Custom Progress Display
use indicatif :: { ProgressBar , ProgressStyle };
let bar = ProgressBar :: new ( 100 );
bar . set_style (
ProgressStyle :: default_bar ()
. template ( "[{elapsed_precise}] {bar:40.cyan/blue} {pos}% {msg}" )
. unwrap ()
);
while let Some ( Some ( delivery )) = client . recv () . await {
if let libffmpeg :: libcmd :: CommandMonitorMessage :: Stdout { line } = delivery {
if progress . with_line ( & line ) {
if let Some ( update ) = progress . finish () {
let percent = ( 100.0 * update . out_time . as_secs_f64 () / total ) as u64 ;
bar . set_position ( percent );
bar . set_message ( format! ( "{}fps @ {:.1}x" , update . fps, update . speed));
}
}
}
}
bar . finish_with_message ( "Done!" );
Troubleshooting
No progress output from ffmpeg
Problem : Progress not appearing even with -progress pipe:1.Solution : Ensure you’re using the ffmpeg or ffmpeg_graceful function (not ffmpeg_slim) and passing the CommandMonitorServer.
Problem : Encoding at 0.1x realtime speed.Solution :
Try faster preset: -preset ultrafast
Use hardware acceleration: -c:v h264_videotoolbox (macOS) or -c:v h264_nvenc (NVIDIA)
Reduce quality: increase -crf value (e.g., 28)
Output file incomplete after cancellation
Problem : File corrupted when cancelled with Ctrl+C.Solution : Use ffmpeg_graceful instead of ffmpeg. It sends q to stdin allowing ffmpeg to finalize the file.
Duration extraction fails
Full Example Source
The complete example (with signal handling) is available in the libffmpeg repository:
# Clone the repository
git clone https://github.com/charliethomson/libffmpeg
cd libffmpeg
# Run the example
cargo run --example transcode_with_progress -- --input input.mp4 --output output.mp4
Source: libffmpeg/examples/transcode_with_progress.rs
Next Steps
API Reference Explore detailed type signatures and all available functions
Examples Browse more examples including audio extraction, thumbnail generation, and streaming
Advanced Usage Learn about custom error handling, tracing integration, and performance optimization
GitHub Repository View source code, report issues, and contribute