deepshrink_ffmpeg/
vmaf.rs1use std::path::Path;
8use std::process::Command;
9
10use crate::FfmpegError;
11
12pub fn has_libvmaf(ffmpeg: &Path) -> bool {
14 Command::new(ffmpeg)
15 .args(["-hide_banner", "-filters"])
16 .output()
17 .map(|o| {
18 let text = String::from_utf8_lossy(&o.stdout);
19 text.contains("libvmaf")
20 })
21 .unwrap_or(false)
22}
23
24pub fn measure_vmaf(
31 ffmpeg: &Path,
32 distorted: &Path,
33 reference: &Path,
34 ref_width: u32,
35 ref_height: u32,
36 fps: f64,
37 n_threads: usize,
38) -> Result<f64, FfmpegError> {
39 let fps_term = if fps.is_finite() && fps > 0.0 {
41 format!(",fps={fps}")
42 } else {
43 String::new()
44 };
45 let threads = n_threads.max(1);
46 let filter = format!(
47 "[0:v]scale={ref_width}:{ref_height}:flags=bicubic,setsar=1{fps_term},\
48 settb=AVTB,setpts=PTS-STARTPTS[dist];\
49 [1:v]setsar=1{fps_term},settb=AVTB,setpts=PTS-STARTPTS[ref];\
50 [dist][ref]libvmaf=n_threads={threads}"
51 );
52
53 let output = Command::new(ffmpeg)
54 .args(["-hide_banner", "-nostdin", "-i"])
55 .arg(distorted)
56 .arg("-i")
57 .arg(reference)
58 .args(["-lavfi", &filter, "-f", "null", "-"])
59 .output()
60 .map_err(|source| FfmpegError::Spawn {
61 tool: "ffmpeg",
62 source,
63 })?;
64
65 if !output.status.success() {
66 return Err(FfmpegError::CommandFailed {
67 tool: "ffmpeg",
68 status: output.status.to_string(),
69 stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(),
70 });
71 }
72
73 let stderr = String::from_utf8_lossy(&output.stderr);
75 parse_vmaf_score(&stderr)
76 .ok_or_else(|| FfmpegError::Parse("could not find a VMAF score in ffmpeg output".into()))
77}
78
79fn parse_vmaf_score(stderr: &str) -> Option<f64> {
81 const MARKER: &str = "VMAF score:";
82 for line in stderr.lines() {
83 if let Some(idx) = line.find(MARKER) {
84 let tail = line[idx + MARKER.len()..].trim();
85 if let Some(score) = tail
86 .split_whitespace()
87 .next()
88 .and_then(|t| t.parse::<f64>().ok())
89 {
90 return Some(score);
91 }
92 }
93 }
94 None
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn parses_score_line() {
103 let s = "frame= 150 fps=...\n[libvmaf @ 0x7f] VMAF score: 91.234567\n";
104 assert_eq!(parse_vmaf_score(s), Some(91.234567));
105 }
106
107 #[test]
108 fn parses_bare_score_line() {
109 assert_eq!(parse_vmaf_score("VMAF score: 100.000000"), Some(100.0));
110 }
111
112 #[test]
113 fn none_when_absent() {
114 assert_eq!(parse_vmaf_score("frame=1 fps=2\nEncoding done\n"), None);
115 }
116}