1use std::env;
2use std::path::PathBuf;
3use std::process::Command;
4
5pub fn ffmpeg_path() -> String {
12 if let Ok(p) = env::var("VISER_FFMPEG") {
13 if !p.is_empty() {
14 return p;
15 }
16 }
17 if let Some(p) = local_binary("ffmpeg") {
18 return p;
19 }
20 "ffmpeg".into()
21}
22
23pub fn ffprobe_path() -> String {
30 if let Ok(p) = env::var("VISER_FFPROBE") {
31 if !p.is_empty() {
32 return p;
33 }
34 }
35 if let Some(p) = local_binary("ffprobe") {
36 return p;
37 }
38 "ffprobe".into()
39}
40
41const MIN_FFMPEG_MAJOR: u32 = 6;
43
44#[derive(Debug, Clone)]
46pub struct FfmpegVersion {
47 pub binary: String,
48 pub major: u32,
49 pub minor: u32,
50 pub raw: String,
51}
52
53pub fn check_ffmpeg() -> anyhow::Result<FfmpegVersion> {
56 let path = ffmpeg_path();
57 let output = Command::new(&path)
58 .arg("-version")
59 .output()
60 .map_err(|e| anyhow::anyhow!("ffmpeg not found at '{path}': {e}"))?;
61 if !output.status.success() {
62 anyhow::bail!("ffmpeg at '{path}' exited with error");
63 }
64 let stdout = String::from_utf8_lossy(&output.stdout);
65 let first_line = stdout.lines().next().unwrap_or("").trim().to_string();
66 let version = parse_ffmpeg_version(&first_line, path)?;
67 if version.major < MIN_FFMPEG_MAJOR {
68 anyhow::bail!(
69 "ffmpeg {}.{} is too old — viser requires FFmpeg >= {MIN_FFMPEG_MAJOR}.0 (found {})",
70 version.major,
71 version.minor,
72 version.raw,
73 );
74 }
75 Ok(version)
76}
77
78pub fn check_ffprobe() -> anyhow::Result<FfmpegVersion> {
81 let path = ffprobe_path();
82 let output = Command::new(&path)
83 .arg("-version")
84 .output()
85 .map_err(|e| anyhow::anyhow!("ffprobe not found at '{path}': {e}"))?;
86 if !output.status.success() {
87 anyhow::bail!("ffprobe at '{path}' exited with error");
88 }
89 let stdout = String::from_utf8_lossy(&output.stdout);
90 let first_line = stdout.lines().next().unwrap_or("").trim().to_string();
91 parse_ffmpeg_version(&first_line, path)
92}
93
94fn parse_ffmpeg_version(line: &str, path: String) -> anyhow::Result<FfmpegVersion> {
95 let version_str = line
98 .strip_prefix("ffmpeg version ")
99 .or_else(|| line.strip_prefix("ffprobe version "))
100 .and_then(|s| s.split_whitespace().next())
101 .map(|s| s.trim_start_matches('n'))
102 .ok_or_else(|| anyhow::anyhow!("could not parse version from: {line}"))?;
103 let parts: Vec<&str> = version_str.split('.').collect();
104 let major: u32 = parts
105 .first()
106 .and_then(|s| s.parse().ok())
107 .ok_or_else(|| anyhow::anyhow!("could not parse major version from: {version_str}"))?;
108 let minor: u32 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
109 Ok(FfmpegVersion { binary: path, major, minor, raw: version_str.to_string() })
110}
111
112const KNOWN_VMAF_MODELS: &[&str] =
115 &["vmaf_v0.6.1", "vmaf_v0.6.1neg", "vmaf_4k_v0.6.1", "vmaf_b_v0.6.3", "vmaf_4k_v0.6.1neg"];
116
117pub fn validate_vmaf_model(model: &str) -> anyhow::Result<()> {
119 if KNOWN_VMAF_MODELS.contains(&model) {
120 return Ok(());
121 }
122 anyhow::bail!("unknown VMAF model '{model}'. Known models: {}", KNOWN_VMAF_MODELS.join(", "));
123}
124
125fn local_binary(name: &str) -> Option<String> {
126 let mut path = PathBuf::from("bin").join("ffmpeg");
127 if cfg!(windows) {
128 path = path.join(format!("{name}.exe"));
129 } else {
130 path = path.join(name);
131 }
132 if path.exists() { Some(path.to_string_lossy().into_owned()) } else { None }
133}