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;