cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

fix/website-download-wiring
goq / crates / sigil-host / src / source / gstreamer_properties.rs
gstreamer_properties.rs 12.9K
1use std::path::Path;23use anyhow::Result;45use crate::config::VaapiRateControl;67#[cfg(target_os = "linux")]8mod linux {9 use std::ffi::{CStr, CString, c_char, c_int, c_uint, c_void};10 use std::mem::MaybeUninit;11 use std::ptr;1213 use anyhow::{Context, Result, ensure};14 use libloading::Library;1516 use super::{Path, VaapiRateControl};1718 const G_PARAM_READABLE: u32 = 1 << 0;19 const G_PARAM_WRITABLE: u32 = 1 << 1;20 const G_TYPE_ENUM: usize = 12 << 2;2122 #[repr(C)]23 struct GTypeInstance {24 g_class: *mut c_void,25 }2627 #[repr(C)]28 struct GParamSpec {29 g_type_instance: GTypeInstance,30 name: *const c_char,31 flags: u32,32 value_type: usize,33 owner_type: usize,34 }3536 #[repr(C)]37 union GValueData {38 v_int: c_int,39 v_uint: c_uint,40 v_long: isize,41 v_ulong: usize,42 v_int64: i64,43 v_uint64: u64,44 v_float: f32,45 v_double: f64,46 v_pointer: *mut c_void,47 }4849 #[repr(C)]50 struct GValue {51 g_type: usize,52 data: [GValueData; 2],53 }5455 #[repr(C)]56 struct GTypeClass {57 g_type: usize,58 }5960 #[repr(C)]61 struct GEnumValue {62 value: c_int,63 value_name: *const c_char,64 value_nick: *const c_char,65 }6667 #[repr(C)]68 struct GEnumClass {69 g_type_class: GTypeClass,70 minimum: c_int,71 maximum: c_int,72 n_values: c_uint,73 values: *mut GEnumValue,74 }7576 #[repr(C)]77 struct GError {78 domain: c_uint,79 code: c_int,80 message: *mut c_char,81 }8283 type GstInitCheck =84 unsafe extern "C" fn(*mut c_int, *mut *mut *mut c_char, *mut *mut GError) -> c_int;85 type GstElementFactoryMake = unsafe extern "C" fn(*const c_char, *const c_char) -> *mut c_void;86 type GstObjectUnref = unsafe extern "C" fn(*mut c_void);87 type GObjectClassFindProperty =88 unsafe extern "C" fn(*mut c_void, *const c_char) -> *mut GParamSpec;89 type GObjectGetProperty = unsafe extern "C" fn(*mut c_void, *const c_char, *mut GValue);90 type GValueInit = unsafe extern "C" fn(*mut GValue, usize) -> *mut GValue;91 type GValueGetString = unsafe extern "C" fn(*const GValue) -> *const c_char;92 type GValueUnset = unsafe extern "C" fn(*mut GValue);93 type GTypeFundamental = unsafe extern "C" fn(usize) -> usize;94 type GTypeClassRef = unsafe extern "C" fn(usize) -> *mut c_void;95 type GTypeClassUnref = unsafe extern "C" fn(*mut c_void);96 type GErrorFree = unsafe extern "C" fn(*mut GError);9798 struct GstreamerApi {99 _gstreamer: Library,100 _gobject: Library,101 _glib: Library,102 gst_init_check: GstInitCheck,103 gst_element_factory_make: GstElementFactoryMake,104 gst_object_unref: GstObjectUnref,105 g_object_class_find_property: GObjectClassFindProperty,106 g_object_get_property: GObjectGetProperty,107 g_value_init: GValueInit,108 g_value_get_string: GValueGetString,109 g_value_unset: GValueUnset,110 g_type_fundamental: GTypeFundamental,111 g_type_class_ref: GTypeClassRef,112 g_type_class_unref: GTypeClassUnref,113 g_error_free: GErrorFree,114 }115116 impl GstreamerApi {117 unsafe fn load() -> Result<Self> {118 let gstreamer = unsafe { Library::new("libgstreamer-1.0.so.0") }119 .context("loading libgstreamer-1.0.so.0")?;120 let gobject = unsafe { Library::new("libgobject-2.0.so.0") }121 .context("loading libgobject-2.0.so.0")?;122 let glib =123 unsafe { Library::new("libglib-2.0.so.0") }.context("loading libglib-2.0.so.0")?;124125 // SAFETY: every symbol uses the public C ABI declared by the matching126 // versioned runtime SONAME. The libraries remain owned by this value127 // for at least as long as all function symbols below.128 unsafe {129 Ok(Self {130 gst_init_check: *gstreamer.get(b"gst_init_check\0")?,131 gst_element_factory_make: *gstreamer.get(b"gst_element_factory_make\0")?,132 gst_object_unref: *gstreamer.get(b"gst_object_unref\0")?,133 g_object_class_find_property: *gobject134 .get(b"g_object_class_find_property\0")?,135 g_object_get_property: *gobject.get(b"g_object_get_property\0")?,136 g_value_init: *gobject.get(b"g_value_init\0")?,137 g_value_get_string: *gobject.get(b"g_value_get_string\0")?,138 g_value_unset: *gobject.get(b"g_value_unset\0")?,139 g_type_fundamental: *gobject.get(b"g_type_fundamental\0")?,140 g_type_class_ref: *gobject.get(b"g_type_class_ref\0")?,141 g_type_class_unref: *gobject.get(b"g_type_class_unref\0")?,142 g_error_free: *glib.get(b"g_error_free\0")?,143 _gstreamer: gstreamer,144 _gobject: gobject,145 _glib: glib,146 })147 }148 }149150 unsafe fn property(&self, object: *mut c_void, name: &CStr) -> Result<&GParamSpec> {151 // SAFETY: GstElement begins with a GTypeInstance, whose class pointer152 // is valid while the element is alive. GObject returns a class-owned153 // GParamSpec that remains valid for the lifetime of that class.154 let class = unsafe { (*(object.cast::<GTypeInstance>())).g_class };155 let property = unsafe { (self.g_object_class_find_property)(class, name.as_ptr()) };156 ensure!(157 !property.is_null(),158 "encoder has no {} property",159 name.to_string_lossy()160 );161 Ok(unsafe { &*property })162 }163164 unsafe fn string_property(165 &self,166 object: *mut c_void,167 name: &CStr,168 property: &GParamSpec,169 ) -> Result<String> {170 let mut value = MaybeUninit::<GValue>::zeroed();171 let value = value.as_mut_ptr();172 unsafe {173 (self.g_value_init)(value, property.value_type);174 (self.g_object_get_property)(object, name.as_ptr(), value);175 }176 let observed = unsafe { (self.g_value_get_string)(value) };177 let result = ensure_non_null_string(observed, name);178 unsafe { (self.g_value_unset)(value) };179 result180 }181182 unsafe fn enum_nicks(&self, property: &GParamSpec) -> Result<Vec<String>> {183 ensure!(184 unsafe { (self.g_type_fundamental)(property.value_type) } == G_TYPE_ENUM,185 "encoder rate-control property is not an enum"186 );187 let class = unsafe { (self.g_type_class_ref)(property.value_type) };188 ensure!(189 !class.is_null(),190 "loading encoder rate-control enum metadata"191 );192 let class = class.cast::<GEnumClass>();193 let count = unsafe { (*class).n_values as usize };194 let values = unsafe { (*class).values };195 ensure!(196 (1..=1024).contains(&count) && !values.is_null(),197 "encoder rate-control enum metadata is empty or unreasonably large"198 );199 let values = unsafe { std::slice::from_raw_parts(values, count) };200 let mut nicks = Vec::with_capacity(values.len());201 for value in values {202 if !value.value_nick.is_null() {203 nicks.push(204 unsafe { CStr::from_ptr(value.value_nick) }205 .to_str()206 .context("encoder rate-control nickname is not UTF-8")?207 .to_owned(),208 );209 }210 }211 unsafe { (self.g_type_class_unref)(class.cast()) };212 Ok(nicks)213 }214 }215216 fn ensure_non_null_string(value: *const c_char, name: &CStr) -> Result<String> {217 ensure!(218 !value.is_null(),219 "encoder {} property is null",220 name.to_string_lossy()221 );222 Ok(unsafe { CStr::from_ptr(value) }223 .to_str()224 .with_context(|| format!("encoder {} property is not UTF-8", name.to_string_lossy()))?225 .to_owned())226 }227228 pub(super) fn probe(229 factory: &str,230 expected_device_path: &Path,231 rate_controls: &[VaapiRateControl],232 ) -> Result<()> {233 let factory = CString::new(factory).context("VA encoder factory contains NUL")?;234 let expected_device_path = expected_device_path235 .to_str()236 .context("VAAPI render node is not UTF-8")?;237 let device_path = c"device-path";238 let rate_control = c"rate-control";239240 // SAFETY: all interaction is through the versioned public GStreamer and241 // GObject C ABIs. Host preflight invokes this probe in a bounded child,242 // so plugin faults cannot unwind through the daemon.243 unsafe {244 let api = GstreamerApi::load()?;245 let mut error = ptr::null_mut();246 if (api.gst_init_check)(ptr::null_mut(), ptr::null_mut(), &mut error) == 0 {247 let message = if error.is_null() || (*error).message.is_null() {248 "unknown GStreamer initialization error".to_owned()249 } else {250 CStr::from_ptr((*error).message)251 .to_string_lossy()252 .into_owned()253 };254 if !error.is_null() {255 (api.g_error_free)(error);256 }257 anyhow::bail!("initializing GStreamer: {message}");258 }259260 let element = (api.gst_element_factory_make)(factory.as_ptr(), ptr::null());261 ensure!(262 !element.is_null(),263 "creating configured VA encoder factory {:?}",264 factory.to_string_lossy()265 );266267 let result = (|| {268 let device_property = api.property(element, device_path)?;269 ensure!(270 device_property.flags & G_PARAM_READABLE != 0,271 "encoder device-path property is not readable"272 );273 let observed_device_path =274 api.string_property(element, device_path, device_property)?;275 ensure!(276 observed_device_path == expected_device_path,277 "configured VA encoder factory {:?} uses device-path {observed_device_path:?}, expected {expected_device_path:?}; on multi-GPU hosts select the per-device GstVA factory reported for that render node",278 factory.to_string_lossy()279 );280281 for property_name in [282 c"aud",283 c"b-frames",284 c"key-int-max",285 rate_control,286 c"ref-frames",287 c"target-usage",288 ] {289 let property = api.property(element, property_name)?;290 ensure!(291 property.flags & G_PARAM_WRITABLE != 0,292 "encoder {} property is not writable",293 property_name.to_string_lossy()294 );295 }296297 let rate_control_property = api.property(element, rate_control)?;298 let rate_control_nicks = api.enum_nicks(rate_control_property)?;299 for required in rate_controls {300 let (nickname, mode_properties): (&str, &[&CStr]) = match required {301 VaapiRateControl::Cbr => ("cbr", &[c"bitrate"]),302 VaapiRateControl::Cqp => ("cqp", &[c"qpi", c"qpp"]),303 };304 ensure!(305 rate_control_nicks.iter().any(|value| value == nickname),306 "configured VA encoder factory {:?} does not support {nickname} rate control",307 factory.to_string_lossy()308 );309 for property_name in mode_properties {310 let property = api.property(element, property_name)?;311 ensure!(312 property.flags & G_PARAM_WRITABLE != 0,313 "encoder {} property is not writable",314 property_name.to_string_lossy()315 );316 }317 }318 Ok(())319 })();320 (api.gst_object_unref)(element);321 result322 }323 }324}325326pub(crate) fn probe_encoder_properties(327 factory: &str,328 expected_device_path: &Path,329 rate_controls: &[VaapiRateControl],330) -> Result<()> {331 #[cfg(target_os = "linux")]332 {333 linux::probe(factory, expected_device_path, rate_controls)334 }335 #[cfg(not(target_os = "linux"))]336 {337 let _ = (factory, expected_device_path, rate_controls);338 anyhow::bail!("GstVA encoder property probing requires Linux")339 }340}