From 7de6e243ed5f65c0bc648e1db8e182fba78a41bc Mon Sep 17 00:00:00 2001 From: Kristian Ollikainen <14197772+DatCaptainHorse@users.noreply.github.com> Date: Tue, 11 Feb 2025 12:03:03 +0200 Subject: [PATCH] =?UTF-8?q?=E2=AD=90=20feat(runner):=20DMA-BUF=20support?= =?UTF-8?q?=20(for=20NVIDIA)=20(#181)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also includes other improvements and hopefully reducing LOC with some cleanup. --------- Co-authored-by: DatCaptainHorse --- containers/runner.Containerfile | 2 +- packages/scripts/entrypoint_nestri.sh | 4 +- packages/server/src/args/device_args.rs | 11 +- packages/server/src/args/encoding_args.rs | 74 ++- packages/server/src/enc_helper.rs | 600 +++++++++------------- packages/server/src/gpu.rs | 246 ++++----- packages/server/src/main.rs | 26 +- 7 files changed, 429 insertions(+), 534 deletions(-) diff --git a/containers/runner.Containerfile b/containers/runner.Containerfile index 5d162ac0..6851e56f 100644 --- a/containers/runner.Containerfile +++ b/containers/runner.Containerfile @@ -74,7 +74,7 @@ RUN --mount=type=cache,target=/var/cache/pacman/pkg \ libxkbcommon wayland gstreamer gst-plugins-base gst-plugins-good libinput # Clone repository with proper directory structure -RUN git clone https://github.com/games-on-whales/gst-wayland-display.git +RUN git clone -b dev-dmabuf https://github.com/games-on-whales/gst-wayland-display.git #-------------------------------------------------------------------- FROM gst-wayland-deps AS gst-wayland-planner diff --git a/packages/scripts/entrypoint_nestri.sh b/packages/scripts/entrypoint_nestri.sh index 60007468..e5b307ed 100644 --- a/packages/scripts/entrypoint_nestri.sh +++ b/packages/scripts/entrypoint_nestri.sh @@ -3,9 +3,9 @@ set -euo pipefail # Make user directory owned by the default user chown -f "$(id -nu):$(id -ng)" ~ || \ - sudo-root chown -f "$(id -nu):$(id -ng)" ~ || \ + sudo chown -f "$(id -nu):$(id -ng)" ~ || \ chown -R -f -h --no-preserve-root "$(id -nu):$(id -ng)" ~ || \ - sudo-root chown -R -f -h --no-preserve-root "$(id -nu):$(id -ng)" ~ || \ + sudo chown -R -f -h --no-preserve-root "$(id -nu):$(id -ng)" ~ || \ echo 'Failed to change user directory permissions, there may be permission issues' # Source environment variables from envs.sh diff --git a/packages/server/src/args/device_args.rs b/packages/server/src/args/device_args.rs index 2a55d8ad..a7a672b5 100644 --- a/packages/server/src/args/device_args.rs +++ b/packages/server/src/args/device_args.rs @@ -36,6 +36,13 @@ impl DeviceArgs { println!("> gpu_vendor: {}", self.gpu_vendor); println!("> gpu_name: {}", self.gpu_name); println!("> gpu_index: {}", self.gpu_index); - println!("> gpu_card_path: {}", self.gpu_card_path); + println!( + "> gpu_card_path: {}", + if self.gpu_card_path.is_empty() { + "Auto-Selection" + } else { + &self.gpu_card_path + } + ); } -} \ No newline at end of file +} diff --git a/packages/server/src/args/encoding_args.rs b/packages/server/src/args/encoding_args.rs index ae891a2a..02b61a9a 100644 --- a/packages/server/src/args/encoding_args.rs +++ b/packages/server/src/args/encoding_args.rs @@ -39,7 +39,14 @@ pub struct EncodingOptionsBase { impl EncodingOptionsBase { pub fn debug_print(&self) { println!("> Codec: {}", self.codec); - println!("> Encoder: {}", self.encoder); + println!( + "> Encoder: {}", + if self.encoder.is_empty() { + "Auto-Selection" + } else { + &self.encoder + } + ); match &self.rate_control { RateControl::CQP(cqp) => { println!("> Rate Control: CQP"); @@ -72,21 +79,44 @@ impl VideoEncodingOptions { .get_one::("video-encoder") .unwrap_or(&"".to_string()) .clone(), - rate_control: match matches.get_one::("video-rate-control").unwrap().as_str() { + rate_control: match matches + .get_one::("video-rate-control") + .unwrap() + .as_str() + { "cqp" => RateControl::CQP(RateControlCQP { - quality: matches.get_one::("video-cqp").unwrap().parse::().unwrap(), + quality: matches + .get_one::("video-cqp") + .unwrap() + .parse::() + .unwrap(), }), "cbr" => RateControl::CBR(RateControlCBR { - target_bitrate: matches.get_one::("video-bitrate").unwrap().parse::().unwrap(), + target_bitrate: matches + .get_one::("video-bitrate") + .unwrap() + .parse::() + .unwrap(), }), "vbr" => RateControl::VBR(RateControlVBR { - target_bitrate: matches.get_one::("video-bitrate").unwrap().parse::().unwrap(), - max_bitrate: matches.get_one::("video-bitrate-max").unwrap().parse::().unwrap(), + target_bitrate: matches + .get_one::("video-bitrate") + .unwrap() + .parse::() + .unwrap(), + max_bitrate: matches + .get_one::("video-bitrate-max") + .unwrap() + .parse::() + .unwrap(), }), _ => panic!("Invalid rate control method for video"), }, }, - encoder_type: matches.get_one::("video-encoder-type").unwrap_or(&"hardware".to_string()).clone(), + encoder_type: matches + .get_one::("video-encoder-type") + .unwrap_or(&"hardware".to_string()) + .clone(), } } @@ -133,18 +163,38 @@ impl AudioEncodingOptions { .get_one::("audio-encoder") .unwrap_or(&"".to_string()) .clone(), - rate_control: match matches.get_one::("audio-rate-control").unwrap().as_str() { + rate_control: match matches + .get_one::("audio-rate-control") + .unwrap() + .as_str() + { "cbr" => RateControl::CBR(RateControlCBR { - target_bitrate: matches.get_one::("audio-bitrate").unwrap().parse::().unwrap(), + target_bitrate: matches + .get_one::("audio-bitrate") + .unwrap() + .parse::() + .unwrap(), }), "vbr" => RateControl::VBR(RateControlVBR { - target_bitrate: matches.get_one::("audio-bitrate").unwrap().parse::().unwrap(), - max_bitrate: matches.get_one::("audio-bitrate-max").unwrap().parse::().unwrap(), + target_bitrate: matches + .get_one::("audio-bitrate") + .unwrap() + .parse::() + .unwrap(), + max_bitrate: matches + .get_one::("audio-bitrate-max") + .unwrap() + .parse::() + .unwrap(), }), _ => panic!("Invalid rate control method for audio"), }, }, - capture_method: match matches.get_one::("audio-capture-method").unwrap().as_str() { + capture_method: match matches + .get_one::("audio-capture-method") + .unwrap() + .as_str() + { "pulseaudio" => AudioCaptureMethod::PulseAudio, "pipewire" => AudioCaptureMethod::PipeWire, "alsa" => AudioCaptureMethod::ALSA, diff --git a/packages/server/src/enc_helper.rs b/packages/server/src/enc_helper.rs index 18a1666b..27642619 100644 --- a/packages/server/src/enc_helper.rs +++ b/packages/server/src/enc_helper.rs @@ -1,3 +1,4 @@ +use crate::gpu::{self, get_gpu_by_card_path, get_gpus_by_vendor, GPUInfo}; use gst::prelude::*; #[derive(Debug, Eq, PartialEq, Clone)] @@ -7,47 +8,23 @@ pub enum VideoCodec { AV1, UNKNOWN, } + impl VideoCodec { pub fn to_str(&self) -> &'static str { match self { - VideoCodec::H264 => "H.264", - VideoCodec::H265 => "H.265", - VideoCodec::AV1 => "AV1", - VideoCodec::UNKNOWN => "Unknown", - } - } - - // unlike to_str, converts to gstreamer friendly codec name - pub fn to_gst_str(&self) -> &'static str { - match self { - VideoCodec::H264 => "h264", - VideoCodec::H265 => "h265", - VideoCodec::AV1 => "av1", - VideoCodec::UNKNOWN => "unknown", - } - } - - // returns mime-type string - pub fn to_mime_str(&self) -> &'static str { - match self { - VideoCodec::H264 => "video/H264", - VideoCodec::H265 => "video/H265", - VideoCodec::AV1 => "video/AV1", - VideoCodec::UNKNOWN => "unknown", + Self::H264 => "H.264", + Self::H265 => "H.265", + Self::AV1 => "AV1", + Self::UNKNOWN => "Unknown", } } pub fn from_str(s: &str) -> Self { match s.to_lowercase().as_str() { - "h264" => VideoCodec::H264, - "h.264" => VideoCodec::H264, - "avc" => VideoCodec::H264, - "h265" => VideoCodec::H265, - "h.265" => VideoCodec::H265, - "hevc" => VideoCodec::H265, - "hev1" => VideoCodec::H265, - "av1" => VideoCodec::AV1, - _ => VideoCodec::UNKNOWN, + "h264" | "h.264" | "avc" => Self::H264, + "h265" | "h.265" | "hevc" | "hev1" => Self::H265, + "av1" => Self::AV1, + _ => Self::UNKNOWN, } } } @@ -61,15 +38,16 @@ pub enum EncoderAPI { SOFTWARE, UNKNOWN, } + impl EncoderAPI { pub fn to_str(&self) -> &'static str { match self { - EncoderAPI::QSV => "Intel QuickSync Video", - EncoderAPI::VAAPI => "Video Acceleration API", - EncoderAPI::NVENC => "NVIDIA NVENC", - EncoderAPI::AMF => "AMD Media Framework", - EncoderAPI::SOFTWARE => "Software", - EncoderAPI::UNKNOWN => "Unknown", + Self::QSV => "Intel QuickSync Video", + Self::VAAPI => "Video Acceleration API", + Self::NVENC => "NVIDIA NVENC", + Self::AMF => "AMD Media Framework", + Self::SOFTWARE => "Software", + Self::UNKNOWN => "Unknown", } } } @@ -80,20 +58,21 @@ pub enum EncoderType { HARDWARE, UNKNOWN, } + impl EncoderType { pub fn to_str(&self) -> &'static str { match self { - EncoderType::SOFTWARE => "Software", - EncoderType::HARDWARE => "Hardware", - EncoderType::UNKNOWN => "Unknown", + Self::SOFTWARE => "Software", + Self::HARDWARE => "Hardware", + Self::UNKNOWN => "Unknown", } } pub fn from_str(s: &str) -> Self { match s.to_lowercase().as_str() { - "software" => EncoderType::SOFTWARE, - "hardware" => EncoderType::HARDWARE, - _ => EncoderType::UNKNOWN, + "software" => Self::SOFTWARE, + "hardware" => Self::HARDWARE, + _ => Self::UNKNOWN, } } } @@ -105,6 +84,7 @@ pub struct VideoEncoderInfo { pub encoder_type: EncoderType, pub encoder_api: EncoderAPI, pub parameters: Vec<(String, String)>, + pub gpu_info: Option, } impl VideoEncoderInfo { @@ -120,26 +100,26 @@ impl VideoEncoderInfo { encoder_type, encoder_api, parameters: Vec::new(), + gpu_info: None, } } pub fn get_parameters_string(&self) -> String { self.parameters .iter() - .map(|(key, value)| format!("{}={}", key, value)) - .collect::>() + .map(|(k, v)| format!("{}={}", k, v)) + .collect::>() .join(" ") } pub fn set_parameter(&mut self, key: &str, value: &str) { - self.parameters.push((key.to_string(), value.to_string())); + self.parameters.push((key.into(), value.into())); } - pub fn apply_parameters(&self, element: &gst::Element, verbose: &bool) { + pub fn apply_parameters(&self, element: &gst::Element, verbose: bool) { for (key, value) in &self.parameters { if element.has_property(key) { - // If verbose, log property sets - if *verbose { + if verbose { println!("Setting property {} to {}", key, value); } element.set_property_from_str(key, value); @@ -148,199 +128,126 @@ impl VideoEncoderInfo { } } -/// Converts VA-API encoder name to low-power variant. -/// # Arguments -/// * `encoder` - The name of the VA-API encoder. -/// # Returns -/// * `&str` - The name of the low-power variant of the encoder. -fn get_low_power_encoder(encoder: &String) -> String { - if encoder.starts_with("va") && !encoder.ends_with("enc") && !encoder.ends_with("lpenc") { - // Replace "enc" substring at end with "lpenc" - let mut encoder = encoder.to_string(); - encoder.truncate(encoder.len() - 3); - encoder.push_str("lpenc"); - encoder - } else { - encoder.to_string() - } -} - -/// Returns best guess for encoder API based on the encoder name. -/// # Arguments -/// * `encoder` - The name of the encoder. -/// # Returns -/// * `EncoderAPI` - The best guess for the encoder API. -fn get_encoder_api(encoder: &String, encoder_type: &EncoderType) -> EncoderAPI { - if *encoder_type == EncoderType::HARDWARE { - if encoder.starts_with("qsv") { - EncoderAPI::QSV - } else if encoder.starts_with("va") { - EncoderAPI::VAAPI - } else if encoder.starts_with("nv") { - EncoderAPI::NVENC - } else if encoder.starts_with("amf") { - EncoderAPI::AMF - } else { - EncoderAPI::UNKNOWN - } - } else if *encoder_type == EncoderType::SOFTWARE { - EncoderAPI::SOFTWARE - } else { - EncoderAPI::UNKNOWN - } -} - -/// Returns true if system supports given encoder. -/// # Returns -/// * `bool` - True if encoder is supported, false otherwise. -fn is_encoder_supported(encoder: &String) -> bool { - gst::ElementFactory::find(encoder.as_str()).is_some() -} - -fn set_element_property(element: &gst::Element, property: &str, value: &dyn ToValue) { - element.set_property(property, value.to_value()); -} - - -/// Helper to set CQP value of known encoder -/// # Arguments -/// * `encoder` - Information about the encoder. -/// * `quality` - Constant quantization parameter (CQP) quality, recommended values are between 20-30. -/// # Returns -/// * `EncoderInfo` - Encoder with maybe updated parameters. -pub fn encoder_cqp_params(encoder: &VideoEncoderInfo, quality: u32) -> VideoEncoderInfo { - let mut encoder_optz = encoder.clone(); - - // Look for known keys by factory creation - let encoder = gst::ElementFactory::make(encoder_optz.name.as_str()) - .build() - .unwrap(); - - // Get properties of the encoder - for prop in encoder.list_properties() { - let prop_name = prop.name(); - - // Look for known keys - if prop_name.to_lowercase().contains("qp") - && (prop_name.to_lowercase().contains("i") || prop_name.to_lowercase().contains("min")) - { - encoder_optz.set_parameter(prop_name, &quality.to_string()); - } else if prop_name.to_lowercase().contains("qp") - && (prop_name.to_lowercase().contains("p") || prop_name.to_lowercase().contains("max")) - { - encoder_optz.set_parameter(prop_name, &(quality + 2).to_string()); - } - } - - encoder_optz -} - -/// Helper to set VBR values of known encoder -/// # Arguments -/// * `encoder` - Information about the encoder. -/// * `bitrate` - Target bitrate in bits per second. -/// * `max_bitrate` - Maximum bitrate in bits per second. -/// # Returns -/// * `EncoderInfo` - Encoder with maybe updated parameters. -pub fn encoder_vbr_params(encoder: &VideoEncoderInfo, bitrate: u32, max_bitrate: u32) -> VideoEncoderInfo { - let mut encoder_optz = encoder.clone(); - - // Look for known keys by factory creation - let encoder = gst::ElementFactory::make(encoder_optz.name.as_str()) - .build() - .unwrap(); - - // Get properties of the encoder - for prop in encoder.list_properties() { - let prop_name = prop.name(); - - // Look for known keys - if prop_name.to_lowercase().contains("bitrate") - && !prop_name.to_lowercase().contains("max") - { - encoder_optz.set_parameter(prop_name, &bitrate.to_string()); - } else if prop_name.to_lowercase().contains("bitrate") - && prop_name.to_lowercase().contains("max") - { - // If SVT-AV1, don't set max bitrate - if encoder_optz.name == "svtav1enc" { - continue; +fn get_encoder_api(encoder: &str, encoder_type: &EncoderType) -> EncoderAPI { + match encoder_type { + EncoderType::HARDWARE => { + if encoder.starts_with("qsv") { + EncoderAPI::QSV + } else if encoder.starts_with("va") { + EncoderAPI::VAAPI + } else if encoder.starts_with("nv") { + EncoderAPI::NVENC + } else if encoder.starts_with("amf") { + EncoderAPI::AMF + } else { + EncoderAPI::UNKNOWN } - encoder_optz.set_parameter(prop_name, &max_bitrate.to_string()); } + EncoderType::SOFTWARE => EncoderAPI::SOFTWARE, + _ => EncoderAPI::UNKNOWN, } +} + +fn codec_from_encoder_name(name: &str) -> Option { + if name.contains("h264") { + Some(VideoCodec::H264) + } else if name.contains("h265") { + Some(VideoCodec::H265) + } else if name.contains("av1") { + Some(VideoCodec::AV1) + } else { + None + } +} + +fn modify_encoder_params(encoder: &VideoEncoderInfo, mut param_check: F) -> VideoEncoderInfo +where + F: FnMut(&str) -> Option<(String, String)>, +{ + let mut encoder_optz = encoder.clone(); + let element = match gst::ElementFactory::make(&encoder_optz.name).build() { + Ok(e) => e, + Err(_) => return encoder_optz, // Return original if element creation fails + }; + + element.list_properties().iter().for_each(|prop| { + let prop_name = prop.name(); + if let Some((key, value)) = param_check(prop_name) { + encoder_optz.set_parameter(&key, &value); + } + }); encoder_optz } -/// Helper to set CBR value of known encoder -/// # Arguments -/// * `encoder` - Information about the encoder. -/// * `bitrate` - Target bitrate in bits per second. -/// # Returns -/// * `EncoderInfo` - Encoder with maybe updated parameters. +// Parameter setting helpers +pub fn encoder_cqp_params(encoder: &VideoEncoderInfo, quality: u32) -> VideoEncoderInfo { + modify_encoder_params(encoder, |prop| { + let pl = prop.to_lowercase(); + if !pl.contains("qp") { + return None; + } + + if pl.contains("i") || pl.contains("min") { + Some((prop.into(), quality.to_string())) + } else if pl.contains("p") || pl.contains("max") { + Some((prop.into(), (quality + 2).to_string())) + } else { + None + } + }) +} + +pub fn encoder_vbr_params( + encoder: &VideoEncoderInfo, + bitrate: u32, + max_bitrate: u32, +) -> VideoEncoderInfo { + modify_encoder_params(encoder, |prop| { + let pl = prop.to_lowercase(); + if !pl.contains("bitrate") { + return None; + } + + if !pl.contains("max") { + Some((prop.into(), bitrate.to_string())) + } else if encoder.name != "svtav1enc" { + Some((prop.into(), max_bitrate.to_string())) + } else { + None + } + }) +} + pub fn encoder_cbr_params(encoder: &VideoEncoderInfo, bitrate: u32) -> VideoEncoderInfo { - let mut encoder_optz = encoder.clone(); - - // Look for known keys by factory creation - let encoder = gst::ElementFactory::make(encoder_optz.name.as_str()) - .build() - .unwrap(); - - // Get properties of the encoder - for prop in encoder.list_properties() { - let prop_name = prop.name(); - - // Look for known keys - if prop_name.to_lowercase().contains("bitrate") - && !prop_name.to_lowercase().contains("max") - { - encoder_optz.set_parameter(prop_name, &bitrate.to_string()); + modify_encoder_params(encoder, |prop| { + let pl = prop.to_lowercase(); + if pl.contains("bitrate") && !pl.contains("max") { + Some((prop.into(), bitrate.to_string())) + } else { + None } - } - - encoder_optz + }) } -/// Helper to set GOP size of known encoder -/// # Arguments -/// * `encoder` - Information about the encoder. -/// * `gop_size` - Group of pictures (GOP) size. -/// # Returns -/// * `EncoderInfo` - Encoder with maybe updated parameters. pub fn encoder_gop_params(encoder: &VideoEncoderInfo, gop_size: u32) -> VideoEncoderInfo { - let mut encoder_optz = encoder.clone(); - - // Look for known keys by factory creation - let encoder = gst::ElementFactory::make(encoder_optz.name.as_str()) - .build() - .unwrap(); - - // Get properties of the encoder - for prop in encoder.list_properties() { - let prop_name = prop.name(); - - // Look for known keys - if prop_name.to_lowercase().contains("gop-size") - || prop_name.to_lowercase().contains("int-max") - || prop_name.to_lowercase().contains("max-dist") - || prop_name.to_lowercase().contains("intra-period-length") + modify_encoder_params(encoder, |prop| { + let pl = prop.to_lowercase(); + if pl.contains("gop-size") + || pl.contains("int-max") + || pl.contains("max-dist") + || pl.contains("intra-period-length") { - encoder_optz.set_parameter(prop_name, &gop_size.to_string()); + Some((prop.into(), gop_size.to_string())) + } else { + None } - } - - encoder_optz + }) } -/// Sets parameters of known encoders for low latency operation. -/// # Arguments -/// * `encoder` - Information about the encoder. -/// # Returns -/// * `EncoderInfo` - Encoder with maybe updated parameters. pub fn encoder_low_latency_params(encoder: &VideoEncoderInfo) -> VideoEncoderInfo { - let mut encoder_optz = encoder.clone(); - encoder_optz = encoder_gop_params(&encoder_optz, 30); + let mut encoder_optz = encoder_gop_params(encoder, 30); + match encoder_optz.encoder_api { EncoderAPI::QSV => { encoder_optz.set_parameter("low-latency", "true"); @@ -350,147 +257,138 @@ pub fn encoder_low_latency_params(encoder: &VideoEncoderInfo) -> VideoEncoderInf encoder_optz.set_parameter("target-usage", "7"); } EncoderAPI::NVENC => { - match encoder_optz.codec { - // nvcudah264enc supports newer presets and tunes - VideoCodec::H264 => { - encoder_optz.set_parameter("multi-pass", "disabled"); - encoder_optz.set_parameter("preset", "p1"); - encoder_optz.set_parameter("tune", "ultra-low-latency"); - } - // same goes for nvcudah265enc - VideoCodec::H265 => { - encoder_optz.set_parameter("multi-pass", "disabled"); - encoder_optz.set_parameter("preset", "p1"); - encoder_optz.set_parameter("tune", "ultra-low-latency"); - } - // nvav1enc only supports older presets - VideoCodec::AV1 => { - encoder_optz.set_parameter("preset", "low-latency-hp"); - } - _ => {} - } + encoder_optz.set_parameter("multi-pass", "disabled"); + encoder_optz.set_parameter("preset", "p1"); + encoder_optz.set_parameter("tune", "ultra-low-latency"); } EncoderAPI::AMF => { encoder_optz.set_parameter("preset", "speed"); - match encoder_optz.codec { - // Only H.264 supports "ultra-low-latency" usage - VideoCodec::H264 => { - encoder_optz.set_parameter("usage", "ultra-low-latency"); - } - // Same goes for H.265 - VideoCodec::H265 => { - encoder_optz.set_parameter("usage", "ultra-low-latency"); - } - VideoCodec::AV1 => { - encoder_optz.set_parameter("usage", "low-latency"); - } - _ => {} + let usage = match encoder_optz.codec { + VideoCodec::H264 | VideoCodec::H265 => "ultra-low-latency", + VideoCodec::AV1 => "low-latency", + _ => "", + }; + if !usage.is_empty() { + encoder_optz.set_parameter("usage", usage); } } - EncoderAPI::SOFTWARE => { - // Check encoder name for software encoders - match encoder_optz.name.as_str() { - "openh264enc" => { - encoder_optz.set_parameter("complexity", "low"); - encoder_optz.set_parameter("usage-type", "screen"); - } - "x264enc" => { - encoder_optz.set_parameter("rc-lookahead", "0"); - encoder_optz.set_parameter("speed-preset", "ultrafast"); - encoder_optz.set_parameter("tune", "zerolatency"); - } - "svtav1enc" => { - encoder_optz.set_parameter("preset", "12"); - // Add ":pred-struct=1" only in CBR mode - let params_string = format!( - "lookahead=0{}", - if encoder_optz.get_parameters_string().contains("cbr") { - ":pred-struct=1" - } else { - "" - } - ); - encoder_optz.set_parameter("parameters-string", params_string.as_str()); - } - "av1enc" => { - encoder_optz.set_parameter("usage-profile", "realtime"); - encoder_optz.set_parameter("cpu-used", "10"); - encoder_optz.set_parameter("lag-in-frames", "0"); - } - _ => {} + EncoderAPI::SOFTWARE => match encoder_optz.name.as_str() { + "openh264enc" => { + encoder_optz.set_parameter("complexity", "low"); + encoder_optz.set_parameter("usage-type", "screen"); } - } + "x264enc" => { + encoder_optz.set_parameter("rc-lookahead", "0"); + encoder_optz.set_parameter("speed-preset", "ultrafast"); + encoder_optz.set_parameter("tune", "zerolatency"); + } + "svtav1enc" => { + encoder_optz.set_parameter("preset", "12"); + let suffix = if encoder_optz.get_parameters_string().contains("cbr") { + ":pred-struct=1" + } else { + "" + }; + encoder_optz.set_parameter("parameters-string", &format!("lookahead=0{}", suffix)); + } + "av1enc" => { + encoder_optz.set_parameter("usage-profile", "realtime"); + encoder_optz.set_parameter("cpu-used", "10"); + encoder_optz.set_parameter("lag-in-frames", "0"); + } + _ => {} + }, _ => {} } encoder_optz } -/// Returns all compatible encoders for the system. -/// # Returns -/// * `Vec` - List of compatible encoders. pub fn get_compatible_encoders() -> Vec { - let mut encoders: Vec = Vec::new(); - + let mut encoders = Vec::new(); let registry = gst::Registry::get(); - let plugins = registry.plugins(); - for plugin in plugins { - let features = registry.features_by_plugin(plugin.plugin_name().as_str()); - for feature in features { - let encoder = feature.name().to_string(); - let factory = gst::ElementFactory::find(encoder.as_str()); - if factory.is_some() { - let factory = factory.unwrap(); - // Get klass metadata - let klass = factory.metadata("klass"); - if klass.is_some() { - // Make sure klass contains "Encoder/Video/..." - let klass = klass.unwrap().to_string(); - if !klass.to_lowercase().contains("encoder/video") { - continue; - } + let gpus = gpu::get_gpus(); - // If contains "/hardware" in klass, it's a hardware encoder - let encoder_type = if klass.to_lowercase().contains("/hardware") { - EncoderType::HARDWARE - } else { - EncoderType::SOFTWARE - }; + for plugin in registry.plugins() { + for feature in registry.features_by_plugin(plugin.plugin_name().as_str()) { + let encoder_name = feature.name(); - let api = get_encoder_api(&encoder, &encoder_type); - if is_encoder_supported(&encoder) { - // Match codec by looking for "264" or "av1" in encoder name - let codec = if encoder.contains("264") { - VideoCodec::H264 - } else if encoder.contains("265") { - VideoCodec::H265 - } else if encoder.contains("av1") { - VideoCodec::AV1 - } else { - continue; - }; - let encoder_info = VideoEncoderInfo::new(encoder, codec, encoder_type, api); - encoders.push(encoder_info); - } else if api == EncoderAPI::VAAPI { - // Try low-power variant of VA-API encoder - let low_power_encoder = get_low_power_encoder(&encoder); - if is_encoder_supported(&low_power_encoder) { - let codec = if low_power_encoder.contains("264") { - VideoCodec::H264 - } else if low_power_encoder.contains("265") { - VideoCodec::H265 - } else if low_power_encoder.contains("av1") { - VideoCodec::AV1 - } else { - continue; - }; - let encoder_info = - VideoEncoderInfo::new(low_power_encoder, codec, encoder_type, api); - encoders.push(encoder_info); - } - } - } + let factory = match gst::ElementFactory::find(encoder_name.as_str()) { + Some(f) => f, + None => continue, + }; + + let klass = match factory.metadata("klass") { + Some(k) => k.to_lowercase(), + None => continue, + }; + + if !klass.contains("encoder/video") { + continue; } + + let encoder_type = if klass.contains("/hardware") { + EncoderType::HARDWARE + } else { + EncoderType::SOFTWARE + }; + + let api = get_encoder_api(encoder_name.as_str(), &encoder_type); + let codec = match codec_from_encoder_name(encoder_name.as_str()) { + Some(c) => c, + None => continue, + }; + + let element = match factory.create().build() { + Ok(e) => e, + Err(_) => continue, + }; + + let mut gpu_info = None; + + if encoder_type == EncoderType::HARDWARE { + gpu_info = std::panic::catch_unwind(|| { + match api { + EncoderAPI::QSV | EncoderAPI::VAAPI => { + // Safe property access with panic protection, gstreamer-rs is fun + let path = if element.has_property("device-path") { + Some(element.property::("device-path")) + } else if element.has_property("device") { + Some(element.property::("device")) + } else { + None + }; + + path.and_then(|p| get_gpu_by_card_path(&gpus, &p)) + } + EncoderAPI::NVENC if element.has_property("cuda-device-id") => { + let cuda_id = element.property::("cuda-device-id"); + get_gpus_by_vendor(&gpus, "nvidia") + .get(cuda_id as usize) + .cloned() + } + EncoderAPI::AMF if element.has_property("device") => { + let device_id = element.property::("device"); + get_gpus_by_vendor(&gpus, "amd") + .get(device_id as usize) + .cloned() + } + _ => None, + } + }) + .unwrap_or_else(|_| { + log::error!( + "Panic occurred while querying properties for {}", + encoder_name + ); + None + }); + } + + let mut encoder_info = + VideoEncoderInfo::new(encoder_name.into(), codec, encoder_type.clone(), api); + encoder_info.gpu_info = gpu_info; + encoders.push(encoder_info); } } diff --git a/packages/server/src/gpu.rs b/packages/server/src/gpu.rs index 8b33799b..f68ab274 100644 --- a/packages/server/src/gpu.rs +++ b/packages/server/src/gpu.rs @@ -1,6 +1,5 @@ use regex::Regex; use std::fs; -use std::path::Path; use std::process::Command; use std::str; @@ -49,9 +48,9 @@ impl GPUInfo { fn get_gpu_vendor(vendor_id: &str) -> GPUVendor { match vendor_id { - "8086" => GPUVendor::INTEL, // Intel - "10de" => GPUVendor::NVIDIA, // NVIDIA - "1002" => GPUVendor::AMD, // AMD/ATI + "8086" => GPUVendor::INTEL, + "10de" => GPUVendor::NVIDIA, + "1002" => GPUVendor::AMD, _ => GPUVendor::UNKNOWN, } } @@ -60,174 +59,105 @@ fn get_gpu_vendor(vendor_id: &str) -> GPUVendor { /// # Returns /// * `Vec` - A vector containing information about each GPU. pub fn get_gpus() -> Vec { - let mut gpus = Vec::new(); - - // Run lspci to get PCI devices related to GPUs - let lspci_output = Command::new("lspci") - .arg("-mmnn") // Get machine-readable output with IDs + let output = Command::new("lspci") + .args(["-mm", "-nn"]) .output() .expect("Failed to execute lspci"); - let output = str::from_utf8(&lspci_output.stdout).unwrap(); - - // Filter lines that mention VGA or 3D controller - for line in output.lines() { - if line.to_lowercase().contains("vga compatible controller") - || line.to_lowercase().contains("3d controller") - || line.to_lowercase().contains("display controller") - { - if let Some((pci_addr, vendor_id, device_name)) = parse_pci_device(line) { - // Run udevadm to get the device path - if let Some((card_path, render_path)) = get_dri_device_path(&pci_addr) { - let vendor = get_gpu_vendor(&vendor_id); - gpus.push(GPUInfo { - vendor, - card_path, - render_path, - device_name, - }); - } - } - } - } - - gpus -} - -/// Parses a line from the lspci output to extract PCI device information. -/// # Arguments -/// * `line` - A string slice that holds a line from the 'lspci -mmnn' output. -/// # Returns -/// * `Option<(String, String, String)>` - A tuple containing the PCI address, vendor ID, and device name if parsing is successful. -fn parse_pci_device(line: &str) -> Option<(String, String, String)> { - // Define a regex pattern to match PCI device lines - let re = Regex::new(r#"(?P[0-9a-fA-F]{1,2}:[0-9a-fA-F]{2}\.[0-9]) "[^"]+" "(?P[^"]+)" "(?P[^"]+)"#).unwrap(); - - // Collect all matched groups - let parts: Vec<(String, String, String)> = re - .captures_iter(line) - .filter_map(|cap| { - Some(( - cap["pci_addr"].to_string(), - cap["vendor_name"].to_string(), - cap["device_name"].to_string(), - )) + str::from_utf8(&output.stdout) + .unwrap() + .lines() + .filter_map(|line| parse_pci_device(line)) + .filter(|(class_id, _, _, _)| matches!(class_id.as_str(), "0300" | "0302" | "0380")) + .filter_map(|(_, vendor_id, device_name, pci_addr)| { + get_dri_device_path(&pci_addr) + .map(|(card, render)| (vendor_id, card, render, device_name)) }) - .collect(); - - if let Some((pci_addr, vendor_name, device_name)) = parts.first() { - // Create a mutable copy of the device name to modify - let mut device_name = device_name.clone(); - - // If more than 1 square-bracketed item is found, remove last one, otherwise remove all - if let Some(start) = device_name.rfind('[') { - if let Some(_) = device_name.rfind(']') { - device_name = device_name[..start].trim().to_string(); - } - } - - // Extract vendor ID from vendor name (e.g., "Intel Corporation [8086]" or "Advanced Micro Devices, Inc. [AMD/ATI] [1002]") - let vendor_id = vendor_name - .split_whitespace() - .last() - .unwrap_or_default() - .trim_matches(|c: char| !c.is_ascii_hexdigit()) - .to_string(); - - return Some((pci_addr.clone(), vendor_id, device_name)); - } - - None + .map(|(vid, card_path, render_path, device_name)| GPUInfo { + vendor: get_gpu_vendor(&vid), + card_path, + render_path, + device_name, + }) + .collect() +} + +fn parse_pci_device(line: &str) -> Option<(String, String, String, String)> { + let re = Regex::new( + r#"^(?P\S+)\s+"[^\[]*\[(?P[0-9a-f]{4})\].*?"\s+"[^\[]*\[(?P[0-9a-f]{4})\].*?"\s+"(?P[^"]+?)""#, + ).unwrap(); + + let caps = re.captures(line)?; + + // Clean device name by removing only the trailing device ID + let device_name = caps.name("device_name")?.as_str().trim(); + let clean_re = Regex::new(r"\s+\[[0-9a-f]{4}\]$").unwrap(); + let cleaned_name = clean_re.replace(device_name, "").trim().to_string(); + + Some(( + caps.name("class_id")?.as_str().to_lowercase(), + caps.name("vendor_id")?.as_str().to_lowercase(), + cleaned_name, + caps.name("pci_addr")?.as_str().to_string(), + )) } -/// Retrieves the DRI device paths for a given PCI address. -/// Doubles as a way to verify the device is a GPU. -/// # Arguments -/// * `pci_addr` - A string slice that holds the PCI address. -/// # Returns -/// * `Option<(String, String)>` - A tuple containing the card path and render path if found. fn get_dri_device_path(pci_addr: &str) -> Option<(String, String)> { - // Construct the base directory in /sys/bus/pci/devices to start the search - let base_dir = Path::new("/sys/bus/pci/devices"); + let target_dir = format!("0000:{}", pci_addr); + let entries = fs::read_dir("/sys/bus/pci/devices").ok()?; - // Define the target PCI address with "0000:" prefix - let target_addr = format!("0000:{}", pci_addr); + for entry in entries.flatten() { + if !entry.path().to_string_lossy().contains(&target_dir) { + continue; + } - // Search for a matching directory that contains the target PCI address - for entry in fs::read_dir(base_dir).ok()?.flatten() { - let path = entry.path(); + let mut card = String::new(); + let mut render = String::new(); + let drm_path = entry.path().join("drm"); - // Check if the path matches the target PCI address - if path.to_string_lossy().contains(&target_addr) { - // Look for any files under the 'drm' subdirectory, like 'card0' or 'renderD128' - let drm_path = path.join("drm"); - if drm_path.exists() { - let mut card_path = String::new(); - let mut render_path = String::new(); - for drm_entry in fs::read_dir(drm_path).ok()?.flatten() { - let file_name = drm_entry.file_name(); - if let Some(name) = file_name.to_str() { - if name.starts_with("card") { - card_path = format!("/dev/dri/{}", name); - } - if name.starts_with("renderD") { - render_path = format!("/dev/dri/{}", name); - } - // If both paths are found, break the loop - if !card_path.is_empty() && !render_path.is_empty() { - break; - } - } - } - return Some((card_path, render_path)); + for drm_entry in fs::read_dir(drm_path).ok()?.flatten() { + let name = drm_entry.file_name().to_string_lossy().into_owned(); + + if name.starts_with("card") { + card = format!("/dev/dri/{}", name); + } else if name.starts_with("renderD") { + render = format!("/dev/dri/{}", name); + } + + if !card.is_empty() && !render.is_empty() { + break; } } - } - None -} - -/// Helper method to get GPUs from vector by vendor name (case-insensitive). -/// # Arguments -/// * `gpus` - A vector containing information about each GPU. -/// * `vendor` - A string slice that holds the vendor name. -/// # Returns -/// * `Vec` - A vector containing GPUInfo structs if found. -pub fn get_gpus_by_vendor(gpus: &Vec, vendor: &str) -> Vec { - let vendor = vendor.to_lowercase(); - gpus.iter() - .filter(|gpu| gpu.vendor_string().to_lowercase().contains(&vendor)) - .cloned() - .collect() -} - -/// Helper method to get GPUs from vector by device name substring (case-insensitive). -/// # Arguments -/// * `gpus` - A vector containing information about each GPU. -/// * `device_name` - A string slice that holds the device name substring. -/// # Returns -/// * `Vec` - A vector containing GPUInfo structs if found. -pub fn get_gpus_by_device_name(gpus: &Vec, device_name: &str) -> Vec { - let device_name = device_name.to_lowercase(); - gpus.iter() - .filter(|gpu| gpu.device_name.to_lowercase().contains(&device_name)) - .cloned() - .collect() -} - -/// Helper method to get a GPU from vector of GPUInfo by card path (either /dev/dri/cardX or /dev/dri/renderDX, case-insensitive). -/// # Arguments -/// * `gpus` - A vector containing information about each GPU. -/// * `card_path` - A string slice that holds the card path. -/// # Returns -/// * `Option` - A reference to a GPUInfo struct if found. -pub fn get_gpu_by_card_path(gpus: &Vec, card_path: &str) -> Option { - for gpu in gpus { - if gpu.card_path().to_lowercase() == card_path.to_lowercase() - || gpu.render_path().to_lowercase() == card_path.to_lowercase() - { - return Some(gpu.clone()); + if !card.is_empty() { + return Some((card, render)); } } + None } + +// Helper functions remain similar with improved readability: +pub fn get_gpus_by_vendor(gpus: &[GPUInfo], vendor: &str) -> Vec { + let target = vendor.to_lowercase(); + gpus.iter() + .filter(|gpu| gpu.vendor_string().to_lowercase() == target) + .cloned() + .collect() +} + +pub fn get_gpus_by_device_name(gpus: &[GPUInfo], substring: &str) -> Vec { + let target = substring.to_lowercase(); + gpus.iter() + .filter(|gpu| gpu.device_name.to_lowercase().contains(&target)) + .cloned() + .collect() +} + +pub fn get_gpu_by_card_path(gpus: &[GPUInfo], path: &str) -> Option { + gpus.iter() + .find(|gpu| { + gpu.card_path.eq_ignore_ascii_case(path) || gpu.render_path.eq_ignore_ascii_case(path) + }) + .cloned() +} diff --git a/packages/server/src/main.rs b/packages/server/src/main.rs index a2d3ef89..cdcd5822 100644 --- a/packages/server/src/main.rs +++ b/packages/server/src/main.rs @@ -78,11 +78,12 @@ fn handle_encoder_video(args: &args::Args) -> Option [Video Encoder] Name: '{}', Codec: '{}', API: '{}', Type: '{}'", + "> [Video Encoder] Name: '{}', Codec: '{}', API: '{}', Type: '{}', Device: '{}'", encoder.name, encoder.codec.to_str(), encoder.encoder_api.to_str(), - encoder.encoder_type.to_str() + encoder.encoder_type.to_str(), + if let Some(gpu) = &encoder.gpu_info { gpu.device_name() } else { "CPU" }, ); } // Pick most suitable video encoder based on given arguments @@ -152,7 +153,7 @@ fn handle_encoder_audio(args: &args::Args) -> String { #[tokio::main] async fn main() -> Result<(), Box> { // Parse command line arguments - let args = args::Args::new(); + let mut args = args::Args::new(); if args.app.verbose { args.debug_print(); } @@ -186,6 +187,12 @@ async fn main() -> Result<(), Box> { } let gpu = gpu.unwrap(); + // TODO: Currently DMA-BUF only works for NVIDIA + if args.app.dma_buf && *gpu.vendor() != GPUVendor::NVIDIA { + log::warn!("DMA-BUF is currently unsupported outside NVIDIA GPUs, force disabling.."); + args.app.dma_buf = false; + } + // Handle video encoder selection let video_encoder_info = handle_encoder_video(&args); if video_encoder_info.is_none() { @@ -261,9 +268,12 @@ async fn main() -> Result<(), Box> { // GL Upload Element let glupload = gst::ElementFactory::make("glupload").build()?; + // GL color convert element + let glcolorconvert = gst::ElementFactory::make("glcolorconvert").build()?; + // GL upload caps filter let gl_caps_filter = gst::ElementFactory::make("capsfilter").build()?; - let gl_caps = gst::Caps::from_str("video/x-raw(memory:VAMemory)")?; + let gl_caps = gst::Caps::from_str("video/x-raw(memory:GLMemory),format=NV12")?; gl_caps_filter.set_property("caps", &gl_caps); // Video Converter Element @@ -271,7 +281,7 @@ async fn main() -> Result<(), Box> { // Video Encoder Element let video_encoder = gst::ElementFactory::make(video_encoder_info.name.as_str()).build()?; - video_encoder_info.apply_parameters(&video_encoder, &args.app.verbose); + video_encoder_info.apply_parameters(&video_encoder, args.app.verbose); /* Output */ // WebRTC sink Element @@ -294,9 +304,9 @@ async fn main() -> Result<(), Box> { &audio_source, ])?; - // If DMA-BUF is enabled, add glupload and gl caps filter + // If DMA-BUF is enabled, add glupload, color conversion and caps filter if args.app.dma_buf { - pipeline.add_many(&[&glupload, &gl_caps_filter])?; + pipeline.add_many(&[&glupload, &glcolorconvert, &gl_caps_filter])?; } // Link main audio branch @@ -316,8 +326,8 @@ async fn main() -> Result<(), Box> { &video_source, &caps_filter, &glupload, + &glcolorconvert, &gl_caps_filter, - &video_converter, &video_encoder, webrtcsink.upcast_ref(), ])?;