Skip to content

Commit

Permalink
Use a single ffmpeg process to calculate VMAF (#177)
Browse files Browse the repository at this point in the history
* Use a single ffmpeg process to calculate VMAF

* do format first & setpts last
  • Loading branch information
alexheretic authored Jan 14, 2024
1 parent 8e79bd2 commit 9012146
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 272 deletions.
17 changes: 9 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ jobs:
- run: cargo run --locked -- print-completions fish
- run: cargo run --locked -- print-completions zsh

test-windows:
runs-on: windows-latest
env:
RUST_BACKTRACE: 1
steps:
- run: rustup update stable
- uses: actions/checkout@v4
- run: cargo check
# Disabled while we no longer need cfg(windows) code
# test-windows:
# runs-on: windows-latest
# env:
# RUST_BACKTRACE: 1
# steps:
# - run: rustup update stable
# - uses: actions/checkout@v4
# - run: cargo check

rustfmt:
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Unreleased
* Use a single ffmpeg process to calculate VMAF replacing multi process piping.

# v0.7.12
* Improve eta stability.

Expand Down
49 changes: 3 additions & 46 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 0 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,6 @@ tokio = { version = "1.15", features = ["rt", "macros", "process", "fs", "signal
tokio-process-stream = "0.4"
tokio-stream = "0.1"

[target.'cfg(unix)'.dependencies]
unix-named-pipe = "0.2"

[target.'cfg(windows)'.dependencies.tokio]
version = "1.15"
features = ["net"]

[profile.release]
lto = true
opt-level = "s"
Expand Down
103 changes: 73 additions & 30 deletions src/command/args/vmaf.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::command::args::PixelFormat;
use anyhow::Context;
use clap::Parser;
use std::{fmt::Display, sync::Arc, thread};
use std::{borrow::Cow, fmt::Display, sync::Arc, thread};

/// Common vmaf options.
#[derive(Parser, Clone, Hash)]
Expand Down Expand Up @@ -39,7 +40,12 @@ impl Vmaf {
}

/// Returns ffmpeg `filter_complex`/`lavfi` value for calculating vmaf.
pub fn ffmpeg_lavfi(&self, distorted_res: Option<(u32, u32)>) -> String {
pub fn ffmpeg_lavfi(
&self,
distorted_res: Option<(u32, u32)>,
pix_fmt: PixelFormat,
ref_vfilter: Option<&str>,
) -> String {
let mut args = self.vmaf_args.clone();
if !args.iter().any(|a| a.contains("n_threads")) {
// default n_threads to all cores
Expand All @@ -63,14 +69,30 @@ impl Vmaf {
}
}

if let Some((w, h)) = self.vf_scale(model.unwrap_or_default(), distorted_res) {
// scale both streams to the vmaf width
lavfi.insert_str(
0,
&format!("[0:v]scale={w}:{h}:flags=bicubic[dis];[1:v]scale={w}:{h}:flags=bicubic[ref];[dis][ref]"),
);
}
let ref_vf: Cow<_> = match ref_vfilter {
None => "".into(),
Some(vf) if vf.ends_with(',') => vf.into(),
Some(vf) => format!("{vf},").into(),
};

// prefix:
// * Add reference-vfilter if any
// * convert both streams to common pixel format
// * scale to vmaf width if necessary
// * sync presentation timestamp
let prefix = if let Some((w, h)) = self.vf_scale(model.unwrap_or_default(), distorted_res) {
format!(
"[0:v]format={pix_fmt},scale={w}:{h}:flags=bicubic,setpts=PTS-STARTPTS[dis];\
[1:v]format={pix_fmt},{ref_vf}scale={w}:{h}:flags=bicubic,setpts=PTS-STARTPTS[ref];[dis][ref]"
)
} else {
format!(
"[0:v]format={pix_fmt},setpts=PTS-STARTPTS[dis];\
[1:v]format={pix_fmt},{ref_vf}setpts=PTS-STARTPTS[ref];[dis][ref]"
)
};

lavfi.insert_str(0, &prefix);
lavfi
}

Expand Down Expand Up @@ -176,7 +198,12 @@ fn vmaf_lavfi() {
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");
assert_eq!(
vmaf.ffmpeg_lavfi(None, PixelFormat::Yuv420p, Some("scale=1280:-1,fps=24")),
"[0:v]format=yuv420p,setpts=PTS-STARTPTS[dis];\
[1:v]format=yuv420p,scale=1280:-1,fps=24,setpts=PTS-STARTPTS[ref];\
[dis][ref]libvmaf=n_threads=5:n_subsample=4"
);
}

#[test]
Expand All @@ -186,10 +213,15 @@ fn vmaf_lavfi_default() {
vmaf_scale: VmafScale::Auto,
};
let expected = format!(
"libvmaf=n_threads={}",
"[0:v]format=yuv420p10le,setpts=PTS-STARTPTS[dis];\
[1:v]format=yuv420p10le,setpts=PTS-STARTPTS[ref];\
[dis][ref]libvmaf=n_threads={}",
thread::available_parallelism().map_or(1, |p| p.get())
);
assert_eq!(vmaf.ffmpeg_lavfi(None), expected);
assert_eq!(
vmaf.ffmpeg_lavfi(None, PixelFormat::Yuv420p10le, None),
expected
);
}

#[test]
Expand All @@ -199,10 +231,15 @@ fn vmaf_lavfi_include_n_threads() {
vmaf_scale: VmafScale::Auto,
};
let expected = format!(
"libvmaf=log_path=output.xml:n_threads={}",
"[0:v]format=yuv420p,setpts=PTS-STARTPTS[dis];\
[1:v]format=yuv420p,setpts=PTS-STARTPTS[ref];\
[dis][ref]libvmaf=log_path=output.xml:n_threads={}",
thread::available_parallelism().map_or(1, |p| p.get())
);
assert_eq!(vmaf.ffmpeg_lavfi(None), expected);
assert_eq!(
vmaf.ffmpeg_lavfi(None, PixelFormat::Yuv420p, None),
expected
);
}

/// Low resolution videos should be upscaled to 1080p
Expand All @@ -213,9 +250,9 @@ fn vmaf_lavfi_small_width() {
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];\
vmaf.ffmpeg_lavfi(Some((1280, 720)), PixelFormat::Yuv420p, None),
"[0:v]format=yuv420p,scale=1920:-1:flags=bicubic,setpts=PTS-STARTPTS[dis];\
[1:v]format=yuv420p,scale=1920:-1:flags=bicubic,setpts=PTS-STARTPTS[ref];\
[dis][ref]libvmaf=n_threads=5:n_subsample=4"
);
}
Expand All @@ -228,8 +265,10 @@ fn vmaf_lavfi_4k() {
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"
vmaf.ffmpeg_lavfi(Some((3840, 2160)), PixelFormat::Yuv420p, None),
"[0:v]format=yuv420p,setpts=PTS-STARTPTS[dis];\
[1:v]format=yuv420p,setpts=PTS-STARTPTS[ref];\
[dis][ref]libvmaf=n_threads=5:n_subsample=4:model=version=vmaf_4k_v0.6.1"
);
}

Expand All @@ -241,9 +280,9 @@ fn vmaf_lavfi_3k_upscale_to_4k() {
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];\
vmaf.ffmpeg_lavfi(Some((3008, 1692)), PixelFormat::Yuv420p, None),
"[0:v]format=yuv420p,scale=3840:-1:flags=bicubic,setpts=PTS-STARTPTS[dis];\
[1:v]format=yuv420p,scale=3840:-1:flags=bicubic,setpts=PTS-STARTPTS[ref];\
[dis][ref]libvmaf=n_threads=5:model=version=vmaf_4k_v0.6.1"
);
}
Expand All @@ -260,8 +299,10 @@ fn vmaf_lavfi_small_width_custom_model() {
vmaf_scale: VmafScale::Auto,
};
assert_eq!(
vmaf.ffmpeg_lavfi(Some((1280, 720))),
"libvmaf=model=version=foo:n_threads=5:n_subsample=4"
vmaf.ffmpeg_lavfi(Some((1280, 720)), PixelFormat::Yuv420p, None),
"[0:v]format=yuv420p,setpts=PTS-STARTPTS[dis];\
[1:v]format=yuv420p,setpts=PTS-STARTPTS[ref];\
[dis][ref]libvmaf=model=version=foo:n_threads=5:n_subsample=4"
);
}

Expand All @@ -280,10 +321,10 @@ fn vmaf_lavfi_custom_model_and_width() {
},
};
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"
vmaf.ffmpeg_lavfi(Some((1280, 720)), PixelFormat::Yuv420p, None),
"[0:v]format=yuv420p,scale=123:-1:flags=bicubic,setpts=PTS-STARTPTS[dis];\
[1:v]format=yuv420p,scale=123:-1:flags=bicubic,setpts=PTS-STARTPTS[ref];\
[dis][ref]libvmaf=model=version=foo:n_threads=5:n_subsample=4"
);
}

Expand All @@ -294,7 +335,9 @@ fn vmaf_lavfi_1080p() {
vmaf_scale: VmafScale::Auto,
};
assert_eq!(
vmaf.ffmpeg_lavfi(Some((1920, 1080))),
"libvmaf=n_threads=5:n_subsample=4"
vmaf.ffmpeg_lavfi(Some((1920, 1080)), PixelFormat::Yuv420p, None),
"[0:v]format=yuv420p,setpts=PTS-STARTPTS[dis];\
[1:v]format=yuv420p,setpts=PTS-STARTPTS[ref];\
[dis][ref]libvmaf=n_threads=5:n_subsample=4"
);
}
12 changes: 7 additions & 5 deletions src/command/sample_encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,14 @@ pub async fn run(
bar.set_message("vmaf running,");
let mut vmaf = vmaf::run(
&sample,
args.vfilter.as_deref(),
&encoded_sample,
&vmaf.ffmpeg_lavfi(encoded_probe.resolution),
enc_args
.pix_fmt
.max(input_pixel_format.unwrap_or(PixelFormat::Yuv444p10le)),
&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_score = -1.0;
while let Some(vmaf) = vmaf.next().await {
Expand Down
10 changes: 6 additions & 4 deletions src/command/vmaf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub struct Args {
pub reference: PathBuf,

/// Ffmpeg video filter applied to the reference before analysis.
/// E.g. --vfilter "scale=1280:-1,fps=24".
/// E.g. --reference-vfilter "scale=1280:-1,fps=24".
#[arg(long)]
pub reference_vfilter: Option<String>,

Expand Down Expand Up @@ -68,10 +68,12 @@ pub async fn vmaf(

let mut vmaf = vmaf::run(
&reference,
reference_vfilter.as_deref(),
&distorted,
&vmaf.ffmpeg_lavfi(dprobe.resolution),
dpix_fmt.max(rpix_fmt),
&vmaf.ffmpeg_lavfi(
dprobe.resolution,
dpix_fmt.max(rpix_fmt),
reference_vfilter.as_deref(),
),
)?;
let mut vmaf_score = -1.0;
while let Some(vmaf) = vmaf.next().await {
Expand Down
Loading

0 comments on commit 9012146

Please sign in to comment.