#![forbid(unsafe_code)] use gstreamer as gst; pub const SOFTWARE_VIDEO_FALLBACK_ENV: &str = "LESAVKA_ALLOW_SOFTWARE_VIDEO"; /// Return whether software video fallback is explicitly allowed. /// /// Inputs: `LESAVKA_ALLOW_SOFTWARE_VIDEO`. /// Outputs: `true` only for intentional opt-in values. /// Why: production Lesavka should fail loudly when GPU decode is unavailable, /// instead of silently shifting downstream video onto the CPU. #[must_use] pub fn software_video_fallback_allowed() -> bool { std::env::var(SOFTWARE_VIDEO_FALLBACK_ENV) .ok() .is_some_and(|value| { let trimmed = value.trim(); !(trimmed.is_empty() || trimmed.eq_ignore_ascii_case("0") || trimmed.eq_ignore_ascii_case("false") || trimmed.eq_ignore_ascii_case("no") || trimmed.eq_ignore_ascii_case("off")) }) } #[must_use] pub fn is_hardware_h264_decoder(name: &str) -> bool { matches!( name, "nvh264dec" | "nvh264sldec" | "vulkanh264dec" | "vah264dec" | "vaapih264dec" | "v4l2h264dec" | "v4l2slh264dec" ) } /// Pick the client-side H.264 decoder in a predictable preference order. /// /// Inputs: none, though operators may override the choice with /// `LESAVKA_H264_DECODER=` or bias automatic fallback order with /// `LESAVKA_H264_DECODER_PREFERENCE=hardware|software`. /// Outputs: the chosen decoder element name, or `decodebin` as a last-resort /// error when no hardware decoder is present. /// Why: Lesavka should use GPU decode on NVIDIA/Vulkan/VAAPI/V4L2-capable clients /// and should not hide hardware failures behind CPU decode. #[must_use] #[allow(dead_code)] // retained for include-based tests and diagnostics. pub fn pick_h264_decoder() -> String { require_h264_decoder().unwrap_or_else(|_| "missing-hardware-h264dec".to_string()) } /// Require a buildable H.264 decoder that satisfies the production policy. /// /// Inputs: optional decoder override plus local GStreamer registry. /// Outputs: a selected decoder or a human-readable error. /// Why: callers that create live downstream video must fail before constructing /// a CPU-bound pipeline when hardware decode is unavailable. pub fn require_h264_decoder() -> Result { if let Ok(raw) = std::env::var("LESAVKA_H264_DECODER") { let name = raw.trim(); if !name.is_empty() { if name.eq_ignore_ascii_case("decodebin") { if software_video_fallback_allowed() { return Ok("decodebin".to_string()); } return Err(format!( "requested H.264 decoder '{name}' is not hardware-specific; set {SOFTWARE_VIDEO_FALLBACK_ENV}=1 only for lab fallback" )); } if !buildable_decoder(name) { return Err(format!("requested H.264 decoder '{name}' is not buildable")); } if is_hardware_h264_decoder(name) || software_video_fallback_allowed() { return Ok(name.to_string()); } return Err(format!( "requested H.264 decoder '{name}' is not a hardware decoder; set {SOFTWARE_VIDEO_FALLBACK_ENV}=1 only for lab fallback" )); } } for name in h264_decoder_preference_order() { if buildable_decoder(name) { return Ok(name.to_string()); } } Err("hardware H.264 decoder required, but no buildable NVIDIA/Vulkan/VAAPI/V4L2 decoder was found".to_string()) } /// Return automatic H.264 decoder candidates in selection order. /// /// Inputs: `LESAVKA_H264_DECODER_PREFERENCE`, if set. Output: ordered decoder /// element names. Why: tests and diagnostics need to prove proprietary /// NVIDIA, Vulkan, and VAAPI/V4L2 routes stay ahead of explicit lab fallback. #[must_use] pub fn h264_decoder_preference_order() -> Vec<&'static str> { const HARDWARE: &[&str] = &[ "nvh264dec", "nvh264sldec", "vulkanh264dec", "vah264dec", "vaapih264dec", "v4l2h264dec", "v4l2slh264dec", ]; const SOFTWARE: &[&str] = &["avdec_h264", "openh264dec"]; let prefer_software = software_video_fallback_allowed() && std::env::var("LESAVKA_H264_DECODER_PREFERENCE") .ok() .map(|value| { matches!( value.trim().to_ascii_lowercase().as_str(), "software" | "sw" | "cpu" ) }) .unwrap_or(false); let mut candidates = Vec::with_capacity(HARDWARE.len() + SOFTWARE.len()); if prefer_software { candidates.extend_from_slice(SOFTWARE); candidates.extend_from_slice(HARDWARE); } else { candidates.extend_from_slice(HARDWARE); if software_video_fallback_allowed() { candidates.extend_from_slice(SOFTWARE); } } candidates } /// Return a parse-launch fragment for the selected H.264 decoder. /// /// Inputs: decoder element name. Output: a pipeline fragment with a stable /// `decoder` element name. Why: Vulkan decoders output GPU memory, so they need /// an explicit download step before the existing CPU-side sinks can consume /// frames; keeping that in one helper prevents hardware decode from being /// selected and then immediately failing link negotiation. #[must_use] pub fn h264_decoder_launch_fragment(decoder_name: &str) -> String { h264_decoder_launch_fragment_named(decoder_name, "decoder") } /// Return a parse-launch fragment for the selected H.264 decoder with a caller-owned element name. /// /// Inputs: decoder element name plus the element name to put in the pipeline. /// Output: a pipeline fragment. Why: unified downstream rendering needs two /// independent decoder elements, while Vulkan still needs an explicit /// download-to-system-memory stage after each decoder. #[must_use] pub fn h264_decoder_launch_fragment_named(decoder_name: &str, element_name: &str) -> String { match decoder_name { "vulkanh264dec" => concat!( "vulkanh264dec name={element_name} discard-corrupted-frames=true ", "automatic-request-sync-points=true ! vulkandownload" ) .replace("{element_name}", element_name), name => format!("{name} name={element_name}"), } } fn buildable_decoder(name: &str) -> bool { #[cfg(coverage)] if std::env::var("TEST_FAIL_GST_INIT").is_ok() { return false; } if gst::init().is_err() { return false; } #[cfg(coverage)] if std::env::var("TEST_DISABLE_H264_DECODER_FACTORY").is_ok() { return false; } gst::ElementFactory::find(name).is_some() && gst::ElementFactory::make(name).build().is_ok() }