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";
|
|
|
|
|
|
|
|
|
|
/// 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"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
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-11 16:32:37 -03:00
|
|
|
/// Why: Lesavka should use GPU decode on NVIDIA/Vulkan/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 01:04:31 -03:00
|
|
|
Err("hardware H.264 decoder required, but no buildable NVIDIA/Vulkan/VAAPI/V4L2 decoder was found".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
|
|
|
|
|
/// NVIDIA, Vulkan, and VAAPI/V4L2 routes stay ahead of explicit lab fallback.
|
2026-05-10 23:14:15 -03:00
|
|
|
#[must_use]
|
|
|
|
|
pub fn h264_decoder_preference_order() -> Vec<&'static str> {
|
|
|
|
|
const HARDWARE: &[&str] = &[
|
2026-04-17 06:14:54 -03:00
|
|
|
"nvh264dec",
|
|
|
|
|
"nvh264sldec",
|
2026-05-11 16:32:37 -03:00
|
|
|
"vulkanh264dec",
|
2026-04-17 06:14:54 -03:00
|
|
|
"vah264dec",
|
|
|
|
|
"vaapih264dec",
|
|
|
|
|
"v4l2h264dec",
|
|
|
|
|
"v4l2slh264dec",
|
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 01:04:31 -03:00
|
|
|
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);
|
2026-05-10 23:14:15 -03:00
|
|
|
|
|
|
|
|
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);
|
2026-05-12 01:04:31 -03:00
|
|
|
if software_video_fallback_allowed() {
|
|
|
|
|
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-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()
|
|
|
|
|
}
|