use std::{
io::{self, Write as _},
path::Path,
process,
sync::{Arc, Mutex},
time::Instant,
};
use avio::{AudioCodec, EncoderConfig, HwAccel, Pipeline, PipelineError, Progress, VideoCodec};
fn format_elapsed(d: std::time::Duration) -> String {
let s = d.as_secs();
let m = s / 60;
let h = m / 60;
if h > 0 {
format!("{h:02}:{:02}:{:02}", m % 60, s % 60)
} else {
format!("{:02}:{:02}", m, s % 60)
}
}
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);
let elapsed = format_elapsed(p.elapsed);
print!("\r{pct:5.1}% [{bar}] {elapsed} ");
}
None => {
print!(
"\r{} frames {} ",
p.frames_processed,
format_elapsed(p.elapsed)
);
}
}
let _ = io::stdout().flush();
}
fn main() {
let mut args = std::env::args().skip(1);
let mut input = None::<String>;
let mut output = None::<String>;
let mut hw_str = None::<String>;
let mut codec_str = "h264".to_string();
let mut crf: u32 = 23;
while let Some(flag) = args.next() {
match flag.as_str() {
"--input" | "-i" => input = Some(args.next().unwrap_or_default()),
"--output" | "-o" => output = Some(args.next().unwrap_or_default()),
"--hw" => hw_str = Some(args.next().unwrap_or_default()),
"--codec" | "-c" => codec_str = args.next().unwrap_or_else(|| "h264".to_string()),
"--crf" => {
let v = args.next().unwrap_or_default();
crf = v.parse().unwrap_or(23);
}
other => {
eprintln!("Unknown flag: {other}");
process::exit(1);
}
}
}
let input = input.unwrap_or_else(|| {
eprintln!(
"Usage: hw_transcode --input <file> --output <file> \
--hw cuda|videotoolbox|vaapi|none [--codec h264|h265] [--crf N]"
);
process::exit(1);
});
let output = output.unwrap_or_else(|| {
eprintln!("--output is required");
process::exit(1);
});
let hw_str = hw_str.unwrap_or_else(|| {
eprintln!("--hw is required (cuda|videotoolbox|vaapi|none)");
process::exit(1);
});
let hw_accel: Option<HwAccel> = match hw_str.to_lowercase().as_str() {
"cuda" | "nvenc" => Some(HwAccel::Cuda),
"videotoolbox" => Some(HwAccel::VideoToolbox),
"vaapi" => Some(HwAccel::Vaapi),
"none" | "software" => None,
other => {
eprintln!("Unknown hw backend '{other}' (try cuda, videotoolbox, vaapi, none)");
process::exit(1);
}
};
let video_codec = match codec_str.to_lowercase().as_str() {
"h264" | "avc" => VideoCodec::H264,
"h265" | "hevc" => VideoCodec::H265,
other => {
eprintln!("Unknown codec '{other}' (try h264, h265)");
process::exit(1);
}
};
let hw_label = hw_accel.map_or("software", |hw| match hw {
HwAccel::Cuda => "CUDA (NVENC)",
HwAccel::VideoToolbox => "VideoToolbox",
HwAccel::Vaapi => "VA-API",
});
let codec_label = match video_codec {
VideoCodec::H264 => "H.264",
VideoCodec::H265 => "H.265",
_ => "unknown",
};
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);
println!("Input: {in_name}");
println!("Output: {out_name} {codec_label} {hw_label} crf={crf}");
println!();
let mut b = EncoderConfig::builder()
.video_codec(video_codec)
.audio_codec(AudioCodec::Aac)
.crf(crf);
if let Some(hw) = hw_accel {
b = b.hardware(hw);
}
let config = b.build();
let start = Instant::now();
let last_frames: Arc<Mutex<u64>> = Arc::new(Mutex::new(0));
let last_frames_cb = Arc::clone(&last_frames);
let pipeline = match Pipeline::builder()
.input(&input)
.output(&output, config)
.on_progress(move |p: &Progress| {
render_progress(p);
if let Ok(mut f) = last_frames_cb.lock() {
*f = p.frames_processed;
}
true
})
.build()
{
Ok(p) => p,
Err(e) => {
eprintln!("Error: {e}");
process::exit(1);
}
};
match pipeline.run() {
Ok(()) | Err(PipelineError::Cancelled) => {}
Err(e) => {
println!();
eprintln!("Error: {e}");
if hw_accel.is_some() {
eprintln!(
"Hint: hardware backend may be unavailable on this system. \
Try --hw none to use software encoding."
);
}
process::exit(1);
}
}
println!();
let elapsed = format_elapsed(start.elapsed());
let frames = *last_frames.lock().unwrap_or_else(|e| e.into_inner());
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} {frames} frames {elapsed}");
}