Skip to main content

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}