Skip to main content

pito_codecs/media/front/
route.rs

1use std::path::PathBuf;
2
3use super::ffmpeg::Ffmpeg;
4use super::native;
5use crate::media::{Codec, Direction, Error, Result, Route};
6
7#[derive(Debug, Clone, PartialEq, Eq, Default)]
8pub enum Choice {
9    #[default]
10    Auto,
11    Gpu,
12    Cpu,
13    Ffmpeg(UserFfmpeg),
14}
15
16#[derive(Debug, Clone, PartialEq, Eq, Default)]
17pub struct UserFfmpeg {
18    pub path: Option<PathBuf>,
19    pub codec: Option<String>,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum Fallback {
24    H264Gpu,
25    H264Cpu,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct Prefs {
30    pub decode: Choice,
31    pub encode: Choice,
32    pub hevc_encode_fallback: Vec<Fallback>,
33}
34
35impl Default for Prefs {
36    fn default() -> Self {
37        Self {
38            decode: Choice::Auto,
39            encode: Choice::Auto,
40            hevc_encode_fallback: vec![Fallback::H264Gpu, Fallback::H264Cpu],
41        }
42    }
43}
44
45impl Prefs {
46    pub fn ffmpeg() -> Self {
47        Self {
48            decode: Choice::Ffmpeg(UserFfmpeg::default()),
49            encode: Choice::Ffmpeg(UserFfmpeg::default()),
50            ..Self::default()
51        }
52    }
53
54    pub fn user_ffmpeg(&self) -> Option<&UserFfmpeg> {
55        [&self.decode, &self.encode]
56            .into_iter()
57            .find_map(|c| match c {
58                Choice::Ffmpeg(u) => Some(u),
59                _ => None,
60            })
61    }
62}
63
64#[derive(Debug, Clone, Default)]
65pub struct Caps {
66    pub gpu: Vec<(Codec, Direction)>,
67    pub gpu_missing: Option<String>,
68    pub cpu: Vec<(Codec, Direction)>,
69    pub ffmpeg: Option<Ffmpeg>,
70    pub ffmpeg_missing: Option<String>,
71}
72
73impl Caps {
74    pub fn detect(prefs: &Prefs) -> Self {
75        let (gpu, gpu_missing) = native::gpu();
76        let mut caps = Caps {
77            gpu,
78            gpu_missing,
79            cpu: native::cpu(),
80            ..Caps::default()
81        };
82        if let Some(user) = prefs.user_ffmpeg() {
83            match Ffmpeg::find(user.path.as_deref()) {
84                Ok(f) => caps.ffmpeg = Some(f),
85                Err(e) => caps.ffmpeg_missing = Some(e.to_string()),
86            }
87        }
88        caps
89    }
90
91    fn has(&self, route: Route, codec: Codec, dir: Direction) -> bool {
92        match route {
93            Route::Gpu => self.gpu.contains(&(codec, dir)),
94            Route::Cpu => self.cpu.contains(&(codec, dir)),
95            Route::Ffmpeg => self.ffmpeg.as_ref().is_some_and(|f| f.can(codec, dir)),
96        }
97    }
98
99    fn why_not(&self, route: Route, codec: Codec, dir: Direction) -> String {
100        let what = match dir {
101            Direction::Decode => "decoder",
102            Direction::Encode => "encoder",
103        };
104        match route {
105            Route::Gpu => match &self.gpu_missing {
106                Some(why) => format!("no GPU {codec} {what}: {why}"),
107                None => format!("no GPU {codec} {what} on this machine"),
108            },
109            Route::Cpu if codec == Codec::Hevc && dir == Direction::Encode => {
110                "HEVC encodes on the GPU only".to_string()
111            }
112            Route::Cpu => format!("no CPU {codec} {what} in this build"),
113            Route::Ffmpeg => match (&self.ffmpeg, &self.ffmpeg_missing) {
114                (Some(f), _) => format!("{} has no {codec} {what}", f.path.display()),
115                (None, Some(why)) => why.clone(),
116                (None, None) => "ffmpeg is not picked in the settings".to_string(),
117            },
118        }
119    }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct Picked {
124    pub route: Route,
125    pub codec: Codec,
126    pub ffmpeg_codec: Option<String>,
127    pub reasons: Vec<String>,
128}
129
130impl Picked {
131    pub fn said(&self) -> String {
132        let head = format!("{} {}", self.codec, self.route);
133        if self.reasons.is_empty() {
134            head
135        } else {
136            format!("{head} ({})", self.reasons.join("; "))
137        }
138    }
139}
140
141pub fn pick_decode(codec: Codec, prefs: &Prefs, caps: &Caps) -> Result<Picked> {
142    let (order, ffmpeg_codec): (&[Route], _) = match &prefs.decode {
143        Choice::Auto | Choice::Gpu => (&[Route::Gpu, Route::Cpu], None),
144        Choice::Cpu => (&[Route::Cpu], None),
145        Choice::Ffmpeg(user) => (&[Route::Ffmpeg, Route::Gpu, Route::Cpu], user.codec.clone()),
146    };
147    let tries = order.iter().map(|&r| (r, codec)).collect();
148    pick(codec, Direction::Decode, tries, ffmpeg_codec, caps)
149}
150
151pub fn pick_encode(codec: Codec, prefs: &Prefs, caps: &Caps) -> Result<Picked> {
152    let (tries, ffmpeg_codec) = match &prefs.encode {
153        Choice::Ffmpeg(user) => {
154            let mut tries = vec![(Route::Ffmpeg, codec)];
155            tries.extend(native_order(codec, prefs, &[Route::Gpu, Route::Cpu]));
156            (tries, user.codec.clone())
157        }
158        Choice::Auto | Choice::Gpu => (native_order(codec, prefs, &[Route::Gpu, Route::Cpu]), None),
159        Choice::Cpu => (native_order(codec, prefs, &[Route::Cpu]), None),
160    };
161    pick(codec, Direction::Encode, tries, ffmpeg_codec, caps)
162}
163
164fn pick(
165    codec: Codec,
166    dir: Direction,
167    tries: Vec<(Route, Codec)>,
168    ffmpeg_codec: Option<String>,
169    caps: &Caps,
170) -> Result<Picked> {
171    let mut reasons = Vec::new();
172    for (route, c) in tries {
173        if route == Route::Gpu && (c == Codec::Aac || (c == Codec::Vp9 && dir == Direction::Encode))
174        {
175            continue;
176        }
177        if caps.has(route, c, dir) {
178            if c != codec {
179                reasons.push(format!("encoding {c} on the {route} instead"));
180            }
181            return Ok(Picked {
182                route,
183                codec: c,
184                ffmpeg_codec: if route == Route::Ffmpeg {
185                    ffmpeg_codec
186                } else {
187                    None
188                },
189                reasons,
190            });
191        }
192        let why = caps.why_not(route, c, dir);
193        if !reasons.contains(&why) {
194            reasons.push(why);
195        }
196    }
197    let verb = match dir {
198        Direction::Decode => "decode",
199        Direction::Encode => "encode",
200    };
201    Err(Error::Unsupported(format!(
202        "can't {verb} {codec}: {}",
203        reasons.join("; ")
204    )))
205}
206
207fn native_order(codec: Codec, prefs: &Prefs, routes: &[Route]) -> Vec<(Route, Codec)> {
208    if codec != Codec::Hevc {
209        return routes.iter().map(|&r| (r, codec)).collect();
210    }
211    let mut out = Vec::new();
212    if routes.contains(&Route::Gpu) {
213        out.push((Route::Gpu, Codec::Hevc));
214    } else {
215        out.push((Route::Cpu, Codec::Hevc));
216    }
217    for f in &prefs.hevc_encode_fallback {
218        let r = match f {
219            Fallback::H264Gpu => Route::Gpu,
220            Fallback::H264Cpu => Route::Cpu,
221        };
222        if routes.contains(&r) {
223            out.push((r, Codec::H264));
224        }
225    }
226    out
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    fn caps(gpu: &[(Codec, Direction)], cpu: &[(Codec, Direction)]) -> Caps {
234        Caps {
235            gpu: gpu.to_vec(),
236            gpu_missing: None,
237            cpu: cpu.to_vec(),
238            ..Caps::default()
239        }
240    }
241
242    const D: Direction = Direction::Decode;
243    const E: Direction = Direction::Encode;
244
245    #[test]
246    fn auto_prefers_the_gpu_then_the_cpu_and_never_ffmpeg() {
247        let both = caps(&[(Codec::H264, D)], &[(Codec::H264, D)]);
248        let p = pick_decode(Codec::H264, &Prefs::default(), &both).unwrap();
249        assert_eq!((p.route, p.reasons.len()), (Route::Gpu, 0));
250
251        let cpu = caps(&[], &[(Codec::H264, D)]);
252        let p = pick_decode(Codec::H264, &Prefs::default(), &cpu).unwrap();
253        assert_eq!(p.route, Route::Cpu);
254        assert_eq!(p.reasons, ["no GPU H.264 decoder on this machine"]);
255
256        let none = caps(&[], &[]);
257        let e = pick_decode(Codec::Hevc, &Prefs::default(), &none).unwrap_err();
258        assert!(e.to_string().contains("no CPU HEVC decoder"), "{e}");
259    }
260
261    #[test]
262    fn hevc_without_a_gpu_encoder_falls_back_to_h264_gpu_then_cpu_saying_why() {
263        let gpu264 = caps(&[(Codec::H264, E)], &[(Codec::H264, E)]);
264        let p = pick_encode(Codec::Hevc, &Prefs::default(), &gpu264).unwrap();
265        assert_eq!((p.route, p.codec), (Route::Gpu, Codec::H264));
266        assert_eq!(
267            p.reasons,
268            [
269                "no GPU HEVC encoder on this machine",
270                "encoding H.264 on the GPU instead"
271            ]
272        );
273
274        let cpu264 = caps(&[], &[(Codec::H264, E)]);
275        let p = pick_encode(Codec::Hevc, &Prefs::default(), &cpu264).unwrap();
276        assert_eq!((p.route, p.codec), (Route::Cpu, Codec::H264));
277        assert!(
278            p.reasons
279                .contains(&"no GPU H.264 encoder on this machine".into())
280        );
281
282        let only_cpu = Prefs {
283            encode: Choice::Cpu,
284            ..Prefs::default()
285        };
286        let p = pick_encode(Codec::Hevc, &only_cpu, &cpu264).unwrap();
287        assert_eq!((p.route, p.codec), (Route::Cpu, Codec::H264));
288        assert_eq!(p.reasons[0], "HEVC encodes on the GPU only");
289
290        let no_fallback = Prefs {
291            hevc_encode_fallback: vec![],
292            ..Prefs::default()
293        };
294        assert!(pick_encode(Codec::Hevc, &no_fallback, &cpu264).is_err());
295    }
296
297    #[test]
298    fn a_missing_ffmpeg_falls_back_to_our_routes_saying_why() {
299        let mut c = caps(&[], &[(Codec::H264, D)]);
300        c.ffmpeg_missing = Some("no ffmpeg at /nowhere/ffmpeg".into());
301        let p = pick_decode(Codec::H264, &Prefs::ffmpeg(), &c).unwrap();
302        assert_eq!(p.route, Route::Cpu);
303        assert_eq!(p.reasons[0], "no ffmpeg at /nowhere/ffmpeg");
304    }
305}