nv_media/decode.rs
1//! Codec handling and hardware acceleration negotiation.
2//!
3//! Provides the public [`DecodePreference`] type for user-facing decode
4//! configuration and the internal `DecoderSelection` type used by the pipeline
5//! builder.
6//!
7//! # Decode selection model
8//!
9//! Selection proceeds in two stages:
10//!
11//! 1. **Preflight** — at session creation time, cached capability data
12//! from the backend registry is checked against the user's
13//! [`DecodePreference`]. If [`RequireHardware`](DecodePreference::RequireHardware)
14//! is set and no hardware decoder is discovered, the session fails
15//! immediately. This is a best-effort availability check — it does
16//! **not** guarantee the hardware decoder can handle the specific
17//! stream codec or profile.
18//!
19//! 2. **Post-selection verification** — after the backend has negotiated
20//! a decoder and the stream is confirmed flowing, the source layer
21//! inspects which decoder was actually selected. For
22//! `RequireHardware`, a software effective decoder triggers a typed
23//! `MediaError` instead of silent
24//! success. For all modes, a `HealthEvent::DecodeDecision` is
25//! emitted with the effective [`DecodeOutcome`].
26//!
27//! # Adaptive fallback
28//!
29//! For [`PreferHardware`](DecodePreference::PreferHardware), repeated
30//! hardware decoder failures (before `StreamStarted` confirmation) cause
31//! an internal `HwFailureTracker` to temporarily demote the selection
32//! to `DecoderSelection::Auto`, preventing reconnect thrash. The
33//! demotion has a bounded TTL and resets on success.
34
35use std::sync::{Arc, Mutex};
36use std::time::{Duration, Instant};
37
38// Re-export DecodeOutcome from nv-core so downstream can use it via nv-media.
39pub use nv_core::health::DecodeOutcome;
40
41// Re-export DecodePreference from nv-core (canonical definition).
42pub use nv_core::health::DecodePreference;
43
44// ---------------------------------------------------------------------------
45// Internal — GStreamer decoder selection
46// ---------------------------------------------------------------------------
47
48/// Decoder selection strategy (internal to `nv-media`).
49///
50/// The pipeline builder uses this to determine which GStreamer decode element
51/// to instantiate.
52#[derive(Clone, Debug, Default)]
53pub(crate) enum DecoderSelection {
54 /// Automatically select the best decoder: prefer hardware, fall back to software.
55 #[default]
56 Auto,
57 /// Force software decoding (useful for environments without GPU access).
58 ForceSoftware,
59 /// Require hardware decoding — configure `decodebin` to reject software
60 /// video decoders via `autoplug-select`. If no hardware decoder can
61 /// handle the stream, `decodebin` will error out.
62 ForceHardware,
63 /// Request a specific GStreamer element by name (e.g., `"nvh264dec"`).
64 ///
65 /// Falls back to `Auto` if the named element is not available.
66 /// Not reachable from the public `DecodePreference` API;
67 /// used for internal tests.
68 #[allow(dead_code)]
69 Named(String),
70}
71
72// ---------------------------------------------------------------------------
73// Mapping: DecodePreference → DecoderSelection
74// ---------------------------------------------------------------------------
75
76/// Extension methods mapping [`DecodePreference`] to media-internal types.
77///
78/// `DecodePreference` is defined in `nv-core` (backend-neutral). This trait
79/// adds the backend-specific mapping to [`DecoderSelection`] that only
80/// `nv-media` needs.
81pub(crate) trait DecodePreferenceExt {
82 /// Map a user-facing preference to the internal decoder selection.
83 ///
84 /// - `Auto` → `Auto` (decodebin default ranking).
85 /// - `CpuOnly` → `ForceSoftware`.
86 /// - `PreferHardware` → `ForceHardware` (biased toward hardware; the
87 /// adaptive fallback cache may demote to `Auto` after repeated
88 /// failures).
89 /// - `RequireHardware` → `ForceHardware` (rejects software video
90 /// decoders at the `autoplug-select` level).
91 fn to_selection(self) -> DecoderSelection;
92
93 /// Returns `true` if this preference demands hardware decode (fail-fast
94 /// when unavailable).
95 fn requires_hardware(self) -> bool;
96
97 /// Returns `true` if this preference favours hardware but accepts
98 /// software as a fallback.
99 fn prefers_hardware(self) -> bool;
100}
101
102impl DecodePreferenceExt for DecodePreference {
103 fn to_selection(self) -> DecoderSelection {
104 match self {
105 Self::Auto => DecoderSelection::Auto,
106 Self::CpuOnly => DecoderSelection::ForceSoftware,
107 Self::PreferHardware | Self::RequireHardware => DecoderSelection::ForceHardware,
108 }
109 }
110
111 fn requires_hardware(self) -> bool {
112 matches!(self, Self::RequireHardware)
113 }
114
115 fn prefers_hardware(self) -> bool {
116 matches!(self, Self::PreferHardware)
117 }
118}
119
120// ---------------------------------------------------------------------------
121// Capability discovery
122// ---------------------------------------------------------------------------
123
124/// Lightweight capability information about the decode backend.
125///
126/// Obtained via [`discover_decode_capabilities()`]. This is a snapshot; the
127/// underlying system state may change after construction (e.g., a GPU driver
128/// crash).
129///
130/// # Examples
131///
132/// ```
133/// use nv_media::DecodeCapabilities;
134///
135/// let caps = nv_media::discover_decode_capabilities();
136/// if !caps.backend_available {
137/// eprintln!("Media backend not compiled in or failed to initialise");
138/// } else if caps.hardware_decode_available {
139/// println!("Hardware decoders: {:?}", caps.known_decoder_names);
140/// }
141/// ```
142#[derive(Clone, Debug)]
143pub struct DecodeCapabilities {
144 /// Whether the media backend is compiled in and initialized
145 /// successfully.
146 ///
147 /// When `false`, the remaining fields are meaningless — the backend
148 /// is absent or failed to initialize. Operators can use this to
149 /// distinguish a misbuild/misconfiguration from a genuine
150 /// no-hardware condition (`backend_available == true` but
151 /// `hardware_decode_available == false`).
152 pub backend_available: bool,
153
154 /// Whether at least one hardware video decoder was detected in the
155 /// backend registry. Only meaningful when `backend_available` is `true`.
156 pub hardware_decode_available: bool,
157
158 /// Names of known hardware video decoder elements discovered in the
159 /// backend registry. Empty when `hardware_decode_available` is `false`
160 /// or the backend is not compiled in.
161 pub known_decoder_names: Vec<String>,
162}
163
164// ---------------------------------------------------------------------------
165// Decode decision report (public)
166// ---------------------------------------------------------------------------
167
168/// Diagnostic report for a decode selection decision.
169///
170/// Created internally when a session starts and the backend identifies
171/// which decoder element was selected. Published via
172/// [`HealthEvent::DecodeDecision`](nv_core::health::HealthEvent::DecodeDecision)
173/// and available for operator inspection via logging.
174///
175/// # Backend detail
176///
177/// The `backend_detail` field carries backend-specific information (e.g.,
178/// the GStreamer element name). **Do not match on its contents** — it is
179/// intended for logging and diagnostics only. Its format is not part of
180/// the semver contract.
181#[derive(Clone, Debug)]
182pub struct DecodeDecisionInfo {
183 /// The user-facing preference that was in effect.
184 pub preference: DecodePreference,
185 /// The effective decode outcome after backend negotiation.
186 pub outcome: DecodeOutcome,
187 /// Whether the adaptive fallback cache overrode the preference.
188 pub fallback_active: bool,
189 /// Human-readable reason for fallback (populated when
190 /// `fallback_active` is `true`).
191 pub fallback_reason: Option<String>,
192 /// Backend-specific detail (e.g., element name). Debug-only —
193 /// do not match on contents.
194 pub backend_detail: String,
195}
196
197// ---------------------------------------------------------------------------
198// Selected decoder info (internal)
199// ---------------------------------------------------------------------------
200
201/// Information about the decoder element selected by the backend.
202///
203/// Captured at pipeline negotiation time via the `element-added` signal
204/// on `decodebin`. For named / non-decodebin decoders, set directly.
205#[derive(Clone, Debug)]
206pub(crate) struct SelectedDecoderInfo {
207 /// Backend element name (e.g., `"nvh264dec"`, `"avdec_h264"`).
208 pub element_name: String,
209 /// Whether this element was classified as a hardware decoder.
210 pub is_hardware: bool,
211}
212
213/// Thread-safe slot for communicating the selected decoder from the
214/// pipeline's signal callback to the source layer.
215pub(crate) type SelectedDecoderSlot = Arc<Mutex<Option<SelectedDecoderInfo>>>;
216
217// ---------------------------------------------------------------------------
218// Adaptive fallback cache (internal)
219// ---------------------------------------------------------------------------
220
221/// Number of consecutive hardware-decode failures that trigger a temporary
222/// software fallback for [`DecodePreference::PreferHardware`] feeds.
223const HW_FAILURE_THRESHOLD: u32 = 3;
224
225/// Duration of the temporary software-only fallback window after a
226/// threshold-triggered inhibition.
227const FALLBACK_COOLDOWN: Duration = Duration::from_secs(60);
228
229/// Bounded per-source hardware decoder failure memory.
230///
231/// Tracks consecutive hardware decoder failures and temporarily falls back
232/// to [`DecoderSelection::Auto`] (from the normal `ForceHardware`) to
233/// prevent reconnect thrash. Resets on success or after the cooldown
234/// period expires.
235///
236/// This only applies to [`DecodePreference::PreferHardware`]. For
237/// `RequireHardware`, the preflight and post-selection checks handle
238/// enforcement; adding silent software fallback would violate the
239/// user's explicit guarantee demand.
240pub(crate) struct HwFailureTracker {
241 /// Number of consecutive hardware decoder failures.
242 consecutive_failures: u32,
243 /// Timestamp of the most recent failure.
244 last_failure: Option<Instant>,
245 /// If set, hardware decode is temporarily inhibited until this time.
246 fallback_until: Option<Instant>,
247}
248
249impl HwFailureTracker {
250 pub fn new() -> Self {
251 Self {
252 consecutive_failures: 0,
253 last_failure: None,
254 fallback_until: None,
255 }
256 }
257
258 /// Record a hardware decoder failure.
259 pub fn record_failure(&mut self) {
260 self.consecutive_failures += 1;
261 self.last_failure = Some(Instant::now());
262 if self.consecutive_failures >= HW_FAILURE_THRESHOLD && self.fallback_until.is_none() {
263 self.fallback_until = Some(Instant::now() + FALLBACK_COOLDOWN);
264 tracing::warn!(
265 consecutive_failures = self.consecutive_failures,
266 cooldown_secs = FALLBACK_COOLDOWN.as_secs(),
267 "hardware decoder failure threshold reached, \
268 enabling temporary software fallback",
269 );
270 }
271 }
272
273 /// Record a successful decode session (stream confirmed flowing).
274 pub fn record_success(&mut self) {
275 self.consecutive_failures = 0;
276 self.last_failure = None;
277 self.fallback_until = None;
278 }
279
280 /// Whether the tracker recommends falling back to software.
281 pub fn should_fallback(&self) -> bool {
282 match self.fallback_until {
283 Some(deadline) => Instant::now() < deadline,
284 None => false,
285 }
286 }
287
288 /// Adjust the decoder selection based on failure history.
289 ///
290 /// Returns `Some((adjusted_selection, reason))` if the tracker
291 /// recommends overriding. Only applies to `PreferHardware` — other
292 /// preferences are not adjusted.
293 pub fn adjust_selection(&self, pref: DecodePreference) -> Option<(DecoderSelection, String)> {
294 if !self.should_fallback() {
295 return None;
296 }
297 match pref {
298 DecodePreference::PreferHardware => Some((
299 DecoderSelection::Auto,
300 format!(
301 "adaptive fallback: {} consecutive hardware failures, \
302 temporarily allowing software decode",
303 self.consecutive_failures,
304 ),
305 )),
306 // RequireHardware: never silently downgrade (violates user contract).
307 // CpuOnly / Auto: no adjustment needed.
308 _ => None,
309 }
310 }
311
312 /// Number of consecutive failures (for testing/diagnostics).
313 #[cfg(test)]
314 pub fn consecutive_failures(&self) -> u32 {
315 self.consecutive_failures
316 }
317
318 /// Whether the tracker is currently in the fallback window.
319 #[cfg(test)]
320 pub fn is_in_fallback(&self) -> bool {
321 self.should_fallback()
322 }
323}
324
325/// Probe the media backend for hardware decode capabilities.
326///
327/// When the `gst-backend` feature is enabled, this queries the GStreamer
328/// element registry for video decoder elements whose metadata hints at
329/// hardware acceleration. The classification uses the element's klass
330/// string (`"Hardware"` keyword) and a built-in list of known hardware
331/// decoder name prefixes — see `is_hardware_video_decoder` for details.
332///
333/// When the `gst-backend` feature is **disabled**, this returns a
334/// capabilities struct with `hardware_decode_available = false` and an
335/// empty decoder list.
336///
337/// This function is intentionally cheap — it reads only the plugin registry
338/// (no pipeline construction or device probing).
339pub fn discover_decode_capabilities() -> DecodeCapabilities {
340 #[cfg(feature = "gst-backend")]
341 {
342 discover_gst_hw_decoders()
343 }
344 #[cfg(not(feature = "gst-backend"))]
345 {
346 DecodeCapabilities {
347 backend_available: false,
348 hardware_decode_available: false,
349 known_decoder_names: Vec::new(),
350 }
351 }
352}
353
354// ---------------------------------------------------------------------------
355// Shared hardware-decoder classification
356// ---------------------------------------------------------------------------
357
358/// Known element-name prefixes for hardware video decoders.
359///
360/// This list covers the major GStreamer hardware decoder families.
361/// If a hardware decoder plugin uses a prefix not in this list **and**
362/// the element's `klass` metadata omits the `"Hardware"` keyword, the
363/// decoder will not be recognized. File an issue or extend this list if
364/// that happens.
365#[allow(dead_code)] // used under gst-backend
366pub(crate) const HW_DECODER_PREFIXES: &[&str] =
367 &["nv", "va", "msdk", "amf", "qsv", "d3d11", "d3d12"];
368
369/// Classify a GStreamer element as a hardware video decoder.
370///
371/// Returns `true` when the element is a video decoder that appears to be
372/// hardware-accelerated. The check is heuristic — it succeeds when
373/// **either** condition holds:
374///
375/// 1. The element's `klass` metadata contains both `"Decoder"` and
376/// `"Video"` **and** `"Hardware"`.
377/// 2. The element's name starts with a prefix in [`HW_DECODER_PREFIXES`]
378/// **and** the klass contains `"Decoder"` and `"Video"`.
379///
380/// This function is the **single source of truth** for hardware decoder
381/// classification. Both capability discovery and the `autoplug-select`
382/// callback delegate to it.
383#[allow(dead_code)] // used under gst-backend
384pub(crate) fn is_hardware_video_decoder(klass: &str, element_name: &str) -> bool {
385 let is_video_decoder = klass.contains("Decoder") && klass.contains("Video");
386 if !is_video_decoder {
387 return false;
388 }
389 klass.contains("Hardware")
390 || HW_DECODER_PREFIXES
391 .iter()
392 .any(|p| element_name.starts_with(p))
393}
394
395/// GStreamer-specific hardware decoder discovery.
396#[cfg(feature = "gst-backend")]
397fn discover_gst_hw_decoders() -> DecodeCapabilities {
398 use gst::prelude::*;
399 use gstreamer as gst;
400
401 // Ensure GStreamer is initialized.
402 if gst::init().is_err() {
403 return DecodeCapabilities {
404 backend_available: false,
405 hardware_decode_available: false,
406 known_decoder_names: Vec::new(),
407 };
408 }
409
410 let registry = gst::Registry::get();
411 let mut hw_names: Vec<String> = Vec::new();
412
413 for plugin in registry.plugins() {
414 let features = registry.features_by_plugin(plugin.plugin_name().as_str());
415 for feature in features {
416 let factory: gst::ElementFactory = match feature.downcast() {
417 Ok(f) => f,
418 Err(_) => continue,
419 };
420 let klass: String = factory.metadata("klass").unwrap_or_default().into();
421 let name = factory.name().to_string();
422 if is_hardware_video_decoder(&klass, &name) {
423 hw_names.push(name);
424 }
425 }
426 }
427
428 hw_names.sort();
429 hw_names.dedup();
430
431 DecodeCapabilities {
432 backend_available: true,
433 hardware_decode_available: !hw_names.is_empty(),
434 known_decoder_names: hw_names,
435 }
436}
437
438// ===========================================================================
439// Tests
440// ===========================================================================
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 #[test]
447 fn default_is_auto() {
448 assert_eq!(DecodePreference::default(), DecodePreference::Auto);
449 }
450
451 #[test]
452 fn auto_maps_to_auto_selection() {
453 assert!(matches!(
454 DecodePreference::Auto.to_selection(),
455 DecoderSelection::Auto
456 ));
457 }
458
459 #[test]
460 fn cpu_only_maps_to_force_software() {
461 assert!(matches!(
462 DecodePreference::CpuOnly.to_selection(),
463 DecoderSelection::ForceSoftware
464 ));
465 }
466
467 #[test]
468 fn prefer_hardware_maps_to_force_hardware_selection() {
469 assert!(matches!(
470 DecodePreference::PreferHardware.to_selection(),
471 DecoderSelection::ForceHardware
472 ));
473 }
474
475 #[test]
476 fn require_hardware_maps_to_force_hardware() {
477 assert!(matches!(
478 DecodePreference::RequireHardware.to_selection(),
479 DecoderSelection::ForceHardware
480 ));
481 }
482
483 #[test]
484 fn requires_hardware_only_for_require_hardware() {
485 assert!(!DecodePreference::Auto.requires_hardware());
486 assert!(!DecodePreference::CpuOnly.requires_hardware());
487 assert!(!DecodePreference::PreferHardware.requires_hardware());
488 assert!(DecodePreference::RequireHardware.requires_hardware());
489 }
490
491 #[test]
492 fn decode_preference_clone_and_eq() {
493 let a = DecodePreference::PreferHardware;
494 let b = a;
495 assert_eq!(a, b);
496 }
497
498 #[test]
499 fn prefers_hardware_flag() {
500 assert!(!DecodePreference::Auto.prefers_hardware());
501 assert!(!DecodePreference::CpuOnly.prefers_hardware());
502 assert!(DecodePreference::PreferHardware.prefers_hardware());
503 assert!(!DecodePreference::RequireHardware.prefers_hardware());
504 }
505
506 #[test]
507 fn capabilities_struct_consistent() {
508 let caps = discover_decode_capabilities();
509 if caps.hardware_decode_available {
510 assert!(caps.backend_available, "hardware implies backend available");
511 assert!(!caps.known_decoder_names.is_empty());
512 }
513 if !caps.backend_available {
514 assert!(!caps.hardware_decode_available);
515 assert!(caps.known_decoder_names.is_empty());
516 }
517 }
518
519 #[test]
520 fn capabilities_backend_available_reflects_feature() {
521 let caps = discover_decode_capabilities();
522 // When gst-backend is compiled in AND gst::init succeeds,
523 // backend_available is true. Without the feature it is false.
524 // We can't hard-assert the value because the feature may or
525 // may not be enabled, but the struct must be self-consistent.
526 if caps.backend_available {
527 // backend OK — hardware may or may not be present
528 } else {
529 assert!(!caps.hardware_decode_available);
530 }
531 }
532
533 // -----------------------------------------------------------------------
534 // is_hardware_video_decoder classification tests
535 // -----------------------------------------------------------------------
536
537 #[test]
538 fn hw_classifier_rejects_non_video_decoder() {
539 // A demuxer is never a hardware video decoder, regardless of name.
540 assert!(!is_hardware_video_decoder("Codec/Demuxer", "nvdemux"));
541 assert!(!is_hardware_video_decoder("Source/Video", "vasrc"));
542 }
543
544 #[test]
545 fn hw_classifier_klass_hardware_keyword() {
546 // Element with explicit "Hardware" in klass → hardware decoder.
547 assert!(is_hardware_video_decoder(
548 "Codec/Decoder/Video/Hardware",
549 "exotic_decoder"
550 ));
551 }
552
553 #[test]
554 fn hw_classifier_known_prefix_without_hardware_klass() {
555 // Elements matching known prefixes but without "Hardware" in klass.
556 for prefix in HW_DECODER_PREFIXES {
557 let name = format!("{prefix}h264dec");
558 assert!(
559 is_hardware_video_decoder("Codec/Decoder/Video", &name),
560 "expected {name} to be classified as hardware",
561 );
562 }
563 }
564
565 #[test]
566 fn hw_classifier_rejects_software_decoder() {
567 assert!(!is_hardware_video_decoder(
568 "Codec/Decoder/Video",
569 "avdec_h264"
570 ));
571 assert!(!is_hardware_video_decoder(
572 "Codec/Decoder/Video",
573 "openh264dec"
574 ));
575 assert!(!is_hardware_video_decoder(
576 "Codec/Decoder/Video",
577 "libde265dec"
578 ));
579 }
580
581 #[test]
582 fn hw_classifier_unknown_prefix_with_hardware_klass() {
583 // A hypothetical new vendor decoder that doesn't match any prefix
584 // but correctly sets "Hardware" in its klass.
585 assert!(is_hardware_video_decoder(
586 "Codec/Decoder/Video/Hardware",
587 "newvendordec"
588 ));
589 }
590}