avio 0.14.2

Safe, high-level audio/video/image processing for Rust — backend-agnostic multimedia toolkit
Documentation
//! Apply video transition effects using `FilterGraphBuilder` + `Pipeline`.
//!
//! Available effects:
//!   `fade-in`       — fade from black at the start of the clip
//!   `fade-out`      — fade to black at the end of the clip
//!   `fade-in-white` — fade from white at the start of the clip
//!   `fade-out-white`— fade to white at the end of the clip
//!   `xfade`         — cross-dissolve transition between two input clips
//!
//! # Usage
//!
//! ```bash
//! cargo run --example transitions --features pipeline -- \
//!   --input    clip_a.mp4   \
//!   [--input-b clip_b.mp4]  \   # required for xfade
//!   --output   out.mp4      \
//!   --effect   fade-in      \
//!   [--start    0.0]         # fade start time in seconds (default: 0.0)
//!   [--duration 1.0]         # fade / transition duration in seconds (default: 1.0)
//!   [--offset   4.0]         # xfade: PTS offset where clip B starts (default: 4.0)
//! ```

use std::{
    io::{self, Write as _},
    path::Path,
    process,
};

use avio::{
    AudioCodec, EncoderConfig, FilterGraphBuilder, Pipeline, Progress, VideoCodec, XfadeTransition,
};

fn render_progress(p: &Progress) {
    match p.percent() {
        Some(pct) => {
            let bar_width = 20usize;
            #[allow(
                clippy::cast_possible_truncation,
                clippy::cast_sign_loss,
                clippy::cast_precision_loss
            )]
            let filled = ((pct / 100.0) * bar_width as f64).round() as usize;
            let filled = filled.min(bar_width);
            let bar = "=".repeat(filled) + &" ".repeat(bar_width - filled);
            print!("\r{pct:5.1}%  [{bar}]    ");
        }
        None => {
            print!("\r{} frames    ", p.frames_processed);
        }
    }
    let _ = io::stdout().flush();
}

fn main() {
    let mut args = std::env::args().skip(1);
    let mut input = None::<String>;
    let mut input_b = None::<String>;
    let mut output = None::<String>;
    let mut effect = None::<String>;
    let mut start: f64 = 0.0;
    let mut duration: f64 = 1.0;
    let mut offset: f64 = 4.0;

    while let Some(flag) = args.next() {
        match flag.as_str() {
            "--input" | "-i" => input = Some(args.next().unwrap_or_default()),
            "--input-b" => input_b = Some(args.next().unwrap_or_default()),
            "--output" | "-o" => output = Some(args.next().unwrap_or_default()),
            "--effect" | "-e" => effect = Some(args.next().unwrap_or_default()),
            "--start" => start = args.next().unwrap_or_default().parse().unwrap_or(0.0),
            "--duration" => duration = args.next().unwrap_or_default().parse().unwrap_or(1.0),
            "--offset" => offset = args.next().unwrap_or_default().parse().unwrap_or(4.0),
            other => {
                eprintln!("Unknown flag: {other}");
                process::exit(1);
            }
        }
    }

    let input = input.unwrap_or_else(|| {
        eprintln!(
            "Usage: transitions --input <file> --output <file> \
             --effect fade-in|fade-out|fade-in-white|fade-out-white|xfade [options]"
        );
        process::exit(1);
    });
    let output = output.unwrap_or_else(|| {
        eprintln!("--output is required");
        process::exit(1);
    });
    let effect = effect.unwrap_or_else(|| {
        eprintln!(
            "--effect is required \
             (fade-in|fade-out|fade-in-white|fade-out-white|xfade)"
        );
        process::exit(1);
    });

    let in_name = Path::new(&input)
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or(&input);
    let out_name = Path::new(&output)
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or(&output);

    // ── Build filter graph ────────────────────────────────────────────────────

    let filter_result = match effect.as_str() {
        "fade-in" => {
            println!("Input:   {in_name}");
            println!("Effect:  fade_in  (start={start:.1}s  duration={duration:.1}s)");
            println!("Output:  {out_name}");
            FilterGraphBuilder::new().fade_in(start, duration).build()
        }
        "fade-out" => {
            println!("Input:   {in_name}");
            println!("Effect:  fade_out  (start={start:.1}s  duration={duration:.1}s)");
            println!("Output:  {out_name}");
            FilterGraphBuilder::new().fade_out(start, duration).build()
        }
        "fade-in-white" => {
            println!("Input:   {in_name}");
            println!("Effect:  fade_in_white  (start={start:.1}s  duration={duration:.1}s)");
            println!("Output:  {out_name}");
            FilterGraphBuilder::new()
                .fade_in_white(start, duration)
                .build()
        }
        "fade-out-white" => {
            println!("Input:   {in_name}");
            println!("Effect:  fade_out_white  (start={start:.1}s  duration={duration:.1}s)");
            println!("Output:  {out_name}");
            FilterGraphBuilder::new()
                .fade_out_white(start, duration)
                .build()
        }
        "xfade" => {
            let b = input_b.unwrap_or_else(|| {
                eprintln!("--input-b is required for xfade");
                process::exit(1);
            });
            let b_name = Path::new(&b)
                .file_name()
                .and_then(|n| n.to_str())
                .unwrap_or(&b);
            println!("Input A: {in_name}");
            println!("Input B: {b_name}");
            println!("Effect:  xfade (dissolve  duration={duration:.1}s  offset={offset:.1}s)");
            println!("Output:  {out_name}");

            let filter = match FilterGraphBuilder::new()
                .xfade(XfadeTransition::Dissolve, duration, offset)
                .build()
            {
                Ok(fg) => fg,
                Err(e) => {
                    eprintln!("Error building filter graph: {e}");
                    process::exit(1);
                }
            };

            println!();

            let config = EncoderConfig::builder()
                .video_codec(VideoCodec::H264)
                .audio_codec(AudioCodec::Aac)
                .crf(23)
                .build();

            let pipeline = match Pipeline::builder()
                .input(&input)
                .input(&b)
                .filter(filter)
                .output(&output, config)
                .on_progress(|p: &Progress| {
                    render_progress(p);
                    true
                })
                .build()
            {
                Ok(p) => p,
                Err(e) => {
                    eprintln!("Error: {e}");
                    process::exit(1);
                }
            };

            if let Err(e) = pipeline.run() {
                println!();
                eprintln!("Error: {e}");
                process::exit(1);
            }

            println!();

            let size_str = match std::fs::metadata(&output) {
                Ok(m) => {
                    #[allow(clippy::cast_precision_loss)]
                    let kb = m.len() as f64 / 1024.0;
                    if kb < 1024.0 {
                        format!("{kb:.0} KB")
                    } else {
                        format!("{:.1} MB", kb / 1024.0)
                    }
                }
                Err(_) => "(unknown size)".to_string(),
            };

            println!("Done. {out_name}  {size_str}");
            return;
        }
        other => {
            eprintln!(
                "Unknown effect '{other}' \
                 (try fade-in, fade-out, fade-in-white, fade-out-white, xfade)"
            );
            process::exit(1);
        }
    };

    let filter = match filter_result {
        Ok(fg) => fg,
        Err(e) => {
            eprintln!("Error building filter graph: {e}");
            process::exit(1);
        }
    };

    println!();

    // ── Assemble pipeline ─────────────────────────────────────────────────────

    let config = EncoderConfig::builder()
        .video_codec(VideoCodec::H264)
        .audio_codec(AudioCodec::Aac)
        .crf(23)
        .build();

    let pipeline = match Pipeline::builder()
        .input(&input)
        .filter(filter)
        .output(&output, config)
        .on_progress(|p: &Progress| {
            render_progress(p);
            true
        })
        .build()
    {
        Ok(p) => p,
        Err(e) => {
            eprintln!("Error: {e}");
            process::exit(1);
        }
    };

    if let Err(e) = pipeline.run() {
        println!();
        eprintln!("Error: {e}");
        process::exit(1);
    }

    println!();

    let size_str = match std::fs::metadata(&output) {
        Ok(m) => {
            #[allow(clippy::cast_precision_loss)]
            let kb = m.len() as f64 / 1024.0;
            if kb < 1024.0 {
                format!("{kb:.0} KB")
            } else {
                format!("{:.1} MB", kb / 1024.0)
            }
        }
        Err(_) => "(unknown size)".to_string(),
    };

    println!("Done. {out_name}  {size_str}");
}