From 667586a3a089946c03e344c49844d7b87c47fe18 Mon Sep 17 00:00:00 2001 From: Alex Butler Date: Fri, 15 Nov 2024 11:33:11 +0000 Subject: [PATCH 1/5] sample-encode: allow rhs progress info to resize --- Cargo.lock | 142 +++++++++++++++++++++++++++++------ src/command/sample_encode.rs | 12 +-- 2 files changed, 123 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d6797ec..28f5de2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,6 +181,12 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "byteorder" version = "1.5.0" @@ -195,9 +201,9 @@ checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "cc" -version = "1.1.36" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baee610e9452a8f6f0a1b6194ec09ff9e2d85dea54432acdae41aa0761c95d70" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" dependencies = [ "shlex", ] @@ -210,9 +216,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.20" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" dependencies = [ "clap_builder", "clap_derive", @@ -220,9 +226,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.20" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" dependencies = [ "anstream", "anstyle", @@ -233,9 +239,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.37" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11611dca53440593f38e6b25ec629de50b14cdfa63adc0fb856115a2c6d97595" +checksum = "d9647a559c112175f17cf724dc72d3645680a883c58481332779192b0d8e7a01" dependencies = [ "clap", ] @@ -254,9 +260,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" [[package]] name = "colorchoice" @@ -273,7 +279,7 @@ dependencies = [ "encode_unicode", "lazy_static", "libc", - "unicode-width", + "unicode-width 0.1.14", "windows-sys 0.52.0", ] @@ -536,15 +542,15 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "indicatif" -version = "0.17.8" +version = "0.17.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281" dependencies = [ "console", - "instant", "number_prefix", "portable-atomic", - "unicode-width", + "unicode-width 0.2.0", + "web-time", ] [[package]] @@ -574,6 +580,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -666,6 +681,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + [[package]] name = "option-ext" version = "0.2.0" @@ -767,9 +788,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.39" +version = "0.38.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" +checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" dependencies = [ "bitflags 2.6.0", "errno", @@ -792,18 +813,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", @@ -903,18 +924,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", @@ -1029,6 +1050,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1041,6 +1068,71 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/src/command/sample_encode.rs b/src/command/sample_encode.rs index a24353e..3d7ee52 100644 --- a/src/command/sample_encode.rs +++ b/src/command/sample_encode.rs @@ -69,7 +69,7 @@ pub struct Args { pub async fn sample_encode(mut args: Args) -> anyhow::Result<()> { let bar = ProgressBar::new(12).with_style( ProgressStyle::default_bar() - .template("{spinner:.cyan.bold} {elapsed_precise:.bold} {prefix} {wide_bar:.cyan/blue} ({msg:13} eta {eta})")? + .template("{spinner:.cyan.bold} {elapsed_precise:.bold} {prefix} {wide_bar:.cyan/blue} ({msg}eta {eta})")? .progress_chars(PROGRESS_CHARS) ); bar.enable_steady_tick(Duration::from_millis(100)); @@ -156,7 +156,7 @@ pub async fn run( let mut results = Vec::new(); loop { - bar.set_message("sampling,"); + bar.set_message("sampling, "); let (sample_idx, sample) = match sample_tasks.recv().await { Some(s) => s, None => break, @@ -205,7 +205,7 @@ pub async fn run( result } (None, key) => { - bar.set_message("encoding,"); + bar.set_message("encoding, "); let b = Instant::now(); let mut logger = ProgressLogger::new(module_path!(), b); let (encoded_sample, mut output) = ffmpeg::encode_sample( @@ -222,7 +222,7 @@ pub async fn run( time.as_micros_u64() + sample_idx * sample_duration_us * 2, ); if fps > 0.0 { - bar.set_message(format!("enc {fps} fps,")); + bar.set_message(format!("enc {fps} fps, ")); } logger.update(sample_duration, time, fps); } @@ -232,7 +232,7 @@ pub async fn run( let encoded_probe = ffprobe::probe(&encoded_sample); // calculate vmaf - bar.set_message("vmaf running,"); + bar.set_message("vmaf running, "); let mut vmaf = pin!(vmaf::run( &sample, &encoded_sample, @@ -260,7 +260,7 @@ pub async fn run( + sample_idx * sample_duration_us * 2, ); if fps > 0.0 { - bar.set_message(format!("vmaf {fps} fps,")); + bar.set_message(format!("vmaf {fps} fps, ")); } logger.update(sample_duration, time, fps); } From 7135bbe6c721ced930213dd2bce1cc7fb81cedf6 Mon Sep 17 00:00:00 2001 From: Alex Butler Date: Fri, 15 Nov 2024 13:56:51 +0000 Subject: [PATCH 2/5] Add sample/n enc+vmaf fps info to crf-search progress bar Reword sample_encode::run as stream output without progress bar logic Tweak sample-encode progress output to be more consistent --- src/command/crf_search.rs | 72 +++-- src/command/sample_encode.rs | 573 ++++++++++++++++++++--------------- 2 files changed, 371 insertions(+), 274 deletions(-) diff --git a/src/command/crf_search.rs b/src/command/crf_search.rs index 3028a5b..e6439fb 100644 --- a/src/command/crf_search.rs +++ b/src/command/crf_search.rs @@ -3,24 +3,31 @@ mod err; pub use err::Error; use crate::{ - command::{args, crf_search::err::ensure_or_no_good_crf, sample_encode, PROGRESS_CHARS}, + command::{ + args, + crf_search::err::ensure_or_no_good_crf, + sample_encode::{self, Work}, + PROGRESS_CHARS, + }, console_ext::style, - ffprobe, - ffprobe::Ffprobe, + ffprobe::{self, Ffprobe}, float::TerseF32, }; +use anyhow::Context; use clap::{ArgAction, Parser}; use console::style; use err::ensure_other; +use futures_util::StreamExt; use indicatif::{HumanBytes, HumanDuration, ProgressBar, ProgressStyle}; use log::info; use std::{ io::{self, IsTerminal}, + pin::pin, sync::Arc, time::Duration, }; -const BAR_LEN: u64 = 1_000_000_000; +const BAR_LEN: u64 = 1024 * 1024 * 1024; const DEFAULT_MIN_CRF: f32 = 10.0; /// Interpolated binary search using sample-encode to find the best crf @@ -91,9 +98,10 @@ pub struct Args { pub async fn crf_search(mut args: Args) -> anyhow::Result<()> { let bar = ProgressBar::new(12).with_style( ProgressStyle::default_bar() - .template("{spinner:.cyan.bold} {elapsed_precise:.bold} {wide_bar:.cyan/blue} ({msg}eta {eta})")? + .template("{spinner:.cyan.bold} {elapsed_precise:.bold} {prefix} {wide_bar:.cyan/blue} ({msg}eta {eta})")? .progress_chars(PROGRESS_CHARS) ); + bar.enable_steady_tick(Duration::from_millis(100)); let probe = ffprobe::probe(&args.args.input); let input_is_image = probe.is_image; @@ -177,7 +185,6 @@ async fn _run( }; bar.set_length(BAR_LEN); - let sample_bar = ProgressBar::hidden(); let mut crf_attempts = Vec::new(); for run in 1.. { @@ -189,32 +196,41 @@ async fn _run( _ => (crf_increment * 2_f32.powi(run as i32 - 1) * 0.1).max(0.1), }; args.crf = q.to_crf(crf_increment); - bar.set_message(format!("sampling crf {}, ", TerseF32(args.crf))); - let mut sample_task = tokio::task::spawn_local(sample_encode::run( - args.clone(), - input_probe.clone(), - sample_bar.clone(), - false, - )); - - let sample_task = loop { - match tokio::time::timeout(Duration::from_millis(100), &mut sample_task).await { - Err(_) => { - let sample_progress = sample_bar.position() as f64 - / sample_bar.length().unwrap_or(1).max(1) as f64; - bar.set_position(guess_progress(run, sample_progress, *thorough) as _); - } - Ok(o) => { - sample_bar.set_position(0); - break o; + let terse_crf = TerseF32(args.crf); + + let mut sample_enc = pin!(sample_encode::run(args.clone(), input_probe.clone())); + let mut sample_enc_output = None; + while let Some(update) = sample_enc.next().await { + match update? { + sample_encode::Update::Status { + work, + fps, + progress, + sample, + samples, + full_pass, + } => { + bar.set_position(guess_progress(run, progress, *thorough) as _); + match full_pass { + true => bar.set_prefix(format!("crf {terse_crf} full pass")), + false => bar.set_prefix(format!("crf {terse_crf} {sample}/{samples}")), + } + match work { + Work::Encode if fps <= 0.0 => bar.set_message("encoding, "), + Work::Encode => bar.set_message(format!("enc {fps} fps, ")), + Work::Vmaf if fps <= 0.0 => bar.set_message("vmaf, "), + Work::Vmaf => bar.set_message(format!("vmaf {fps} fps, ")), + } } + sample_encode::Update::SampleResult { .. } => {} + sample_encode::Update::Done(output) => sample_enc_output = Some(output), } - }; + } let sample = Sample { crf_increment, q, - enc: sample_task??, + enc: sample_enc_output.context("no sample output?")?, }; let from_cache = sample.enc.from_cache; crf_attempts.push(sample.clone()); @@ -389,7 +405,7 @@ fn vmaf_lerp_q(min_vmaf: f32, worse_q: &Sample, better_q: &Sample) -> u64 { } /// sample_progress: [0, 1] -fn guess_progress(run: usize, sample_progress: f64, thorough: bool) -> f64 { +fn guess_progress(run: usize, sample_progress: f32, thorough: bool) -> f64 { let total_runs_guess = match () { // Guess 6 iterations for a "thorough" search _ if thorough && run < 7 => 6.0, @@ -398,7 +414,7 @@ fn guess_progress(run: usize, sample_progress: f64, thorough: bool) -> f64 { // Otherwise guess next will work _ => run as f64, }; - ((run - 1) as f64 + sample_progress) * BAR_LEN as f64 / total_runs_guess + ((run - 1) as f64 + sample_progress as f64) * BAR_LEN as f64 / total_runs_guess } /// Calculate "q" as a quality value integer multiple of crf. diff --git a/src/command/sample_encode.rs b/src/command/sample_encode.rs index 3d7ee52..c906a20 100644 --- a/src/command/sample_encode.rs +++ b/src/command/sample_encode.rs @@ -16,10 +16,11 @@ use crate::{ use anyhow::{ensure, Context}; use clap::{ArgAction, Parser}; use console::style; +use futures_util::Stream; use indicatif::{HumanBytes, HumanDuration, ProgressBar, ProgressStyle}; use log::info; use std::{ - io::IsTerminal, + io::{self, IsTerminal}, path::{Path, PathBuf}, pin::pin, sync::Arc, @@ -67,7 +68,10 @@ pub struct Args { } pub async fn sample_encode(mut args: Args) -> anyhow::Result<()> { - let bar = ProgressBar::new(12).with_style( + const BAR_LEN: u64 = 1024 * 1024 * 1024; + const BAR_LEN_F: f32 = BAR_LEN as _; + + let bar = ProgressBar::new(BAR_LEN).with_style( ProgressStyle::default_bar() .template("{spinner:.cyan.bold} {elapsed_precise:.bold} {prefix} {wide_bar:.cyan/blue} ({msg}eta {eta})")? .progress_chars(PROGRESS_CHARS) @@ -77,285 +81,331 @@ pub async fn sample_encode(mut args: Args) -> anyhow::Result<()> { let probe = ffprobe::probe(&args.args.input); args.sample .set_extension_from_input(&args.args.input, &args.args.encoder, &probe); - run(args, probe.into(), bar, true).await?; + + let enc_args = args.args.clone(); + let crf = args.crf; + let stdout_fmt = args.stdout_format; + let input_is_image = probe.is_image; + + let mut run = pin!(run(args, probe.into())); + while let Some(update) = run.next().await { + match update? { + Update::Status { + work, + fps, + progress, + sample, + samples, + full_pass, + } => { + match full_pass { + true => bar.set_prefix("Full pass"), + false => bar.set_prefix(format!("Sample {sample}/{samples}")), + } + match work { + Work::Encode if fps <= 0.0 => bar.set_message("encoding, "), + Work::Encode => bar.set_message(format!("enc {fps} fps, ")), + Work::Vmaf if fps <= 0.0 => bar.set_message("vmaf, "), + Work::Vmaf => bar.set_message(format!("vmaf {fps} fps, ")), + } + 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::Done(output) => { + bar.finish(); + if io::stderr().is_terminal() { + // encode how-to hint + eprintln!( + "\n{} {}\n", + style("Encode with:").dim(), + style(enc_args.encode_hint(crf)).dim().italic(), + ); + } + // stdout result + stdout_fmt.print_result( + output.vmaf, + output.predicted_encode_size, + output.encode_percent, + output.predicted_encode_time, + input_is_image, + ); + } + } + } Ok(()) } -pub async fn run( +pub fn run( Args { args, crf, sample: sample_args, cache, - stdout_format, + stdout_format: _, vmaf, }: Args, input_probe: Arc, - bar: ProgressBar, - print_output: bool, -) -> anyhow::Result { - let input = Arc::new(args.input.clone()); - let input_pixel_format = input_probe.pixel_format(); - let input_is_image = input_probe.is_image; - let input_len = fs::metadata(&*input).await?.len(); - let enc_args = args.to_encoder_args(crf, &input_probe)?; - let duration = input_probe.duration.clone()?; - let input_fps = input_probe.fps.clone()?; - let samples = sample_args.sample_count(duration).max(1); - let keep = sample_args.keep; - let temp_dir = sample_args.temp_dir; - - let (samples, sample_duration, full_pass) = { - if input_is_image { - (1, duration.max(Duration::from_secs(1)), true) - } else if sample_args.sample_duration.is_zero() - || sample_args.sample_duration * samples as _ >= duration.mul_f64(0.85) - { - // if the sample time is most of the full input time just encode the whole thing - (1, duration, true) - } else { - let sample_duration = if input_fps > 0.0 { - // if sample-length is lower than a single frame use the frame time - let one_frame_duration = Duration::from_secs_f64(1.0 / input_fps); - sample_args.sample_duration.max(one_frame_duration) +) -> impl Stream> { + async_stream::try_stream! { + let input = Arc::new(args.input.clone()); + let input_pixel_format = input_probe.pixel_format(); + let input_is_image = input_probe.is_image; + let input_len = fs::metadata(&*input).await?.len(); + let enc_args = args.to_encoder_args(crf, &input_probe)?; + let duration = input_probe.duration.clone()?; + let input_fps = input_probe.fps.clone()?; + let samples = sample_args.sample_count(duration).max(1); + let keep = sample_args.keep; + let temp_dir = sample_args.temp_dir; + + let (samples, sample_duration, full_pass) = { + if input_is_image { + (1, duration.max(Duration::from_secs(1)), true) + } else if sample_args.sample_duration.is_zero() + || sample_args.sample_duration * samples as _ >= duration.mul_f64(0.85) + { + // if the sample time is most of the full input time just encode the whole thing + (1, duration, true) } else { - sample_args.sample_duration - }; - (samples, sample_duration, false) - } - }; - let sample_duration_us = sample_duration.as_micros_u64(); - bar.set_length(sample_duration_us * samples * 2); - - // Start creating copy samples async, this is IO bound & not cpu intensive - let (tx, mut sample_tasks) = tokio::sync::mpsc::unbounded_channel(); - let sample_temp = temp_dir.clone(); - let sample_in = input.clone(); - tokio::task::spawn_local(async move { - if full_pass { - // Use the entire video as a single sample - let _ = tx.send((0, Ok((sample_in.clone(), input_len)))); - } else { - for sample_idx in 0..samples { - let sample = sample( - sample_in.clone(), - sample_idx, - samples, - sample_duration, - duration, - input_fps, - sample_temp.clone(), - ) - .await; - if tx.send((sample_idx, sample)).is_err() { - break; - } + let sample_duration = if input_fps > 0.0 { + // if sample-length is lower than a single frame use the frame time + let one_frame_duration = Duration::from_secs_f64(1.0 / input_fps); + sample_args.sample_duration.max(one_frame_duration) + } else { + sample_args.sample_duration + }; + (samples, sample_duration, false) } - } - }); - - let mut results = Vec::new(); - loop { - bar.set_message("sampling, "); - let (sample_idx, sample) = match sample_tasks.recv().await { - Some(s) => s, - None => break, - }; - let sample_n = sample_idx + 1; - match full_pass { - true => bar.set_prefix("Full pass"), - false => bar.set_prefix(format!("Sample {sample_n}/{samples}")), }; - - let (sample, sample_size) = sample?; - - info!("encoding sample {sample_n}/{samples} crf {crf}",); - - // encode sample - let result = match cache::cached_encode( - cache, - &sample, - duration, - input.extension(), - input_len, - full_pass, - &enc_args, - &vmaf, - ) - .await - { - (Some(result), _) => { - bar.set_position(sample_n * sample_duration_us * 2); - bar.println( - style!( - "- Sample {sample_n} ({:.0}%) vmaf {:.2} (cache)", - 100.0 * result.encoded_size as f32 / sample_size as f32, - result.vmaf_score, + let sample_duration_us = sample_duration.as_micros_u64(); + + // Start creating copy samples async, this is IO bound & not cpu intensive + let (tx, mut sample_tasks) = tokio::sync::mpsc::unbounded_channel(); + let sample_temp = temp_dir.clone(); + let sample_in = input.clone(); + tokio::task::spawn_local(async move { + if full_pass { + // Use the entire video as a single sample + let _ = tx.send((0, Ok((sample_in.clone(), input_len)))); + } else { + for sample_idx in 0..samples { + let sample = sample( + sample_in.clone(), + sample_idx, + samples, + sample_duration, + duration, + input_fps, + sample_temp.clone(), ) - .dim() - .to_string(), - ); - if samples > 1 { - info!( - "sample {sample_n}/{samples} crf {crf} VMAF {:.2} ({:.0}%) (cache)", - result.vmaf_score, - 100.0 * result.encoded_size as f32 / sample_size as f32, - ); + .await; + if tx.send((sample_idx, sample)).is_err() { + break; + } } - result } - (None, key) => { - bar.set_message("encoding, "); - let b = Instant::now(); - let mut logger = ProgressLogger::new(module_path!(), b); - let (encoded_sample, mut output) = ffmpeg::encode_sample( - FfmpegEncodeArgs { - input: &sample, - ..enc_args.clone() - }, - temp_dir.clone(), - sample_args.extension.as_deref().unwrap_or("mkv"), - )?; - while let Some(progress) = output.next().await { - if let FfmpegOut::Progress { time, fps, .. } = progress? { - bar.set_position( - time.as_micros_u64() + sample_idx * sample_duration_us * 2, + }); + + let mut results = Vec::new(); + loop { + let (sample_idx, sample) = match sample_tasks.recv().await { + Some(s) => s, + None => break, + }; + let sample_n = sample_idx + 1; + let (sample, sample_size) = sample?; + + info!("encoding sample {sample_n}/{samples} crf {crf}"); + yield Update::Status { + work: Work::Encode, + fps: 0.0, + progress: sample_idx as f32 / samples as f32, + full_pass, + sample: sample_n, + samples, + }; + + // encode sample + let result = match cache::cached_encode( + cache, + &sample, + duration, + input.extension(), + input_len, + full_pass, + &enc_args, + &vmaf, + ) + .await + { + (Some(result), _) => { + if samples > 1 { + info!( + "sample {sample_n}/{samples} crf {crf} VMAF {:.2} ({:.0}%) (cache)", + result.vmaf_score, + 100.0 * result.encoded_size as f32 / sample_size as f32, ); - if fps > 0.0 { - bar.set_message(format!("enc {fps} fps, ")); - } - logger.update(sample_duration, time, fps); } + result } - let encode_time = b.elapsed(); - let encoded_size = fs::metadata(&encoded_sample).await?.len(); - let encoded_probe = ffprobe::probe(&encoded_sample); - - // calculate vmaf - bar.set_message("vmaf running, "); - let mut vmaf = pin!(vmaf::run( - &sample, - &encoded_sample, - &vmaf.ffmpeg_lavfi( - encoded_probe.resolution, - enc_args - .pix_fmt - .max(input_pixel_format.unwrap_or(PixelFormat::Yuv444p10le)), - args.vfilter.as_deref(), - ), - )?); - let mut logger = ProgressLogger::new("ab_av1::vmaf", Instant::now()); - let mut vmaf_score = None; - while let Some(vmaf) = vmaf.next().await { - match vmaf { - VmafOut::Done(score) => { - vmaf_score = Some(score); - break; + (None, key) => { + let b = Instant::now(); + let mut logger = ProgressLogger::new(module_path!(), b); + let (encoded_sample, mut output) = ffmpeg::encode_sample( + FfmpegEncodeArgs { + input: &sample, + ..enc_args.clone() + }, + temp_dir.clone(), + sample_args.extension.as_deref().unwrap_or("mkv"), + )?; + while let Some(enc_progress) = output.next().await { + if let FfmpegOut::Progress { time, fps, .. } = enc_progress? { + yield Update::Status { + work: Work::Encode, + fps, + progress: (time.as_micros_u64() + sample_idx * sample_duration_us * 2) as f32 + / (sample_duration_us * samples * 2) as f32, + full_pass, + sample: sample_n, + samples, + }; + logger.update(sample_duration, time, fps); } - VmafOut::Progress(FfmpegOut::Progress { time, fps, .. }) => { - bar.set_position( - sample_duration_us - // *24/fps adjusts for vmaf `-r 24` - + (time.as_micros_u64() as f64 * (24.0 / input_fps)).round() as u64 - + sample_idx * sample_duration_us * 2, - ); - if fps > 0.0 { - bar.set_message(format!("vmaf {fps} fps, ")); + } + let encode_time = b.elapsed(); + let encoded_size = fs::metadata(&encoded_sample).await?.len(); + let encoded_probe = ffprobe::probe(&encoded_sample); + + // calculate vmaf + yield Update::Status { + work: Work::Vmaf, + fps: 0.0, + progress: (sample_idx as f32 + 0.5) / samples as f32, + full_pass, + sample: sample_n, + samples, + }; + let vmaf = vmaf::run( + &sample, + &encoded_sample, + &vmaf.ffmpeg_lavfi( + encoded_probe.resolution, + enc_args + .pix_fmt + .max(input_pixel_format.unwrap_or(PixelFormat::Yuv444p10le)), + args.vfilter.as_deref(), + ), + )?; + let mut vmaf = pin!(vmaf); + let mut logger = ProgressLogger::new("ab_av1::vmaf", Instant::now()); + let mut vmaf_score = None; + while let Some(vmaf) = vmaf.next().await { + match vmaf { + VmafOut::Done(score) => { + vmaf_score = Some(score); + break; } - logger.update(sample_duration, time, fps); + VmafOut::Progress(FfmpegOut::Progress { time, fps, .. }) => { + yield Update::Status { + work: Work::Vmaf, + fps, + progress: (sample_duration_us + + time.as_micros_u64() + + sample_idx * sample_duration_us * 2) as f32 + / (sample_duration_us * samples * 2) as f32, + full_pass, + sample: sample_n, + samples, + }; + logger.update(sample_duration, time, fps); + } + VmafOut::Progress(_) => {} + VmafOut::Err(e) => Err(e)?, } - VmafOut::Progress(_) => {} - VmafOut::Err(e) => return Err(e), } - } - let vmaf_score = vmaf_score.context("no vmaf score")?; + let vmaf_score = vmaf_score.context("no vmaf score")?; - bar.println( - style!( - "- Sample {sample_n} ({:.0}%) vmaf {vmaf_score:.2}", - 100.0 * encoded_size as f32 / sample_size as f32 - ) - .dim() - .to_string(), - ); - if samples > 1 { - info!( - "sample {sample_n}/{samples} crf {crf} VMAF {vmaf_score:.2} ({:.0}%)", - 100.0 * encoded_size as f32 / sample_size as f32, - ); - } + if samples > 1 { + info!( + "sample {sample_n}/{samples} crf {crf} VMAF {vmaf_score:.2} ({:.0}%)", + 100.0 * encoded_size as f32 / sample_size as f32, + ); + } - let result = EncodeResult { - vmaf_score, - sample_size, - encoded_size, - encode_time, - sample_duration: encoded_probe - .duration - .ok() - .filter(|d| !d.is_zero()) - .unwrap_or(sample_duration), - from_cache: false, - }; + let result = EncodeResult { + vmaf_score, + sample_size, + encoded_size, + encode_time, + sample_duration: encoded_probe + .duration + .ok() + .filter(|d| !d.is_zero()) + .unwrap_or(sample_duration), + from_cache: false, + }; + + if let Some(k) = key { + cache::cache_result(k, &result).await?; + } - if let Some(k) = key { - cache::cache_result(k, &result).await?; - } + // Early clean. Note: Avoid cleaning copy samples + temporary::clean(true).await; + if !keep { + let _ = tokio::fs::remove_file(encoded_sample).await; + } - // Early clean. Note: Avoid cleaning copy samples - temporary::clean(true).await; - if !keep { - let _ = tokio::fs::remove_file(encoded_sample).await; + result } + }; - result - } - }; - - results.push(result); - } - bar.finish(); - - let output = Output { - vmaf: results.mean_vmaf(), - // Using file size * encode_percent can over-estimate. However, if it ends up less - // than the duration estimation it may turn out to be more accurate. - predicted_encode_size: results - .estimate_encode_size_by_duration(duration, full_pass) - .min(estimate_encode_size_by_file_percent(&results, &input, full_pass).await?), - encode_percent: results.encoded_percent_size(), - predicted_encode_time: results.estimate_encode_time(duration, full_pass), - from_cache: results.iter().all(|r| r.from_cache), - }; - info!( - "crf {crf} VMAF {:.2} predicted video stream size {} ({:.0}%) taking {}{}", - output.vmaf, - HumanBytes(output.predicted_encode_size), - output.encode_percent, - HumanDuration(output.predicted_encode_time), - if output.from_cache { " (cache)" } else { "" } - ); - - if print_output { - if std::io::stderr().is_terminal() { - // encode how-to hint - eprintln!( - "\n{} {}\n", - style("Encode with:").dim(), - style(args.encode_hint(crf)).dim().italic(), - ); + results.push(result.clone()); + yield Update::SampleResult { sample: sample_n, result }; } - // stdout result - stdout_format.print_result( + + let output = Output { + vmaf: results.mean_vmaf(), + // Using file size * encode_percent can over-estimate. However, if it ends up less + // than the duration estimation it may turn out to be more accurate. + predicted_encode_size: results + .estimate_encode_size_by_duration(duration, full_pass) + .min(estimate_encode_size_by_file_percent(&results, &input, full_pass).await?), + encode_percent: results.encoded_percent_size(), + predicted_encode_time: results.estimate_encode_time(duration, full_pass), + from_cache: results.iter().all(|r| r.from_cache), + }; + info!( + "crf {crf} VMAF {:.2} predicted video stream size {} ({:.0}%) taking {}{}", output.vmaf, - output.predicted_encode_size, + HumanBytes(output.predicted_encode_size), output.encode_percent, - output.predicted_encode_time, - input_is_image, + HumanDuration(output.predicted_encode_time), + if output.from_cache { " (cache)" } else { "" } ); - } - Ok(output) + yield Update::Done(output); + } } /// Copy a sample from the input to the temp_dir (or input dir). @@ -559,3 +609,34 @@ pub struct Output { /// All sample results were read from the cache. pub from_cache: bool, } + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum Work { + #[default] + Encode, + Vmaf, +} + +#[derive(Debug)] +pub enum Update { + Status { + /// Kind of work being performed + work: Work, + /// fps, `0.0` may be interpreted as "unknown" + fps: f32, + /// sample progress `[0, 1]` + progress: f32, + /// Sample number `1,....,n` + sample: u64, + /// Total samples + samples: u64, + /// Encoding the entire input video + full_pass: bool, + }, + SampleResult { + /// Sample number `1,....,n` + sample: u64, + result: EncodeResult, + }, + Done(Output), +} From a6aee240164c774edc08694b629c09e98a798fa1 Mon Sep 17 00:00:00 2001 From: Alex Butler Date: Fri, 15 Nov 2024 15:50:45 +0000 Subject: [PATCH 3/5] auto-encode: Add sample progress & fps info Rework crf_search::run as stream output without progress bar logic --- src/command/auto_encode.rs | 106 ++++++---- src/command/crf_search.rs | 359 ++++++++++++++++++---------------- src/command/crf_search/err.rs | 37 ++-- src/command/sample_encode.rs | 53 ++--- 4 files changed, 308 insertions(+), 247 deletions(-) diff --git a/src/command/auto_encode.rs b/src/command/auto_encode.rs index f8f520f..077e7fd 100644 --- a/src/command/auto_encode.rs +++ b/src/command/auto_encode.rs @@ -2,6 +2,7 @@ use crate::{ command::{ args, crf_search, encode::{self, default_output_name}, + sample_encode::{self, Work}, PROGRESS_CHARS, }, console_ext::style, @@ -9,10 +10,14 @@ use crate::{ float::TerseF32, temporary, }; +use anyhow::Context; use clap::Parser; use console::style; +use futures_util::StreamExt; use indicatif::{ProgressBar, ProgressStyle}; -use std::{sync::Arc, time::Duration}; +use std::{pin::pin, sync::Arc, time::Duration}; + +const BAR_LEN: u64 = 1024 * 1024 * 1024; /// Automatically determine the best crf to deliver the min-vmaf and use it to encode a video or image. /// @@ -32,9 +37,9 @@ pub struct Args { pub async fn auto_encode(Args { mut search, encode }: Args) -> anyhow::Result<()> { const SPINNER_RUNNING: &str = - "{spinner:.cyan.bold} {prefix} {elapsed_precise:.bold} {wide_bar:.cyan/blue} ({msg}eta {eta})"; + "{spinner:.cyan.bold} {elapsed_precise:.bold} {prefix} {wide_bar:.cyan/blue} ({msg}eta {eta})"; const SPINNER_FINISHED: &str = - "{spinner:.cyan.bold} {prefix} {elapsed_precise:.bold} {wide_bar:.cyan/blue} ({msg})"; + "{spinner:.cyan.bold} {elapsed_precise:.bold} {prefix} {wide_bar:.cyan/blue} ({msg})"; search.quiet = true; let defaulting_output = encode.output.is_none(); @@ -49,53 +54,86 @@ pub async fn auto_encode(Args { mut search, encode }: Args) -> anyhow::Result<() }); search.sample.set_extension_from_output(&output); - let bar = ProgressBar::new(12).with_style( + let bar = ProgressBar::new(BAR_LEN).with_style( ProgressStyle::default_bar() .template(SPINNER_RUNNING)? .progress_chars(PROGRESS_CHARS), ); - bar.set_prefix("Searching"); if defaulting_output { let out = shell_escape::escape(output.display().to_string().into()); bar.println(style!("Encoding {out}").dim().to_string()); } - let best = match crf_search::run(&search, input_probe.clone(), bar.clone()).await { - Ok(best) => best, - Err(err) => { - if let crf_search::Error::NoGoodCrf { last } = &err { - // show last sample attempt in progress bar - bar.set_style( - ProgressStyle::default_bar() - .template(SPINNER_FINISHED)? - .progress_chars(PROGRESS_CHARS), - ); - let mut vmaf = style(last.enc.vmaf); - if last.enc.vmaf < search.min_vmaf { - vmaf = vmaf.red(); + let min_vmaf = search.min_vmaf; + let max_encoded_percent = search.max_encoded_percent; + let enc_args = search.args.clone(); + let thorough = search.thorough; + + let mut crf_search = pin!(crf_search::run(search, input_probe.clone())); + let mut best = None; + while let Some(update) = crf_search.next().await { + match update { + Err(err) => { + if let crf_search::Error::NoGoodCrf { last } = &err { + // show last sample attempt in progress bar + bar.set_style( + ProgressStyle::default_bar() + .template(SPINNER_FINISHED)? + .progress_chars(PROGRESS_CHARS), + ); + let mut vmaf = style(last.enc.vmaf); + if last.enc.vmaf < min_vmaf { + vmaf = vmaf.red(); + } + let mut percent = style!("{:.0}%", last.enc.encode_percent); + if last.enc.encode_percent > max_encoded_percent as _ { + percent = percent.red(); + } + bar.finish_with_message(format!("VMAF {vmaf:.2}, size {percent}")); + } + bar.finish(); + return Err(err.into()); + } + Ok(crf_search::Update::Status { + crf_run, + crf, + sample: + sample_encode::Status { + work, + fps, + progress, + sample, + samples, + full_pass, + }, + }) => { + bar.set_position(crf_search::guess_progress(crf_run, progress, thorough) as _); + let crf = TerseF32(crf); + match full_pass { + true => bar.set_prefix(format!("crf {crf} full pass")), + false => bar.set_prefix(format!("crf {crf} {sample}/{samples}")), } - let mut percent = style!("{:.0}%", last.enc.encode_percent); - if last.enc.encode_percent > search.max_encoded_percent as _ { - percent = percent.red(); + match work { + Work::Encode if fps <= 0.0 => bar.set_message("encoding, "), + Work::Encode => bar.set_message(format!("enc {fps} fps, ")), + Work::Vmaf if fps <= 0.0 => bar.set_message("vmaf, "), + Work::Vmaf => bar.set_message(format!("vmaf {fps} fps, ")), } - bar.finish_with_message(format!( - "crf {}, VMAF {vmaf:.2}, size {percent}", - style(TerseF32(last.crf())).red(), - )); } - bar.finish(); - return Err(err.into()); + Ok(crf_search::Update::RunResult(..)) => {} + Ok(crf_search::Update::Done(result)) => best = Some(result), } - }; + } + let best = best.context("no crf-search best?")?; + bar.set_style( ProgressStyle::default_bar() .template(SPINNER_FINISHED)? .progress_chars(PROGRESS_CHARS), ); bar.finish_with_message(format!( - "crf {}, VMAF {:.2}, size {}", - style(TerseF32(best.crf())).green(), + "VMAF {:.2}, size {}", style(best.enc.vmaf).green(), style(format!("{:.0}%", best.enc.encode_percent)).green(), )); @@ -103,15 +141,15 @@ pub async fn auto_encode(Args { mut search, encode }: Args) -> anyhow::Result<() let bar = ProgressBar::new(12).with_style( ProgressStyle::default_bar() - .template("{spinner:.cyan.bold} {prefix} {elapsed_precise:.bold} {wide_bar:.cyan/blue} ({msg}eta {eta})")? - .progress_chars(PROGRESS_CHARS) + .template(SPINNER_RUNNING)? + .progress_chars(PROGRESS_CHARS), ); - bar.set_prefix("Encoding "); + bar.set_prefix("Encoding"); bar.enable_steady_tick(Duration::from_millis(100)); encode::run( encode::Args { - args: search.args, + args: enc_args, crf: best.crf(), encode: args::EncodeToOutput { output: Some(output), diff --git a/src/command/crf_search.rs b/src/command/crf_search.rs index e6439fb..47fc3bc 100644 --- a/src/command/crf_search.rs +++ b/src/command/crf_search.rs @@ -5,7 +5,6 @@ pub use err::Error; use crate::{ command::{ args, - crf_search::err::ensure_or_no_good_crf, sample_encode::{self, Work}, PROGRESS_CHARS, }, @@ -16,8 +15,7 @@ use crate::{ use anyhow::Context; use clap::{ArgAction, Parser}; use console::style; -use err::ensure_other; -use futures_util::StreamExt; +use futures_util::{Stream, StreamExt}; use indicatif::{HumanBytes, HumanDuration, ProgressBar, ProgressStyle}; use log::info; use std::{ @@ -96,7 +94,7 @@ pub struct Args { } pub async fn crf_search(mut args: Args) -> anyhow::Result<()> { - let bar = ProgressBar::new(12).with_style( + let bar = ProgressBar::new(BAR_LEN).with_style( ProgressStyle::default_bar() .template("{spinner:.cyan.bold} {elapsed_precise:.bold} {prefix} {wide_bar:.cyan/blue} ({msg}eta {eta})")? .progress_chars(PROGRESS_CHARS) @@ -108,35 +106,67 @@ pub async fn crf_search(mut args: Args) -> anyhow::Result<()> { args.sample .set_extension_from_input(&args.args.input, &args.args.encoder, &probe); - let best = run(&args, probe.into(), bar.clone()).await; - bar.finish(); - let best = best?; - - if std::io::stderr().is_terminal() { - // encode how-to hint - eprintln!( - "\n{} {}\n", - style("Encode with:").dim(), - style(args.args.encode_hint(best.crf())).dim().italic(), - ); - } - - StdoutFormat::Human.print_result(&best, input_is_image); - - Ok(()) -} + let min_vmaf = args.min_vmaf; + let max_encoded_percent = args.max_encoded_percent; + let thorough = args.thorough; + let enc_args = args.args.clone(); -pub async fn run( - args: &Args, - input_probe: Arc, - bar: ProgressBar, -) -> Result { - _run(args, input_probe, bar) - .await - .inspect(|s| info!("crf {} successful", s.crf())) + 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); + } + })?; + match update { + Update::Status { + crf_run, + crf, + sample: + sample_encode::Status { + work, + fps, + progress, + sample, + samples, + full_pass, + }, + } => { + bar.set_position(guess_progress(crf_run, progress, thorough) as _); + let crf = TerseF32(crf); + match full_pass { + true => bar.set_prefix(format!("crf {crf} full pass")), + false => bar.set_prefix(format!("crf {crf} {sample}/{samples}")), + } + match work { + Work::Encode if fps <= 0.0 => bar.set_message("encoding, "), + Work::Encode => bar.set_message(format!("enc {fps} fps, ")), + Work::Vmaf if fps <= 0.0 => bar.set_message("vmaf, "), + Work::Vmaf => bar.set_message(format!("vmaf {fps} fps, ")), + } + } + Update::RunResult(result) => { + result.print_attempt(&bar, min_vmaf, max_encoded_percent, false) + } + Update::Done(best) => { + info!("crf {} successful", best.crf()); + bar.finish_with_message(""); + if std::io::stderr().is_terminal() { + eprintln!( + "\n{} {}\n", + style("Encode with:").dim(), + style(enc_args.encode_hint(best.crf())).dim().italic(), + ); + } + StdoutFormat::Human.print_result(&best, input_is_image); + return Ok(()); + } + } + } + unreachable!() } -async fn _run( +pub fn run( Args { args, min_vmaf, @@ -146,154 +176,138 @@ async fn _run( crf_increment, thorough, sample, - quiet, + quiet: _, cache, vmaf, - }: &Args, + }: Args, input_probe: Arc, - bar: ProgressBar, -) -> Result { - let default_max_crf = args.encoder.default_max_crf(); - let max_crf = max_crf.unwrap_or(default_max_crf); - ensure_other!(*min_crf < max_crf, "Invalid --min-crf & --max-crf"); - - // Whether to make the 2nd iteration on the ~20%/~80% crf point instead of the min/max to - // improve interpolation by narrowing the crf range a 20% (or 30%) subrange. - // - // 20/80% is preferred to 25/75% to account for searches in the "middle" benefitting from - // having both bounds computed after the 2nd iteration, whereas the two edges must compute - // the min/max crf on the 3rd iter. - // - // If a custom crf range is being used under half the default, this 2nd cut is not needed. - let cut_on_iter2 = (max_crf - *min_crf) > (default_max_crf - DEFAULT_MIN_CRF) * 0.5; - - let crf_increment = crf_increment - .unwrap_or_else(|| args.encoder.default_crf_increment()) - .max(0.001); - - let min_q = q_from_crf(*min_crf, crf_increment); - let max_q = q_from_crf(max_crf, crf_increment); - let mut q: u64 = (min_q + max_q) / 2; - - let mut args = sample_encode::Args { - args: args.clone(), - crf: 0.0, - sample: sample.clone(), - cache: *cache, - stdout_format: sample_encode::StdoutFormat::Json, - vmaf: vmaf.clone(), - }; +) -> impl Stream> { + async_stream::try_stream! { + let default_max_crf = args.encoder.default_max_crf(); + let max_crf = max_crf.unwrap_or(default_max_crf); + Error::ensure_other(min_crf < max_crf, "Invalid --min-crf & --max-crf")?; + + // Whether to make the 2nd iteration on the ~20%/~80% crf point instead of the min/max to + // improve interpolation by narrowing the crf range a 20% (or 30%) subrange. + // + // 20/80% is preferred to 25/75% to account for searches in the "middle" benefitting from + // having both bounds computed after the 2nd iteration, whereas the two edges must compute + // the min/max crf on the 3rd iter. + // + // If a custom crf range is being used under half the default, this 2nd cut is not needed. + let cut_on_iter2 = (max_crf - min_crf) > (default_max_crf - DEFAULT_MIN_CRF) * 0.5; + + let crf_increment = crf_increment + .unwrap_or_else(|| args.encoder.default_crf_increment()) + .max(0.001); + + let min_q = q_from_crf(min_crf, crf_increment); + let max_q = q_from_crf(max_crf, crf_increment); + let mut q: u64 = (min_q + max_q) / 2; + + let mut args = sample_encode::Args { + args: args.clone(), + crf: 0.0, + sample: sample.clone(), + cache, + stdout_format: sample_encode::StdoutFormat::Json, + vmaf: vmaf.clone(), + }; - bar.set_length(BAR_LEN); - let mut crf_attempts = Vec::new(); + let mut crf_attempts = Vec::new(); - for run in 1.. { - // how much we're prepared to go higher than the min-vmaf - let higher_tolerance = match thorough { - true => 0.05, - // increment 1.0 => +0.1, +0.2, +0.4, +0.8 .. - // increment 0.1 => +0.1, +0.1, +0.1, +0.16 .. - _ => (crf_increment * 2_f32.powi(run as i32 - 1) * 0.1).max(0.1), - }; - args.crf = q.to_crf(crf_increment); - let terse_crf = TerseF32(args.crf); - - let mut sample_enc = pin!(sample_encode::run(args.clone(), input_probe.clone())); - let mut sample_enc_output = None; - while let Some(update) = sample_enc.next().await { - match update? { - sample_encode::Update::Status { - work, - fps, - progress, - sample, - samples, - full_pass, - } => { - bar.set_position(guess_progress(run, progress, *thorough) as _); - match full_pass { - true => bar.set_prefix(format!("crf {terse_crf} full pass")), - false => bar.set_prefix(format!("crf {terse_crf} {sample}/{samples}")), - } - match work { - Work::Encode if fps <= 0.0 => bar.set_message("encoding, "), - Work::Encode => bar.set_message(format!("enc {fps} fps, ")), - Work::Vmaf if fps <= 0.0 => bar.set_message("vmaf, "), - Work::Vmaf => bar.set_message(format!("vmaf {fps} fps, ")), + for run in 1.. { + // how much we're prepared to go higher than the min-vmaf + let higher_tolerance = match thorough { + true => 0.05, + // increment 1.0 => +0.1, +0.2, +0.4, +0.8 .. + // increment 0.1 => +0.1, +0.1, +0.1, +0.16 .. + _ => (crf_increment * 2_f32.powi(run as i32 - 1) * 0.1).max(0.1), + }; + args.crf = q.to_crf(crf_increment); + + let mut sample_enc = pin!(sample_encode::run(args.clone(), input_probe.clone())); + let mut sample_enc_output = None; + while let Some(update) = sample_enc.next().await { + match update? { + sample_encode::Update::Status(status) => { + yield Update::Status { crf_run: run, crf: args.crf, sample: status }; } + sample_encode::Update::SampleResult { .. } => {} + sample_encode::Update::Done(output) => sample_enc_output = Some(output), } - sample_encode::Update::SampleResult { .. } => {} - sample_encode::Update::Done(output) => sample_enc_output = Some(output), } - } - let sample = Sample { - crf_increment, - q, - enc: sample_enc_output.context("no sample output?")?, - }; - let from_cache = sample.enc.from_cache; - crf_attempts.push(sample.clone()); - let sample_small_enough = sample.enc.encode_percent <= *max_encoded_percent as _; - - if sample.enc.vmaf > *min_vmaf { - // good - if sample_small_enough && sample.enc.vmaf < min_vmaf + higher_tolerance { - return Ok(sample); - } - let u_bound = crf_attempts - .iter() - .filter(|s| s.q > sample.q) - .min_by_key(|s| s.q); - - match u_bound { - Some(upper) if upper.q == sample.q + 1 => { - ensure_or_no_good_crf!(sample_small_enough, sample); - return Ok(sample); - } - Some(upper) => { - q = vmaf_lerp_q(*min_vmaf, upper, &sample); - } - None if sample.q == max_q => { - ensure_or_no_good_crf!(sample_small_enough, sample); - return Ok(sample); - } - None if cut_on_iter2 && run == 1 && sample.q + 1 < max_q => { - q = (sample.q as f32 * 0.4 + max_q as f32 * 0.6).round() as _; - } - None => q = max_q, + let sample = Sample { + crf_increment, + q, + enc: sample_enc_output.context("no sample output?")?, }; - } else { - // not good enough - if !sample_small_enough || sample.q == min_q { - sample.print_attempt(&bar, *min_vmaf, *max_encoded_percent, *quiet, from_cache); - ensure_or_no_good_crf!(false, sample); - } - let l_bound = crf_attempts - .iter() - .filter(|s| s.q < sample.q) - .max_by_key(|s| s.q); - - match l_bound { - Some(lower) if lower.q + 1 == sample.q => { - sample.print_attempt(&bar, *min_vmaf, *max_encoded_percent, *quiet, from_cache); - let lower_small_enough = lower.enc.encode_percent <= *max_encoded_percent as _; - ensure_or_no_good_crf!(lower_small_enough, sample); - return Ok(lower.clone()); - } - Some(lower) => { - q = vmaf_lerp_q(*min_vmaf, &sample, lower); + crf_attempts.push(sample.clone()); + let sample_small_enough = sample.enc.encode_percent <= max_encoded_percent as _; + + if sample.enc.vmaf > min_vmaf { + // good + if sample_small_enough && sample.enc.vmaf < min_vmaf + higher_tolerance { + yield Update::Done(sample); + return; } - None if cut_on_iter2 && run == 1 && sample.q > min_q + 1 => { - q = (sample.q as f32 * 0.4 + min_q as f32 * 0.6).round() as _; + let u_bound = crf_attempts + .iter() + .filter(|s| s.q > sample.q) + .min_by_key(|s| s.q); + + match u_bound { + Some(upper) if upper.q == sample.q + 1 => { + Error::ensure_or_no_good_crf(sample_small_enough, &sample)?; + yield Update::Done(sample); + return; + } + Some(upper) => { + q = vmaf_lerp_q(min_vmaf, upper, &sample); + } + None if sample.q == max_q => { + Error::ensure_or_no_good_crf(sample_small_enough, &sample)?; + yield Update::Done(sample); + return; + } + None if cut_on_iter2 && run == 1 && sample.q + 1 < max_q => { + q = (sample.q as f32 * 0.4 + max_q as f32 * 0.6).round() as _; + } + None => q = max_q, + }; + } else { + // not good enough + if !sample_small_enough || sample.q == min_q { + Err(Error::NoGoodCrf { last: sample.clone() })?; } - None => q = min_q, - }; + + let l_bound = crf_attempts + .iter() + .filter(|s| s.q < sample.q) + .max_by_key(|s| s.q); + + match l_bound { + Some(lower) if lower.q + 1 == sample.q => { + Error::ensure_or_no_good_crf(lower.enc.encode_percent <= max_encoded_percent as _, &sample)?; + yield Update::RunResult(sample.clone()); + yield Update::Done(lower.clone()); + return; + } + Some(lower) => { + q = vmaf_lerp_q(min_vmaf, &sample, lower); + } + None if cut_on_iter2 && run == 1 && sample.q > min_q + 1 => { + q = (sample.q as f32 * 0.4 + min_q as f32 * 0.6).round() as _; + } + None => q = min_q, + }; + } + yield Update::RunResult(sample.clone()); } - sample.print_attempt(&bar, *min_vmaf, *max_encoded_percent, *quiet, from_cache); + unreachable!(); } - unreachable!(); } #[derive(Debug, Clone)] @@ -314,7 +328,6 @@ impl Sample { min_vmaf: f32, max_encoded_percent: f32, quiet: bool, - from_cache: bool, ) { if quiet { return; @@ -326,7 +339,7 @@ impl Sample { let mut percent = style!("{:.0}%", self.enc.encode_percent); let open = style("(").dim(); let close = style(")").dim(); - let cache_msg = match from_cache { + let cache_msg = match self.enc.from_cache { true => style(" (cache)").dim(), false => style(""), }; @@ -405,7 +418,7 @@ fn vmaf_lerp_q(min_vmaf: f32, worse_q: &Sample, better_q: &Sample) -> u64 { } /// sample_progress: [0, 1] -fn guess_progress(run: usize, sample_progress: f32, thorough: bool) -> f64 { +pub fn guess_progress(run: usize, sample_progress: f32, thorough: bool) -> f64 { let total_runs_guess = match () { // Guess 6 iterations for a "thorough" search _ if thorough && run < 7 => 6.0, @@ -441,3 +454,17 @@ fn q_crf_conversions() { assert_eq!(q_from_crf(33.5, 0.1), 335); assert_eq!(q_from_crf(27.0, 1.0), 27); } + +#[derive(Debug)] +pub enum Update { + Status { + /// run number starting from `1`. + crf_run: usize, + /// crf of this run + crf: f32, + sample: sample_encode::Status, + }, + /// Run result (excludes successful final runs) + RunResult(Sample), + Done(Sample), +} diff --git a/src/command/crf_search/err.rs b/src/command/crf_search/err.rs index e2b61ec..a43013c 100644 --- a/src/command/crf_search/err.rs +++ b/src/command/crf_search/err.rs @@ -7,6 +7,22 @@ pub enum Error { Other(anyhow::Error), } +impl Error { + pub fn ensure_other(condition: bool, reason: &'static str) -> Result<(), Self> { + if !condition { + return Err(Self::Other(anyhow::anyhow!(reason))); + } + Ok(()) + } + + pub fn ensure_or_no_good_crf(condition: bool, last: &Sample) -> Result<(), Self> { + if !condition { + return Err(Self::NoGoodCrf { last: last.clone() }); + } + Ok(()) + } +} + impl From for Error { fn from(err: anyhow::Error) -> Self { Self::Other(err) @@ -29,24 +45,3 @@ impl fmt::Display for Error { } impl std::error::Error for Error {} - -macro_rules! ensure_other { - ($condition:expr, $reason:expr) => { - #[allow(clippy::neg_cmp_op_on_partial_ord)] - if !$condition { - return Err($crate::command::crf_search::err::Error::Other( - anyhow::anyhow!($reason), - )); - } - }; -} -pub(crate) use ensure_other; - -macro_rules! ensure_or_no_good_crf { - ($condition:expr, $last_sample:expr) => { - if !$condition { - return Err($crate::command::crf_search::err::Error::NoGoodCrf { last: $last_sample }); - } - }; -} -pub(crate) use ensure_or_no_good_crf; diff --git a/src/command/sample_encode.rs b/src/command/sample_encode.rs index c906a20..4d6634f 100644 --- a/src/command/sample_encode.rs +++ b/src/command/sample_encode.rs @@ -90,14 +90,14 @@ pub async fn sample_encode(mut args: Args) -> anyhow::Result<()> { let mut run = pin!(run(args, probe.into())); while let Some(update) = run.next().await { match update? { - Update::Status { + Update::Status(Status { work, fps, progress, sample, samples, full_pass, - } => { + }) => { match full_pass { true => bar.set_prefix("Full pass"), false => bar.set_prefix(format!("Sample {sample}/{samples}")), @@ -134,14 +134,12 @@ pub async fn sample_encode(mut args: Args) -> anyhow::Result<()> { Update::Done(output) => { bar.finish(); if io::stderr().is_terminal() { - // encode how-to hint eprintln!( "\n{} {}\n", style("Encode with:").dim(), style(enc_args.encode_hint(crf)).dim().italic(), ); } - // stdout result stdout_fmt.print_result( output.vmaf, output.predicted_encode_size, @@ -236,14 +234,14 @@ pub fn run( let (sample, sample_size) = sample?; info!("encoding sample {sample_n}/{samples} crf {crf}"); - yield Update::Status { + yield Update::Status(Status { work: Work::Encode, fps: 0.0, progress: sample_idx as f32 / samples as f32, full_pass, sample: sample_n, samples, - }; + }); // encode sample let result = match cache::cached_encode( @@ -281,7 +279,7 @@ pub fn run( )?; while let Some(enc_progress) = output.next().await { if let FfmpegOut::Progress { time, fps, .. } = enc_progress? { - yield Update::Status { + yield Update::Status(Status { work: Work::Encode, fps, progress: (time.as_micros_u64() + sample_idx * sample_duration_us * 2) as f32 @@ -289,7 +287,7 @@ pub fn run( full_pass, sample: sample_n, samples, - }; + }); logger.update(sample_duration, time, fps); } } @@ -298,14 +296,14 @@ pub fn run( let encoded_probe = ffprobe::probe(&encoded_sample); // calculate vmaf - yield Update::Status { + yield Update::Status(Status { work: Work::Vmaf, fps: 0.0, progress: (sample_idx as f32 + 0.5) / samples as f32, full_pass, sample: sample_n, samples, - }; + }); let vmaf = vmaf::run( &sample, &encoded_sample, @@ -327,7 +325,7 @@ pub fn run( break; } VmafOut::Progress(FfmpegOut::Progress { time, fps, .. }) => { - yield Update::Status { + yield Update::Status(Status { work: Work::Vmaf, fps, progress: (sample_duration_us + @@ -337,7 +335,7 @@ pub fn run( full_pass, sample: sample_n, samples, - }; + }); logger.update(sample_duration, time, fps); } VmafOut::Progress(_) => {} @@ -617,22 +615,25 @@ pub enum Work { Vmaf, } +#[derive(Debug)] +pub struct Status { + /// Kind of work being performed + pub work: Work, + /// fps, `0.0` may be interpreted as "unknown" + pub fps: f32, + /// sample progress `[0, 1]` + pub progress: f32, + /// Sample number `1,....,n` + pub sample: u64, + /// Total samples + pub samples: u64, + /// Encoding the entire input video + pub full_pass: bool, +} + #[derive(Debug)] pub enum Update { - Status { - /// Kind of work being performed - work: Work, - /// fps, `0.0` may be interpreted as "unknown" - fps: f32, - /// sample progress `[0, 1]` - progress: f32, - /// Sample number `1,....,n` - sample: u64, - /// Total samples - samples: u64, - /// Encoding the entire input video - full_pass: bool, - }, + Status(Status), SampleResult { /// Sample number `1,....,n` sample: u64, From 6c935989b265bf77a48974da383fbf73e8a53d6b Mon Sep 17 00:00:00 2001 From: Alex Butler Date: Fri, 15 Nov 2024 15:58:30 +0000 Subject: [PATCH 4/5] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8683b96..c84a68c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ * crf-search: Tweak 2nd iteration logic that slices the crf range at the 25% or 75% crf point. - Widen to 20%/80% to account for searches of the "middle" two subranges being more optimal. - 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. # v0.7.19 * Fix stdin handling sometimes breaking bash shells. From 59d5cf8fbee6562d3232009674a994f97afcd932 Mon Sep 17 00:00:00 2001 From: Alex Butler Date: Fri, 15 Nov 2024 16:03:24 +0000 Subject: [PATCH 5/5] minor refactor --- src/vmaf.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vmaf.rs b/src/vmaf.rs index f96f914..92af767 100644 --- a/src/vmaf.rs +++ b/src/vmaf.rs @@ -31,10 +31,9 @@ pub fn run( let cmd_str = cmd.to_cmd_str(); debug!("cmd `{cmd_str}`"); - let vmaf: ProcessChunkStream = cmd.try_into().context("ffmpeg vmaf")?; + let mut vmaf: ProcessChunkStream = cmd.try_into().context("ffmpeg vmaf")?; Ok(async_stream::stream! { - let mut vmaf = vmaf; let mut chunks = Chunks::default(); let mut parsed_done = false; while let Some(next) = vmaf.next().await {