2026-04-17 06:14:54 -03:00
#![ forbid(unsafe_code) ]
use gstreamer as gst ;
2026-05-12 01:04:31 -03:00
pub const SOFTWARE_VIDEO_FALLBACK_ENV : & str = " LESAVKA_ALLOW_SOFTWARE_VIDEO " ;
2026-05-12 03:02:57 -03:00
pub const ALLOW_VULKAN_H264_DECODER_ENV : & str = " LESAVKA_ALLOW_VULKAN_H264_DECODER " ;
2026-05-12 01:04:31 -03:00
/// 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 {
2026-05-12 03:02:57 -03:00
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 )
2026-05-12 01:04:31 -03:00
}
#[ must_use ]
pub fn is_hardware_h264_decoder ( name : & str ) -> bool {
matches! (
name ,
" nvh264dec "
| " nvh264sldec "
| " vulkanh264dec "
| " vah264dec "
| " vaapih264dec "
| " v4l2h264dec "
| " v4l2slh264dec "
)
}
2026-04-17 06:14:54 -03:00
/// Pick the client-side H.264 decoder in a predictable preference order.
///
/// Inputs: none, though operators may override the choice with
2026-05-10 23:14:15 -03:00
/// `LESAVKA_H264_DECODER=<element>` or bias automatic fallback order with
/// `LESAVKA_H264_DECODER_PREFERENCE=hardware|software`.
2026-04-17 06:14:54 -03:00
/// Outputs: the chosen decoder element name, or `decodebin` as a last-resort
2026-05-12 01:04:31 -03:00
/// error when no hardware decoder is present.
2026-05-12 03:02:57 -03:00
/// Why: Lesavka should use GPU decode on NVIDIA/VAAPI/V4L2-capable clients
2026-05-12 01:04:31 -03:00
/// and should not hide hardware failures behind CPU decode.
2026-04-17 06:14:54 -03:00
#[ must_use ]
2026-05-12 01:04:31 -03:00
#[ allow(dead_code) ] // retained for include-based tests and diagnostics.
2026-04-17 06:14:54 -03:00
pub fn pick_h264_decoder ( ) -> String {
2026-05-12 01:04:31 -03:00
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 < String , String > {
2026-04-17 06:14:54 -03:00
if let Ok ( raw ) = std ::env ::var ( " LESAVKA_H264_DECODER " ) {
let name = raw . trim ( ) ;
2026-05-12 01:04:31 -03:00
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 "
) ) ;
2026-04-17 06:14:54 -03:00
}
}
2026-05-10 23:14:15 -03:00
for name in h264_decoder_preference_order ( ) {
if buildable_decoder ( name ) {
2026-05-12 01:04:31 -03:00
return Ok ( name . to_string ( ) ) ;
2026-05-10 23:14:15 -03:00
}
}
2026-05-12 03:02:57 -03:00
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 ( ) )
2026-05-10 23:14:15 -03:00
}
/// Return automatic H.264 decoder candidates in selection order.
///
/// Inputs: `LESAVKA_H264_DECODER_PREFERENCE`, if set. Output: ordered decoder
2026-05-12 01:04:31 -03:00
/// element names. Why: tests and diagnostics need to prove proprietary
2026-05-12 03:02:57 -03:00
/// 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.
2026-05-10 23:14:15 -03:00
#[ must_use ]
pub fn h264_decoder_preference_order ( ) -> Vec < & 'static str > {
2026-05-12 03:02:57 -03:00
const PRIMARY_HARDWARE : & [ & str ] = & [
2026-04-17 06:14:54 -03:00
" nvh264dec " ,
" nvh264sldec " ,
" vah264dec " ,
" vaapih264dec " ,
" v4l2h264dec " ,
" v4l2slh264dec " ,
2026-05-10 23:14:15 -03:00
] ;
2026-05-12 03:02:57 -03:00
const VULKAN_HARDWARE : & [ & str ] = & [ " vulkanh264dec " ] ;
2026-05-10 23:14:15 -03:00
const SOFTWARE : & [ & str ] = & [ " avdec_h264 " , " openh264dec " ] ;
2026-04-17 06:14:54 -03:00
2026-05-12 03:02:57 -03:00
let auto_software_allowed = stability_software_decode_allowed ( ) ;
let prefer_software = auto_software_allowed
2026-05-12 01:04:31 -03:00
& & std ::env ::var ( " LESAVKA_H264_DECODER_PREFERENCE " )
. ok ( )
. map ( | value | {
matches! (
value . trim ( ) . to_ascii_lowercase ( ) . as_str ( ) ,
" software " | " sw " | " cpu "
)
} )
. unwrap_or ( false ) ;
2026-05-10 23:14:15 -03:00
2026-05-12 03:02:57 -03:00
let mut candidates =
Vec ::with_capacity ( PRIMARY_HARDWARE . len ( ) + SOFTWARE . len ( ) + VULKAN_HARDWARE . len ( ) ) ;
2026-05-10 23:14:15 -03:00
if prefer_software {
candidates . extend_from_slice ( SOFTWARE ) ;
2026-05-12 03:02:57 -03:00
candidates . extend_from_slice ( PRIMARY_HARDWARE ) ;
if vulkan_h264_decoder_allowed ( ) {
candidates . extend_from_slice ( VULKAN_HARDWARE ) ;
}
2026-05-10 23:14:15 -03:00
} else {
2026-05-12 03:02:57 -03:00
candidates . extend_from_slice ( PRIMARY_HARDWARE ) ;
if vulkan_h264_decoder_allowed ( ) {
candidates . extend_from_slice ( VULKAN_HARDWARE ) ;
}
if auto_software_allowed {
2026-05-12 01:04:31 -03:00
candidates . extend_from_slice ( SOFTWARE ) ;
}
2026-05-10 23:14:15 -03:00
}
candidates
2026-04-17 06:14:54 -03:00
}
2026-04-21 01:31:21 -03:00
2026-05-11 16:32:37 -03:00
/// 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} " ) ,
}
}
2026-05-17 13:18:45 -03:00
/// 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
}
2026-04-21 01:31:21 -03:00
fn buildable_decoder ( name : & str ) -> bool {
2026-04-23 03:49:49 -03:00
#[ cfg(coverage) ]
if std ::env ::var ( " TEST_FAIL_GST_INIT " ) . is_ok ( ) {
return false ;
}
2026-04-22 22:10:39 -03:00
if gst ::init ( ) . is_err ( ) {
return false ;
}
2026-04-23 03:49:49 -03:00
#[ cfg(coverage) ]
if std ::env ::var ( " TEST_DISABLE_H264_DECODER_FACTORY " ) . is_ok ( ) {
return false ;
}
2026-04-21 01:31:21 -03:00
gst ::ElementFactory ::find ( name ) . is_some ( ) & & gst ::ElementFactory ::make ( name ) . build ( ) . is_ok ( )
}