1#![forbid(unsafe_code)]
11
12use std::ffi::OsString;
13use std::path::PathBuf;
14
15use thiserror::Error;
16
17pub mod probe;
18pub mod progress;
19pub mod run;
20pub mod vmaf;
21
22pub use probe::{probe, Ffprobe};
23pub use run::run_pass;
24pub use vmaf::{has_libvmaf, measure_vmaf};
25
26#[derive(Debug, Error)]
28pub enum FfmpegError {
29 #[error(
30 "`{tool}` not found in PATH. Install ffmpeg (macOS: `brew install ffmpeg`) \
31 or point {env} at the binary."
32 )]
33 NotFound {
34 tool: &'static str,
35 env: &'static str,
36 },
37 #[error("failed to spawn `{tool}`: {source}")]
38 Spawn {
39 tool: &'static str,
40 source: std::io::Error,
41 },
42 #[error("`{tool}` exited with {status}:\n{stderr}")]
43 CommandFailed {
44 tool: &'static str,
45 status: String,
46 stderr: String,
47 },
48 #[error("failed to parse ffprobe output: {0}")]
49 Parse(String),
50}
51
52#[derive(Debug, Clone)]
54pub struct Tools {
55 pub ffmpeg: PathBuf,
56 pub ffprobe: PathBuf,
57}
58
59pub fn locate() -> Result<Tools, FfmpegError> {
61 Ok(Tools {
62 ffmpeg: locate_one("ffmpeg", "DEEPSHRINK_FFMPEG")?,
63 ffprobe: locate_one("ffprobe", "DEEPSHRINK_FFPROBE")?,
64 })
65}
66
67pub fn locate_one(tool: &'static str, env: &'static str) -> Result<PathBuf, FfmpegError> {
69 if let Some(explicit) = std::env::var_os(env) {
70 if !explicit.is_empty() {
71 return Ok(PathBuf::from(explicit));
72 }
73 }
74 which_in_path(tool).ok_or(FfmpegError::NotFound { tool, env })
75}
76
77fn which_in_path(tool: &str) -> Option<PathBuf> {
79 let path = std::env::var_os("PATH")?;
80 let names = candidate_names(tool);
81 for dir in std::env::split_paths(&path) {
82 if dir.as_os_str().is_empty() {
83 continue;
84 }
85 for name in &names {
86 let candidate = dir.join(name);
87 if candidate.is_file() {
88 return Some(candidate);
89 }
90 }
91 }
92 None
93}
94
95#[cfg(windows)]
96fn candidate_names(tool: &str) -> Vec<OsString> {
97 vec![
99 OsString::from(format!("{tool}.exe")),
100 OsString::from(format!("{tool}.bat")),
101 OsString::from(format!("{tool}.cmd")),
102 OsString::from(tool),
103 ]
104}
105
106#[cfg(not(windows))]
107fn candidate_names(tool: &str) -> Vec<OsString> {
108 vec![OsString::from(tool)]
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn missing_binary_is_none() {
117 assert!(which_in_path("deepshrink-definitely-not-a-real-binary-xyz").is_none());
118 }
119
120 #[test]
121 fn candidate_names_include_bare_tool() {
122 let names = candidate_names("ffmpeg");
123 assert!(names.contains(&OsString::from("ffmpeg")));
124 }
125}