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, run_pass_cancellable, CancelToken};
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 #[error("cancelled")]
54 Cancelled,
55}
56
57#[derive(Debug, Clone)]
59pub struct Tools {
60 pub ffmpeg: PathBuf,
61 pub ffprobe: PathBuf,
62 pub cancel: CancelToken,
65}
66
67impl Tools {
68 pub fn with_cancel(mut self, cancel: CancelToken) -> Self {
70 self.cancel = cancel;
71 self
72 }
73
74 pub fn run_pass<S: AsRef<std::ffi::OsStr>>(
76 &self,
77 args: &[S],
78 total_secs: f64,
79 on_progress: &mut dyn FnMut(f64),
80 ) -> Result<(), FfmpegError> {
81 run_pass_cancellable(&self.ffmpeg, args, total_secs, on_progress, &self.cancel)
82 }
83}
84
85pub fn locate() -> Result<Tools, FfmpegError> {
87 Ok(Tools {
88 ffmpeg: locate_one("ffmpeg", "DEEPSHRINK_FFMPEG")?,
89 ffprobe: locate_one("ffprobe", "DEEPSHRINK_FFPROBE")?,
90 cancel: CancelToken::default(),
91 })
92}
93
94pub fn locate_one(tool: &'static str, env: &'static str) -> Result<PathBuf, FfmpegError> {
96 if let Some(explicit) = std::env::var_os(env) {
97 if !explicit.is_empty() {
98 return Ok(PathBuf::from(explicit));
99 }
100 }
101 which_in_path(tool).ok_or(FfmpegError::NotFound { tool, env })
102}
103
104fn which_in_path(tool: &str) -> Option<PathBuf> {
106 let path = std::env::var_os("PATH")?;
107 let names = candidate_names(tool);
108 for dir in std::env::split_paths(&path) {
109 if dir.as_os_str().is_empty() {
110 continue;
111 }
112 for name in &names {
113 let candidate = dir.join(name);
114 if candidate.is_file() {
115 return Some(candidate);
116 }
117 }
118 }
119 None
120}
121
122#[cfg(windows)]
123fn candidate_names(tool: &str) -> Vec<OsString> {
124 vec![
126 OsString::from(format!("{tool}.exe")),
127 OsString::from(format!("{tool}.bat")),
128 OsString::from(format!("{tool}.cmd")),
129 OsString::from(tool),
130 ]
131}
132
133#[cfg(not(windows))]
134fn candidate_names(tool: &str) -> Vec<OsString> {
135 vec![OsString::from(tool)]
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn missing_binary_is_none() {
144 assert!(which_in_path("deepshrink-definitely-not-a-real-binary-xyz").is_none());
145 }
146
147 #[test]
148 fn candidate_names_include_bare_tool() {
149 let names = candidate_names("ffmpeg");
150 assert!(names.contains(&OsString::from("ffmpeg")));
151 }
152}