Skip to main content

deepshrink_ffmpeg/
caps.rs

1//! What the local ffmpeg build can actually do.
2//!
3//! ffmpeg is an external dependency we don't control: the same version can ship
4//! with or without a given encoder (AV1 in particular). Callers gate on these
5//! probes and either fall back or fail with a clear message, instead of letting
6//! ffmpeg reject the argv with a wall of text.
7
8use std::path::Path;
9use std::process::Command;
10
11/// Whether this ffmpeg build exposes an encoder by name (e.g. `libsvtav1`).
12///
13/// Matches on the encoder-name column of `ffmpeg -encoders`, so a name that only
14/// appears inside a description doesn't count as a match.
15pub fn has_encoder(ffmpeg: &Path, name: &str) -> bool {
16    Command::new(ffmpeg)
17        .args(["-hide_banner", "-encoders"])
18        .output()
19        .map(|o| {
20            let text = String::from_utf8_lossy(&o.stdout);
21            encoder_listed(&text, name)
22        })
23        .unwrap_or(false)
24}
25
26/// Whether this ffmpeg has the video filter `name` (e.g. `zscale`, which needs
27/// a build with libzimg). Spawns `ffmpeg -filters`; callers ask only when needed.
28pub fn has_filter(ffmpeg: &Path, name: &str) -> bool {
29    Command::new(ffmpeg)
30        .args(["-hide_banner", "-filters"])
31        .output()
32        .map(|o| filter_listed(&String::from_utf8_lossy(&o.stdout), name))
33        .unwrap_or(false)
34}
35
36/// Parse the `ffmpeg -filters` table (" TS colorspace   V->V   Convert…").
37pub(crate) fn filter_listed(listing: &str, name: &str) -> bool {
38    listing.lines().any(|line| {
39        let mut cols = line.split_whitespace();
40        let flags = cols.next().unwrap_or("");
41        flags.len() <= 3 && flags.chars().all(|c| "TSC.".contains(c)) && cols.next() == Some(name)
42    })
43}
44
45/// Parse the `ffmpeg -encoders` table for an exact encoder name. Split out from
46/// the process call so the parsing is testable without ffmpeg.
47pub(crate) fn encoder_listed(listing: &str, name: &str) -> bool {
48    listing.lines().any(|line| {
49        // " V..... libsvtav1            SVT-AV1 … encoder (codec av1)"
50        let mut cols = line.split_whitespace();
51        let flags = cols.next().unwrap_or("");
52        // The flag column is a fixed-width capability mask, never a word.
53        flags.len() == 6 && cols.next() == Some(name)
54    })
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn filters_table_is_parsed_by_exact_name() {
63        let listing = " TS colorspace        V->V       Convert between colorspaces.\n \
64                        .S tonemap           V->V       Conversion to/from different dynamic ranges.\n";
65        assert!(filter_listed(listing, "tonemap"));
66        assert!(filter_listed(listing, "colorspace"));
67        assert!(!filter_listed(listing, "zscale"));
68        assert!(!filter_listed(listing, "V->V"));
69    }
70
71    const LISTING: &str = "Encoders:
72 V..... libx264              libx264 H.264 / AVC (codec h264)
73 V..... libsvtav1            SVT-AV1 encoder (codec av1)
74 A....D aac                  AAC (Advanced Audio Coding)
75";
76
77    #[test]
78    fn finds_a_listed_encoder() {
79        assert!(encoder_listed(LISTING, "libsvtav1"));
80        assert!(encoder_listed(LISTING, "libx264"));
81        assert!(encoder_listed(LISTING, "aac"));
82    }
83
84    #[test]
85    fn rejects_absent_and_description_only_matches() {
86        assert!(!encoder_listed(LISTING, "libaom-av1"));
87        // "AV1" and "codec av1" appear in the description column only.
88        assert!(!encoder_listed(LISTING, "av1"));
89        assert!(!encoder_listed(LISTING, "Encoders:"));
90    }
91}