master-of-zen/Av1an

View on GitHub
av1an-core/src/progress_bar.rs

Summary

Maintainability
Test Coverage
use std::fmt::Write;
use std::time::Duration;

use indicatif::{
  HumanBytes, HumanDuration, MultiProgress, ProgressBar, ProgressDrawTarget, ProgressState,
  ProgressStyle,
};
use once_cell::sync::OnceCell;

use crate::util::printable_base10_digits;
use crate::{get_done, Verbosity};

const PROGRESS_CHARS: &str = if cfg!(windows) {
  "█▓▒░  "
} else {
  "█▉▊▋▌▍▎▏  "
};

const INDICATIF_PROGRESS_TEMPLATE: &str = if cfg!(windows) {
  // Do not use a spinner on Windows since the default console cannot display
  // the characters used for the spinner
  "{elapsed_precise:.bold} ▐{wide_bar:.blue/white.dim}▌ {percent:.bold} {pos} ({fps:.bold}, eta {fixed_eta}{msg})"
} else {
  "{spinner:.green.bold} {elapsed_precise:.bold} ▕{wide_bar:.blue/white.dim}▏ {percent:.bold}  {pos} ({fps:.bold}, eta {fixed_eta}{msg})"
};

const INDICATIF_SPINNER_TEMPLATE: &str = if cfg!(windows) {
  // Do not use a spinner on Windows since the default console cannot display
  // the characters used for the spinner
  "{elapsed_precise:.bold} [{wide_bar:.blue/white.dim}]  {pos} frames ({fps:.bold})"
} else {
  "{spinner:.green.bold} {elapsed_precise:.bold} [{wide_bar:.blue/white.dim}]  {pos} frames ({fps:.bold})"
};

static PROGRESS_BAR: OnceCell<ProgressBar> = OnceCell::new();
static AUDIO_BYTES: OnceCell<u64> = OnceCell::new();

pub fn set_audio_size(val: u64) {
  AUDIO_BYTES.get_or_init(|| val);
}

pub fn get_audio_size() -> u64 {
  *AUDIO_BYTES.get().unwrap_or(&0u64)
}

pub fn get_progress_bar() -> Option<&'static ProgressBar> {
  PROGRESS_BAR.get()
}

fn pretty_progress_style(resume_frames: u64) -> ProgressStyle {
  ProgressStyle::default_bar()
    .template(INDICATIF_PROGRESS_TEMPLATE)
    .unwrap()
    .with_key("fps", move |state: &ProgressState, w: &mut dyn Write| {
      let resume_pos = state.pos() - resume_frames;
      if resume_pos == 0 || state.elapsed().as_secs_f32() < f32::EPSILON {
        write!(w, "0 fps").unwrap();
      } else {
        let fps = resume_pos as f32 / state.elapsed().as_secs_f32();
        if fps < 1.0 {
          write!(w, "{:.2} s/fr", 1.0 / fps).unwrap();
        } else {
          write!(w, "{fps:.2} fps").unwrap();
        }
      }
    })
    .with_key(
      "fixed_eta",
      move |state: &ProgressState, w: &mut dyn Write| {
        let resume_pos = state.pos() - resume_frames;
        if resume_pos == 0 || state.elapsed().as_secs_f32() < f32::EPSILON {
          write!(w, "unknown").unwrap();
        } else {
          let spf = state.elapsed().as_secs_f32() / resume_pos as f32;
          let remaining = state.len().unwrap_or(0) - state.pos();
          write!(
            w,
            "{:#}",
            HumanDuration(Duration::from_secs_f32(spf * remaining as f32))
          )
          .unwrap();
        }
      },
    )
    .with_key("pos", |state: &ProgressState, w: &mut dyn Write| {
      write!(w, "{}/{}", state.pos(), state.len().unwrap_or(0)).unwrap();
    })
    .with_key("percent", |state: &ProgressState, w: &mut dyn Write| {
      write!(w, "{:>3.0}%", state.fraction() * 100_f32).unwrap();
    })
    .progress_chars(PROGRESS_CHARS)
}

fn spinner_style(resume_frames: u64) -> ProgressStyle {
  ProgressStyle::default_spinner()
    .template(INDICATIF_SPINNER_TEMPLATE)
    .unwrap()
    .with_key("fps", move |state: &ProgressState, w: &mut dyn Write| {
      let resume_pos = state.pos() - resume_frames;
      if resume_pos == 0 || state.elapsed().as_secs_f32() < f32::EPSILON {
        write!(w, "0 fps").unwrap();
      } else {
        let fps = resume_pos as f32 / state.elapsed().as_secs_f32();
        if fps < 1.0 {
          write!(w, "{:.2} s/fr", 1.0 / fps).unwrap();
        } else {
          write!(w, "{fps:.2} fps",).unwrap();
        }
      }
    })
    .with_key("pos", |state: &ProgressState, w: &mut dyn Write| {
      write!(w, "{}", state.pos()).unwrap();
    })
    .progress_chars(PROGRESS_CHARS)
}

/// Initialize progress bar
/// Enables steady 100 ms tick
pub fn init_progress_bar(len: u64, resume_frames: u64) {
  let pb = if len > 0 {
    PROGRESS_BAR
      .get_or_init(|| ProgressBar::new(len).with_style(pretty_progress_style(resume_frames)))
  } else {
    // Avoid showing `xxx/0` if we don't know the length yet.
    // Affects scenechange progress.
    PROGRESS_BAR.get_or_init(|| ProgressBar::new(len).with_style(spinner_style(resume_frames)))
  };
  pb.set_draw_target(ProgressDrawTarget::stderr());
  pb.enable_steady_tick(Duration::from_millis(100));
  pb.reset();
  pb.reset_eta();
  pb.reset_elapsed();
  pb.set_position(0);
}

pub fn convert_to_progress(resume_frames: u64) {
  if let Some(pb) = PROGRESS_BAR.get() {
    pb.set_style(pretty_progress_style(resume_frames));
  }
}

pub fn inc_bar(inc: u64) {
  if let Some(pb) = PROGRESS_BAR.get() {
    pb.inc(inc);
  }
}

pub fn dec_bar(dec: u64) {
  if let Some(pb) = PROGRESS_BAR.get() {
    pb.set_position(pb.position().saturating_sub(dec));
  }

  if let Some((_, pbs)) = MULTI_PROGRESS_BAR.get() {
    let pb = pbs.last().unwrap();
    pb.set_position(pb.position().saturating_sub(dec));
  }
}

pub fn update_bar_info(kbps: f64, est_size: HumanBytes) {
  if let Some(pb) = PROGRESS_BAR.get() {
    pb.set_message(format!(", {kbps:.1} Kbps, est. {est_size}"));
  }
}

pub fn set_pos(pos: u64) {
  if let Some(pb) = PROGRESS_BAR.get() {
    pb.set_position(pos);
  }
}

pub fn finish_progress_bar() {
  if let Some(pb) = PROGRESS_BAR.get() {
    pb.finish();
  }

  if let Some((_, pbs)) = MULTI_PROGRESS_BAR.get() {
    for pb in pbs {
      pb.finish();
    }
  }
}

static MULTI_PROGRESS_BAR: OnceCell<(MultiProgress, Vec<ProgressBar>)> = OnceCell::new();

pub fn get_first_multi_progress_bar() -> Option<&'static ProgressBar> {
  if let Some((_, pbars)) = MULTI_PROGRESS_BAR.get() {
    pbars.first()
  } else {
    None
  }
}

pub fn set_len(len: u64) {
  let pb = PROGRESS_BAR.get().unwrap();
  pb.set_length(len);
}

pub fn reset_bar_at(pos: u64) {
  if let Some(pb) = PROGRESS_BAR.get() {
    pb.reset();
    pb.set_position(pos);
    pb.reset_eta();
    pb.reset_elapsed();
  }
}

pub fn reset_mp_bar_at(pos: u64) {
  if let Some((_, pbs)) = MULTI_PROGRESS_BAR.get() {
    if let Some(pb) = pbs.last() {
      pb.reset();
      pb.set_position(pos);
      pb.reset_eta();
      pb.reset_elapsed();
    }
  }
}

pub fn init_multi_progress_bar(len: u64, workers: usize, total_chunks: usize, resume_frames: u64) {
  MULTI_PROGRESS_BAR.get_or_init(|| {
    let mpb = MultiProgress::new();

    let mut pbs = Vec::new();

    let digits = printable_base10_digits(total_chunks) as usize;

    for _ in 1..=workers {
      let pb = ProgressBar::hidden()
        // no spinner on windows, so we remove the prefix to line up with the progress bar
        .with_style(
          ProgressStyle::default_spinner()
            .template(if cfg!(windows) {
              "{prefix:.dim} {msg}"
            } else {
              "  {prefix:.dim} {msg}"
            })
            .unwrap(),
        );
      pb.set_prefix(format!("[Idle  {digits:digits$}]"));
      pbs.push(mpb.add(pb));
    }

    let pb = ProgressBar::hidden();
    pb.set_style(pretty_progress_style(resume_frames));
    pb.enable_steady_tick(Duration::from_millis(100));
    pb.reset_elapsed();
    pb.reset_eta();
    pb.set_position(0);
    pb.set_length(len);
    pb.reset();
    pbs.push(mpb.add(pb));

    mpb.set_draw_target(ProgressDrawTarget::stderr());

    (mpb, pbs)
  });
}

pub fn update_mp_chunk(worker_idx: usize, chunk: usize, padding: usize) {
  if let Some((_, pbs)) = MULTI_PROGRESS_BAR.get() {
    pbs[worker_idx].set_prefix(format!("[Chunk {chunk:>padding$}]"));
  }
}

pub fn update_mp_msg(worker_idx: usize, msg: String) {
  if let Some((_, pbs)) = MULTI_PROGRESS_BAR.get() {
    pbs[worker_idx].set_message(msg);
  }
}

pub fn inc_mp_bar(inc: u64) {
  if let Some((_, pbs)) = MULTI_PROGRESS_BAR.get() {
    pbs.last().unwrap().inc(inc);
  }
}

pub fn update_mp_bar_info(kbps: f64, est_size: HumanBytes) {
  if let Some((_, pbs)) = MULTI_PROGRESS_BAR.get() {
    pbs
      .last()
      .unwrap()
      .set_message(format!(", {kbps:.1} Kbps, est. {est_size}"));
  }
}

pub fn update_progress_bar_estimates(frame_rate: f64, total_frames: usize, verbosity: Verbosity) {
  let completed_frames: usize = get_done()
    .done
    .iter()
    .map(|ref_multi| ref_multi.value().frames)
    .sum();
  let total_size: u64 = get_done()
    .done
    .iter()
    .map(|ref_multi| ref_multi.value().size_bytes)
    .sum::<u64>();
  let seconds_completed = completed_frames as f64 / frame_rate;
  let kbps = total_size as f64 * 8. / 1000. / seconds_completed;
  let progress = completed_frames as f64 / total_frames as f64;

  let audio_size_byte = get_audio_size();

  let est_size = total_size as f64 / progress + audio_size_byte as f64;
  if verbosity == Verbosity::Normal {
    update_bar_info(kbps, HumanBytes(est_size as u64));
  } else if verbosity == Verbosity::Verbose {
    update_mp_bar_info(kbps, HumanBytes(est_size as u64));
  }
}