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}