#![forbid(unsafe_code)] use gstreamer as gst; pub const SOFTWARE_VIDEO_FALLBACK_ENV: &str = "LESAVKA_ALLOW_SOFTWARE_VIDEO"; pub const ALLOW_VULKAN_H264_DECODER_ENV: &str = "LESAVKA_ALLOW_VULKAN_H264_DECODER"; /// 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 { env_flag_enabled(SOFTWARE_VIDEO_FALLBACK_ENV) } fn env_flag_enabled(name: &str) -> bool { std::env::var(name).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")) }) } fn stability_software_decode_allowed() -> bool { software_video_fallback_allowed() } fn vulkan_h264_decoder_allowed() -> bool { env_flag_enabled(ALLOW_VULKAN_H264_DECODER_ENV) } #[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/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/VAAPI/V4L2 decoder was found; install gst-plugin-va for the libva-nvidia NVDEC route, or set LESAVKA_ALLOW_VULKAN_H264_DECODER=1 only for Vulkan diagnostics".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 and VAAPI/V4L2 routes stay ahead of explicit lab fallback; Vulkan is /// opt-in because it has been choppy on the NVIDIA desktop path. #[must_use] pub fn h264_decoder_preference_order() -> Vec<&'static str> { const PRIMARY_HARDWARE: &[&str] = &[ "nvh264dec", "nvh264sldec", "vah264dec", "vaapih264dec", "v4l2h264dec", "v4l2slh264dec", ]; const VULKAN_HARDWARE: &[&str] = &["vulkanh264dec"]; const SOFTWARE: &[&str] = &["avdec_h264", "openh264dec"]; let auto_software_allowed = stability_software_decode_allowed(); let prefer_software = auto_software_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(PRIMARY_HARDWARE.len() + SOFTWARE.len() + VULKAN_HARDWARE.len()); if prefer_software { candidates.extend_from_slice(SOFTWARE); candidates.extend_from_slice(PRIMARY_HARDWARE); if vulkan_h264_decoder_allowed() { candidates.extend_from_slice(VULKAN_HARDWARE); } } else { candidates.extend_from_slice(PRIMARY_HARDWARE); if vulkan_h264_decoder_allowed() { candidates.extend_from_slice(VULKAN_HARDWARE); } if auto_software_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}"), } } /// Detect whether an Annex-B H.264 access unit carries an IDR frame. /// /// Inputs: one H.264 access unit. Output: true when it includes NAL type 5. /// Why: if the downstream renderer drops a predicted frame, resuming on the /// next non-IDR packet can smear until a keyframe repairs decoder state. #[must_use] pub fn contains_idr(h264: &[u8]) -> bool { let mut index = 0; while index + 4 < h264.len() { if h264[index] == 0 && h264[index + 1] == 0 { let offset = if h264[index + 2] == 1 { 3 } else if h264[index + 2] == 0 && h264[index + 3] == 1 { 4 } else { index += 1; continue; }; let nal_index = index + offset; if nal_index < h264.len() && (h264[nal_index] & 0x1F) == 5 { return true; } } index += 1; } false } 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() }