Skip to main content
This example demonstrates how to monitor FFmpeg’s progress in real-time, displaying percentage complete, encoding speed, bitrate, and other metrics during transcoding.

Complete Example

This is the full transcode_with_progress.rs example from the libffmpeg repository:
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 {
    #[arg(short, long)]
    input: PathBuf,

    #[arg(short, long)]
    output: PathBuf,

    #[arg(short, long)]
    time: Option<u64>,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let args = Args::parse();

    let root_token = CancellationToken::new();
    let transcode_token = root_token.child_token();
    let exit_token = root_token.child_token();
    libsignal::cancel_after_signal(root_token.clone());

    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;

    let monitor = libffmpeg::libcmd::CommandMonitor::with_capacity(100);

    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 } => {
                        if !progress.with_line(&line) {
                            println!("{}[O] {}{}", "\x1b[32m", line, "\x1b[0m");
                            continue;
                        }

                        if let Some(update) = progress.finish() {
                            println!(
                                "{}[P] {:.2}% ({} / {}); bitrate={} @ {}fps ({:.1}x realtime) {}",
                                "\x1b[33m",
                                100f64 * update.out_time.as_secs_f64() / total,
                                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,
                                "\x1b[0m"
                            );
                        }
                    }
                    libffmpeg::libcmd::CommandMonitorMessage::Stderr { line } => {
                        eprintln!("{}[E] {}{}", "\x1b[31m", line, "\x1b[0m")
                    }
                }
            }
        })
    };

    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?;

    exit_token.cancel();

    if let Err(e) = monitor_fut.await {
        eprintln!("Failed to wait for monitor: {}", e);
    }

    println!("");

    if result
        .exit_code
        .as_ref()
        .map(|exit| exit.success)
        .unwrap_or_default()
    {
        println!("Transcoding completed successfully");
    } else {
        println!("Transcoding failed: {:#?}", result);
    }

    Ok(())
}

Breaking Down the Code

1. Setup Cancellation Tokens

let root_token = CancellationToken::new();
let transcode_token = root_token.child_token();
let exit_token = root_token.child_token();
libsignal::cancel_after_signal(root_token.clone());
This creates a cancellation hierarchy:
  • root_token: The main cancellation source, triggered by Ctrl+C
  • transcode_token: Child token for the FFmpeg process
  • exit_token: Child token for the monitoring task
When root_token is cancelled, all child tokens are automatically cancelled.

2. Get Video Duration

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;
Before starting transcoding, we probe the input file to get its duration. This allows us to:
  • Calculate percentage progress during transcoding
  • Use the full video duration if no time limit is specified

3. Create Command Monitor

let monitor = libffmpeg::libcmd::CommandMonitor::with_capacity(100);
The CommandMonitor is a bounded channel system that captures FFmpeg’s stdout and stderr output. The capacity of 100 means it can buffer up to 100 messages. Components:
  • monitor.server: Passed to the FFmpeg function to capture output
  • monitor.client: Used to receive and process the output lines

4. Spawn 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
        {
            // Process messages...
        }
    })
};
This spawns an asynchronous task that:
  • Receives output lines from FFmpeg via the monitor client
  • Parses progress information
  • Displays formatted progress updates
  • Continues until the exit token is cancelled

5. Parse Progress Output

let mut progress = libffmpeg::ffmpeg::progress::PartialProgress::default();

match delivery {
    libffmpeg::libcmd::CommandMonitorMessage::Stdout { line } => {
        if !progress.with_line(&line) {
            // Line wasn't a progress update, print it normally
            println!("{}[O] {}{}", "\x1b[32m", line, "\x1b[0m");
            continue;
        }

        if let Some(update) = progress.finish() {
            // Complete progress update received, display it
            println!("...");
        }
    }
    libffmpeg::libcmd::CommandMonitorMessage::Stderr { line } => {
        eprintln!("{}[E] {}{}", "\x1b[31m", line, "\x1b[0m")
    }
}
PartialProgress is an accumulator that:
  • Receives FFmpeg progress lines one at a time via with_line()
  • Returns true if the line was a recognized progress field
  • Returns false if the line is regular output (not progress data)
  • Produces a complete Progress snapshot via finish() when a full block is received

6. FFmpeg Progress Format

cmd.arg("-progress").arg("pipe:1");
This tells FFmpeg to output machine-readable progress information to stdout in this format:
frame=120
fps=30.00
bitrate=1500.0kbits/s
total_size=2048000
out_time_us=4000000
dup_frames=0
drop_frames=0
speed=1.0x
progress=continue
Each progress block ends with a progress=continue or progress=end line.

7. Display Progress Information

if let Some(update) = progress.finish() {
    println!(
        "{}[P] {:.2}% ({} / {}); bitrate={} @ {}fps ({:.1}x realtime) {}",
        "\x1b[33m",
        100f64 * update.out_time.as_secs_f64() / total,
        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,
        "\x1b[0m"
    );
}
The Progress struct contains:
  • frame: Number of frames processed
  • fps: Current encoding speed (frames per second)
  • bitrate: Current bitrate in bytes per second
  • total_size: Total output size in bytes
  • out_time: Elapsed output time (position in the output stream)
  • dup_frames: Number of duplicated frames
  • drop_frames: Number of dropped frames
  • speed: Encoding speed as a multiplier of realtime (2.0 = 2x speed)
  • progress: State (Continue, End, or Unknown)
Example output:
[P] 45.23% (1m 30s / 3m 20s); bitrate=1.5 Mps @ 30fps (2.1x realtime)

8. Run FFmpeg with Monitoring

let result = ffmpeg(transcode_token, &monitor.server, |cmd| {
    cmd.arg("-i").arg(&args.input);
    cmd.arg("-progress").arg("pipe:1");
    // ... other arguments
}).await?;
The ffmpeg() function (not ffmpeg_slim()) accepts a CommandMonitorServer that captures stdout and stderr, routing them through the monitor channel.

9. Cleanup

exit_token.cancel();

if let Err(e) = monitor_fut.await {
    eprintln!("Failed to wait for monitor: {}", e);
}
After FFmpeg completes:
  1. Cancel the exit token to stop the monitoring task
  2. Wait for the monitoring task to finish
  3. Check the final result

Key Components

CommandMonitor

pub struct CommandMonitor {
    pub client: CommandMonitorClient,
    pub server: CommandMonitorServer,
}
A channel-based system for capturing command output:
  • Server: Attached to the FFmpeg process to capture stdout/stderr
  • Client: Used by your code to receive the output lines

PartialProgress

pub struct PartialProgress {
    // Internal state...
}

impl PartialProgress {
    pub fn with_line(&mut self, line: &str) -> bool;
    pub fn finish(&self) -> Option<Progress>;
}
An accumulator for parsing FFmpeg’s -progress pipe:1 output line by line:
  • Feed lines via with_line()
  • Call finish() to get a complete Progress snapshot when ready

Progress

pub struct Progress {
    pub frame: usize,
    pub fps: f64,
    pub bitrate: isize,
    pub total_size: usize,
    pub out_time: Duration,
    pub dup_frames: usize,
    pub drop_frames: usize,
    pub speed: f64,
    pub progress: ProgressState,
}
A complete progress snapshot containing all metrics from a single progress block.

Running the Example

# Basic usage
cargo run --example transcode_with_progress -- \
    --input input.mp4 \
    --output output.mp4

# Transcode only first 30 seconds
cargo run --example transcode_with_progress -- \
    --input input.mp4 \
    --output output.mp4 \
    --time 30

Customizing Progress Display

You can customize how progress is displayed by modifying the monitoring task:
if let Some(update) = progress.finish() {
    // Simple percentage only
    println!("Progress: {:.1}%", 100.0 * update.out_time.as_secs_f64() / total);
    
    // Or detailed with ETA calculation
    let progress_ratio = update.out_time.as_secs_f64() / total;
    let elapsed = start_time.elapsed().as_secs_f64();
    let eta = elapsed / progress_ratio - elapsed;
    println!("{}% complete, ETA: {:.0}s", progress_ratio * 100.0, eta);
}

See Also

Build docs developers (and LLMs) love