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);
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!();
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}");
}