Skip to main content

viser_ffmpeg/
path.rs

1use std::env;
2use std::path::PathBuf;
3use std::process::Command;
4
5/// Returns the path to the ffmpeg binary.
6///
7/// Resolution order:
8/// 1. `VISER_FFMPEG` environment variable
9/// 2. `bin/ffmpeg/ffmpeg` relative to the working directory
10/// 3. `"ffmpeg"` (system PATH)
11pub 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
23/// Returns the path to the ffprobe binary.
24///
25/// Resolution order:
26/// 1. `VISER_FFPROBE` environment variable
27/// 2. `bin/ffmpeg/ffprobe` relative to the working directory
28/// 3. `"ffprobe"` (system PATH)
29pub 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
41/// Minimum FFmpeg version required (major).
42const MIN_FFMPEG_MAJOR: u32 = 6;
43
44/// Parsed FFmpeg version.
45#[derive(Debug, Clone)]
46pub struct FfmpegVersion {
47    pub binary: String,
48    pub major: u32,
49    pub minor: u32,
50    pub raw: String,
51}
52
53/// Run `ffmpeg -version` and parse the version line. Returns an error if the
54/// binary is not found or the version is too old.
55pub 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
78/// Run `ffprobe -version` and parse the version line. Returns an error if ffprobe
79/// is not found.
80pub 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    // Typical first line: "ffmpeg version 7.1.1 Copyright ..."
96    // or "ffmpeg version n7.1.1-... Copyright ..."
97    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
112/// Known valid libvmaf model names. Models are resolved by FFmpeg's built-in
113/// libvmaf library; these are the names FFmpeg recognizes.
114const 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
117/// Validate that the given VMAF model name is recognized by libvmaf.
118pub 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}