1use bytes::Bytes;
17use moq_net::Timestamp;
18
19use super::decoder::{Config, Kind};
20use crate::{Error, Frame};
21
22mod openh264;
23
24#[cfg(test)]
25pub(crate) mod probe;
26
27#[cfg(target_os = "macos")]
28mod videotoolbox;
29
30#[cfg(target_os = "windows")]
31mod mediafoundation;
32
33#[cfg(all(target_os = "android", feature = "mediacodec"))]
34mod mediacodec;
35
36#[cfg(all(target_os = "linux", feature = "nvidia"))]
37mod nvdec;
38
39#[cfg(all(target_os = "linux", feature = "vaapi"))]
42pub(crate) mod vaapi;
43
44#[cfg(all(target_os = "linux", feature = "v4l2"))]
45mod v4l2;
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49#[non_exhaustive]
50pub enum Codec {
51 H264,
53 H265,
55 Av1,
57}
58
59impl Codec {
60 fn label(self) -> &'static str {
61 match self {
62 Codec::H264 => "H.264",
63 Codec::H265 => "H.265",
64 Codec::Av1 => "AV1",
65 }
66 }
67}
68
69pub(crate) trait Backend: Send {
73 fn decode(&mut self, access_unit: Bytes, timestamp: Timestamp, keyframe: bool) -> Result<Vec<Frame>, Error>;
80
81 fn flush(&mut self) -> Result<Vec<Frame>, Error>;
87
88 fn name(&self) -> &str;
90}
91
92pub const NAMES: &[&str] = &[
97 "videotoolbox",
98 "mediafoundation",
99 "mediacodec",
100 "nvdec",
101 "vaapi",
102 "v4l2",
103 "openh264",
104];
105
106type Open = fn(Codec, &Config) -> Result<Box<dyn Backend>, Error>;
108
109struct Candidate {
111 name: &'static str,
112 supports: fn(Codec) -> bool,
113 open: Open,
114}
115
116const HARDWARE: &[Candidate] = &[
119 #[cfg(target_os = "macos")]
120 Candidate {
121 name: videotoolbox::NAME,
122 supports: |c| matches!(c, Codec::H264 | Codec::H265),
123 open: videotoolbox::VideoToolbox::open,
124 },
125 #[cfg(target_os = "windows")]
126 Candidate {
127 name: mediafoundation::NAME,
128 supports: |c| matches!(c, Codec::H264 | Codec::H265),
129 open: mediafoundation::MediaFoundation::open,
130 },
131 #[cfg(all(target_os = "android", feature = "mediacodec"))]
132 Candidate {
133 name: mediacodec::NAME,
134 supports: |c| matches!(c, Codec::H264 | Codec::H265 | Codec::Av1),
135 open: mediacodec::MediaCodec::open,
136 },
137 #[cfg(all(target_os = "linux", feature = "nvidia"))]
138 Candidate {
139 name: nvdec::NAME,
140 supports: |c| matches!(c, Codec::H264 | Codec::H265 | Codec::Av1),
141 open: nvdec::Nvdec::open,
142 },
143 #[cfg(all(target_os = "linux", feature = "vaapi"))]
144 Candidate {
145 name: vaapi::NAME,
146 supports: |c| matches!(c, Codec::H264),
147 open: vaapi::Vaapi::open,
148 },
149 #[cfg(all(target_os = "linux", feature = "v4l2"))]
153 Candidate {
154 name: v4l2::NAME,
155 supports: |c| matches!(c, Codec::H264),
156 open: v4l2::V4l2::open,
157 },
158];
159
160const SOFTWARE: Candidate = Candidate {
161 name: openh264::NAME,
162 supports: |c| matches!(c, Codec::H264),
163 open: openh264::Openh264::open,
164};
165
166#[cfg(test)]
170const NAMED_ONLY: &[Candidate] = &[
171 Candidate {
172 name: probe::NAME,
173 supports: |c| matches!(c, Codec::H264),
174 open: probe::Probe::open,
175 },
176 Candidate {
177 name: probe::BUFFERED_NAME,
178 supports: |c| matches!(c, Codec::H264),
179 open: probe::Buffered::open,
180 },
181 #[cfg(not(target_os = "macos"))]
182 Candidate {
183 name: probe::BLOCKING_FLUSH_NAME,
184 supports: |c| matches!(c, Codec::H264),
185 open: probe::BlockingFlush::open,
186 },
187];
188
189#[cfg(not(test))]
190const NAMED_ONLY: &[Candidate] = &[];
191
192struct Attempt<'a> {
196 candidate: &'a Candidate,
197 hardware: bool,
198}
199
200impl<'a> Attempt<'a> {
201 fn hardware(candidate: &'a Candidate) -> Self {
202 Self {
203 candidate,
204 hardware: true,
205 }
206 }
207
208 fn software(candidate: &'a Candidate) -> Self {
209 Self {
210 candidate,
211 hardware: false,
212 }
213 }
214}
215
216pub(crate) fn open(codec: Codec, config: &Config) -> Result<Box<dyn Backend>, Error> {
220 let attempts: Vec<Attempt> = match &config.kind {
221 Kind::Auto => HARDWARE
222 .iter()
223 .map(Attempt::hardware)
224 .chain(std::iter::once(Attempt::software(&SOFTWARE)))
225 .collect(),
226 Kind::Hardware => HARDWARE.iter().map(Attempt::hardware).collect(),
227 Kind::Software => vec![Attempt::software(&SOFTWARE)],
228 Kind::Named(name) => HARDWARE
229 .iter()
230 .map(Attempt::hardware)
231 .chain(
232 std::iter::once(&SOFTWARE)
233 .chain(NAMED_ONLY.iter())
234 .map(Attempt::software),
235 )
236 .filter(|a| a.candidate.name == name)
237 .collect(),
238 };
239
240 select(codec, attempts, config)
241}
242
243fn select(codec: Codec, attempts: Vec<Attempt>, config: &Config) -> Result<Box<dyn Backend>, Error> {
250 let mut tried: Vec<String> = Vec::new();
254 let mut refused = Vec::new();
255
256 for attempt in attempts {
257 if !(attempt.candidate.supports)(codec) {
258 continue;
259 }
260
261 let name = attempt.candidate.name;
262
263 match (attempt.candidate.open)(codec, config) {
264 Ok(backend) => {
265 if !attempt.hardware && !refused.is_empty() {
269 tracing::warn!(
270 decoder = name,
271 refused = %refused.join(", "),
272 "no hardware decoder available, falling back to software"
273 );
274 }
275 return Ok(backend);
276 }
277 Err(e) => {
278 tracing::debug!(decoder = name, error = %e, "decoder unavailable, trying next");
279 tried.push(format!("{name}: {e}"));
280 if attempt.hardware {
281 refused.push(format!("{name}: {e}"));
282 }
283 }
284 }
285 }
286
287 if tried.is_empty() {
292 let available = available_names(codec);
293 return match &config.kind {
294 Kind::Named(name) => Err(Error::UnknownDecoder {
295 name: name.clone(),
296 codec,
297 available: available.join(", "),
298 }),
299 kind => Err(Error::NoDecoder(format!(
300 "nothing compiled in for {} at {kind:?} (this build has: {})",
301 codec.label(),
302 available.join(", "),
303 ))),
304 };
305 }
306 Err(Error::NoDecoder(tried.join(", ")))
307}
308
309fn available_names(codec: Codec) -> Vec<&'static str> {
314 HARDWARE
315 .iter()
316 .chain(std::iter::once(&SOFTWARE))
317 .filter(|candidate| (candidate.supports)(codec))
318 .map(|candidate| candidate.name)
319 .collect()
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 struct Stub;
329
330 impl Stub {
331 fn open(_codec: Codec, _config: &Config) -> Result<Box<dyn Backend>, Error> {
332 Ok(Box::new(Self))
333 }
334 }
335
336 impl Backend for Stub {
337 fn decode(&mut self, _access_unit: Bytes, _timestamp: Timestamp, _keyframe: bool) -> Result<Vec<Frame>, Error> {
338 Ok(Vec::new())
339 }
340
341 fn flush(&mut self) -> Result<Vec<Frame>, Error> {
342 Ok(Vec::new())
343 }
344
345 fn name(&self) -> &str {
346 "stub"
347 }
348 }
349
350 const WORKING: Candidate = Candidate {
351 name: "stub",
352 supports: |c| matches!(c, Codec::H264),
353 open: Stub::open,
354 };
355
356 const REFUSING: Candidate = Candidate {
359 name: "driverless",
360 supports: |c| matches!(c, Codec::H264),
361 open: |_, _| Err(Error::Codec(anyhow::anyhow!("driver libraries not found"))),
362 };
363
364 #[tracing_test::traced_test]
365 #[test]
366 fn falling_past_hardware_warns() {
367 let config = Config::new();
368 let attempts = vec![Attempt::hardware(&REFUSING), Attempt::software(&WORKING)];
369 let backend = select(Codec::H264, attempts, &config).unwrap();
370 assert_eq!(backend.name(), "stub");
371
372 logs_assert(
373 |lines: &[&str]| match lines.iter().find(|line| line.contains("falling back to software")) {
374 Some(warning) if warning.contains("driverless") && warning.contains("driver libraries not found") => {
375 Ok(())
376 }
377 Some(warning) => Err(format!("warning does not name the refusal: {warning}")),
378 None => Err("no fallback warning".to_owned()),
379 },
380 );
381 }
382
383 #[tracing_test::traced_test]
387 #[test]
388 fn hardware_that_cannot_decode_the_codec_is_not_a_fallback() {
389 const H265_ONLY: Candidate = Candidate {
390 name: "driverless",
391 supports: |c| matches!(c, Codec::H265),
392 open: |_, _| Err(Error::Codec(anyhow::anyhow!("driver libraries not found"))),
393 };
394
395 let attempts = vec![Attempt::hardware(&H265_ONLY), Attempt::software(&WORKING)];
396 select(Codec::H264, attempts, &Config::new()).unwrap();
397 assert!(!logs_contain("no hardware decoder available"));
398 }
399
400 #[test]
403 fn an_unknown_name_names_itself_and_the_alternatives() {
404 let mut config = Config::new();
405 config.kind = Kind::Named("vappi".to_owned());
406
407 match open(Codec::H264, &config) {
408 Err(Error::UnknownDecoder { name, codec, available }) => {
409 assert_eq!(name, "vappi");
410 assert_eq!(codec, crate::decode::Codec::H264);
411 assert!(available.contains(openh264::NAME), "nothing offered: {available}");
413 }
414 Err(other) => panic!("expected UnknownDecoder, got {other:?}"),
415 Ok(backend) => panic!("expected UnknownDecoder, opened {}", backend.name()),
416 }
417 }
418
419 #[test]
422 fn every_candidate_refusing_reports_why() {
423 let mut config = Config::new();
424 config.kind = Kind::Named("driverless".to_owned());
425
426 match select(Codec::H264, vec![Attempt::hardware(&REFUSING)], &config) {
427 Err(Error::NoDecoder(tried)) => {
428 assert!(tried.contains("driverless"), "does not name the backend: {tried}");
429 assert!(
430 tried.contains("driver libraries not found"),
431 "does not carry the reason: {tried}"
432 );
433 }
434 Err(other) => panic!("expected NoDecoder, got {other:?}"),
435 Ok(backend) => panic!("expected NoDecoder, opened {}", backend.name()),
436 }
437 }
438
439 #[test]
441 fn every_compiled_backend_is_named_publicly() {
442 for candidate in HARDWARE.iter().chain(std::iter::once(&SOFTWARE)) {
443 assert!(
444 NAMES.contains(&candidate.name),
445 "{} is compiled in but missing from NAMES",
446 candidate.name,
447 );
448 }
449 }
450}