feat(runner): DMA-BUF support (for NVIDIA) (#181)

Also includes other improvements and hopefully reducing LOC with some
cleanup.

---------

Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
This commit is contained in:
Kristian Ollikainen
2025-02-11 12:03:03 +02:00
committed by GitHub
parent 060718d8b0
commit 7de6e243ed
7 changed files with 429 additions and 534 deletions

View File

@@ -74,7 +74,7 @@ RUN --mount=type=cache,target=/var/cache/pacman/pkg \
libxkbcommon wayland gstreamer gst-plugins-base gst-plugins-good libinput libxkbcommon wayland gstreamer gst-plugins-base gst-plugins-good libinput
# Clone repository with proper directory structure # 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 FROM gst-wayland-deps AS gst-wayland-planner

View File

@@ -3,9 +3,9 @@ set -euo pipefail
# Make user directory owned by the default user # Make user directory owned by the default user
chown -f "$(id -nu):$(id -ng)" ~ || \ 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)" ~ || \ 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' echo 'Failed to change user directory permissions, there may be permission issues'
# Source environment variables from envs.sh # Source environment variables from envs.sh

View File

@@ -36,6 +36,13 @@ impl DeviceArgs {
println!("> gpu_vendor: {}", self.gpu_vendor); println!("> gpu_vendor: {}", self.gpu_vendor);
println!("> gpu_name: {}", self.gpu_name); println!("> gpu_name: {}", self.gpu_name);
println!("> gpu_index: {}", self.gpu_index); 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
}
);
} }
} }

View File

@@ -39,7 +39,14 @@ pub struct EncodingOptionsBase {
impl EncodingOptionsBase { impl EncodingOptionsBase {
pub fn debug_print(&self) { pub fn debug_print(&self) {
println!("> Codec: {}", self.codec); println!("> Codec: {}", self.codec);
println!("> Encoder: {}", self.encoder); println!(
"> Encoder: {}",
if self.encoder.is_empty() {
"Auto-Selection"
} else {
&self.encoder
}
);
match &self.rate_control { match &self.rate_control {
RateControl::CQP(cqp) => { RateControl::CQP(cqp) => {
println!("> Rate Control: CQP"); println!("> Rate Control: CQP");
@@ -72,21 +79,44 @@ impl VideoEncodingOptions {
.get_one::<String>("video-encoder") .get_one::<String>("video-encoder")
.unwrap_or(&"".to_string()) .unwrap_or(&"".to_string())
.clone(), .clone(),
rate_control: match matches.get_one::<String>("video-rate-control").unwrap().as_str() { rate_control: match matches
.get_one::<String>("video-rate-control")
.unwrap()
.as_str()
{
"cqp" => RateControl::CQP(RateControlCQP { "cqp" => RateControl::CQP(RateControlCQP {
quality: matches.get_one::<String>("video-cqp").unwrap().parse::<u32>().unwrap(), quality: matches
.get_one::<String>("video-cqp")
.unwrap()
.parse::<u32>()
.unwrap(),
}), }),
"cbr" => RateControl::CBR(RateControlCBR { "cbr" => RateControl::CBR(RateControlCBR {
target_bitrate: matches.get_one::<String>("video-bitrate").unwrap().parse::<i32>().unwrap(), target_bitrate: matches
.get_one::<String>("video-bitrate")
.unwrap()
.parse::<i32>()
.unwrap(),
}), }),
"vbr" => RateControl::VBR(RateControlVBR { "vbr" => RateControl::VBR(RateControlVBR {
target_bitrate: matches.get_one::<String>("video-bitrate").unwrap().parse::<i32>().unwrap(), target_bitrate: matches
max_bitrate: matches.get_one::<String>("video-bitrate-max").unwrap().parse::<i32>().unwrap(), .get_one::<String>("video-bitrate")
.unwrap()
.parse::<i32>()
.unwrap(),
max_bitrate: matches
.get_one::<String>("video-bitrate-max")
.unwrap()
.parse::<i32>()
.unwrap(),
}), }),
_ => panic!("Invalid rate control method for video"), _ => panic!("Invalid rate control method for video"),
}, },
}, },
encoder_type: matches.get_one::<String>("video-encoder-type").unwrap_or(&"hardware".to_string()).clone(), encoder_type: matches
.get_one::<String>("video-encoder-type")
.unwrap_or(&"hardware".to_string())
.clone(),
} }
} }
@@ -133,18 +163,38 @@ impl AudioEncodingOptions {
.get_one::<String>("audio-encoder") .get_one::<String>("audio-encoder")
.unwrap_or(&"".to_string()) .unwrap_or(&"".to_string())
.clone(), .clone(),
rate_control: match matches.get_one::<String>("audio-rate-control").unwrap().as_str() { rate_control: match matches
.get_one::<String>("audio-rate-control")
.unwrap()
.as_str()
{
"cbr" => RateControl::CBR(RateControlCBR { "cbr" => RateControl::CBR(RateControlCBR {
target_bitrate: matches.get_one::<String>("audio-bitrate").unwrap().parse::<i32>().unwrap(), target_bitrate: matches
.get_one::<String>("audio-bitrate")
.unwrap()
.parse::<i32>()
.unwrap(),
}), }),
"vbr" => RateControl::VBR(RateControlVBR { "vbr" => RateControl::VBR(RateControlVBR {
target_bitrate: matches.get_one::<String>("audio-bitrate").unwrap().parse::<i32>().unwrap(), target_bitrate: matches
max_bitrate: matches.get_one::<String>("audio-bitrate-max").unwrap().parse::<i32>().unwrap(), .get_one::<String>("audio-bitrate")
.unwrap()
.parse::<i32>()
.unwrap(),
max_bitrate: matches
.get_one::<String>("audio-bitrate-max")
.unwrap()
.parse::<i32>()
.unwrap(),
}), }),
_ => panic!("Invalid rate control method for audio"), _ => panic!("Invalid rate control method for audio"),
}, },
}, },
capture_method: match matches.get_one::<String>("audio-capture-method").unwrap().as_str() { capture_method: match matches
.get_one::<String>("audio-capture-method")
.unwrap()
.as_str()
{
"pulseaudio" => AudioCaptureMethod::PulseAudio, "pulseaudio" => AudioCaptureMethod::PulseAudio,
"pipewire" => AudioCaptureMethod::PipeWire, "pipewire" => AudioCaptureMethod::PipeWire,
"alsa" => AudioCaptureMethod::ALSA, "alsa" => AudioCaptureMethod::ALSA,

View File

@@ -1,3 +1,4 @@
use crate::gpu::{self, get_gpu_by_card_path, get_gpus_by_vendor, GPUInfo};
use gst::prelude::*; use gst::prelude::*;
#[derive(Debug, Eq, PartialEq, Clone)] #[derive(Debug, Eq, PartialEq, Clone)]
@@ -7,47 +8,23 @@ pub enum VideoCodec {
AV1, AV1,
UNKNOWN, UNKNOWN,
} }
impl VideoCodec { impl VideoCodec {
pub fn to_str(&self) -> &'static str { pub fn to_str(&self) -> &'static str {
match self { match self {
VideoCodec::H264 => "H.264", Self::H264 => "H.264",
VideoCodec::H265 => "H.265", Self::H265 => "H.265",
VideoCodec::AV1 => "AV1", Self::AV1 => "AV1",
VideoCodec::UNKNOWN => "Unknown", Self::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",
} }
} }
pub fn from_str(s: &str) -> Self { pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() { match s.to_lowercase().as_str() {
"h264" => VideoCodec::H264, "h264" | "h.264" | "avc" => Self::H264,
"h.264" => VideoCodec::H264, "h265" | "h.265" | "hevc" | "hev1" => Self::H265,
"avc" => VideoCodec::H264, "av1" => Self::AV1,
"h265" => VideoCodec::H265, _ => Self::UNKNOWN,
"h.265" => VideoCodec::H265,
"hevc" => VideoCodec::H265,
"hev1" => VideoCodec::H265,
"av1" => VideoCodec::AV1,
_ => VideoCodec::UNKNOWN,
} }
} }
} }
@@ -61,15 +38,16 @@ pub enum EncoderAPI {
SOFTWARE, SOFTWARE,
UNKNOWN, UNKNOWN,
} }
impl EncoderAPI { impl EncoderAPI {
pub fn to_str(&self) -> &'static str { pub fn to_str(&self) -> &'static str {
match self { match self {
EncoderAPI::QSV => "Intel QuickSync Video", Self::QSV => "Intel QuickSync Video",
EncoderAPI::VAAPI => "Video Acceleration API", Self::VAAPI => "Video Acceleration API",
EncoderAPI::NVENC => "NVIDIA NVENC", Self::NVENC => "NVIDIA NVENC",
EncoderAPI::AMF => "AMD Media Framework", Self::AMF => "AMD Media Framework",
EncoderAPI::SOFTWARE => "Software", Self::SOFTWARE => "Software",
EncoderAPI::UNKNOWN => "Unknown", Self::UNKNOWN => "Unknown",
} }
} }
} }
@@ -80,20 +58,21 @@ pub enum EncoderType {
HARDWARE, HARDWARE,
UNKNOWN, UNKNOWN,
} }
impl EncoderType { impl EncoderType {
pub fn to_str(&self) -> &'static str { pub fn to_str(&self) -> &'static str {
match self { match self {
EncoderType::SOFTWARE => "Software", Self::SOFTWARE => "Software",
EncoderType::HARDWARE => "Hardware", Self::HARDWARE => "Hardware",
EncoderType::UNKNOWN => "Unknown", Self::UNKNOWN => "Unknown",
} }
} }
pub fn from_str(s: &str) -> Self { pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() { match s.to_lowercase().as_str() {
"software" => EncoderType::SOFTWARE, "software" => Self::SOFTWARE,
"hardware" => EncoderType::HARDWARE, "hardware" => Self::HARDWARE,
_ => EncoderType::UNKNOWN, _ => Self::UNKNOWN,
} }
} }
} }
@@ -105,6 +84,7 @@ pub struct VideoEncoderInfo {
pub encoder_type: EncoderType, pub encoder_type: EncoderType,
pub encoder_api: EncoderAPI, pub encoder_api: EncoderAPI,
pub parameters: Vec<(String, String)>, pub parameters: Vec<(String, String)>,
pub gpu_info: Option<GPUInfo>,
} }
impl VideoEncoderInfo { impl VideoEncoderInfo {
@@ -120,26 +100,26 @@ impl VideoEncoderInfo {
encoder_type, encoder_type,
encoder_api, encoder_api,
parameters: Vec::new(), parameters: Vec::new(),
gpu_info: None,
} }
} }
pub fn get_parameters_string(&self) -> String { pub fn get_parameters_string(&self) -> String {
self.parameters self.parameters
.iter() .iter()
.map(|(key, value)| format!("{}={}", key, value)) .map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<String>>() .collect::<Vec<_>>()
.join(" ") .join(" ")
} }
pub fn set_parameter(&mut self, key: &str, value: &str) { 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 { for (key, value) in &self.parameters {
if element.has_property(key) { if element.has_property(key) {
// If verbose, log property sets if verbose {
if *verbose {
println!("Setting property {} to {}", key, value); println!("Setting property {} to {}", key, value);
} }
element.set_property_from_str(key, value); element.set_property_from_str(key, value);
@@ -148,199 +128,126 @@ impl VideoEncoderInfo {
} }
} }
/// Converts VA-API encoder name to low-power variant. fn get_encoder_api(encoder: &str, encoder_type: &EncoderType) -> EncoderAPI {
/// # Arguments match encoder_type {
/// * `encoder` - The name of the VA-API encoder. EncoderType::HARDWARE => {
/// # Returns if encoder.starts_with("qsv") {
/// * `&str` - The name of the low-power variant of the encoder. EncoderAPI::QSV
fn get_low_power_encoder(encoder: &String) -> String { } else if encoder.starts_with("va") {
if encoder.starts_with("va") && !encoder.ends_with("enc") && !encoder.ends_with("lpenc") { EncoderAPI::VAAPI
// Replace "enc" substring at end with "lpenc" } else if encoder.starts_with("nv") {
let mut encoder = encoder.to_string(); EncoderAPI::NVENC
encoder.truncate(encoder.len() - 3); } else if encoder.starts_with("amf") {
encoder.push_str("lpenc"); EncoderAPI::AMF
encoder } else {
} else { EncoderAPI::UNKNOWN
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;
} }
encoder_optz.set_parameter(prop_name, &max_bitrate.to_string());
} }
EncoderType::SOFTWARE => EncoderAPI::SOFTWARE,
_ => EncoderAPI::UNKNOWN,
} }
}
fn codec_from_encoder_name(name: &str) -> Option<VideoCodec> {
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<F>(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 encoder_optz
} }
/// Helper to set CBR value of known encoder // Parameter setting helpers
/// # Arguments pub fn encoder_cqp_params(encoder: &VideoEncoderInfo, quality: u32) -> VideoEncoderInfo {
/// * `encoder` - Information about the encoder. modify_encoder_params(encoder, |prop| {
/// * `bitrate` - Target bitrate in bits per second. let pl = prop.to_lowercase();
/// # Returns if !pl.contains("qp") {
/// * `EncoderInfo` - Encoder with maybe updated parameters. 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 { pub fn encoder_cbr_params(encoder: &VideoEncoderInfo, bitrate: u32) -> VideoEncoderInfo {
let mut encoder_optz = encoder.clone(); modify_encoder_params(encoder, |prop| {
let pl = prop.to_lowercase();
// Look for known keys by factory creation if pl.contains("bitrate") && !pl.contains("max") {
let encoder = gst::ElementFactory::make(encoder_optz.name.as_str()) Some((prop.into(), bitrate.to_string()))
.build() } else {
.unwrap(); None
// 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());
} }
} })
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 { pub fn encoder_gop_params(encoder: &VideoEncoderInfo, gop_size: u32) -> VideoEncoderInfo {
let mut encoder_optz = encoder.clone(); modify_encoder_params(encoder, |prop| {
let pl = prop.to_lowercase();
// Look for known keys by factory creation if pl.contains("gop-size")
let encoder = gst::ElementFactory::make(encoder_optz.name.as_str()) || pl.contains("int-max")
.build() || pl.contains("max-dist")
.unwrap(); || pl.contains("intra-period-length")
// 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")
{ {
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 { pub fn encoder_low_latency_params(encoder: &VideoEncoderInfo) -> VideoEncoderInfo {
let mut encoder_optz = encoder.clone(); let mut encoder_optz = encoder_gop_params(encoder, 30);
encoder_optz = encoder_gop_params(&encoder_optz, 30);
match encoder_optz.encoder_api { match encoder_optz.encoder_api {
EncoderAPI::QSV => { EncoderAPI::QSV => {
encoder_optz.set_parameter("low-latency", "true"); 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"); encoder_optz.set_parameter("target-usage", "7");
} }
EncoderAPI::NVENC => { EncoderAPI::NVENC => {
match encoder_optz.codec { encoder_optz.set_parameter("multi-pass", "disabled");
// nvcudah264enc supports newer presets and tunes encoder_optz.set_parameter("preset", "p1");
VideoCodec::H264 => { encoder_optz.set_parameter("tune", "ultra-low-latency");
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");
}
_ => {}
}
} }
EncoderAPI::AMF => { EncoderAPI::AMF => {
encoder_optz.set_parameter("preset", "speed"); encoder_optz.set_parameter("preset", "speed");
match encoder_optz.codec { let usage = match encoder_optz.codec {
// Only H.264 supports "ultra-low-latency" usage VideoCodec::H264 | VideoCodec::H265 => "ultra-low-latency",
VideoCodec::H264 => { VideoCodec::AV1 => "low-latency",
encoder_optz.set_parameter("usage", "ultra-low-latency"); _ => "",
} };
// Same goes for H.265 if !usage.is_empty() {
VideoCodec::H265 => { encoder_optz.set_parameter("usage", usage);
encoder_optz.set_parameter("usage", "ultra-low-latency");
}
VideoCodec::AV1 => {
encoder_optz.set_parameter("usage", "low-latency");
}
_ => {}
} }
} }
EncoderAPI::SOFTWARE => { EncoderAPI::SOFTWARE => match encoder_optz.name.as_str() {
// Check encoder name for software encoders "openh264enc" => {
match encoder_optz.name.as_str() { encoder_optz.set_parameter("complexity", "low");
"openh264enc" => { encoder_optz.set_parameter("usage-type", "screen");
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");
}
_ => {}
} }
} "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 encoder_optz
} }
/// Returns all compatible encoders for the system.
/// # Returns
/// * `Vec<EncoderInfo>` - List of compatible encoders.
pub fn get_compatible_encoders() -> Vec<VideoEncoderInfo> { pub fn get_compatible_encoders() -> Vec<VideoEncoderInfo> {
let mut encoders: Vec<VideoEncoderInfo> = Vec::new(); let mut encoders = Vec::new();
let registry = gst::Registry::get(); let registry = gst::Registry::get();
let plugins = registry.plugins(); let gpus = gpu::get_gpus();
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;
}
// If contains "/hardware" in klass, it's a hardware encoder for plugin in registry.plugins() {
let encoder_type = if klass.to_lowercase().contains("/hardware") { for feature in registry.features_by_plugin(plugin.plugin_name().as_str()) {
EncoderType::HARDWARE let encoder_name = feature.name();
} else {
EncoderType::SOFTWARE
};
let api = get_encoder_api(&encoder, &encoder_type); let factory = match gst::ElementFactory::find(encoder_name.as_str()) {
if is_encoder_supported(&encoder) { Some(f) => f,
// Match codec by looking for "264" or "av1" in encoder name None => continue,
let codec = if encoder.contains("264") { };
VideoCodec::H264
} else if encoder.contains("265") { let klass = match factory.metadata("klass") {
VideoCodec::H265 Some(k) => k.to_lowercase(),
} else if encoder.contains("av1") { None => continue,
VideoCodec::AV1 };
} else {
continue; if !klass.contains("encoder/video") {
}; 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 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::<String>("device-path"))
} else if element.has_property("device") {
Some(element.property::<String>("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::<u32>("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::<u32>("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);
} }
} }

View File

@@ -1,6 +1,5 @@
use regex::Regex; use regex::Regex;
use std::fs; use std::fs;
use std::path::Path;
use std::process::Command; use std::process::Command;
use std::str; use std::str;
@@ -49,9 +48,9 @@ impl GPUInfo {
fn get_gpu_vendor(vendor_id: &str) -> GPUVendor { fn get_gpu_vendor(vendor_id: &str) -> GPUVendor {
match vendor_id { match vendor_id {
"8086" => GPUVendor::INTEL, // Intel "8086" => GPUVendor::INTEL,
"10de" => GPUVendor::NVIDIA, // NVIDIA "10de" => GPUVendor::NVIDIA,
"1002" => GPUVendor::AMD, // AMD/ATI "1002" => GPUVendor::AMD,
_ => GPUVendor::UNKNOWN, _ => GPUVendor::UNKNOWN,
} }
} }
@@ -60,174 +59,105 @@ fn get_gpu_vendor(vendor_id: &str) -> GPUVendor {
/// # Returns /// # Returns
/// * `Vec<GPUInfo>` - A vector containing information about each GPU. /// * `Vec<GPUInfo>` - A vector containing information about each GPU.
pub fn get_gpus() -> Vec<GPUInfo> { pub fn get_gpus() -> Vec<GPUInfo> {
let mut gpus = Vec::new(); let output = Command::new("lspci")
.args(["-mm", "-nn"])
// Run lspci to get PCI devices related to GPUs
let lspci_output = Command::new("lspci")
.arg("-mmnn") // Get machine-readable output with IDs
.output() .output()
.expect("Failed to execute lspci"); .expect("Failed to execute lspci");
let output = str::from_utf8(&lspci_output.stdout).unwrap(); str::from_utf8(&output.stdout)
.unwrap()
// Filter lines that mention VGA or 3D controller .lines()
for line in output.lines() { .filter_map(|line| parse_pci_device(line))
if line.to_lowercase().contains("vga compatible controller") .filter(|(class_id, _, _, _)| matches!(class_id.as_str(), "0300" | "0302" | "0380"))
|| line.to_lowercase().contains("3d controller") .filter_map(|(_, vendor_id, device_name, pci_addr)| {
|| line.to_lowercase().contains("display controller") get_dri_device_path(&pci_addr)
{ .map(|(card, render)| (vendor_id, card, render, device_name))
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<pci_addr>[0-9a-fA-F]{1,2}:[0-9a-fA-F]{2}\.[0-9]) "[^"]+" "(?P<vendor_name>[^"]+)" "(?P<device_name>[^"]+)"#).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(),
))
}) })
.collect(); .map(|(vid, card_path, render_path, device_name)| GPUInfo {
vendor: get_gpu_vendor(&vid),
if let Some((pci_addr, vendor_name, device_name)) = parts.first() { card_path,
// Create a mutable copy of the device name to modify render_path,
let mut device_name = device_name.clone(); device_name,
})
// If more than 1 square-bracketed item is found, remove last one, otherwise remove all .collect()
if let Some(start) = device_name.rfind('[') { }
if let Some(_) = device_name.rfind(']') {
device_name = device_name[..start].trim().to_string(); fn parse_pci_device(line: &str) -> Option<(String, String, String, String)> {
} let re = Regex::new(
} r#"^(?P<pci_addr>\S+)\s+"[^\[]*\[(?P<class_id>[0-9a-f]{4})\].*?"\s+"[^\[]*\[(?P<vendor_id>[0-9a-f]{4})\].*?"\s+"(?P<device_name>[^"]+?)""#,
).unwrap();
// Extract vendor ID from vendor name (e.g., "Intel Corporation [8086]" or "Advanced Micro Devices, Inc. [AMD/ATI] [1002]")
let vendor_id = vendor_name let caps = re.captures(line)?;
.split_whitespace()
.last() // Clean device name by removing only the trailing device ID
.unwrap_or_default() let device_name = caps.name("device_name")?.as_str().trim();
.trim_matches(|c: char| !c.is_ascii_hexdigit()) let clean_re = Regex::new(r"\s+\[[0-9a-f]{4}\]$").unwrap();
.to_string(); let cleaned_name = clean_re.replace(device_name, "").trim().to_string();
return Some((pci_addr.clone(), vendor_id, device_name)); Some((
} caps.name("class_id")?.as_str().to_lowercase(),
caps.name("vendor_id")?.as_str().to_lowercase(),
None 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)> { 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 target_dir = format!("0000:{}", pci_addr);
let base_dir = Path::new("/sys/bus/pci/devices"); let entries = fs::read_dir("/sys/bus/pci/devices").ok()?;
// Define the target PCI address with "0000:" prefix for entry in entries.flatten() {
let target_addr = format!("0000:{}", pci_addr); if !entry.path().to_string_lossy().contains(&target_dir) {
continue;
}
// Search for a matching directory that contains the target PCI address let mut card = String::new();
for entry in fs::read_dir(base_dir).ok()?.flatten() { let mut render = String::new();
let path = entry.path(); let drm_path = entry.path().join("drm");
// Check if the path matches the target PCI address for drm_entry in fs::read_dir(drm_path).ok()?.flatten() {
if path.to_string_lossy().contains(&target_addr) { let name = drm_entry.file_name().to_string_lossy().into_owned();
// Look for any files under the 'drm' subdirectory, like 'card0' or 'renderD128'
let drm_path = path.join("drm"); if name.starts_with("card") {
if drm_path.exists() { card = format!("/dev/dri/{}", name);
let mut card_path = String::new(); } else if name.starts_with("renderD") {
let mut render_path = String::new(); render = format!("/dev/dri/{}", name);
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 !card.is_empty() && !render.is_empty() {
if name.starts_with("card") { break;
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));
} }
} }
}
None if !card.is_empty() {
} return Some((card, render));
/// 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<GPUInfo>` - A vector containing GPUInfo structs if found.
pub fn get_gpus_by_vendor(gpus: &Vec<GPUInfo>, vendor: &str) -> Vec<GPUInfo> {
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<GPUInfo>` - A vector containing GPUInfo structs if found.
pub fn get_gpus_by_device_name(gpus: &Vec<GPUInfo>, device_name: &str) -> Vec<GPUInfo> {
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<GPUInfo>` - A reference to a GPUInfo struct if found.
pub fn get_gpu_by_card_path(gpus: &Vec<GPUInfo>, card_path: &str) -> Option<GPUInfo> {
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());
} }
} }
None None
} }
// Helper functions remain similar with improved readability:
pub fn get_gpus_by_vendor(gpus: &[GPUInfo], vendor: &str) -> Vec<GPUInfo> {
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<GPUInfo> {
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<GPUInfo> {
gpus.iter()
.find(|gpu| {
gpu.card_path.eq_ignore_ascii_case(path) || gpu.render_path.eq_ignore_ascii_case(path)
})
.cloned()
}

View File

@@ -78,11 +78,12 @@ fn handle_encoder_video(args: &args::Args) -> Option<enc_helper::VideoEncoderInf
} }
for encoder in &video_encoders { for encoder in &video_encoders {
println!( println!(
"> [Video Encoder] Name: '{}', Codec: '{}', API: '{}', Type: '{}'", "> [Video Encoder] Name: '{}', Codec: '{}', API: '{}', Type: '{}', Device: '{}'",
encoder.name, encoder.name,
encoder.codec.to_str(), encoder.codec.to_str(),
encoder.encoder_api.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 // Pick most suitable video encoder based on given arguments
@@ -152,7 +153,7 @@ fn handle_encoder_audio(args: &args::Args) -> String {
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
// Parse command line arguments // Parse command line arguments
let args = args::Args::new(); let mut args = args::Args::new();
if args.app.verbose { if args.app.verbose {
args.debug_print(); args.debug_print();
} }
@@ -186,6 +187,12 @@ async fn main() -> Result<(), Box<dyn Error>> {
} }
let gpu = gpu.unwrap(); 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 // Handle video encoder selection
let video_encoder_info = handle_encoder_video(&args); let video_encoder_info = handle_encoder_video(&args);
if video_encoder_info.is_none() { if video_encoder_info.is_none() {
@@ -261,9 +268,12 @@ async fn main() -> Result<(), Box<dyn Error>> {
// GL Upload Element // GL Upload Element
let glupload = gst::ElementFactory::make("glupload").build()?; let glupload = gst::ElementFactory::make("glupload").build()?;
// GL color convert element
let glcolorconvert = gst::ElementFactory::make("glcolorconvert").build()?;
// GL upload caps filter // GL upload caps filter
let gl_caps_filter = gst::ElementFactory::make("capsfilter").build()?; 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); gl_caps_filter.set_property("caps", &gl_caps);
// Video Converter Element // Video Converter Element
@@ -271,7 +281,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
// Video Encoder Element // Video Encoder Element
let video_encoder = gst::ElementFactory::make(video_encoder_info.name.as_str()).build()?; 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 */ /* Output */
// WebRTC sink Element // WebRTC sink Element
@@ -294,9 +304,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
&audio_source, &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 { if args.app.dma_buf {
pipeline.add_many(&[&glupload, &gl_caps_filter])?; pipeline.add_many(&[&glupload, &glcolorconvert, &gl_caps_filter])?;
} }
// Link main audio branch // Link main audio branch
@@ -316,8 +326,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
&video_source, &video_source,
&caps_filter, &caps_filter,
&glupload, &glupload,
&glcolorconvert,
&gl_caps_filter, &gl_caps_filter,
&video_converter,
&video_encoder, &video_encoder,
webrtcsink.upcast_ref(), webrtcsink.upcast_ref(),
])?; ])?;