1use bytes::Bytes;
18use moq_net::Timestamp;
19
20use super::decoder::{Config, Kind};
21use crate::{Error, Frame};
22
23#[cfg(feature = "openh264")]
24mod openh264;
25
26#[cfg(test)]
27pub(crate) mod probe;
28
29#[cfg(target_os = "macos")]
30mod videotoolbox;
31
32#[cfg(target_os = "windows")]
33mod mediafoundation;
34
35#[cfg(all(target_os = "android", feature = "mediacodec"))]
36mod mediacodec;
37
38#[cfg(all(target_os = "linux", feature = "nvidia"))]
39mod nvdec;
40
41#[cfg(all(target_os = "linux", feature = "vaapi"))]
44pub(crate) mod vaapi;
45
46#[cfg(all(target_os = "linux", feature = "v4l2"))]
47mod v4l2;
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51#[non_exhaustive]
52pub enum Codec {
53 H264,
55 H265,
57 Av1,
59}
60
61impl Codec {
62 fn label(self) -> &'static str {
63 match self {
64 Codec::H264 => "H.264",
65 Codec::H265 => "H.265",
66 Codec::Av1 => "AV1",
67 }
68 }
69}
70
71pub(crate) trait Backend {
75 fn decode(&mut self, access_unit: Bytes, timestamp: Timestamp, keyframe: bool) -> Result<Vec<Frame>, Error>;
82
83 fn flush(&mut self) -> Result<Vec<Frame>, Error>;
89
90 fn name(&self) -> &str;
92}
93
94pub const NAMES: &[&str] = &[
99 "videotoolbox",
100 "mediafoundation",
101 "mediacodec",
102 "nvdec",
103 "vaapi",
104 "v4l2",
105 "openh264",
106];
107
108type Open = fn(Codec, &Config) -> Result<Box<dyn Backend>, Error>;
110
111struct Candidate {
113 name: &'static str,
114 supports: fn(Codec) -> bool,
115 open: Open,
116}
117
118const HARDWARE: &[Candidate] = &[
121 #[cfg(target_os = "macos")]
122 Candidate {
123 name: videotoolbox::NAME,
124 supports: |c| matches!(c, Codec::H264 | Codec::H265),
125 open: videotoolbox::VideoToolbox::open,
126 },
127 #[cfg(target_os = "windows")]
128 Candidate {
129 name: mediafoundation::NAME,
130 supports: |c| matches!(c, Codec::H264 | Codec::H265),
131 open: mediafoundation::MediaFoundation::open,
132 },
133 #[cfg(all(target_os = "android", feature = "mediacodec"))]
134 Candidate {
135 name: mediacodec::NAME,
136 supports: |c| matches!(c, Codec::H264 | Codec::H265 | Codec::Av1),
137 open: mediacodec::MediaCodec::open,
138 },
139 #[cfg(all(target_os = "linux", feature = "nvidia"))]
140 Candidate {
141 name: nvdec::NAME,
142 supports: |c| matches!(c, Codec::H264 | Codec::H265 | Codec::Av1),
143 open: nvdec::Nvdec::open,
144 },
145 #[cfg(all(target_os = "linux", feature = "vaapi"))]
146 Candidate {
147 name: vaapi::NAME,
148 supports: |c| matches!(c, Codec::H264),
149 open: vaapi::Vaapi::open,
150 },
151 #[cfg(all(target_os = "linux", feature = "v4l2"))]
155 Candidate {
156 name: v4l2::NAME,
157 supports: |c| matches!(c, Codec::H264),
158 open: v4l2::V4l2::open,
159 },
160];
161
162const SOFTWARE: &[Candidate] = &[
163 #[cfg(feature = "openh264")]
164 Candidate {
165 name: openh264::NAME,
166 supports: |c| matches!(c, Codec::H264),
167 open: openh264::Openh264::open,
168 },
169];
170
171#[cfg(test)]
175const NAMED_ONLY: &[Candidate] = &[
176 Candidate {
177 name: probe::NAME,
178 supports: |c| matches!(c, Codec::H264),
179 open: probe::Probe::open,
180 },
181 Candidate {
182 name: probe::BUFFERED_NAME,
183 supports: |c| matches!(c, Codec::H264),
184 open: probe::Buffered::open,
185 },
186 Candidate {
187 name: probe::NATIVE_NAME,
188 supports: |c| matches!(c, Codec::H264),
189 open: probe::Native::open,
190 },
191 #[cfg(not(target_os = "macos"))]
192 Candidate {
193 name: probe::BLOCKING_FLUSH_NAME,
194 supports: |c| matches!(c, Codec::H264),
195 open: probe::BlockingFlush::open,
196 },
197];
198
199#[cfg(not(test))]
200const NAMED_ONLY: &[Candidate] = &[];
201
202struct Attempt<'a> {
206 candidate: &'a Candidate,
207 hardware: bool,
208}
209
210impl<'a> Attempt<'a> {
211 fn hardware(candidate: &'a Candidate) -> Self {
212 Self {
213 candidate,
214 hardware: true,
215 }
216 }
217
218 fn software(candidate: &'a Candidate) -> Self {
219 Self {
220 candidate,
221 hardware: false,
222 }
223 }
224}
225
226pub(crate) fn open(codec: Codec, config: &Config) -> Result<Box<dyn Backend>, Error> {
230 let attempts: Vec<Attempt> = match &config.kind {
231 Kind::Auto => HARDWARE
232 .iter()
233 .map(Attempt::hardware)
234 .chain(SOFTWARE.iter().map(Attempt::software))
235 .collect(),
236 Kind::Hardware => HARDWARE.iter().map(Attempt::hardware).collect(),
237 Kind::Software => SOFTWARE.iter().map(Attempt::software).collect(),
238 Kind::Named(name) => HARDWARE
239 .iter()
240 .map(Attempt::hardware)
241 .chain(SOFTWARE.iter().chain(NAMED_ONLY.iter()).map(Attempt::software))
242 .filter(|a| a.candidate.name == name)
243 .collect(),
244 };
245
246 select(codec, attempts, config)
247}
248
249fn select(codec: Codec, attempts: Vec<Attempt>, config: &Config) -> Result<Box<dyn Backend>, Error> {
256 let mut tried: Vec<String> = Vec::new();
260 let mut refused = Vec::new();
261
262 for attempt in attempts {
263 if !(attempt.candidate.supports)(codec) {
264 continue;
265 }
266
267 let name = attempt.candidate.name;
268
269 match (attempt.candidate.open)(codec, config) {
270 Ok(backend) => {
271 if !attempt.hardware && !refused.is_empty() {
275 tracing::warn!(
276 decoder = name,
277 refused = %refused.join(", "),
278 "no hardware decoder available, falling back to software"
279 );
280 }
281 return Ok(backend);
282 }
283 Err(e) => {
284 tracing::debug!(decoder = name, error = %e, "decoder unavailable, trying next");
285 tried.push(format!("{name}: {e}"));
286 if attempt.hardware {
287 refused.push(format!("{name}: {e}"));
288 }
289 }
290 }
291 }
292
293 if tried.is_empty() {
298 let available = available_names(codec);
299 return match &config.kind {
300 Kind::Named(name) => Err(Error::UnknownDecoder {
301 name: name.clone(),
302 codec,
303 available: available.join(", "),
304 }),
305 kind => Err(Error::NoDecoder(format!(
306 "nothing compiled in for {} at {kind:?} (this build has: {})",
307 codec.label(),
308 available.join(", "),
309 ))),
310 };
311 }
312 Err(Error::NoDecoder(tried.join(", ")))
313}
314
315fn available_names(codec: Codec) -> Vec<&'static str> {
320 HARDWARE
321 .iter()
322 .chain(SOFTWARE.iter())
323 .filter(|candidate| (candidate.supports)(codec))
324 .map(|candidate| candidate.name)
325 .collect()
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 struct Stub;
335
336 impl Stub {
337 fn open(_codec: Codec, _config: &Config) -> Result<Box<dyn Backend>, Error> {
338 Ok(Box::new(Self))
339 }
340 }
341
342 impl Backend for Stub {
343 fn decode(&mut self, _access_unit: Bytes, _timestamp: Timestamp, _keyframe: bool) -> Result<Vec<Frame>, Error> {
344 Ok(Vec::new())
345 }
346
347 fn flush(&mut self) -> Result<Vec<Frame>, Error> {
348 Ok(Vec::new())
349 }
350
351 fn name(&self) -> &str {
352 "stub"
353 }
354 }
355
356 const WORKING: Candidate = Candidate {
357 name: "stub",
358 supports: |c| matches!(c, Codec::H264),
359 open: Stub::open,
360 };
361
362 const REFUSING: Candidate = Candidate {
365 name: "driverless",
366 supports: |c| matches!(c, Codec::H264),
367 open: |_, _| Err(Error::Codec(anyhow::anyhow!("driver libraries not found"))),
368 };
369
370 #[tracing_test::traced_test]
371 #[test]
372 fn falling_past_hardware_warns() {
373 let config = Config::new();
374 let attempts = vec![Attempt::hardware(&REFUSING), Attempt::software(&WORKING)];
375 let backend = select(Codec::H264, attempts, &config).unwrap();
376 assert_eq!(backend.name(), "stub");
377
378 logs_assert(
379 |lines: &[&str]| match lines.iter().find(|line| line.contains("falling back to software")) {
380 Some(warning) if warning.contains("driverless") && warning.contains("driver libraries not found") => {
381 Ok(())
382 }
383 Some(warning) => Err(format!("warning does not name the refusal: {warning}")),
384 None => Err("no fallback warning".to_owned()),
385 },
386 );
387 }
388
389 #[tracing_test::traced_test]
393 #[test]
394 fn hardware_that_cannot_decode_the_codec_is_not_a_fallback() {
395 const H265_ONLY: Candidate = Candidate {
396 name: "driverless",
397 supports: |c| matches!(c, Codec::H265),
398 open: |_, _| Err(Error::Codec(anyhow::anyhow!("driver libraries not found"))),
399 };
400
401 let attempts = vec![Attempt::hardware(&H265_ONLY), Attempt::software(&WORKING)];
402 select(Codec::H264, attempts, &Config::new()).unwrap();
403 assert!(!logs_contain("no hardware decoder available"));
404 }
405
406 #[test]
409 fn an_unknown_name_names_itself_and_the_alternatives() {
410 let mut config = Config::new();
411 config.kind = Kind::Named("vappi".to_owned());
412
413 match open(Codec::H264, &config) {
414 Err(Error::UnknownDecoder { name, codec, available }) => {
415 assert_eq!(name, "vappi");
416 assert_eq!(codec, crate::decode::Codec::H264);
417 #[cfg(feature = "openh264")]
418 assert!(available.contains(openh264::NAME), "nothing offered: {available}");
419 #[cfg(not(feature = "openh264"))]
420 assert!(!available.contains("openh264"), "disabled backend offered: {available}");
421 }
422 Err(other) => panic!("expected UnknownDecoder, got {other:?}"),
423 Ok(backend) => panic!("expected UnknownDecoder, opened {}", backend.name()),
424 }
425 }
426
427 #[cfg(not(feature = "openh264"))]
428 #[test]
429 fn disabled_software_backend_is_not_selected() {
430 let mut config = Config::new();
431 config.kind = Kind::Software;
432 assert!(matches!(open(Codec::H264, &config), Err(Error::NoDecoder(_))));
433
434 config.kind = Kind::Named("openh264".to_owned());
435 assert!(matches!(open(Codec::H264, &config), Err(Error::UnknownDecoder { .. })));
436 }
437
438 #[test]
441 fn every_candidate_refusing_reports_why() {
442 let mut config = Config::new();
443 config.kind = Kind::Named("driverless".to_owned());
444
445 match select(Codec::H264, vec![Attempt::hardware(&REFUSING)], &config) {
446 Err(Error::NoDecoder(tried)) => {
447 assert!(tried.contains("driverless"), "does not name the backend: {tried}");
448 assert!(
449 tried.contains("driver libraries not found"),
450 "does not carry the reason: {tried}"
451 );
452 }
453 Err(other) => panic!("expected NoDecoder, got {other:?}"),
454 Ok(backend) => panic!("expected NoDecoder, opened {}", backend.name()),
455 }
456 }
457
458 #[test]
460 fn every_compiled_backend_is_named_publicly() {
461 for candidate in HARDWARE.iter().chain(SOFTWARE.iter()) {
462 assert!(
463 NAMES.contains(&candidate.name),
464 "{} is compiled in but missing from NAMES",
465 candidate.name,
466 );
467 }
468 }
469}