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