use std::{path::Path, process};
use avio::{
BitrateMode, ColorPrimaries, ColorSpace, ColorTransfer, H265Options, H265Profile,
Hdr10Metadata, MasteringDisplay, PixelFormat, Preset, VideoCodec, VideoCodecOptions,
VideoDecoder, VideoEncoder,
};
fn main() {
let mut args = std::env::args().skip(1);
let mut input = None::<String>;
let mut output = None::<String>;
let mut max_cll: u16 = 1000;
let mut max_fall: u16 = 400;
let mut hlg = false;
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()),
"--max-cll" => {
let v = args.next().unwrap_or_default();
max_cll = v.parse().unwrap_or(1000);
}
"--max-fall" => {
let v = args.next().unwrap_or_default();
max_fall = v.parse().unwrap_or(400);
}
"--hlg" => hlg = true,
other => {
eprintln!("Unknown flag: {other}");
process::exit(1);
}
}
}
let input = input.unwrap_or_else(|| {
eprintln!(
"Usage: hdr10_encode --input <file> --output <file> \
[--max-cll N] [--max-fall N] [--hlg]"
);
process::exit(1);
});
let output = output.unwrap_or_else(|| {
eprintln!("--output is required");
process::exit(1);
});
let probe = match VideoDecoder::open(&input).build() {
Ok(d) => d,
Err(e) => {
eprintln!("Error opening input: {e}");
process::exit(1);
}
};
let width = probe.width();
let height = probe.height();
let fps = probe.frame_rate();
drop(probe);
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} {width}×{height} {fps:.2} fps");
let mut enc_builder = VideoEncoder::create(&output)
.video(width, height, fps)
.video_codec(VideoCodec::H265)
.bitrate_mode(BitrateMode::Crf(22))
.preset(Preset::Fast)
.pixel_format(PixelFormat::Yuv420p10le)
.codec_options(VideoCodecOptions::H265(H265Options {
profile: H265Profile::Main10,
..H265Options::default()
}));
if hlg {
println!("Mode: HLG colour tags (no MaxCLL/MaxFALL side data)");
enc_builder = enc_builder
.color_transfer(ColorTransfer::Hlg)
.color_space(ColorSpace::Bt2020)
.color_primaries(ColorPrimaries::Bt2020);
} else {
println!("Mode: HDR10 MaxCLL={max_cll} nits MaxFALL={max_fall} nits");
let meta = Hdr10Metadata {
max_cll,
max_fall,
mastering_display: MasteringDisplay {
red_x: 17000,
red_y: 8500,
green_x: 13250,
green_y: 34500,
blue_x: 7500,
blue_y: 3000,
white_x: 15635,
white_y: 16450,
min_luminance: 50,
max_luminance: 10_000_000,
},
};
enc_builder = enc_builder.hdr10_metadata(meta);
}
println!("Codec: H.265 Main10, yuv420p10le");
println!("Output: {out_name}");
println!();
let mut encoder = match enc_builder.build() {
Ok(e) => e,
Err(e) => {
eprintln!("Error building encoder: {e}");
process::exit(1);
}
};
println!(
"Encoder opened: actual_codec={}",
encoder.actual_video_codec()
);
println!("Encoding...");
let mut decoder = match VideoDecoder::open(&input).build() {
Ok(d) => d,
Err(e) => {
eprintln!("Error opening decoder: {e}");
process::exit(1);
}
};
let mut frames: u64 = 0;
loop {
let frame = match decoder.decode_one() {
Ok(Some(f)) => f,
Ok(None) => break,
Err(e) => {
eprintln!("Decode error: {e}");
process::exit(1);
}
};
if let Err(e) = encoder.push_video(&frame) {
eprintln!("Encode error: {e}");
process::exit(1);
}
frames += 1;
}
if let Err(e) = encoder.finish() {
eprintln!("Error finalising output: {e}");
process::exit(1);
}
#[allow(clippy::cast_precision_loss)]
let kb = std::fs::metadata(&output).map_or(0, |m| m.len()) as f64 / 1024.0;
let size_str = if kb < 1024.0 {
format!("{kb:.0} KB")
} else {
format!("{:.1} MB", kb / 1024.0)
};
println!("Done. {out_name} {size_str} {frames} frames");
if !hlg {
println!(
"Verify with: ffprobe -show_streams {out_name} | grep -E 'max_content|max_average'"
);
}
}