aede_core/ffmpeg.rs
1//! The one place that knows where ffmpeg is, and what to say when it is not.
2//!
3//! **ffmpeg is an external program, not a dependency.** Nothing is linked,
4//! nothing is vendored, and a checkout without ffmpeg installed builds and
5//! passes its tests — the features that need it say so and stop. Copying,
6//! spectrum rendering and playback share these install instructions: a fact
7//! hand-copied into several places will eventually be wrong in one of them.
8//!
9//! The search is deliberately plain — `ffmpeg`, on the `PATH`. A GUI
10//! application has to hunt through `/opt/homebrew/bin` and the rest because it
11//! is launched by Finder and inherits no shell environment; a command run from
12//! a terminal inherits the user's `PATH` by construction, and looking anywhere
13//! else would only find a *different* ffmpeg from the one they get when they
14//! type the name themselves. Playback uses it for formats that the native
15//! decoder cannot open, currently Opus, AAC and ALAC.
16
17use std::process::{Command, Stdio};
18
19/// Drains a helper's error pipe while retaining only its last 4096 bytes.
20///
21/// Reading to EOF keeps a verbose child from blocking on a full pipe. A bounded
22/// tail keeps a corrupt source's repeated decoder errors out of process memory.
23pub(crate) fn diagnostic_tail(mut input: impl std::io::Read) -> std::io::Result<Vec<u8>> {
24 let mut tail = Vec::new();
25 let mut buffer = [0; 1024];
26 loop {
27 let count = match input.read(&mut buffer) {
28 Ok(count) => count,
29 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
30 Err(error) => return Err(error),
31 };
32 if count == 0 {
33 return Ok(tail);
34 }
35 tail.extend_from_slice(&buffer[..count]);
36 if tail.len() > 4096 {
37 tail.drain(..tail.len() - 4096);
38 }
39 }
40}
41
42/// Where ffmpeg is, or `None` when it is not installed.
43///
44/// Callers processing many files can retain this answer for their run.
45pub fn find() -> Option<String> {
46 let name = "ffmpeg";
47 Command::new(name)
48 .arg("-version")
49 .stdout(Stdio::null())
50 .stderr(Stdio::null())
51 .status()
52 .is_ok_and(|status| status.success())
53 .then(|| name.to_string())
54}
55
56/// What to tell somebody who has no ffmpeg, worded so they can act on it.
57///
58/// `what` names the feature that wanted it, because "ffmpeg was not found" on
59/// its own leaves the reader wondering what they have lost.
60pub fn missing(what: &str) -> String {
61 format!(
62 "\
63{what} needs ffmpeg, and it was not found.
64
65ffmpeg is an external program, not something Aède ships: it does the decoding
66and the drawing, Aède decides what to hand it. Everything else works without
67it.
68
69 macOS brew install ffmpeg
70 Debian/Ubuntu sudo apt install ffmpeg
71 Arch sudo pacman -S ffmpeg
72 Fedora sudo dnf install ffmpeg"
73 )
74}
75
76#[cfg(test)]
77#[path = "ffmpeg_tests.rs"]
78mod tests;