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, exactly like the encode side. Only backends
13//! that support the requested codec are considered: there is no software H.265
14//! or AV1 decoder, so those tracks have no fallback below the hardware path.
15
16use 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// Crate-visible so `decode::Consumer`'s end-of-track test can name the one
40// backend that holds pictures back; nothing outside the tests reaches for it.
41#[cfg(all(target_os = "linux", feature = "vaapi"))]
42pub(crate) mod vaapi;
43
44#[cfg(all(target_os = "linux", feature = "v4l2"))]
45mod v4l2;
46
47/// The video codec a decoder handles, derived from the catalog.
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49#[non_exhaustive]
50pub enum Codec {
51	/// H.264 / AVC video.
52	H264,
53	/// H.265 / HEVC video.
54	H265,
55	/// AV1 video.
56	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
69/// An opened decoder. Feed it prepared access units in decode order; get back
70/// zero or more decoded frames (zero while the decoder is still buffering, e.g.
71/// before the first keyframe's parameter sets).
72pub(crate) trait Backend: Send {
73	/// Decode one access unit stamped with its presentation `timestamp`.
74	/// `keyframe` marks a random-access frame. Takes an owned [`Bytes`] so a
75	/// backend can split codec units without copying.
76	/// Backends that decode one-in one-out echo the input timestamp; NVDEC and
77	/// MediaCodec thread timestamps through the codec, so they survive decoder
78	/// delay and frame reordering.
79	fn decode(&mut self, access_unit: Bytes, timestamp: Timestamp, keyframe: bool) -> Result<Vec<Frame>, Error>;
80
81	/// Return the pictures the codec still holds once the stream has ended.
82	///
83	/// Backends configured for zero delay return no frames. A backend that
84	/// reorders pictures overrides this so the end of a track does not drop its
85	/// buffered tail.
86	fn flush(&mut self) -> Result<Vec<Frame>, Error>;
87
88	/// The decoder name in use, e.g. `"videotoolbox"` (for logging).
89	fn name(&self) -> &str;
90}
91
92/// Every decoder backend this crate has a name for, on any platform.
93///
94/// The decode counterpart of [`encode::backend::NAMES`](crate::encode::NAMES),
95/// and platform-complete for the same reason.
96pub const NAMES: &[&str] = &[
97	"videotoolbox",
98	"mediafoundation",
99	"mediacodec",
100	"nvdec",
101	"vaapi",
102	"v4l2",
103	"openh264",
104];
105
106/// A backend opener: builds a decoder for a codec and config.
107type Open = fn(Codec, &Config) -> Result<Box<dyn Backend>, Error>;
108
109/// A backend constructor: name, the codecs it can decode, and an opener.
110struct Candidate {
111	name: &'static str,
112	supports: fn(Codec) -> bool,
113	open: Open,
114}
115
116/// Hardware backends, in priority order. Platform-gated so only the ones that
117/// could plausibly work on this target are even listed.
118const 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	// Last of the Linux hardware decoders, for the same reason as its encode
150	// counterpart: the SoC blocks it drives are the only hardware on a board that
151	// has no NVIDIA GPU.
152	#[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/// Test-only backends. Deliberately in neither list above, so `Auto` /
167/// `Hardware` / `Software` can never select one: they exist to be asked for by
168/// name.
169#[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
192/// A candidate paired with the tier it came from, so [`select`] can tell a
193/// software decoder that was asked for from one reached by falling past
194/// hardware that refused to open.
195struct 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
216/// Open the best decoder for `codec` and `config`, trying candidates in priority
217/// order and falling back until one succeeds. Candidates that don't support the
218/// codec are skipped before they're even tried.
219pub(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
243/// Try `attempts` in order and return the first decoder that opens, warning when
244/// that means falling past hardware.
245///
246/// Split out from [`open`] for the same reason as its encode counterpart: the
247/// candidate lists are platform-gated consts, so a test supplies its own
248/// attempts rather than depending on what the host GPU can do.
249fn select(codec: Codec, attempts: Vec<Attempt>, config: &Config) -> Result<Box<dyn Backend>, Error> {
250	// Each entry is "name: why it refused". The names alone say which backends
251	// exist, which is what a reader already knows; the reasons say why this
252	// machine has none, which is the question being asked.
253	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				// Same reasoning as the encode side: a compiled-in hardware decoder that
266				// refuses to open is otherwise invisible, since `Auto` hands back a
267				// working software decoder and says nothing above DEBUG.
268				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	// Nothing was tried at all, so no candidate both matched and takes this
288	// codec. For a named request that is a name this build does not have: a
289	// typo, a feature that is off, or a backend that does not decode this
290	// codec. Naming what is here is most of the answer.
291	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
309/// Returns the decoders this build has for `codec`, in priority order.
310///
311/// Only the ones a user could ask for: the test-only list is left out, since it
312/// exists to be named by a test rather than offered to anybody.
313fn 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	/// A backend that opens and decodes nothing, the decode mirror of the encode
327	/// side's stub.
328	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	/// Compiled in but refusing at runtime, the way NVDEC does on a host whose
357	/// driver libraries aren't on the loader path.
358	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	/// A hardware candidate skipped for not supporting the codec never ran, so it
384	/// refused nothing and the software pick isn't a fallback. Only the decode side
385	/// can hit this: it filters by codec inside the loop rather than up front.
386	#[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	/// A name no candidate answers to has to say so, and say what it could have
401	/// been asked for instead.
402	#[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				// openh264 is unconditional, so every build has one to offer.
412				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	/// The reason each candidate refused belongs in the error. Only the DEBUG
420	/// line used to carry it, which is no use to a caller holding the `Err`.
421	#[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	/// The decode half of the same guarantee the encode side keeps.
440	#[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}