Skip to main content

Overview

The ffmpeg function runs FFmpeg with output monitoring via a CommandMonitorServer. Stdout and stderr lines are streamed in real-time, enabling progress parsing, logging, and user feedback during long-running encoding operations.

When to Use ffmpeg

Use the standard ffmpeg function when:
  • You need to monitor FFmpeg’s stdout/stderr output in real-time
  • You want to parse and display encoding progress
  • You need to capture and log FFmpeg messages
  • Immediate cancellation (SIGKILL) is acceptable
For simple execution without monitoring, use ffmpeg_slim. For graceful shutdown with stdin-based quit, use ffmpeg_graceful.

Function Signature

pub async fn ffmpeg<Prepare>(
    cancellation_token: CancellationToken,
    server: &CommandMonitorServer,
    prepare: Prepare,
) -> Result<CommandExit, FfmpegError>
where
    Prepare: FnOnce(&mut Command),

Parameters

cancellation_token
CancellationToken
required
A tokio cancellation token for stopping the FFmpeg process
server
&CommandMonitorServer
required
The server side of a CommandMonitor for receiving output lines
prepare
FnOnce(&mut Command)
required
A closure that configures the FFmpeg command with arguments

Return Value

Returns Result<CommandExit, FfmpegError> where:
  • CommandExit contains the exit code and captured output
  • FfmpegError::NotFound if the FFmpeg binary cannot be located

Basic Usage

1

Create a CommandMonitor

Create a monitor with a buffer capacity for output lines:
use libffmpeg::libcmd::CommandMonitor;

let monitor = CommandMonitor::with_capacity(100);
2

Spawn a monitoring task

Create a task to receive and process output:
let mut client = monitor.client.clone();
let monitor_task = tokio::spawn(async move {
    while let Some(Some(message)) = client.recv().await {
        match message {
            CommandMonitorMessage::Stdout { line } => {
                println!("[OUT] {}", line);
            }
            CommandMonitorMessage::Stderr { line } => {
                eprintln!("[ERR] {}", line);
            }
        }
    }
});
3

Run FFmpeg with monitoring

Execute FFmpeg with the monitor server:
use libffmpeg::ffmpeg::ffmpeg;

let result = ffmpeg(cancellation_token, &monitor.server, |cmd| {
    cmd.arg("-i").arg("input.mp4");
    cmd.arg("-c:v").arg("libx264");
    cmd.arg("-preset").arg("fast");
    cmd.arg("output.mp4");
}).await?;
4

Wait for monitoring to complete

Ensure the monitoring task finishes:
monitor_task.await?;

Complete Example with Progress Tracking

This example is adapted from libffmpeg/examples/transcode_with_progress.rs:19-110:
use std::{path::PathBuf, time::Duration};
use libffmpeg::ffmpeg::ffmpeg;
use libffmpeg::libcmd::{CommandMonitor, CommandMonitorMessage};
use tokio_util::sync::CancellationToken;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let input = PathBuf::from("input.mp4");
    let output = PathBuf::from("output.mp4");
    
    // Create tokens for lifecycle management
    let root_token = CancellationToken::new();
    let transcode_token = root_token.child_token();
    let exit_token = root_token.child_token();
    
    // Get video duration for progress calculation
    let video_duration = libffmpeg::util::get_duration(
        &input,
        root_token.child_token()
    ).await?;
    let total_seconds = video_duration.as_secs() as f64;
    
    // Create monitor with 100-line buffer
    let monitor = CommandMonitor::with_capacity(100);
    
    // Spawn monitoring task
    let monitor_task = {
        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(message)) = client.recv().await {
                match message {
                    CommandMonitorMessage::Stdout { line } => {
                        // Try to parse as progress line
                        if !progress.with_line(&line) {
                            // Not a progress line, print it
                            println!("[OUT] {}", line);
                            continue;
                        }
                        
                        // Check if progress block is complete
                        if let Some(update) = progress.finish() {
                            let percent = 100.0 * update.out_time.as_secs_f64() / total_seconds;
                            println!(
                                "Progress: {:.2}% | Frame: {} | FPS: {:.1} | Speed: {:.1}x",
                                percent,
                                update.frame,
                                update.fps,
                                update.speed
                            );
                        }
                    }
                    CommandMonitorMessage::Stderr { line } => {
                        eprintln!("[ERR] {}", line);
                    }
                }
            }
        })
    };
    
    // Run FFmpeg with progress output
    let result = ffmpeg(transcode_token, &monitor.server, |cmd| {
        cmd.arg("-i").arg(&input);
        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"); // Output progress to stdout
        cmd.arg("-y"); // Overwrite output
        cmd.arg(&output);
    }).await?;
    
    // Signal monitoring to stop
    exit_token.cancel();
    
    // Wait for monitor task
    if let Err(e) = monitor_task.await {
        eprintln!("Monitor task error: {}", e);
    }
    
    // Check result
    if result.exit_code.as_ref().map(|e| e.success).unwrap_or(false) {
        println!("Transcoding completed successfully!");
    } else {
        eprintln!("Transcoding failed: {:?}", result);
    }
    
    Ok(())
}

Output Monitoring Patterns

Simple Logging

let mut client = monitor.client.clone();
tokio::spawn(async move {
    while let Some(Some(msg)) = client.recv().await {
        match msg {
            CommandMonitorMessage::Stdout { line } => println!("[OUT] {}", line),
            CommandMonitorMessage::Stderr { line } => eprintln!("[ERR] {}", line),
        }
    }
});

Filtering Specific Messages

let mut client = monitor.client.clone();
tokio::spawn(async move {
    while let Some(Some(msg)) = client.recv().await {
        if let CommandMonitorMessage::Stderr { line } = msg {
            if line.contains("warning") {
                eprintln!("Warning: {}", line);
            } else if line.contains("error") {
                eprintln!("Error: {}", line);
            }
        }
    }
});

Cancellation-Aware Monitoring

use tokio_util::future::FutureExt;

let mut client = monitor.client.clone();
let exit_token = CancellationToken::new();

tokio::spawn(async move {
    while let Some(Some(msg)) = client.recv()
        .with_cancellation_token(&exit_token)
        .await 
    {
        // Process messages
    }
});

// Later, stop monitoring
exit_token.cancel();
Use -progress pipe:1 in your FFmpeg command to output structured progress information to stdout. See the Progress Parsing guide for details.

Cancellation Behavior

When the cancellation token is cancelled, ffmpeg immediately kills the FFmpeg process:
let cancellation_token = CancellationToken::new();
let token_clone = cancellation_token.clone();

// Start FFmpeg
let ffmpeg_task = tokio::spawn(async move {
    ffmpeg(token_clone, &monitor.server, |cmd| {
        cmd.arg("-i").arg("input.mp4");
        cmd.arg("output.mp4");
    }).await
});

// Cancel after timeout
tokio::time::sleep(Duration::from_secs(30)).await;
cancellation_token.cancel();
Immediate cancellation may result in incomplete or corrupted output files. FFmpeg doesn’t get a chance to finalize the file header or write the moov atom. For proper file finalization, use ffmpeg_graceful which sends a quit command to FFmpeg before killing it.

Implementation Details

From the source code at libffmpeg/src/ffmpeg/standard.rs:16-52:
pub async fn ffmpeg<Prepare>(
    cancellation_token: CancellationToken,
    server: &CommandMonitorServer,
    prepare: Prepare,
) -> Result<CommandExit, FfmpegError>
where
    Prepare: FnOnce(&mut Command),
{
    tracing::debug!("Starting ffmpeg execution");

    let ffmpeg_path = find_ffmpeg().ok_or(FfmpegError::NotFound).inspect_err(
        |e| tracing::error!(error =% e, error_context =? e, "ffmpeg binary not found"),
    )?;

    tracing::info!(
        ffmpeg_path = %ffmpeg_path.display(),
        "Executing ffmpeg"
    );

    libcmd::run(
        ffmpeg_path,
        Some(server.clone()),
        cancellation_token.child_token(),
        prepare,
    )
    .await
    .inspect(|exit| {
        tracing::debug!(exit = exit.as_value(), "ffmpeg completed");
    })
    .inspect_err(|e| {
        tracing::error!(
            error = %e,
            "ffmpeg execution failed"
        );
    })
    .map_err(Into::into)
}
Key differences from ffmpeg_slim:
  • Passes Some(server.clone()) instead of None for monitoring
  • Output lines are streamed to the monitor’s client
  • Includes structured tracing

Next Steps

Parse Progress

Use PartialProgress to parse FFmpeg progress output

Graceful Shutdown

Properly finalize files with ffmpeg_graceful

Simple Execution

Use ffmpeg_slim when monitoring isn’t needed

Get Duration

Extract media duration before transcoding

Build docs developers (and LLMs) love