diff --git a/CHANGELOG.md b/CHANGELOG.md index c84a68c..1903d6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - Disable when using custom min/max crf ranges under half the default. * Add sample-encode info to crf-search & auto-encode. Show sample progress and encoding/vmaf fps. * Improve sample-encode progress format consistency. +* Add crf-search `-v` flag to print per-sample results. +* Add auto-encode `-v` flag to print per-crf results, `-vv` to also print per-sample results. # v0.7.19 * Fix stdin handling sometimes breaking bash shells. diff --git a/Cargo.lock b/Cargo.lock index 28f5de2..f389fb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,6 +10,7 @@ dependencies = [ "async-stream", "blake3", "clap", + "clap-verbosity-flag", "clap_complete", "console", "dirs", @@ -224,6 +225,16 @@ dependencies = [ "clap_derive", ] +[[package]] +name = "clap-verbosity-flag" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e099138e1807662ff75e2cebe4ae2287add879245574489f9b1588eb5e5564ed" +dependencies = [ + "clap", + "log", +] + [[package]] name = "clap_builder" version = "4.5.21" diff --git a/Cargo.toml b/Cargo.toml index 333bc14..3722672 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ anyhow = "1.0.53" async-stream = "0.3.5" blake3 = "1.3.3" clap = { version = "4", features = ["derive", "env", "wrap_help"] } +clap-verbosity-flag = "2.2.2" clap_complete = "4.4.10" console = "0.15.4" dirs = "5" diff --git a/src/command/auto_encode.rs b/src/command/auto_encode.rs index 077e7fd..ebf8cc5 100644 --- a/src/command/auto_encode.rs +++ b/src/command/auto_encode.rs @@ -24,6 +24,9 @@ const BAR_LEN: u64 = 1024 * 1024 * 1024; /// Two phases: /// * crf-search to determine the best --crf value /// * ffmpeg & SvtAv1EncApp to encode using the settings +/// +/// Use -v to print per-crf results. +/// Use -vv to print per-sample results. #[derive(Parser)] #[clap(verbatim_doc_comment)] #[group(skip)] @@ -41,7 +44,6 @@ pub async fn auto_encode(Args { mut search, encode }: Args) -> anyhow::Result<() const SPINNER_FINISHED: &str = "{spinner:.cyan.bold} {elapsed_precise:.bold} {prefix} {wide_bar:.cyan/blue} ({msg})"; - search.quiet = true; let defaulting_output = encode.output.is_none(); let input_probe = Arc::new(ffprobe::probe(&search.args.input)); @@ -69,6 +71,7 @@ pub async fn auto_encode(Args { mut search, encode }: Args) -> anyhow::Result<() let max_encoded_percent = search.max_encoded_percent; let enc_args = search.args.clone(); let thorough = search.thorough; + let verbose = search.verbose.clone(); let mut crf_search = pin!(crf_search::run(search, input_probe.clone())); let mut best = None; @@ -121,7 +124,26 @@ pub async fn auto_encode(Args { mut search, encode }: Args) -> anyhow::Result<() Work::Vmaf => bar.set_message(format!("vmaf {fps} fps, ")), } } - Ok(crf_search::Update::RunResult(..)) => {} + Ok(crf_search::Update::SampleResult { + crf, + sample, + result, + }) => { + if verbose + .log_level() + .is_some_and(|lvl| lvl > log::Level::Warn) + { + result.print_attempt(&bar, sample, Some(crf)) + } + } + Ok(crf_search::Update::RunResult(result)) => { + if verbose + .log_level() + .is_some_and(|lvl| lvl > log::Level::Error) + { + result.print_attempt(&bar, min_vmaf, max_encoded_percent) + } + } Ok(crf_search::Update::Done(result)) => best = Some(result), } } diff --git a/src/command/crf_search.rs b/src/command/crf_search.rs index 47fc3bc..98bf9df 100644 --- a/src/command/crf_search.rs +++ b/src/command/crf_search.rs @@ -36,6 +36,8 @@ const DEFAULT_MIN_CRF: f32 = 10.0; /// * Mean sample VMAF score /// * Predicted full encode size /// * Predicted full encode time +/// +/// Use -v to print per-sample results. #[derive(Parser)] #[clap(verbatim_doc_comment)] #[group(skip)] @@ -89,8 +91,8 @@ pub struct Args { #[clap(flatten)] pub vmaf: args::Vmaf, - #[arg(skip)] - pub quiet: bool, + #[command(flatten)] + pub verbose: clap_verbosity_flag::Verbosity, } pub async fn crf_search(mut args: Args) -> anyhow::Result<()> { @@ -110,12 +112,13 @@ pub async fn crf_search(mut args: Args) -> anyhow::Result<()> { let max_encoded_percent = args.max_encoded_percent; let thorough = args.thorough; let enc_args = args.args.clone(); + let verbose = args.verbose.clone(); let mut run = pin!(run(args, probe.into())); while let Some(update) = run.next().await { let update = update.inspect_err(|e| { if let Error::NoGoodCrf { last } = e { - last.print_attempt(&bar, min_vmaf, max_encoded_percent, false); + last.print_attempt(&bar, min_vmaf, max_encoded_percent); } })?; match update { @@ -145,9 +148,19 @@ pub async fn crf_search(mut args: Args) -> anyhow::Result<()> { Work::Vmaf => bar.set_message(format!("vmaf {fps} fps, ")), } } - Update::RunResult(result) => { - result.print_attempt(&bar, min_vmaf, max_encoded_percent, false) + Update::SampleResult { + crf, + sample, + result, + } => { + if verbose + .log_level() + .is_some_and(|lvl| lvl > log::Level::Error) + { + result.print_attempt(&bar, sample, Some(crf)) + } } + Update::RunResult(result) => result.print_attempt(&bar, min_vmaf, max_encoded_percent), Update::Done(best) => { info!("crf {} successful", best.crf()); bar.finish_with_message(""); @@ -176,9 +189,9 @@ pub fn run( crf_increment, thorough, sample, - quiet: _, cache, vmaf, + verbose: _, }: Args, input_probe: Arc, ) -> impl Stream> { @@ -233,7 +246,9 @@ pub fn run( sample_encode::Update::Status(status) => { yield Update::Status { crf_run: run, crf: args.crf, sample: status }; } - sample_encode::Update::SampleResult { .. } => {} + sample_encode::Update::SampleResult { sample, result } => { + yield Update::SampleResult { crf: args.crf, sample, result }; + } sample_encode::Update::Done(output) => sample_enc_output = Some(output), } } @@ -322,16 +337,7 @@ impl Sample { self.q.to_crf(self.crf_increment) } - fn print_attempt( - &self, - bar: &ProgressBar, - min_vmaf: f32, - max_encoded_percent: f32, - quiet: bool, - ) { - if quiet { - return; - } + pub fn print_attempt(&self, bar: &ProgressBar, min_vmaf: f32, max_encoded_percent: f32) { let crf_label = style("- crf").dim(); let mut crf = style(TerseF32(self.crf())); let vmaf_label = style("VMAF").dim(); @@ -464,6 +470,12 @@ pub enum Update { crf: f32, sample: sample_encode::Status, }, + SampleResult { + crf: f32, + /// Sample number `1,....,n` + sample: u64, + result: sample_encode::EncodeResult, + }, /// Run result (excludes successful final runs) RunResult(Sample), Done(Sample), diff --git a/src/command/sample_encode.rs b/src/command/sample_encode.rs index 4d6634f..508343a 100644 --- a/src/command/sample_encode.rs +++ b/src/command/sample_encode.rs @@ -110,27 +110,7 @@ pub async fn sample_encode(mut args: Args) -> anyhow::Result<()> { } bar.set_position((progress * BAR_LEN_F).round() as _); } - Update::SampleResult { - sample, - result: - EncodeResult { - sample_size, - encoded_size, - vmaf_score, - from_cache, - .. - }, - } => { - bar.println( - style!( - "- Sample {sample} ({:.0}%) vmaf {vmaf_score:.2}{}", - 100.0 * encoded_size as f32 / sample_size as f32, - if from_cache { " (cache)" } else { "" }, - ) - .dim() - .to_string(), - ); - } + Update::SampleResult { sample, result } => result.print_attempt(&bar, sample, None), Update::Done(output) => { bar.finish(); if io::stderr().is_terminal() { @@ -438,16 +418,38 @@ async fn sample( #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct EncodeResult { - sample_size: u64, - encoded_size: u64, - vmaf_score: f32, - encode_time: Duration, + pub sample_size: u64, + pub encoded_size: u64, + pub vmaf_score: f32, + pub encode_time: Duration, /// Duration of the sample. /// /// This should be close to `SAMPLE_SIZE` but may deviate due to how samples are cut. - sample_duration: Duration, + pub sample_duration: Duration, /// Result read from cache. - from_cache: bool, + pub from_cache: bool, +} + +impl EncodeResult { + pub fn print_attempt(&self, bar: &ProgressBar, sample_n: u64, crf: Option) { + let Self { + sample_size, + encoded_size, + vmaf_score, + from_cache, + .. + } = self; + bar.println( + style!( + "- {}Sample {sample_n} ({:.0}%) vmaf {vmaf_score:.2}{}", + crf.map(|crf| format!("crf {crf}: ")).unwrap_or_default(), + 100.0 * *encoded_size as f32 / *sample_size as f32, + if *from_cache { " (cache)" } else { "" }, + ) + .dim() + .to_string(), + ); + } } trait EncodeResults {