lesavka/client/src/video_support.rs

119 lines
4.2 KiB
Rust
Raw Normal View History

#![forbid(unsafe_code)]
use gstreamer as gst;
/// Pick the client-side H.264 decoder in a predictable preference order.
///
/// Inputs: none, though operators may override the choice with
/// `LESAVKA_H264_DECODER=<element>` or bias automatic fallback order with
/// `LESAVKA_H264_DECODER_PREFERENCE=hardware|software`.
/// Outputs: the chosen decoder element name, or `decodebin` as a last-resort
/// fallback when no explicit decoder is present.
/// Why: Lesavka should use GPU decode on NVIDIA/Vulkan/VAAPI/V4L2-capable clients
/// when possible, while keeping an explicit CPU route for open-source driver
/// comparisons and driver debugging.
#[must_use]
pub fn pick_h264_decoder() -> String {
if let Ok(raw) = std::env::var("LESAVKA_H264_DECODER") {
let name = raw.trim();
if name.eq_ignore_ascii_case("decodebin") {
return "decodebin".to_string();
}
if !name.is_empty() && buildable_decoder(name) {
return name.to_string();
}
}
for name in h264_decoder_preference_order() {
if buildable_decoder(name) {
return name.to_string();
}
}
"decodebin".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 both proprietary
/// NVIDIA, Vulkan, and VAAPI/V4L2 routes stay available before CPU 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 = 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);
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()
}