Skip to main content

moq_video/decode/backend/
mod.rs

1//! Pluggable video decoder backends.
2//!
3//! The mirror of [`encode::backend`](crate::encode). [`Backend`] is the seam
4//! between the access-unit prep (keyframe gating plus any codec-specific payload
5//! conversion, owned by [`Decoder`](super::Decoder)) and the codec itself. H.264
6//! / H.265 backends take Annex-B access units with parameter sets inline ahead
7//! of each keyframe; AV1 backends take OBU temporal units directly.
8//!
9//! [`open`] picks the best backend for a [`Codec`] and [`Config`], trying
10//! hardware candidates (platform-gated: VideoToolbox on macOS, Media Foundation
11//! / DXVA on Windows, MediaCodec on Android, NVDEC, VAAPI, then V4L2 on Linux) before
12//! the OpenH264 software fallback when this build enables it, exactly like the
13//! encode side. Only backends that support the requested codec are considered:
14//! there is no software H.265 or AV1 decoder, so those tracks have no fallback
15//! below the hardware path.
16
17use 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// Crate-visible so `decode::Consumer`'s end-of-track test can name the one
42// backend that holds pictures back; nothing outside the tests reaches for it.
43#[cfg(all(target_os = "linux", feature = "vaapi"))]
44pub(crate) mod vaapi;
45
46#[cfg(all(target_os = "linux", feature = "v4l2"))]
47mod v4l2;
48
49/// The video codec a decoder handles, derived from the catalog.
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51#[non_exhaustive]
52pub enum Codec {
53	/// H.264 / AVC video.
54	H264,
55	/// H.265 / HEVC video.
56	H265,
57	/// AV1 video.
58	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
71/// An opened decoder. Feed it prepared access units in decode order; get back
72/// zero or more decoded frames (zero while the decoder is still buffering, e.g.
73/// before the first keyframe's parameter sets).
74pub(crate) trait Backend {
75	/// Decode one access unit stamped with its presentation `timestamp`.
76	/// `keyframe` marks a random-access frame. Takes an owned [`Bytes`] so a
77	/// backend can split codec units without copying.
78	/// Backends that decode one-in one-out echo the input timestamp; NVDEC and
79	/// MediaCodec thread timestamps through the codec, so they survive decoder
80	/// delay and frame reordering.
81	fn decode(&mut self, access_unit: Bytes, timestamp: Timestamp, keyframe: bool) -> Result<Vec<Frame>, Error>;
82
83	/// Return the pictures the codec still holds once the stream has ended.
84	///
85	/// Backends configured for zero delay return no frames. A backend that
86	/// reorders pictures overrides this so the end of a track does not drop its
87	/// buffered tail.
88	fn flush(&mut self) -> Result<Vec<Frame>, Error>;
89
90	/// The decoder name in use, e.g. `"videotoolbox"` (for logging).
91	fn name(&self) -> &str;
92}
93
94/// Every decoder backend this crate has a name for, on any platform.
95///
96/// The decode counterpart of [`encode::backend::NAMES`](crate::encode::NAMES),
97/// and platform-complete for the same reason.
98pub const NAMES: &[&str] = &[
99	"videotoolbox",
100	"mediafoundation",
101	"mediacodec",
102	"nvdec",
103	"vaapi",
104	"v4l2",
105	"openh264",
106];
107
108/// A backend opener: builds a decoder for a codec and config.
109type Open = fn(Codec, &Config) -> Result<Box<dyn Backend>, Error>;
110
111/// A backend constructor: name, the codecs it can decode, and an opener.
112struct Candidate {
113	name: &'static str,
114	supports: fn(Codec) -> bool,
115	open: Open,
116}
117
118/// Hardware backends, in priority order. Platform-gated so only the ones that
119/// could plausibly work on this target are even listed.
120const 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	// Last of the Linux hardware decoders, for the same reason as its encode
152	// counterpart: the SoC blocks it drives are the only hardware on a board that
153	// has no NVIDIA GPU.
154	#[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/// Test-only backends. Deliberately in neither list above, so `Auto` /
172/// `Hardware` / `Software` can never select one: they exist to be asked for by
173/// name.
174#[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
202/// A candidate paired with the tier it came from, so [`select`] can tell a
203/// software decoder that was asked for from one reached by falling past
204/// hardware that refused to open.
205struct 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
226/// Open the best decoder for `codec` and `config`, trying candidates in priority
227/// order and falling back until one succeeds. Candidates that don't support the
228/// codec are skipped before they're even tried.
229pub(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
249/// Try `attempts` in order and return the first decoder that opens, warning when
250/// that means falling past hardware.
251///
252/// Split out from [`open`] for the same reason as its encode counterpart: the
253/// candidate lists are platform-gated consts, so a test supplies its own
254/// attempts rather than depending on what the host GPU can do.
255fn select(codec: Codec, attempts: Vec<Attempt>, config: &Config) -> Result<Box<dyn Backend>, Error> {
256	// Each entry is "name: why it refused". The names alone say which backends
257	// exist, which is what a reader already knows; the reasons say why this
258	// machine has none, which is the question being asked.
259	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				// Same reasoning as the encode side: a compiled-in hardware decoder that
272				// refuses to open is otherwise invisible, since `Auto` hands back a
273				// working software decoder and says nothing above DEBUG.
274				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	// Nothing was tried at all, so no candidate both matched and takes this
294	// codec. For a named request that is a name this build does not have: a
295	// typo, a feature that is off, or a backend that does not decode this
296	// codec. Naming what is here is most of the answer.
297	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
315/// Returns the decoders this build has for `codec`, in priority order.
316///
317/// Only the ones a user could ask for: the test-only list is left out, since it
318/// exists to be named by a test rather than offered to anybody.
319fn 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	/// A backend that opens and decodes nothing, the decode mirror of the encode
333	/// side's stub.
334	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	/// Compiled in but refusing at runtime, the way NVDEC does on a host whose
363	/// driver libraries aren't on the loader path.
364	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	/// A hardware candidate skipped for not supporting the codec never ran, so it
390	/// refused nothing and the software pick isn't a fallback. Only the decode side
391	/// can hit this: it filters by codec inside the loop rather than up front.
392	#[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	/// A name no candidate answers to has to say so, and say what it could have
407	/// been asked for instead.
408	#[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	/// The reason each candidate refused belongs in the error. Only the DEBUG
439	/// line used to carry it, which is no use to a caller holding the `Err`.
440	#[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	/// The decode half of the same guarantee the encode side keeps.
459	#[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}