use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
pub use nv_core::health::DecodeOutcome;
pub use nv_core::health::DecodePreference;
#[derive(Clone, Debug, Default)]
pub(crate) enum DecoderSelection {
#[default]
Auto,
ForceSoftware,
ForceHardware,
#[allow(dead_code)]
Named(String),
}
pub(crate) trait DecodePreferenceExt {
fn to_selection(self) -> DecoderSelection;
fn requires_hardware(self) -> bool;
fn prefers_hardware(self) -> bool;
}
impl DecodePreferenceExt for DecodePreference {
fn to_selection(self) -> DecoderSelection {
match self {
Self::Auto => DecoderSelection::Auto,
Self::CpuOnly => DecoderSelection::ForceSoftware,
Self::PreferHardware | Self::RequireHardware => DecoderSelection::ForceHardware,
}
}
fn requires_hardware(self) -> bool {
matches!(self, Self::RequireHardware)
}
fn prefers_hardware(self) -> bool {
matches!(self, Self::PreferHardware)
}
}
#[derive(Clone, Debug)]
pub struct DecodeCapabilities {
pub backend_available: bool,
pub hardware_decode_available: bool,
pub known_decoder_names: Vec<String>,
}
#[derive(Clone, Debug)]
pub struct DecodeDecisionInfo {
pub preference: DecodePreference,
pub outcome: DecodeOutcome,
pub fallback_active: bool,
pub fallback_reason: Option<String>,
pub backend_detail: String,
}
#[derive(Clone, Debug)]
pub(crate) struct SelectedDecoderInfo {
pub element_name: String,
pub is_hardware: bool,
}
pub(crate) type SelectedDecoderSlot = Arc<Mutex<Option<SelectedDecoderInfo>>>;
const HW_FAILURE_THRESHOLD: u32 = 3;
const FALLBACK_COOLDOWN: Duration = Duration::from_secs(60);
pub(crate) struct HwFailureTracker {
consecutive_failures: u32,
last_failure: Option<Instant>,
fallback_until: Option<Instant>,
}
impl HwFailureTracker {
pub fn new() -> Self {
Self {
consecutive_failures: 0,
last_failure: None,
fallback_until: None,
}
}
pub fn record_failure(&mut self) {
self.consecutive_failures += 1;
self.last_failure = Some(Instant::now());
if self.consecutive_failures >= HW_FAILURE_THRESHOLD && self.fallback_until.is_none() {
self.fallback_until = Some(Instant::now() + FALLBACK_COOLDOWN);
tracing::warn!(
consecutive_failures = self.consecutive_failures,
cooldown_secs = FALLBACK_COOLDOWN.as_secs(),
"hardware decoder failure threshold reached, \
enabling temporary software fallback",
);
}
}
pub fn record_success(&mut self) {
self.consecutive_failures = 0;
self.last_failure = None;
self.fallback_until = None;
}
pub fn should_fallback(&self) -> bool {
match self.fallback_until {
Some(deadline) => Instant::now() < deadline,
None => false,
}
}
pub fn adjust_selection(&self, pref: DecodePreference) -> Option<(DecoderSelection, String)> {
if !self.should_fallback() {
return None;
}
match pref {
DecodePreference::PreferHardware => Some((
DecoderSelection::Auto,
format!(
"adaptive fallback: {} consecutive hardware failures, \
temporarily allowing software decode",
self.consecutive_failures,
),
)),
_ => None,
}
}
#[cfg(test)]
pub fn consecutive_failures(&self) -> u32 {
self.consecutive_failures
}
#[cfg(test)]
pub fn is_in_fallback(&self) -> bool {
self.should_fallback()
}
}
pub fn discover_decode_capabilities() -> DecodeCapabilities {
#[cfg(feature = "gst-backend")]
{
discover_gst_hw_decoders()
}
#[cfg(not(feature = "gst-backend"))]
{
DecodeCapabilities {
backend_available: false,
hardware_decode_available: false,
known_decoder_names: Vec::new(),
}
}
}
#[allow(dead_code)] pub(crate) const HW_DECODER_PREFIXES: &[&str] =
&["nv", "va", "msdk", "amf", "qsv", "d3d11", "d3d12"];
#[allow(dead_code)] pub(crate) fn is_hardware_video_decoder(klass: &str, element_name: &str) -> bool {
let is_video_decoder = klass.contains("Decoder") && klass.contains("Video");
if !is_video_decoder {
return false;
}
klass.contains("Hardware")
|| HW_DECODER_PREFIXES
.iter()
.any(|p| element_name.starts_with(p))
}
#[cfg(feature = "gst-backend")]
fn discover_gst_hw_decoders() -> DecodeCapabilities {
use gst::prelude::*;
use gstreamer as gst;
if gst::init().is_err() {
return DecodeCapabilities {
backend_available: false,
hardware_decode_available: false,
known_decoder_names: Vec::new(),
};
}
let registry = gst::Registry::get();
let mut hw_names: Vec<String> = Vec::new();
for plugin in registry.plugins() {
let features = registry.features_by_plugin(plugin.plugin_name().as_str());
for feature in features {
let factory: gst::ElementFactory = match feature.downcast() {
Ok(f) => f,
Err(_) => continue,
};
let klass: String = factory.metadata("klass").unwrap_or_default().into();
let name = factory.name().to_string();
if is_hardware_video_decoder(&klass, &name) {
hw_names.push(name);
}
}
}
hw_names.sort();
hw_names.dedup();
DecodeCapabilities {
backend_available: true,
hardware_decode_available: !hw_names.is_empty(),
known_decoder_names: hw_names,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_auto() {
assert_eq!(DecodePreference::default(), DecodePreference::Auto);
}
#[test]
fn auto_maps_to_auto_selection() {
assert!(matches!(
DecodePreference::Auto.to_selection(),
DecoderSelection::Auto
));
}
#[test]
fn cpu_only_maps_to_force_software() {
assert!(matches!(
DecodePreference::CpuOnly.to_selection(),
DecoderSelection::ForceSoftware
));
}
#[test]
fn prefer_hardware_maps_to_force_hardware_selection() {
assert!(matches!(
DecodePreference::PreferHardware.to_selection(),
DecoderSelection::ForceHardware
));
}
#[test]
fn require_hardware_maps_to_force_hardware() {
assert!(matches!(
DecodePreference::RequireHardware.to_selection(),
DecoderSelection::ForceHardware
));
}
#[test]
fn requires_hardware_only_for_require_hardware() {
assert!(!DecodePreference::Auto.requires_hardware());
assert!(!DecodePreference::CpuOnly.requires_hardware());
assert!(!DecodePreference::PreferHardware.requires_hardware());
assert!(DecodePreference::RequireHardware.requires_hardware());
}
#[test]
fn decode_preference_clone_and_eq() {
let a = DecodePreference::PreferHardware;
let b = a;
assert_eq!(a, b);
}
#[test]
fn prefers_hardware_flag() {
assert!(!DecodePreference::Auto.prefers_hardware());
assert!(!DecodePreference::CpuOnly.prefers_hardware());
assert!(DecodePreference::PreferHardware.prefers_hardware());
assert!(!DecodePreference::RequireHardware.prefers_hardware());
}
#[test]
fn capabilities_struct_consistent() {
let caps = discover_decode_capabilities();
if caps.hardware_decode_available {
assert!(caps.backend_available, "hardware implies backend available");
assert!(!caps.known_decoder_names.is_empty());
}
if !caps.backend_available {
assert!(!caps.hardware_decode_available);
assert!(caps.known_decoder_names.is_empty());
}
}
#[test]
fn capabilities_backend_available_reflects_feature() {
let caps = discover_decode_capabilities();
if caps.backend_available {
} else {
assert!(!caps.hardware_decode_available);
}
}
#[test]
fn hw_classifier_rejects_non_video_decoder() {
assert!(!is_hardware_video_decoder("Codec/Demuxer", "nvdemux"));
assert!(!is_hardware_video_decoder("Source/Video", "vasrc"));
}
#[test]
fn hw_classifier_klass_hardware_keyword() {
assert!(is_hardware_video_decoder(
"Codec/Decoder/Video/Hardware",
"exotic_decoder"
));
}
#[test]
fn hw_classifier_known_prefix_without_hardware_klass() {
for prefix in HW_DECODER_PREFIXES {
let name = format!("{prefix}h264dec");
assert!(
is_hardware_video_decoder("Codec/Decoder/Video", &name),
"expected {name} to be classified as hardware",
);
}
}
#[test]
fn hw_classifier_rejects_software_decoder() {
assert!(!is_hardware_video_decoder(
"Codec/Decoder/Video",
"avdec_h264"
));
assert!(!is_hardware_video_decoder(
"Codec/Decoder/Video",
"openh264dec"
));
assert!(!is_hardware_video_decoder(
"Codec/Decoder/Video",
"libde265dec"
));
}
#[test]
fn hw_classifier_unknown_prefix_with_hardware_klass() {
assert!(is_hardware_video_decoder(
"Codec/Decoder/Video/Hardware",
"newvendordec"
));
}
}