use anyhow::Context;
use clap::Parser;
use std::{fmt::Display, sync::Arc};
#[derive(Parser, Clone)]
pub struct Vmaf {
#[clap(long = "vmaf", value_parser = parse_vmaf_arg)]
pub vmaf_args: Vec<Arc<str>>,
#[clap(long, default_value_t = VmafScale::Auto, value_parser = parse_vmaf_scale)]
pub vmaf_scale: VmafScale,
}
fn parse_vmaf_arg(arg: &str) -> anyhow::Result<Arc<str>> {
Ok(arg.to_owned().into())
}
impl Vmaf {
pub fn ffmpeg_lavfi(&self, distorted_res: Option<(u32, u32)>) -> String {
let mut args = self.vmaf_args.clone();
if !args.iter().any(|a| a.contains("n_threads")) {
args.push(format!("n_threads={}", num_cpus::get()).into());
}
let mut lavfi = args.join(":");
lavfi.insert_str(0, "libvmaf=");
let mut model = VmafModel::from_args(&args);
if let (None, Some((w, h))) = (model, distorted_res) {
if w > 2560 && h > 1440 {
lavfi.push_str(":model=version=vmaf_4k_v0.6.1");
model = Some(VmafModel::Vmaf4K);
}
}
if let Some((w, h)) = self.vf_scale(model.unwrap_or_default(), distorted_res) {
lavfi.insert_str(
0,
&format!("[0:v]scale={w}:{h}:flags=bicubic[dis];[1:v]scale={w}:{h}:flags=bicubic[ref];[dis][ref]"),
);
}
lavfi
}
fn vf_scale(&self, model: VmafModel, distorted_res: Option<(u32, u32)>) -> Option<(i32, i32)> {
match (self.vmaf_scale, distorted_res) {
(VmafScale::Auto, Some((w, h))) => match model {
VmafModel::Vmaf1K if w < 1728 && h < 972 => {
Some(minimally_scale((w, h), (1920, 1080)))
}
VmafModel::Vmaf4K if w < 3456 && h < 1944 => {
Some(minimally_scale((w, h), (3840, 2160)))
}
_ => None,
},
(VmafScale::Custom { width, height }, Some((w, h))) => {
Some(minimally_scale((w, h), (width, height)))
}
(VmafScale::Custom { width, height }, None) => Some((width as _, height as _)),
_ => None,
}
}
}
fn minimally_scale((from_w, from_h): (u32, u32), (target_w, target_h): (u32, u32)) -> (i32, i32) {
let w_factor = from_w as f64 / target_w as f64;
let h_factor = from_h as f64 / target_h as f64;
if h_factor > w_factor {
(-1, target_h as _) } else {
(target_w as _, -1) }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VmafScale {
None,
Auto,
Custom { width: u32, height: u32 },
}
fn parse_vmaf_scale(vs: &str) -> anyhow::Result<VmafScale> {
const ERR: &str = "vmaf-scale must be 'none', 'auto' or WxH format e.g. '1920x1080'";
match vs {
"none" => Ok(VmafScale::None),
"auto" => Ok(VmafScale::Auto),
_ => {
let (w, h) = vs.split_once('x').context(ERR)?;
let (width, height) = (w.parse().context(ERR)?, h.parse().context(ERR)?);
Ok(VmafScale::Custom { width, height })
}
}
}
impl Display for VmafScale {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::None => "none".fmt(f),
Self::Auto => "auto".fmt(f),
Self::Custom { width, height } => write!(f, "{width}x{height}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum VmafModel {
Vmaf1K,
Vmaf4K,
Custom,
}
impl Default for VmafModel {
fn default() -> Self {
Self::Vmaf1K
}
}
impl VmafModel {
fn from_args(args: &[Arc<str>]) -> Option<Self> {
let mut using_custom_model: Vec<_> = args.iter().filter(|v| v.contains("model")).collect();
match using_custom_model.len() {
0 => None,
1 => Some(match using_custom_model.remove(0) {
v if v.ends_with("version=vmaf_v0.6.1") => Self::Vmaf1K,
v if v.ends_with("version=vmaf_4k_v0.6.1") => Self::Vmaf4K,
_ => Self::Custom,
}),
_ => Some(Self::Custom),
}
}
}
#[test]
fn vmaf_lavfi() {
let vmaf = Vmaf {
vmaf_args: vec!["n_threads=5".into(), "n_subsample=4".into()],
vmaf_scale: VmafScale::Auto,
};
assert_eq!(vmaf.ffmpeg_lavfi(None), "libvmaf=n_threads=5:n_subsample=4");
}
#[test]
fn vmaf_lavfi_default() {
let vmaf = Vmaf {
vmaf_args: vec![],
vmaf_scale: VmafScale::Auto,
};
let expected = format!("libvmaf=n_threads={}", num_cpus::get());
assert_eq!(vmaf.ffmpeg_lavfi(None), expected);
}
#[test]
fn vmaf_lavfi_include_n_threads() {
let vmaf = Vmaf {
vmaf_args: vec!["log_path=output.xml".into()],
vmaf_scale: VmafScale::Auto,
};
let expected = format!("libvmaf=log_path=output.xml:n_threads={}", num_cpus::get());
assert_eq!(vmaf.ffmpeg_lavfi(None), expected);
}
#[test]
fn vmaf_lavfi_small_width() {
let vmaf = Vmaf {
vmaf_args: vec!["n_threads=5".into(), "n_subsample=4".into()],
vmaf_scale: VmafScale::Auto,
};
assert_eq!(
vmaf.ffmpeg_lavfi(Some((1280, 720))),
"[0:v]scale=1920:-1:flags=bicubic[dis];\
[1:v]scale=1920:-1:flags=bicubic[ref];\
[dis][ref]libvmaf=n_threads=5:n_subsample=4"
);
}
#[test]
fn vmaf_lavfi_4k() {
let vmaf = Vmaf {
vmaf_args: vec!["n_threads=5".into(), "n_subsample=4".into()],
vmaf_scale: VmafScale::Auto,
};
assert_eq!(
vmaf.ffmpeg_lavfi(Some((3840, 2160))),
"libvmaf=n_threads=5:n_subsample=4:model=version=vmaf_4k_v0.6.1"
);
}
#[test]
fn vmaf_lavfi_3k_upscale_to_4k() {
let vmaf = Vmaf {
vmaf_args: vec!["n_threads=5".into()],
vmaf_scale: VmafScale::Auto,
};
assert_eq!(
vmaf.ffmpeg_lavfi(Some((3008, 1692))),
"[0:v]scale=3840:-1:flags=bicubic[dis];\
[1:v]scale=3840:-1:flags=bicubic[ref];\
[dis][ref]libvmaf=n_threads=5:model=version=vmaf_4k_v0.6.1"
);
}
#[test]
fn vmaf_lavfi_small_width_custom_model() {
let vmaf = Vmaf {
vmaf_args: vec![
"model=version=foo".into(),
"n_threads=5".into(),
"n_subsample=4".into(),
],
vmaf_scale: VmafScale::Auto,
};
assert_eq!(
vmaf.ffmpeg_lavfi(Some((1280, 720))),
"libvmaf=model=version=foo:n_threads=5:n_subsample=4"
);
}
#[test]
fn vmaf_lavfi_custom_model_and_width() {
let vmaf = Vmaf {
vmaf_args: vec![
"model=version=foo".into(),
"n_threads=5".into(),
"n_subsample=4".into(),
],
vmaf_scale: VmafScale::Custom {
width: 123,
height: 720,
},
};
assert_eq!(
vmaf.ffmpeg_lavfi(Some((1280, 720))),
"[0:v]scale=123:-1:flags=bicubic[dis];\
[1:v]scale=123:-1:flags=bicubic[ref];\
[dis][ref]libvmaf=model=version=foo:n_threads=5:n_subsample=4"
);
}
#[test]
fn vmaf_lavfi_1080p() {
let vmaf = Vmaf {
vmaf_args: vec!["n_threads=5".into(), "n_subsample=4".into()],
vmaf_scale: VmafScale::Auto,
};
assert_eq!(
vmaf.ffmpeg_lavfi(Some((1920, 1080))),
"libvmaf=n_threads=5:n_subsample=4"
);
}