From 20be977d78fee3cb5921bb4587ad3ef21e34d5ed Mon Sep 17 00:00:00 2001 From: Marcus Lian Hanestad Date: Sun, 27 Oct 2024 12:11:17 +0100 Subject: [PATCH 1/9] x11: init --- src/capturer/engine/linux/mod.rs | 380 ++-------------------- src/capturer/engine/linux/portal.rs | 422 ------------------------ src/capturer/engine/linux/pw/mod.rs | 372 ++++++++++++++++++++++ src/capturer/engine/linux/pw/portal.rs | 425 +++++++++++++++++++++++++ src/capturer/engine/linux/x11/mod.rs | 24 ++ src/capturer/engine/mod.rs | 4 +- 6 files changed, 849 insertions(+), 778 deletions(-) create mode 100644 src/capturer/engine/linux/pw/mod.rs create mode 100644 src/capturer/engine/linux/pw/portal.rs create mode 100644 src/capturer/engine/linux/x11/mod.rs diff --git a/src/capturer/engine/linux/mod.rs b/src/capturer/engine/linux/mod.rs index 17ab2e4..9c9c061 100644 --- a/src/capturer/engine/linux/mod.rs +++ b/src/capturer/engine/linux/mod.rs @@ -1,373 +1,45 @@ -use std::{ - mem::size_of, - sync::{ - atomic::{AtomicBool, AtomicU8}, - mpsc::{self, sync_channel, SyncSender}, - }, - thread::JoinHandle, - time::Duration, -}; -use pipewire as pw; -use pw::{ - context::Context, - main_loop::MainLoop, - properties::properties, - spa::{ - self, - param::{ - format::{FormatProperties, MediaSubtype, MediaType}, - video::VideoFormat, - ParamType, - }, - pod::{Pod, Property}, - sys::{ - spa_buffer, spa_meta_header, SPA_META_Header, SPA_PARAM_META_size, SPA_PARAM_META_type, - }, - utils::{Direction, SpaTypes}, - }, - stream::{StreamRef, StreamState}, -}; +use std::{env, sync::mpsc}; + +use pw::PwCapturer; +use x11::X11Capturer; use crate::{ capturer::Options, - frame::{BGRxFrame, Frame, RGBFrame, RGBxFrame, XBGRFrame}, + frame::Frame, }; -use self::{error::LinCapError, portal::ScreenCastPortal}; - mod error; -mod portal; - -static CAPTURER_STATE: AtomicU8 = AtomicU8::new(0); -static STREAM_STATE_CHANGED_TO_ERROR: AtomicBool = AtomicBool::new(false); - -#[derive(Clone)] -struct ListenerUserData { - pub tx: mpsc::Sender, - pub format: spa::param::video::VideoInfoRaw, -} - -fn param_changed_callback( - _stream: &StreamRef, - user_data: &mut ListenerUserData, - id: u32, - param: Option<&Pod>, -) { - let Some(param) = param else { - return; - }; - if id != pw::spa::param::ParamType::Format.as_raw() { - return; - } - let (media_type, media_subtype) = match pw::spa::param::format_utils::parse_format(param) { - Ok(v) => v, - Err(_) => return, - }; - - if media_type != MediaType::Video || media_subtype != MediaSubtype::Raw { - return; - } - - user_data - .format - .parse(param) - // TODO: Tell library user of the error - .expect("Failed to parse format parameter"); -} - -fn state_changed_callback( - _stream: &StreamRef, - _user_data: &mut ListenerUserData, - _old: StreamState, - new: StreamState, -) { - match new { - StreamState::Error(e) => { - eprintln!("pipewire: State changed to error({e})"); - STREAM_STATE_CHANGED_TO_ERROR.store(true, std::sync::atomic::Ordering::Relaxed); - } - _ => {} - } -} - -unsafe fn get_timestamp(buffer: *mut spa_buffer) -> i64 { - let n_metas = (*buffer).n_metas; - if n_metas > 0 { - let mut meta_ptr = (*buffer).metas; - let metas_end = (*buffer).metas.wrapping_add(n_metas as usize); - while meta_ptr != metas_end { - if (*meta_ptr).type_ == SPA_META_Header { - let meta_header: &mut spa_meta_header = - &mut *((*meta_ptr).data as *mut spa_meta_header); - return meta_header.pts; - } - meta_ptr = meta_ptr.wrapping_add(1); - } - 0 - } else { - 0 - } -} - -fn process_callback(stream: &StreamRef, user_data: &mut ListenerUserData) { - let buffer = unsafe { stream.dequeue_raw_buffer() }; - if !buffer.is_null() { - 'outside: { - let buffer = unsafe { (*buffer).buffer }; - if buffer.is_null() { - break 'outside; - } - let timestamp = unsafe { get_timestamp(buffer) }; - - let n_datas = unsafe { (*buffer).n_datas }; - if n_datas < 1 { - return; - } - let frame_size = user_data.format.size(); - let frame_data: Vec = unsafe { - std::slice::from_raw_parts( - (*(*buffer).datas).data as *mut u8, - (*(*buffer).datas).maxsize as usize, - ) - .to_vec() - }; - - if let Err(e) = match user_data.format.format() { - VideoFormat::RGBx => user_data.tx.send(Frame::RGBx(RGBxFrame { - display_time: timestamp as u64, - width: frame_size.width as i32, - height: frame_size.height as i32, - data: frame_data, - })), - VideoFormat::RGB => user_data.tx.send(Frame::RGB(RGBFrame { - display_time: timestamp as u64, - width: frame_size.width as i32, - height: frame_size.height as i32, - data: frame_data, - })), - VideoFormat::xBGR => user_data.tx.send(Frame::XBGR(XBGRFrame { - display_time: timestamp as u64, - width: frame_size.width as i32, - height: frame_size.height as i32, - data: frame_data, - })), - VideoFormat::BGRx => user_data.tx.send(Frame::BGRx(BGRxFrame { - display_time: timestamp as u64, - width: frame_size.width as i32, - height: frame_size.height as i32, - data: frame_data, - })), - _ => panic!("Unsupported frame format received"), - } { - eprintln!("{e}"); - } - } - } else { - eprintln!("Out of buffers"); - } - - unsafe { stream.queue_raw_buffer(buffer) }; -} - -// TODO: Format negotiation -fn pipewire_capturer( - options: Options, - tx: mpsc::Sender, - ready_sender: &SyncSender, - stream_id: u32, -) -> Result<(), LinCapError> { - pw::init(); - - let mainloop = MainLoop::new(None)?; - let context = Context::new(&mainloop)?; - let core = context.connect(None)?; - - let user_data = ListenerUserData { - tx, - format: Default::default(), - }; - - let stream = pw::stream::Stream::new( - &core, - "scap", - properties! { - *pw::keys::MEDIA_TYPE => "Video", - *pw::keys::MEDIA_CATEGORY => "Capture", - *pw::keys::MEDIA_ROLE => "Screen", - }, - )?; - - let _listener = stream - .add_local_listener_with_user_data(user_data.clone()) - .state_changed(state_changed_callback) - .param_changed(param_changed_callback) - .process(process_callback) - .register()?; - - let obj = pw::spa::pod::object!( - pw::spa::utils::SpaTypes::ObjectParamFormat, - pw::spa::param::ParamType::EnumFormat, - pw::spa::pod::property!(FormatProperties::MediaType, Id, MediaType::Video), - pw::spa::pod::property!(FormatProperties::MediaSubtype, Id, MediaSubtype::Raw), - pw::spa::pod::property!( - FormatProperties::VideoFormat, - Choice, - Enum, - Id, - pw::spa::param::video::VideoFormat::RGB, - pw::spa::param::video::VideoFormat::RGBA, - pw::spa::param::video::VideoFormat::RGBx, - pw::spa::param::video::VideoFormat::BGRx, - ), - pw::spa::pod::property!( - FormatProperties::VideoSize, - Choice, - Range, - Rectangle, - pw::spa::utils::Rectangle { - // Default - width: 128, - height: 128, - }, - pw::spa::utils::Rectangle { - // Min - width: 1, - height: 1, - }, - pw::spa::utils::Rectangle { - // Max - width: 4096, - height: 4096, - } - ), - pw::spa::pod::property!( - FormatProperties::VideoFramerate, - Choice, - Range, - Fraction, - pw::spa::utils::Fraction { - num: options.fps, - denom: 1 - }, - pw::spa::utils::Fraction { num: 0, denom: 1 }, - pw::spa::utils::Fraction { - num: 1000, - denom: 1 - } - ), - ); - - let metas_obj = pw::spa::pod::object!( - SpaTypes::ObjectParamMeta, - ParamType::Meta, - Property::new( - SPA_PARAM_META_type, - pw::spa::pod::Value::Id(pw::spa::utils::Id(SPA_META_Header)) - ), - Property::new( - SPA_PARAM_META_size, - pw::spa::pod::Value::Int(size_of::() as i32) - ), - ); - - let values: Vec = pw::spa::pod::serialize::PodSerializer::serialize( - std::io::Cursor::new(Vec::new()), - &pw::spa::pod::Value::Object(obj), - )? - .0 - .into_inner(); - let metas_values: Vec = pw::spa::pod::serialize::PodSerializer::serialize( - std::io::Cursor::new(Vec::new()), - &pw::spa::pod::Value::Object(metas_obj), - )? - .0 - .into_inner(); - - let mut params = [ - pw::spa::pod::Pod::from_bytes(&values).unwrap(), - pw::spa::pod::Pod::from_bytes(&metas_values).unwrap(), - ]; - - stream.connect( - Direction::Input, - Some(stream_id), - pw::stream::StreamFlags::AUTOCONNECT | pw::stream::StreamFlags::MAP_BUFFERS, - &mut params, - )?; - - ready_sender.send(true)?; - - while CAPTURER_STATE.load(std::sync::atomic::Ordering::Relaxed) == 0 { - std::thread::sleep(Duration::from_millis(10)); - } - let pw_loop = mainloop.loop_(); +mod pw; +mod x11; - // User has called Capturer::start() and we start the main loop - while CAPTURER_STATE.load(std::sync::atomic::Ordering::Relaxed) == 1 - && /* If the stream state got changed to `Error`, we exit. TODO: tell user that we exited */ - !STREAM_STATE_CHANGED_TO_ERROR.load(std::sync::atomic::Ordering::Relaxed) - { - pw_loop.iterate(Duration::from_millis(100)); - } - - Ok(()) +pub trait LinuxCapturerImpl { + fn start_capture(&mut self); + fn stop_capture(&mut self); } pub struct LinuxCapturer { - capturer_join_handle: Option>>, - // The pipewire stream is deleted when the connection is dropped. - // That's why we keep it alive - _connection: dbus::blocking::Connection, + pub imp: Box, } -impl LinuxCapturer { - // TODO: Error handling - pub fn new(options: &Options, tx: mpsc::Sender) -> Self { - let connection = - dbus::blocking::Connection::new_session().expect("Failed to create dbus connection"); - let stream_id = ScreenCastPortal::new(&connection) - .show_cursor(options.show_cursor) - .expect("Unsupported cursor mode") - .create_stream() - .expect("Failed to get screencast stream") - .pw_node_id(); - - // TODO: Fix this hack - let options = options.clone(); - let (ready_sender, ready_recv) = sync_channel(1); - let capturer_join_handle = std::thread::spawn(move || { - let res = pipewire_capturer(options, tx, &ready_sender, stream_id); - if res.is_err() { - ready_sender.send(false)?; - } - res - }); - - if !ready_recv.recv().expect("Failed to receive") { - panic!("Failed to setup capturer"); - } - - Self { - capturer_join_handle: Some(capturer_join_handle), - _connection: connection, - } - } - - pub fn start_capture(&self) { - CAPTURER_STATE.store(1, std::sync::atomic::Ordering::Relaxed); - } +type Type = mpsc::Sender; - pub fn stop_capture(&mut self) { - CAPTURER_STATE.store(2, std::sync::atomic::Ordering::Relaxed); - if let Some(handle) = self.capturer_join_handle.take() { - if let Err(e) = handle.join().expect("Failed to join capturer thread") { - eprintln!("Error occured capturing: {e}"); - } +impl LinuxCapturer { + pub fn new(options: &Options, tx: Type) -> Self { + if env::var("WAYLAND_DISPLAY").is_ok() { + println!("[DEBUG] On wayland"); + return Self { + imp: Box::new(PwCapturer::new(options, tx)), + }; + } else if env::var("DISPLAY").is_ok() { + println!("[DEBUG] On X11"); + return Self { + imp: Box::new(X11Capturer::new(options, tx)), + }; + } else { + panic!("Unsupported platform. Could not detect Wayland or X11 displays") } - CAPTURER_STATE.store(0, std::sync::atomic::Ordering::Relaxed); - STREAM_STATE_CHANGED_TO_ERROR.store(false, std::sync::atomic::Ordering::Relaxed); } } diff --git a/src/capturer/engine/linux/portal.rs b/src/capturer/engine/linux/portal.rs index 013d4dd..e69de29 100644 --- a/src/capturer/engine/linux/portal.rs +++ b/src/capturer/engine/linux/portal.rs @@ -1,422 +0,0 @@ -use std::{ - sync::{atomic::AtomicBool, Arc, Mutex}, - time::Duration, -}; - -use dbus::{ - arg::{self, PropMap, RefArg, Variant}, - blocking::{Connection, Proxy}, - message::MatchRule, - strings::{BusName, Interface}, -}; - -use super::error::LinCapError; - -// This code was autogenerated with `dbus-codegen-rust -d org.freedesktop.portal.Desktop -p /org/freedesktop/portal/desktop -f org.freedesktop.portal.ScreenCast`, see https://github.com/diwic/dbus-rs -// { -use dbus::blocking; - -trait OrgFreedesktopPortalScreenCast { - fn create_session(&self, options: arg::PropMap) -> Result, dbus::Error>; - fn select_sources( - &self, - session_handle: dbus::Path, - options: arg::PropMap, - ) -> Result, dbus::Error>; - fn start( - &self, - session_handle: dbus::Path, - parent_window: &str, - options: arg::PropMap, - ) -> Result, dbus::Error>; - fn open_pipe_wire_remote( - &self, - session_handle: dbus::Path, - options: arg::PropMap, - ) -> Result; - fn available_source_types(&self) -> Result; - fn available_cursor_modes(&self) -> Result; - fn version(&self) -> Result; -} - -impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref> - OrgFreedesktopPortalScreenCast for blocking::Proxy<'a, C> -{ - fn create_session(&self, options: arg::PropMap) -> Result, dbus::Error> { - self.method_call( - "org.freedesktop.portal.ScreenCast", - "CreateSession", - (options,), - ) - .and_then(|r: (dbus::Path<'static>,)| Ok(r.0)) - } - - fn select_sources( - &self, - session_handle: dbus::Path, - options: arg::PropMap, - ) -> Result, dbus::Error> { - self.method_call( - "org.freedesktop.portal.ScreenCast", - "SelectSources", - (session_handle, options), - ) - .and_then(|r: (dbus::Path<'static>,)| Ok(r.0)) - } - - fn start( - &self, - session_handle: dbus::Path, - parent_window: &str, - options: arg::PropMap, - ) -> Result, dbus::Error> { - self.method_call( - "org.freedesktop.portal.ScreenCast", - "Start", - (session_handle, parent_window, options), - ) - .and_then(|r: (dbus::Path<'static>,)| Ok(r.0)) - } - - fn open_pipe_wire_remote( - &self, - session_handle: dbus::Path, - options: arg::PropMap, - ) -> Result { - self.method_call( - "org.freedesktop.portal.ScreenCast", - "OpenPipeWireRemote", - (session_handle, options), - ) - .and_then(|r: (arg::OwnedFd,)| Ok(r.0)) - } - - fn available_source_types(&self) -> Result { - ::get( - &self, - "org.freedesktop.portal.ScreenCast", - "AvailableSourceTypes", - ) - } - - fn available_cursor_modes(&self) -> Result { - ::get( - &self, - "org.freedesktop.portal.ScreenCast", - "AvailableCursorModes", - ) - } - - fn version(&self) -> Result { - ::get( - &self, - "org.freedesktop.portal.ScreenCast", - "version", - ) - } -} -// } - -// This code was autogenerated with `dbus-codegen-rust --file org.freedesktop.portal.Request.xml`, see https://github.com/diwic/dbus-rs -// { -trait OrgFreedesktopPortalRequest { - fn close(&self) -> Result<(), dbus::Error>; -} - -#[derive(Debug)] -pub struct OrgFreedesktopPortalRequestResponse { - pub response: u32, - pub results: arg::PropMap, -} - -impl arg::AppendAll for OrgFreedesktopPortalRequestResponse { - fn append(&self, i: &mut arg::IterAppend) { - arg::RefArg::append(&self.response, i); - arg::RefArg::append(&self.results, i); - } -} - -impl arg::ReadAll for OrgFreedesktopPortalRequestResponse { - fn read(i: &mut arg::Iter) -> Result { - Ok(OrgFreedesktopPortalRequestResponse { - response: i.read()?, - results: i.read()?, - }) - } -} - -impl dbus::message::SignalArgs for OrgFreedesktopPortalRequestResponse { - const NAME: &'static str = "Response"; - const INTERFACE: &'static str = "org.freedesktop.portal.Request"; -} - -impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref> OrgFreedesktopPortalRequest - for blocking::Proxy<'a, C> -{ - fn close(&self) -> Result<(), dbus::Error> { - self.method_call("org.freedesktop.portal.Request", "Close", ()) - } -} -// } - -type Response = Option; - -#[derive(Debug)] -#[allow(dead_code)] -pub struct StreamVardict { - id: Option, - position: Option<(i32, i32)>, - size: Option<(i32, i32)>, - source_type: Option, - mapping_id: Option, -} - -#[derive(Debug)] -pub struct Stream(u32, StreamVardict); - -impl Stream { - pub fn pw_node_id(&self) -> u32 { - self.0 - } - - pub fn from_dbus(stream: &Variant>) -> Option { - let mut stream = stream.as_iter()?.next()?.as_iter()?; - let pipewire_node_id = stream.next()?.as_iter()?.next()?.as_u64()?; - - // TODO: Get the rest of the properties - - Some(Self( - pipewire_node_id as u32, - StreamVardict { - id: None, - position: None, - size: None, - source_type: None, - mapping_id: None, - }, - )) - } -} - -macro_rules! match_response { - ( $code:expr ) => { - match $code { - 0 => {} - 1 => { - return Err(LinCapError::new(String::from( - "User cancelled the interaction", - ))); - } - 2 => { - return Err(LinCapError::new(String::from( - "The user interaction was ended in some other way", - ))); - } - _ => unreachable!(), - } - }; -} - -pub struct ScreenCastPortal<'a> { - proxy: Proxy<'a, &'a Connection>, - token: String, - cursor_mode: u32, -} - -impl<'a> ScreenCastPortal<'a> { - pub fn new(connection: &'a Connection) -> Self { - let proxy = connection.with_proxy( - "org.freedesktop.portal.Desktop", - "/org/freedesktop/portal/desktop", - Duration::from_secs(4), - ); - - let token = format!("scap_{}", rand::random::()); - - Self { - proxy, - token, - cursor_mode: 1, - } - } - - fn create_session_args(&self) -> arg::PropMap { - let mut map = arg::PropMap::new(); - map.insert( - String::from("handle_token"), - Variant(Box::new(self.token.clone())), - ); - map.insert( - String::from("session_handle_token"), - Variant(Box::new(self.token.clone())), - ); - map - } - - fn select_sources_args(&self) -> Result { - let mut map = arg::PropMap::new(); - map.insert( - String::from("handle_token"), - Variant(Box::new(self.token.clone())), - ); - map.insert( - String::from("types"), - Variant(Box::new(self.proxy.available_source_types()?)), - ); - map.insert(String::from("multiple"), Variant(Box::new(false))); - map.insert( - String::from("cursor_mode"), - Variant(Box::new(self.cursor_mode)), - ); - Ok(map) - } - - fn handle_req_response( - connection: &Connection, - path: dbus::Path<'static>, - iterations: usize, - timeout: Duration, - response: Arc>, - ) -> Result<(), dbus::Error> { - let got_response = Arc::new(AtomicBool::new(false)); - let got_response_clone = Arc::clone(&got_response); - - let mut rule = MatchRule::new(); - rule.path = Some(dbus::Path::from(path)); - rule.msg_type = Some(dbus::MessageType::Signal); - rule.sender = Some(BusName::from("org.freedesktop.portal.Desktop")); - rule.interface = Some(Interface::from("org.freedesktop.portal.Request")); - connection.add_match( - rule, - move |res: OrgFreedesktopPortalRequestResponse, _chuh, _msg| { - let mut response = response.lock().expect("Failed to lock response mutex"); - *response = Some(res); - got_response_clone.store(true, std::sync::atomic::Ordering::Relaxed); - false - }, - )?; - - for _ in 0..iterations { - connection.process(timeout)?; - - if got_response.load(std::sync::atomic::Ordering::Relaxed) { - break; - } - } - - Ok(()) - } - - fn create_session(&self) -> Result { - let request_handle = self.proxy.create_session(self.create_session_args())?; - - let response = Arc::new(Mutex::new(None)); - let response_clone = Arc::clone(&response); - Self::handle_req_response( - self.proxy.connection, - request_handle, - 100, - Duration::from_millis(100), - response_clone, - )?; - - if let Some(res) = response.lock()?.take() { - match_response!(res.response); - match res - .results - .get("session_handle") - .map(|h| h.0.as_str().map(String::from)) - { - Some(h) => { - let p = dbus::Path::from(match h { - Some(p) => p, - None => { - return Err(LinCapError::new(String::from( - "Invalid session_handle received", - ))) - } - }); - - return Ok(p); - } - None => return Err(LinCapError::new(String::from("Did not get session handle"))), - } - } - - Err(LinCapError::new(String::from("Did not get response"))) - } - - fn select_sources(&self, session_handle: dbus::Path) -> Result<(), LinCapError> { - let request_handle = self - .proxy - .select_sources(session_handle, self.select_sources_args()?)?; - - let response = Arc::new(Mutex::new(None)); - let response_clone = Arc::clone(&response); - Self::handle_req_response( - self.proxy.connection, - request_handle, - 1200, // Wait 2 min - Duration::from_millis(100), - response_clone, - )?; - - if let Some(res) = response.lock()?.take() { - match_response!(res.response); - return Ok(()); - } - - Err(LinCapError::new(String::from("Did not get response"))) - } - - fn start(&self, session_handle: dbus::Path) -> Result { - let request_handle = self.proxy.start(session_handle, "", PropMap::new())?; - - let response = Arc::new(Mutex::new(None)); - let response_clone = Arc::clone(&response); - Self::handle_req_response( - self.proxy.connection, - request_handle, - 100, // Wait 10 s - Duration::from_millis(100), - response_clone, - )?; - - if let Some(res) = response.lock()?.take() { - match_response!(res.response); - match res.results.get("streams") { - Some(s) => match Stream::from_dbus(s) { - Some(s) => return Ok(s), - None => { - return Err(LinCapError::new(String::from( - "Failed to extract stream properties", - ))) - } - }, - None => return Err(LinCapError::new(String::from("Did not get any streams"))), - } - } - - Err(LinCapError::new(String::from("Did not get response"))) - } - - pub fn create_stream(&self) -> Result { - let session_handle = self.create_session()?; - self.select_sources(session_handle.clone())?; - self.start(session_handle) - } - - pub fn show_cursor(mut self, mode: bool) -> Result { - let available_modes = self.proxy.available_cursor_modes()?; - if mode && available_modes & 2 == 2 { - self.cursor_mode = 2; - return Ok(self); - } - if !mode && available_modes & 1 == 1 { - self.cursor_mode = 1; - return Ok(self); - } - - Err(LinCapError::new("Unsupported cursor mode".to_string())) - } -} diff --git a/src/capturer/engine/linux/pw/mod.rs b/src/capturer/engine/linux/pw/mod.rs new file mode 100644 index 0000000..9415bd7 --- /dev/null +++ b/src/capturer/engine/linux/pw/mod.rs @@ -0,0 +1,372 @@ +use std::{ + mem::size_of, + sync::{ + atomic::{AtomicBool, AtomicU8}, + mpsc::{sync_channel, Sender, SyncSender}, + }, + thread::JoinHandle, + time::Duration, +}; + +use pipewire as pw; +use pw::{ + context::Context, + main_loop::MainLoop, + properties::properties, + spa::{ + self, + param::{ + format::{FormatProperties, MediaSubtype, MediaType}, + video::VideoFormat, + ParamType, + }, + pod::{Pod, Property}, + sys::{ + spa_buffer, spa_meta_header, SPA_META_Header, SPA_PARAM_META_size, SPA_PARAM_META_type, + }, + utils::{Direction, SpaTypes}, + }, + stream::{StreamRef, StreamState}, +}; + +use crate::{capturer::Options, frame::{BGRxFrame, Frame, RGBFrame, RGBxFrame, XBGRFrame}}; + +use self::{portal::ScreenCastPortal}; + +use super::{error::LinCapError, LinuxCapturerImpl}; + +mod portal; + +static CAPTURER_STATE: AtomicU8 = AtomicU8::new(0); +static STREAM_STATE_CHANGED_TO_ERROR: AtomicBool = AtomicBool::new(false); + +#[derive(Clone)] +struct ListenerUserData { + pub tx: Sender, + pub format: spa::param::video::VideoInfoRaw, +} + +fn param_changed_callback( + _stream: &StreamRef, + user_data: &mut ListenerUserData, + id: u32, + param: Option<&Pod>, +) { + let Some(param) = param else { + return; + }; + if id != pw::spa::param::ParamType::Format.as_raw() { + return; + } + let (media_type, media_subtype) = match pw::spa::param::format_utils::parse_format(param) { + Ok(v) => v, + Err(_) => return, + }; + + if media_type != MediaType::Video || media_subtype != MediaSubtype::Raw { + return; + } + + user_data + .format + .parse(param) + // TODO: Tell library user of the error + .expect("Failed to parse format parameter"); +} + +fn state_changed_callback( + _stream: &StreamRef, + _user_data: &mut ListenerUserData, + _old: StreamState, + new: StreamState, +) { + match new { + StreamState::Error(e) => { + eprintln!("pipewire: State changed to error({e})"); + STREAM_STATE_CHANGED_TO_ERROR.store(true, std::sync::atomic::Ordering::Relaxed); + } + _ => {} + } +} + +unsafe fn get_timestamp(buffer: *mut spa_buffer) -> i64 { + let n_metas = (*buffer).n_metas; + if n_metas > 0 { + let mut meta_ptr = (*buffer).metas; + let metas_end = (*buffer).metas.wrapping_add(n_metas as usize); + while meta_ptr != metas_end { + if (*meta_ptr).type_ == SPA_META_Header { + let meta_header: &mut spa_meta_header = + &mut *((*meta_ptr).data as *mut spa_meta_header); + return meta_header.pts; + } + meta_ptr = meta_ptr.wrapping_add(1); + } + 0 + } else { + 0 + } +} + +fn process_callback(stream: &StreamRef, user_data: &mut ListenerUserData) { + let buffer = unsafe { stream.dequeue_raw_buffer() }; + if !buffer.is_null() { + 'outside: { + let buffer = unsafe { (*buffer).buffer }; + if buffer.is_null() { + break 'outside; + } + let timestamp = unsafe { get_timestamp(buffer) }; + + let n_datas = unsafe { (*buffer).n_datas }; + if n_datas < 1 { + return; + } + let frame_size = user_data.format.size(); + let frame_data: Vec = unsafe { + std::slice::from_raw_parts( + (*(*buffer).datas).data as *mut u8, + (*(*buffer).datas).maxsize as usize, + ) + .to_vec() + }; + + if let Err(e) = match user_data.format.format() { + VideoFormat::RGBx => user_data.tx.send(Frame::RGBx(RGBxFrame { + display_time: timestamp as u64, + width: frame_size.width as i32, + height: frame_size.height as i32, + data: frame_data, + })), + VideoFormat::RGB => user_data.tx.send(Frame::RGB(RGBFrame { + display_time: timestamp as u64, + width: frame_size.width as i32, + height: frame_size.height as i32, + data: frame_data, + })), + VideoFormat::xBGR => user_data.tx.send(Frame::XBGR(XBGRFrame { + display_time: timestamp as u64, + width: frame_size.width as i32, + height: frame_size.height as i32, + data: frame_data, + })), + VideoFormat::BGRx => user_data.tx.send(Frame::BGRx(BGRxFrame { + display_time: timestamp as u64, + width: frame_size.width as i32, + height: frame_size.height as i32, + data: frame_data, + })), + _ => panic!("Unsupported frame format received"), + } { + eprintln!("{e}"); + } + } + } else { + eprintln!("Out of buffers"); + } + + unsafe { stream.queue_raw_buffer(buffer) }; +} + +// TODO: Format negotiation +fn pipewire_capturer( + options: Options, + tx: Sender, + ready_sender: &SyncSender, + stream_id: u32, +) -> Result<(), LinCapError> { + pw::init(); + + let mainloop = MainLoop::new(None)?; + let context = Context::new(&mainloop)?; + let core = context.connect(None)?; + + let user_data = ListenerUserData { + tx, + format: Default::default(), + }; + + let stream = pw::stream::Stream::new( + &core, + "scap", + properties! { + *pw::keys::MEDIA_TYPE => "Video", + *pw::keys::MEDIA_CATEGORY => "Capture", + *pw::keys::MEDIA_ROLE => "Screen", + }, + )?; + + let _listener = stream + .add_local_listener_with_user_data(user_data.clone()) + .state_changed(state_changed_callback) + .param_changed(param_changed_callback) + .process(process_callback) + .register()?; + + let obj = pw::spa::pod::object!( + pw::spa::utils::SpaTypes::ObjectParamFormat, + pw::spa::param::ParamType::EnumFormat, + pw::spa::pod::property!(FormatProperties::MediaType, Id, MediaType::Video), + pw::spa::pod::property!(FormatProperties::MediaSubtype, Id, MediaSubtype::Raw), + pw::spa::pod::property!( + FormatProperties::VideoFormat, + Choice, + Enum, + Id, + pw::spa::param::video::VideoFormat::RGB, + pw::spa::param::video::VideoFormat::RGBA, + pw::spa::param::video::VideoFormat::RGBx, + pw::spa::param::video::VideoFormat::BGRx, + ), + pw::spa::pod::property!( + FormatProperties::VideoSize, + Choice, + Range, + Rectangle, + pw::spa::utils::Rectangle { + // Default + width: 128, + height: 128, + }, + pw::spa::utils::Rectangle { + // Min + width: 1, + height: 1, + }, + pw::spa::utils::Rectangle { + // Max + width: 4096, + height: 4096, + } + ), + pw::spa::pod::property!( + FormatProperties::VideoFramerate, + Choice, + Range, + Fraction, + pw::spa::utils::Fraction { + num: options.fps, + denom: 1 + }, + pw::spa::utils::Fraction { num: 0, denom: 1 }, + pw::spa::utils::Fraction { + num: 1000, + denom: 1 + } + ), + ); + + let metas_obj = pw::spa::pod::object!( + SpaTypes::ObjectParamMeta, + ParamType::Meta, + Property::new( + SPA_PARAM_META_type, + pw::spa::pod::Value::Id(pw::spa::utils::Id(SPA_META_Header)) + ), + Property::new( + SPA_PARAM_META_size, + pw::spa::pod::Value::Int(size_of::() as i32) + ), + ); + + let values: Vec = pw::spa::pod::serialize::PodSerializer::serialize( + std::io::Cursor::new(Vec::new()), + &pw::spa::pod::Value::Object(obj), + )? + .0 + .into_inner(); + let metas_values: Vec = pw::spa::pod::serialize::PodSerializer::serialize( + std::io::Cursor::new(Vec::new()), + &pw::spa::pod::Value::Object(metas_obj), + )? + .0 + .into_inner(); + + let mut params = [ + pw::spa::pod::Pod::from_bytes(&values).unwrap(), + pw::spa::pod::Pod::from_bytes(&metas_values).unwrap(), + ]; + + stream.connect( + Direction::Input, + Some(stream_id), + pw::stream::StreamFlags::AUTOCONNECT | pw::stream::StreamFlags::MAP_BUFFERS, + &mut params, + )?; + + ready_sender.send(true)?; + + while CAPTURER_STATE.load(std::sync::atomic::Ordering::Relaxed) == 0 { + std::thread::sleep(Duration::from_millis(10)); + } + + let pw_loop = mainloop.loop_(); + + // User has called Capturer::start() and we start the main loop + while CAPTURER_STATE.load(std::sync::atomic::Ordering::Relaxed) == 1 + && /* If the stream state got changed to `Error`, we exit. TODO: tell user that we exited */ + !STREAM_STATE_CHANGED_TO_ERROR.load(std::sync::atomic::Ordering::Relaxed) + { + pw_loop.iterate(Duration::from_millis(100)); + } + + Ok(()) +} + +pub struct PwCapturer { + capturer_join_handle: Option>>, + // The pipewire stream is deleted when the connection is dropped. + // That's why we keep it alive + _connection: dbus::blocking::Connection, +} + +impl PwCapturer { + // TODO: Error handling + pub fn new(options: &Options, tx: Sender) -> Self { + let connection = + dbus::blocking::Connection::new_session().expect("Failed to create dbus connection"); + let stream_id = ScreenCastPortal::new(&connection) + .show_cursor(options.show_cursor) + .expect("Unsupported cursor mode") + .create_stream() + .expect("Failed to get screencast stream") + .pw_node_id(); + + // TODO: Fix this hack + let options = options.clone(); + let (ready_sender, ready_recv) = sync_channel(1); + let capturer_join_handle = std::thread::spawn(move || { + let res = pipewire_capturer(options, tx, &ready_sender, stream_id); + if res.is_err() { + ready_sender.send(false)?; + } + res + }); + + if !ready_recv.recv().expect("Failed to receive") { + panic!("Failed to setup capturer"); + } + + Self { + capturer_join_handle: Some(capturer_join_handle), + _connection: connection, + } + } +} + +impl LinuxCapturerImpl for PwCapturer { + fn start_capture(&mut self) { + CAPTURER_STATE.store(1, std::sync::atomic::Ordering::Relaxed); + } + + fn stop_capture(&mut self) { + CAPTURER_STATE.store(2, std::sync::atomic::Ordering::Relaxed); + if let Some(handle) = self.capturer_join_handle.take() { + if let Err(e) = handle.join().expect("Failed to join capturer thread") { + eprintln!("Error occured capturing: {e}"); + } + } + CAPTURER_STATE.store(0, std::sync::atomic::Ordering::Relaxed); + STREAM_STATE_CHANGED_TO_ERROR.store(false, std::sync::atomic::Ordering::Relaxed); + } +} diff --git a/src/capturer/engine/linux/pw/portal.rs b/src/capturer/engine/linux/pw/portal.rs new file mode 100644 index 0000000..e321176 --- /dev/null +++ b/src/capturer/engine/linux/pw/portal.rs @@ -0,0 +1,425 @@ +use std::{ + sync::{atomic::AtomicBool, Arc, Mutex}, + time::Duration, +}; + +use dbus::{ + arg::{self, PropMap, RefArg, Variant}, + blocking::{Connection, Proxy}, + message::MatchRule, + strings::{BusName, Interface}, +}; + +// This code was autogenerated with `dbus-codegen-rust -d org.freedesktop.portal.Desktop -p /org/freedesktop/portal/desktop -f org.freedesktop.portal.ScreenCast`, see https://github.com/diwic/dbus-rs +// { +use dbus::blocking; + +use crate::capturer::engine::linux::error::LinCapError; + +#[allow(unused)] +trait OrgFreedesktopPortalScreenCast { + fn create_session(&self, options: arg::PropMap) -> Result, dbus::Error>; + fn select_sources( + &self, + session_handle: dbus::Path, + options: arg::PropMap, + ) -> Result, dbus::Error>; + fn start( + &self, + session_handle: dbus::Path, + parent_window: &str, + options: arg::PropMap, + ) -> Result, dbus::Error>; + fn open_pipe_wire_remote( + &self, + session_handle: dbus::Path, + options: arg::PropMap, + ) -> Result; + fn available_source_types(&self) -> Result; + fn available_cursor_modes(&self) -> Result; + fn version(&self) -> Result; +} + +impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref> + OrgFreedesktopPortalScreenCast for blocking::Proxy<'a, C> +{ + fn create_session(&self, options: arg::PropMap) -> Result, dbus::Error> { + self.method_call( + "org.freedesktop.portal.ScreenCast", + "CreateSession", + (options,), + ) + .and_then(|r: (dbus::Path<'static>,)| Ok(r.0)) + } + + fn select_sources( + &self, + session_handle: dbus::Path, + options: arg::PropMap, + ) -> Result, dbus::Error> { + self.method_call( + "org.freedesktop.portal.ScreenCast", + "SelectSources", + (session_handle, options), + ) + .and_then(|r: (dbus::Path<'static>,)| Ok(r.0)) + } + + fn start( + &self, + session_handle: dbus::Path, + parent_window: &str, + options: arg::PropMap, + ) -> Result, dbus::Error> { + self.method_call( + "org.freedesktop.portal.ScreenCast", + "Start", + (session_handle, parent_window, options), + ) + .and_then(|r: (dbus::Path<'static>,)| Ok(r.0)) + } + + fn open_pipe_wire_remote( + &self, + session_handle: dbus::Path, + options: arg::PropMap, + ) -> Result { + self.method_call( + "org.freedesktop.portal.ScreenCast", + "OpenPipeWireRemote", + (session_handle, options), + ) + .and_then(|r: (arg::OwnedFd,)| Ok(r.0)) + } + + fn available_source_types(&self) -> Result { + ::get( + &self, + "org.freedesktop.portal.ScreenCast", + "AvailableSourceTypes", + ) + } + + fn available_cursor_modes(&self) -> Result { + ::get( + &self, + "org.freedesktop.portal.ScreenCast", + "AvailableCursorModes", + ) + } + + fn version(&self) -> Result { + ::get( + &self, + "org.freedesktop.portal.ScreenCast", + "version", + ) + } +} +// } + +// This code was autogenerated with `dbus-codegen-rust --file org.freedesktop.portal.Request.xml`, see https://github.com/diwic/dbus-rs +// { +#[allow(unused)] +trait OrgFreedesktopPortalRequest { + fn close(&self) -> Result<(), dbus::Error>; +} + +#[derive(Debug)] +pub struct OrgFreedesktopPortalRequestResponse { + pub response: u32, + pub results: arg::PropMap, +} + +impl arg::AppendAll for OrgFreedesktopPortalRequestResponse { + fn append(&self, i: &mut arg::IterAppend) { + arg::RefArg::append(&self.response, i); + arg::RefArg::append(&self.results, i); + } +} + +impl arg::ReadAll for OrgFreedesktopPortalRequestResponse { + fn read(i: &mut arg::Iter) -> Result { + Ok(OrgFreedesktopPortalRequestResponse { + response: i.read()?, + results: i.read()?, + }) + } +} + +impl dbus::message::SignalArgs for OrgFreedesktopPortalRequestResponse { + const NAME: &'static str = "Response"; + const INTERFACE: &'static str = "org.freedesktop.portal.Request"; +} + +impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref> OrgFreedesktopPortalRequest + for blocking::Proxy<'a, C> +{ + fn close(&self) -> Result<(), dbus::Error> { + self.method_call("org.freedesktop.portal.Request", "Close", ()) + } +} +// } + +type Response = Option; + +#[derive(Debug)] +#[allow(dead_code)] +pub struct StreamVardict { + id: Option, + position: Option<(i32, i32)>, + size: Option<(i32, i32)>, + source_type: Option, + mapping_id: Option, +} + +#[derive(Debug)] +#[allow(unused)] +pub struct Stream(u32, StreamVardict); + +impl Stream { + pub fn pw_node_id(&self) -> u32 { + self.0 + } + + pub fn from_dbus(stream: &Variant>) -> Option { + let mut stream = stream.as_iter()?.next()?.as_iter()?; + let pipewire_node_id = stream.next()?.as_iter()?.next()?.as_u64()?; + + // TODO: Get the rest of the properties + + Some(Self( + pipewire_node_id as u32, + StreamVardict { + id: None, + position: None, + size: None, + source_type: None, + mapping_id: None, + }, + )) + } +} + +macro_rules! match_response { + ( $code:expr ) => { + match $code { + 0 => {} + 1 => { + return Err(LinCapError::new(String::from( + "User cancelled the interaction", + ))); + } + 2 => { + return Err(LinCapError::new(String::from( + "The user interaction was ended in some other way", + ))); + } + _ => unreachable!(), + } + }; +} + +pub struct ScreenCastPortal<'a> { + proxy: Proxy<'a, &'a Connection>, + token: String, + cursor_mode: u32, +} + +impl<'a> ScreenCastPortal<'a> { + pub fn new(connection: &'a Connection) -> Self { + let proxy = connection.with_proxy( + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + Duration::from_secs(4), + ); + + let token = format!("scap_{}", rand::random::()); + + Self { + proxy, + token, + cursor_mode: 1, + } + } + + fn create_session_args(&self) -> arg::PropMap { + let mut map = arg::PropMap::new(); + map.insert( + String::from("handle_token"), + Variant(Box::new(self.token.clone())), + ); + map.insert( + String::from("session_handle_token"), + Variant(Box::new(self.token.clone())), + ); + map + } + + fn select_sources_args(&self) -> Result { + let mut map = arg::PropMap::new(); + map.insert( + String::from("handle_token"), + Variant(Box::new(self.token.clone())), + ); + map.insert( + String::from("types"), + Variant(Box::new(self.proxy.available_source_types()?)), + ); + map.insert(String::from("multiple"), Variant(Box::new(false))); + map.insert( + String::from("cursor_mode"), + Variant(Box::new(self.cursor_mode)), + ); + Ok(map) + } + + fn handle_req_response( + connection: &Connection, + path: dbus::Path<'static>, + iterations: usize, + timeout: Duration, + response: Arc>, + ) -> Result<(), dbus::Error> { + let got_response = Arc::new(AtomicBool::new(false)); + let got_response_clone = Arc::clone(&got_response); + + let mut rule = MatchRule::new(); + rule.path = Some(dbus::Path::from(path)); + rule.msg_type = Some(dbus::MessageType::Signal); + rule.sender = Some(BusName::from("org.freedesktop.portal.Desktop")); + rule.interface = Some(Interface::from("org.freedesktop.portal.Request")); + connection.add_match( + rule, + move |res: OrgFreedesktopPortalRequestResponse, _chuh, _msg| { + let mut response = response.lock().expect("Failed to lock response mutex"); + *response = Some(res); + got_response_clone.store(true, std::sync::atomic::Ordering::Relaxed); + false + }, + )?; + + for _ in 0..iterations { + connection.process(timeout)?; + + if got_response.load(std::sync::atomic::Ordering::Relaxed) { + break; + } + } + + Ok(()) + } + + fn create_session(&self) -> Result { + let request_handle = self.proxy.create_session(self.create_session_args())?; + + let response = Arc::new(Mutex::new(None)); + let response_clone = Arc::clone(&response); + Self::handle_req_response( + self.proxy.connection, + request_handle, + 100, + Duration::from_millis(100), + response_clone, + )?; + + if let Some(res) = response.lock()?.take() { + match_response!(res.response); + match res + .results + .get("session_handle") + .map(|h| h.0.as_str().map(String::from)) + { + Some(h) => { + let p = dbus::Path::from(match h { + Some(p) => p, + None => { + return Err(LinCapError::new(String::from( + "Invalid session_handle received", + ))) + } + }); + + return Ok(p); + } + None => return Err(LinCapError::new(String::from("Did not get session handle"))), + } + } + + Err(LinCapError::new(String::from("Did not get response"))) + } + + fn select_sources(&self, session_handle: dbus::Path) -> Result<(), LinCapError> { + let request_handle = self + .proxy + .select_sources(session_handle, self.select_sources_args()?)?; + + let response = Arc::new(Mutex::new(None)); + let response_clone = Arc::clone(&response); + Self::handle_req_response( + self.proxy.connection, + request_handle, + 1200, // Wait 2 min + Duration::from_millis(100), + response_clone, + )?; + + if let Some(res) = response.lock()?.take() { + match_response!(res.response); + return Ok(()); + } + + Err(LinCapError::new(String::from("Did not get response"))) + } + + fn start(&self, session_handle: dbus::Path) -> Result { + let request_handle = self.proxy.start(session_handle, "", PropMap::new())?; + + let response = Arc::new(Mutex::new(None)); + let response_clone = Arc::clone(&response); + Self::handle_req_response( + self.proxy.connection, + request_handle, + 100, // Wait 10 s + Duration::from_millis(100), + response_clone, + )?; + + if let Some(res) = response.lock()?.take() { + match_response!(res.response); + match res.results.get("streams") { + Some(s) => match Stream::from_dbus(s) { + Some(s) => return Ok(s), + None => { + return Err(LinCapError::new(String::from( + "Failed to extract stream properties", + ))) + } + }, + None => return Err(LinCapError::new(String::from("Did not get any streams"))), + } + } + + Err(LinCapError::new(String::from("Did not get response"))) + } + + pub fn create_stream(&self) -> Result { + let session_handle = self.create_session()?; + self.select_sources(session_handle.clone())?; + self.start(session_handle) + } + + pub fn show_cursor(mut self, mode: bool) -> Result { + let available_modes = self.proxy.available_cursor_modes()?; + if mode && available_modes & 2 == 2 { + self.cursor_mode = 2; + return Ok(self); + } + if !mode && available_modes & 1 == 1 { + self.cursor_mode = 1; + return Ok(self); + } + + Err(LinCapError::new("Unsupported cursor mode".to_string())) + } +} diff --git a/src/capturer/engine/linux/x11/mod.rs b/src/capturer/engine/linux/x11/mod.rs new file mode 100644 index 0000000..af2df2a --- /dev/null +++ b/src/capturer/engine/linux/x11/mod.rs @@ -0,0 +1,24 @@ +use std::sync::mpsc::Sender; + +use crate::{capturer::Options, frame::Frame}; + +use super::LinuxCapturerImpl; + +pub struct X11Capturer { +} + +impl X11Capturer { + pub fn new(_options: &Options, _tx: Sender) -> Self { + Self {} + } +} + +impl LinuxCapturerImpl for X11Capturer { + fn start_capture(&mut self) { + todo!() + } + + fn stop_capture(&mut self) { + todo!() + } +} diff --git a/src/capturer/engine/mod.rs b/src/capturer/engine/mod.rs index f31de70..fdfcc3e 100644 --- a/src/capturer/engine/mod.rs +++ b/src/capturer/engine/mod.rs @@ -68,7 +68,7 @@ impl Engine { #[cfg(target_os = "linux")] { - self.linux.start_capture(); + self.linux.imp.start_capture(); } } @@ -85,7 +85,7 @@ impl Engine { #[cfg(target_os = "linux")] { - self.linux.stop_capture(); + self.linux.imp.stop_capture(); } } From f725b5d1b48f3f95c5699297e1ee0ec99e00fe9e Mon Sep 17 00:00:00 2001 From: Marcus Lian Hanestad Date: Sun, 27 Oct 2024 21:46:12 +0100 Subject: [PATCH 2/9] linux: rename pw module to wayland --- src/capturer/engine/linux/mod.rs | 5 ++--- src/capturer/engine/linux/{pw => wayland}/mod.rs | 6 +++--- src/capturer/engine/linux/{pw => wayland}/portal.rs | 0 3 files changed, 5 insertions(+), 6 deletions(-) rename src/capturer/engine/linux/{pw => wayland}/mod.rs (99%) rename src/capturer/engine/linux/{pw => wayland}/portal.rs (100%) diff --git a/src/capturer/engine/linux/mod.rs b/src/capturer/engine/linux/mod.rs index 9c9c061..e711de6 100644 --- a/src/capturer/engine/linux/mod.rs +++ b/src/capturer/engine/linux/mod.rs @@ -1,7 +1,6 @@ - use std::{env, sync::mpsc}; -use pw::PwCapturer; +use wayland::WaylandCapturer; use x11::X11Capturer; use crate::{ @@ -11,7 +10,7 @@ use crate::{ mod error; -mod pw; +mod wayland; mod x11; pub trait LinuxCapturerImpl { diff --git a/src/capturer/engine/linux/pw/mod.rs b/src/capturer/engine/linux/wayland/mod.rs similarity index 99% rename from src/capturer/engine/linux/pw/mod.rs rename to src/capturer/engine/linux/wayland/mod.rs index 9415bd7..9fb3a86 100644 --- a/src/capturer/engine/linux/pw/mod.rs +++ b/src/capturer/engine/linux/wayland/mod.rs @@ -313,14 +313,14 @@ fn pipewire_capturer( Ok(()) } -pub struct PwCapturer { +pub struct WaylandCapturer { capturer_join_handle: Option>>, // The pipewire stream is deleted when the connection is dropped. // That's why we keep it alive _connection: dbus::blocking::Connection, } -impl PwCapturer { +impl WaylandCapturer { // TODO: Error handling pub fn new(options: &Options, tx: Sender) -> Self { let connection = @@ -354,7 +354,7 @@ impl PwCapturer { } } -impl LinuxCapturerImpl for PwCapturer { +impl LinuxCapturerImpl for WaylandCapturer { fn start_capture(&mut self) { CAPTURER_STATE.store(1, std::sync::atomic::Ordering::Relaxed); } diff --git a/src/capturer/engine/linux/pw/portal.rs b/src/capturer/engine/linux/wayland/portal.rs similarity index 100% rename from src/capturer/engine/linux/pw/portal.rs rename to src/capturer/engine/linux/wayland/portal.rs From c3e2a03d2b16093471d9d727582ffb6e506b7894 Mon Sep 17 00:00:00 2001 From: Marcus Lian Hanestad Date: Mon, 28 Oct 2024 18:06:02 +0100 Subject: [PATCH 3/9] x11: enumerate targets --- Cargo.toml | 2 + src/capturer/engine/linux/mod.rs | 2 +- src/capturer/engine/linux/wayland/mod.rs | 1 + src/targets/linux/mod.rs | 187 ++++++++++++++++++++++- src/targets/mod.rs | 12 ++ 5 files changed, 200 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 055f936..fdceab7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,3 +39,5 @@ objc = "0.2.7" pipewire = "0.8.0" dbus = "0.9.7" rand = "0.8.5" +xcb = { version = "1.4.0", features = ["randr", "xlib_xcb"] } +x11 = "2.21.0" \ No newline at end of file diff --git a/src/capturer/engine/linux/mod.rs b/src/capturer/engine/linux/mod.rs index e711de6..34f8c0c 100644 --- a/src/capturer/engine/linux/mod.rs +++ b/src/capturer/engine/linux/mod.rs @@ -29,7 +29,7 @@ impl LinuxCapturer { if env::var("WAYLAND_DISPLAY").is_ok() { println!("[DEBUG] On wayland"); return Self { - imp: Box::new(PwCapturer::new(options, tx)), + imp: Box::new(WaylandCapturer::new(options, tx)), }; } else if env::var("DISPLAY").is_ok() { println!("[DEBUG] On X11"); diff --git a/src/capturer/engine/linux/wayland/mod.rs b/src/capturer/engine/linux/wayland/mod.rs index 9fb3a86..ed5164b 100644 --- a/src/capturer/engine/linux/wayland/mod.rs +++ b/src/capturer/engine/linux/wayland/mod.rs @@ -37,6 +37,7 @@ use super::{error::LinCapError, LinuxCapturerImpl}; mod portal; +// TODO: Move to wayland capturer with Arc<> static CAPTURER_STATE: AtomicU8 = AtomicU8::new(0); static STREAM_STATE_CHANGED_TO_ERROR: AtomicBool = AtomicBool::new(false); diff --git a/src/targets/linux/mod.rs b/src/targets/linux/mod.rs index fd0ccdc..7ef2105 100644 --- a/src/targets/linux/mod.rs +++ b/src/targets/linux/mod.rs @@ -1,7 +1,188 @@ +use std::ffi::{CStr, CString, NulError}; + use super::Target; -// On Linux, the target is selected when a Recorder is instanciated because this -// requires user interaction +use x11::xlib::{XFreeStringList, XGetTextProperty, XTextProperty, XmbTextPropertyToTextList}; +use xcb::{ + randr::{Connection, GetCrtcInfo, GetOutputInfo, GetScreenResources}, + x::{self, GetPropertyReply}, + Xid, +}; + +fn get_atom(conn: &xcb::Connection, atom_name: &str) -> Result { + let cookie = conn.send_request(&x::InternAtom { + only_if_exists: true, + name: atom_name.as_bytes(), + }); + Ok(conn.wait_for_reply(cookie)?.atom()) +} + +fn get_property( + conn: &xcb::Connection, + win: x::Window, + prop: x::Atom, + typ: x::Atom, + length: u32, +) -> Result { + let cookie = conn.send_request(&x::GetProperty { + delete: false, + window: win, + property: prop, + r#type: typ, + long_offset: 0, + long_length: length, + }); + Ok(conn.wait_for_reply(cookie)?) +} + +fn decode_compound_text( + conn: &xcb::Connection, + value: &[u8], + client: &xcb::x::Window, + ttype: xcb::x::Atom, +) -> Result { + let display = conn.get_raw_dpy(); + assert!(!display.is_null()); + + let c_string = CString::new(value.to_vec())?; + let mut fuck = XTextProperty { + value: std::ptr::null_mut(), + encoding: 0, + format: 0, + nitems: 0, + }; + let res = unsafe { + XGetTextProperty( + display, + client.resource_id() as u64, + &mut fuck, + x::ATOM_WM_NAME.resource_id() as u64, + ) + }; + if res == 0 || fuck.nitems == 0 { + return Ok(String::from("n/a")); + } + + let mut xname = XTextProperty { + value: c_string.as_ptr() as *mut u8, + encoding: ttype.resource_id() as u64, + format: 8, + nitems: fuck.nitems, + }; + let mut list: *mut *mut i8 = std::ptr::null_mut(); + let mut count: i32 = 0; + let result = unsafe { XmbTextPropertyToTextList(display, &mut xname, &mut list, &mut count) }; + if result < 1 || list.is_null() || count < 1 { + Ok(String::from("n/a")) + } else { + let title = unsafe { CStr::from_ptr(*list).to_string_lossy().into_owned() }; + unsafe { XFreeStringList(list) }; + Ok(title) + } +} + pub fn get_all_targets() -> Vec { - Vec::new() + if std::env::var("WAYLAND_DISPLAY").is_ok() { + // On Wayland, the target is selected when a Recorder is instanciated because it requires user interaction + Vec::new() + } else if std::env::var("DISPLAY").is_ok() { + let (conn, _screen_num) = xcb::Connection::connect_with_xlib_display_and_extensions( + &[xcb::Extension::RandR], + &[], + ) + .unwrap(); + let setup = conn.get_setup(); + let screens = setup.roots(); + + let wm_client_list = get_atom(&conn, "_NET_CLIENT_LIST").unwrap(); + assert!(wm_client_list != x::ATOM_NONE, "EWMH not supported"); + + let atom_net_wm_name = get_atom(&conn, "_NET_WM_NAME").unwrap(); + let atom_text = get_atom(&conn, "TEXT").unwrap(); + let atom_utf8_string = get_atom(&conn, "UTF8_STRING").unwrap(); + let atom_compound_text = get_atom(&conn, "COMPOUND_TEXT").unwrap(); + + let mut targets = Vec::new(); + for screen in screens { + let window_list = + get_property(&conn, screen.root(), wm_client_list, x::ATOM_NONE, 100).unwrap(); + + for client in window_list.value::() { + let cr = + get_property(&conn, *client, atom_net_wm_name, x::ATOM_STRING, 4096).unwrap(); + if !cr.value::().is_empty() { + targets.push(Target::Window(crate::targets::Window { + id: 0, + title: String::from_utf8(cr.value().to_vec()).unwrap(), + raw_handle: *client, + })); + continue; + } + + let reply = + get_property(&conn, *client, x::ATOM_WM_NAME, x::ATOM_ANY, 4096).unwrap(); + let value: &[u8] = reply.value(); + if !value.is_empty() { + let ttype = reply.r#type(); + let title = if ttype == x::ATOM_STRING + || ttype == atom_utf8_string + || ttype == atom_text + { + String::from_utf8(reply.value().to_vec()).unwrap_or(String::from("n/a")) + } else if ttype == atom_compound_text { + decode_compound_text(&conn, value, client, ttype).unwrap() + } else { + String::from_utf8(reply.value().to_vec()).unwrap_or(String::from("n/a")) + }; + + targets.push(Target::Window(crate::targets::Window { + id: 0, + title, + raw_handle: *client, + })); + continue; + } + targets.push(Target::Window(crate::targets::Window { + id: 0, + title: String::from("n/a"), + raw_handle: *client, + })); + } + + let resources = conn.send_request(&GetScreenResources { + window: screen.root(), + }); + let resources = conn.wait_for_reply(resources).unwrap(); + for output in resources.outputs() { + let info = conn.send_request(&GetOutputInfo { + output: *output, + config_timestamp: 0, + }); + let info = conn.wait_for_reply(info).unwrap(); + if info.connection() == Connection::Connected { + let crtc = info.crtc(); + crtc.resource_id(); + let crtc_info = conn.send_request(&GetCrtcInfo { + crtc, + config_timestamp: 0, + }); + let crtc_info = conn.wait_for_reply(crtc_info).unwrap(); + let title = + String::from_utf8(info.name().to_vec()).unwrap_or(String::from("n/a")); + targets.push(Target::Display(crate::targets::Display { + id: crtc.resource_id(), + title, + width: crtc_info.width(), + height: crtc_info.height(), + x_offset: crtc_info.x(), + y_offset: crtc_info.y(), + })); + } + } + } + + targets + } else { + panic!("Unsupported platform. Could not detect Wayland or X11 displays") + } } diff --git a/src/targets/mod.rs b/src/targets/mod.rs index 930ab62..7c0804c 100644 --- a/src/targets/mod.rs +++ b/src/targets/mod.rs @@ -17,6 +17,9 @@ pub struct Window { #[cfg(target_os = "macos")] pub raw_handle: core_graphics_helmer_fork::window::CGWindowID, + + #[cfg(target_os = "linux")] + pub raw_handle: xcb::x::Window, } #[derive(Debug, Clone)] @@ -29,6 +32,15 @@ pub struct Display { #[cfg(target_os = "macos")] pub raw_handle: core_graphics_helmer_fork::display::CGDisplay, + + #[cfg(target_os = "linux")] + pub width: u16, + #[cfg(target_os = "linux")] + pub height: u16, + #[cfg(target_os = "linux")] + pub x_offset: i16, + #[cfg(target_os = "linux")] + pub y_offset: i16, } #[derive(Debug, Clone)] From bdec2af28722cb900c02884917515adb1a1b289c Mon Sep 17 00:00:00 2001 From: Marcus Lian Hanestad Date: Tue, 29 Oct 2024 14:37:46 +0100 Subject: [PATCH 4/9] x11: capture displays and windows --- src/capturer/engine/linux/x11/mod.rs | 139 +++++++++++++++++++++++++-- src/targets/linux/mod.rs | 2 +- src/targets/mod.rs | 2 + 3 files changed, 135 insertions(+), 8 deletions(-) diff --git a/src/capturer/engine/linux/x11/mod.rs b/src/capturer/engine/linux/x11/mod.rs index af2df2a..e347245 100644 --- a/src/capturer/engine/linux/x11/mod.rs +++ b/src/capturer/engine/linux/x11/mod.rs @@ -1,24 +1,149 @@ -use std::sync::mpsc::Sender; +use std::{ + sync::{ + atomic::{AtomicU8, Ordering}, + mpsc::Sender, + Arc, + }, thread::JoinHandle +}; -use crate::{capturer::Options, frame::Frame}; +use xcb::{randr::{GetCrtcInfo, GetOutputInfo, GetOutputPrimary}, x, Xid}; -use super::LinuxCapturerImpl; +use crate::{capturer::Options, frame::Frame, targets::Display, Target}; + +use super::{error::LinCapError, LinuxCapturerImpl}; pub struct X11Capturer { + capturer_join_handle: Option>>, + capturer_state: Arc, } impl X11Capturer { - pub fn new(_options: &Options, _tx: Sender) -> Self { - Self {} + pub fn new(options: &Options, tx: Sender) -> Self { + let (conn, screen_num) = + xcb::Connection::connect_with_extensions(None, &[xcb::Extension::RandR], &[]).unwrap(); + let setup = conn.get_setup(); + + let target = match &options.target { + Some(t) => t.clone(), + None => { + println!("[DEBUG] No target provided getting default display"); + let screen = setup.roots().nth(screen_num as usize).unwrap(); + + let primary_display_cookie = conn.send_request(&GetOutputPrimary { + window: screen.root(), + }); + let primary_display = conn.wait_for_reply(primary_display_cookie).unwrap(); + let info_cookie = conn.send_request(&GetOutputInfo { + output: primary_display.output(), + config_timestamp: 0, + }); + let info = conn.wait_for_reply(info_cookie).unwrap(); + let crtc = info.crtc(); + let crtc_info_cookie = conn.send_request(&GetCrtcInfo { + crtc, + config_timestamp: 0, + }); + let crtc_info = conn.wait_for_reply(crtc_info_cookie).unwrap(); + Target::Display(Display { + id: crtc.resource_id(), + title: String::from_utf8(info.name().to_vec()).unwrap_or(String::from("default")), + width: crtc_info.width(), + height: crtc_info.height(), + x_offset: crtc_info.x(), + y_offset: crtc_info.y(), + raw_handle: screen.root(), + }) + }, + }; + + let framerate = options.fps as f32; + let capturer_state = Arc::new(AtomicU8::new(0)); + let capturer_state_clone = Arc::clone(&capturer_state); + + let jh = std::thread::spawn(move || { + while capturer_state_clone.load(Ordering::Acquire) == 0 { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + let frame_time = std::time::Duration::from_secs_f32(1.0 / framerate); + while capturer_state_clone.load(Ordering::Acquire) == 1 { + let start = std::time::Instant::now(); + match &target { + Target::Window(win) => { + let geom_cookie = conn.send_request(&x::GetGeometry { + drawable: x::Drawable::Window(win.raw_handle), + }); + let geom = conn.wait_for_reply(geom_cookie).unwrap(); + + let img_cookie = conn.send_request(&x::GetImage { + format: x::ImageFormat::ZPixmap, + drawable: x::Drawable::Window(win.raw_handle), + x: 0, + y: 0, + width: geom.width(), + height: geom.height(), + plane_mask: u32::MAX, + }); + let img = conn.wait_for_reply(img_cookie).unwrap(); + + let img_data = img.data(); + + tx.send(Frame::BGRx(crate::frame::BGRxFrame { + display_time: 0, + width: geom.width() as i32, + height: geom.height() as i32, + data: img_data.to_vec(), + })).unwrap(); + } + Target::Display(disp) => { + let img_cookie = conn.send_request(&x::GetImage { + format: x::ImageFormat::ZPixmap, + drawable: x::Drawable::Window(disp.raw_handle), + x: disp.x_offset, + y: disp.y_offset, + width: disp.width, + height: disp.height, + plane_mask: u32::MAX, + }); + let img = conn.wait_for_reply(img_cookie).unwrap(); + + let img_data = img.data(); + + tx.send(Frame::BGRx(crate::frame::BGRxFrame { + display_time: 0, + width: disp.width as i32, + height: disp.height as i32, + data: img_data.to_vec(), + })).unwrap(); + } + } + let elapsed = start.elapsed(); + if elapsed < frame_time { + std::thread::sleep(frame_time - start.elapsed()); + } + } + + Ok(()) + }); + + Self { + capturer_state: capturer_state, + capturer_join_handle: Some(jh), + } } } impl LinuxCapturerImpl for X11Capturer { fn start_capture(&mut self) { - todo!() + self.capturer_state.store(1, Ordering::Release); } fn stop_capture(&mut self) { - todo!() + self.capturer_state.store(2, Ordering::Release); + if let Some(handle) = self.capturer_join_handle.take() { + if let Err(e) = handle.join().expect("Failed to join capturer thread") { + eprintln!("Error occured capturing: {e}"); + } + } } } diff --git a/src/targets/linux/mod.rs b/src/targets/linux/mod.rs index 7ef2105..faf893d 100644 --- a/src/targets/linux/mod.rs +++ b/src/targets/linux/mod.rs @@ -161,7 +161,6 @@ pub fn get_all_targets() -> Vec { let info = conn.wait_for_reply(info).unwrap(); if info.connection() == Connection::Connected { let crtc = info.crtc(); - crtc.resource_id(); let crtc_info = conn.send_request(&GetCrtcInfo { crtc, config_timestamp: 0, @@ -176,6 +175,7 @@ pub fn get_all_targets() -> Vec { height: crtc_info.height(), x_offset: crtc_info.x(), y_offset: crtc_info.y(), + raw_handle: screen.root(), })); } } diff --git a/src/targets/mod.rs b/src/targets/mod.rs index 7c0804c..0cdbf0e 100644 --- a/src/targets/mod.rs +++ b/src/targets/mod.rs @@ -33,6 +33,8 @@ pub struct Display { #[cfg(target_os = "macos")] pub raw_handle: core_graphics_helmer_fork::display::CGDisplay, + #[cfg(target_os = "linux")] + pub raw_handle: xcb::x::Window, #[cfg(target_os = "linux")] pub width: u16, #[cfg(target_os = "linux")] From d1061543f5456274be408597a97938b38aa78b12 Mon Sep 17 00:00:00 2001 From: Marcus Lian Hanestad Date: Tue, 29 Oct 2024 15:25:21 +0100 Subject: [PATCH 5/9] x11: support more scap target APIs --- src/capturer/engine/linux/x11/mod.rs | 35 +--- src/targets/linux/mod.rs | 237 ++++++++++++++++----------- src/targets/mod.rs | 6 +- 3 files changed, 152 insertions(+), 126 deletions(-) diff --git a/src/capturer/engine/linux/x11/mod.rs b/src/capturer/engine/linux/x11/mod.rs index e347245..b8973b8 100644 --- a/src/capturer/engine/linux/x11/mod.rs +++ b/src/capturer/engine/linux/x11/mod.rs @@ -6,9 +6,9 @@ use std::{ }, thread::JoinHandle }; -use xcb::{randr::{GetCrtcInfo, GetOutputInfo, GetOutputPrimary}, x, Xid}; +use xcb::x; -use crate::{capturer::Options, frame::Frame, targets::Display, Target}; +use crate::{capturer::Options, frame::Frame, targets::linux::get_default_x_display, Target}; use super::{error::LinCapError, LinuxCapturerImpl}; @@ -22,38 +22,11 @@ impl X11Capturer { let (conn, screen_num) = xcb::Connection::connect_with_extensions(None, &[xcb::Extension::RandR], &[]).unwrap(); let setup = conn.get_setup(); + let screen = setup.roots().nth(screen_num as usize).unwrap(); let target = match &options.target { Some(t) => t.clone(), - None => { - println!("[DEBUG] No target provided getting default display"); - let screen = setup.roots().nth(screen_num as usize).unwrap(); - - let primary_display_cookie = conn.send_request(&GetOutputPrimary { - window: screen.root(), - }); - let primary_display = conn.wait_for_reply(primary_display_cookie).unwrap(); - let info_cookie = conn.send_request(&GetOutputInfo { - output: primary_display.output(), - config_timestamp: 0, - }); - let info = conn.wait_for_reply(info_cookie).unwrap(); - let crtc = info.crtc(); - let crtc_info_cookie = conn.send_request(&GetCrtcInfo { - crtc, - config_timestamp: 0, - }); - let crtc_info = conn.wait_for_reply(crtc_info_cookie).unwrap(); - Target::Display(Display { - id: crtc.resource_id(), - title: String::from_utf8(info.name().to_vec()).unwrap_or(String::from("default")), - width: crtc_info.width(), - height: crtc_info.height(), - x_offset: crtc_info.x(), - y_offset: crtc_info.y(), - raw_handle: screen.root(), - }) - }, + None => Target::Display(get_default_x_display(&conn, screen).unwrap()), }; let framerate = options.fps as f32; diff --git a/src/targets/linux/mod.rs b/src/targets/linux/mod.rs index faf893d..fedb7b2 100644 --- a/src/targets/linux/mod.rs +++ b/src/targets/linux/mod.rs @@ -1,11 +1,11 @@ use std::ffi::{CStr, CString, NulError}; -use super::Target; +use super::{Display, Target}; use x11::xlib::{XFreeStringList, XGetTextProperty, XTextProperty, XmbTextPropertyToTextList}; use xcb::{ - randr::{Connection, GetCrtcInfo, GetOutputInfo, GetScreenResources}, - x::{self, GetPropertyReply}, + randr::{GetCrtcInfo, GetOutputInfo, GetOutputPrimary, GetScreenResources}, + x::{self, GetPropertyReply, Screen}, Xid, }; @@ -81,108 +81,161 @@ fn decode_compound_text( } } -pub fn get_all_targets() -> Vec { - if std::env::var("WAYLAND_DISPLAY").is_ok() { - // On Wayland, the target is selected when a Recorder is instanciated because it requires user interaction - Vec::new() - } else if std::env::var("DISPLAY").is_ok() { - let (conn, _screen_num) = xcb::Connection::connect_with_xlib_display_and_extensions( - &[xcb::Extension::RandR], - &[], - ) - .unwrap(); - let setup = conn.get_setup(); - let screens = setup.roots(); - - let wm_client_list = get_atom(&conn, "_NET_CLIENT_LIST").unwrap(); - assert!(wm_client_list != x::ATOM_NONE, "EWMH not supported"); - - let atom_net_wm_name = get_atom(&conn, "_NET_WM_NAME").unwrap(); - let atom_text = get_atom(&conn, "TEXT").unwrap(); - let atom_utf8_string = get_atom(&conn, "UTF8_STRING").unwrap(); - let atom_compound_text = get_atom(&conn, "COMPOUND_TEXT").unwrap(); - - let mut targets = Vec::new(); - for screen in screens { - let window_list = - get_property(&conn, screen.root(), wm_client_list, x::ATOM_NONE, 100).unwrap(); - - for client in window_list.value::() { - let cr = - get_property(&conn, *client, atom_net_wm_name, x::ATOM_STRING, 4096).unwrap(); - if !cr.value::().is_empty() { - targets.push(Target::Window(crate::targets::Window { - id: 0, - title: String::from_utf8(cr.value().to_vec()).unwrap(), - raw_handle: *client, - })); - continue; - } - - let reply = - get_property(&conn, *client, x::ATOM_WM_NAME, x::ATOM_ANY, 4096).unwrap(); - let value: &[u8] = reply.value(); - if !value.is_empty() { - let ttype = reply.r#type(); - let title = if ttype == x::ATOM_STRING - || ttype == atom_utf8_string - || ttype == atom_text - { - String::from_utf8(reply.value().to_vec()).unwrap_or(String::from("n/a")) - } else if ttype == atom_compound_text { - decode_compound_text(&conn, value, client, ttype).unwrap() - } else { - String::from_utf8(reply.value().to_vec()).unwrap_or(String::from("n/a")) - }; - - targets.push(Target::Window(crate::targets::Window { - id: 0, - title, - raw_handle: *client, - })); - continue; - } +fn get_x11_targets() -> Result, xcb::Error> { + let (conn, _screen_num) = xcb::Connection::connect_with_xlib_display_and_extensions( + &[xcb::Extension::RandR], + &[], + )?; + let setup = conn.get_setup(); + let screens = setup.roots(); + + let wm_client_list = get_atom(&conn, "_NET_CLIENT_LIST")?; + assert!(wm_client_list != x::ATOM_NONE, "EWMH not supported"); + + let atom_net_wm_name = get_atom(&conn, "_NET_WM_NAME")?; + let atom_text = get_atom(&conn, "TEXT")?; + let atom_utf8_string = get_atom(&conn, "UTF8_STRING")?; + let atom_compound_text = get_atom(&conn, "COMPOUND_TEXT")?; + + let mut targets = Vec::new(); + for screen in screens { + let window_list = + get_property(&conn, screen.root(), wm_client_list, x::ATOM_NONE, 100)?; + + for client in window_list.value::() { + let cr = + get_property(&conn, *client, atom_net_wm_name, x::ATOM_STRING, 4096)?; + if !cr.value::().is_empty() { + targets.push(Target::Window(crate::targets::Window { + id: 0, + title: String::from_utf8(cr.value().to_vec()) + .map_err(|_| xcb::Error::Connection(xcb::ConnError::ClosedParseErr))?, + raw_handle: *client, + })); + continue; + } + + let reply = + get_property(&conn, *client, x::ATOM_WM_NAME, x::ATOM_ANY, 4096)?; + let value: &[u8] = reply.value(); + if !value.is_empty() { + let ttype = reply.r#type(); + let title = if ttype == x::ATOM_STRING + || ttype == atom_utf8_string + || ttype == atom_text + { + String::from_utf8(reply.value().to_vec()).unwrap_or(String::from("n/a")) + } else if ttype == atom_compound_text { + decode_compound_text(&conn, value, client, ttype) + .map_err(|_| xcb::Error::Connection(xcb::ConnError::ClosedParseErr))? + } else { + String::from_utf8(reply.value().to_vec()).unwrap_or(String::from("n/a")) + }; + targets.push(Target::Window(crate::targets::Window { id: 0, - title: String::from("n/a"), + title, raw_handle: *client, })); + continue; } + targets.push(Target::Window(crate::targets::Window { + id: 0, + title: String::from("n/a"), + raw_handle: *client, + })); + } - let resources = conn.send_request(&GetScreenResources { - window: screen.root(), + let resources = conn.send_request(&GetScreenResources { + window: screen.root(), + }); + let resources = conn.wait_for_reply(resources)?; + for output in resources.outputs() { + let info = conn.send_request(&GetOutputInfo { + output: *output, + config_timestamp: 0, }); - let resources = conn.wait_for_reply(resources).unwrap(); - for output in resources.outputs() { - let info = conn.send_request(&GetOutputInfo { - output: *output, + let info = conn.wait_for_reply(info)?; + if info.connection() == xcb::randr::Connection::Connected { + let crtc = info.crtc(); + let crtc_info = conn.send_request(&GetCrtcInfo { + crtc, config_timestamp: 0, }); - let info = conn.wait_for_reply(info).unwrap(); - if info.connection() == Connection::Connected { - let crtc = info.crtc(); - let crtc_info = conn.send_request(&GetCrtcInfo { - crtc, - config_timestamp: 0, - }); - let crtc_info = conn.wait_for_reply(crtc_info).unwrap(); - let title = - String::from_utf8(info.name().to_vec()).unwrap_or(String::from("n/a")); - targets.push(Target::Display(crate::targets::Display { - id: crtc.resource_id(), - title, - width: crtc_info.width(), - height: crtc_info.height(), - x_offset: crtc_info.x(), - y_offset: crtc_info.y(), - raw_handle: screen.root(), - })); - } + let crtc_info = conn.wait_for_reply(crtc_info)?; + let title = + String::from_utf8(info.name().to_vec()).unwrap_or(String::from("n/a")); + targets.push(Target::Display(crate::targets::Display { + id: crtc.resource_id(), + title, + width: crtc_info.width(), + height: crtc_info.height(), + x_offset: crtc_info.x(), + y_offset: crtc_info.y(), + raw_handle: screen.root(), + })); } } + } + + Ok(targets) +} + +pub fn get_all_targets() -> Vec { + if std::env::var("WAYLAND_DISPLAY").is_ok() { + // On Wayland, the target is selected when a Recorder is instanciated because it requires user interaction + Vec::new() + } else if std::env::var("DISPLAY").is_ok() { + get_x11_targets().unwrap() + } else { + panic!("Unsupported platform. Could not detect Wayland or X11 displays") + } +} + +pub(crate) fn get_default_x_display(conn: &xcb::Connection, screen: &Screen) -> Result { + let primary_display_cookie = conn.send_request(&GetOutputPrimary { + window: screen.root(), + }); + let primary_display = conn.wait_for_reply(primary_display_cookie)?; + let info_cookie = conn.send_request(&GetOutputInfo { + output: primary_display.output(), + config_timestamp: 0, + }); + let info = conn.wait_for_reply(info_cookie)?; + let crtc = info.crtc(); + let crtc_info_cookie = conn.send_request(&GetCrtcInfo { + crtc, + config_timestamp: 0, + }); + let crtc_info = conn.wait_for_reply(crtc_info_cookie)?; + Ok(Display { + id: crtc.resource_id(), + title: String::from_utf8(info.name().to_vec()).unwrap_or(String::from("default")), + width: crtc_info.width(), + height: crtc_info.height(), + x_offset: crtc_info.x(), + y_offset: crtc_info.y(), + raw_handle: screen.root(), + }) +} - targets +pub fn get_main_display() -> Display { + if std::env::var("WAYLAND_DISPLAY").is_ok() { + todo!() + } else if std::env::var("DISPLAY").is_ok() { + let (conn, screen_num) = + xcb::Connection::connect_with_extensions(None, &[xcb::Extension::RandR], &[]).unwrap(); + let setup = conn.get_setup(); + let screen = setup.roots().nth(screen_num as usize).unwrap(); + get_default_x_display(&conn, screen).unwrap() } else { panic!("Unsupported platform. Could not detect Wayland or X11 displays") } } + +pub fn get_target_dimensions(target: &Target) -> (u64, u64) { + match target { + Target::Window(_w) => (0, 0), // TODO + Target::Display(d) => (d.width as u64, d.height as u64), + } +} diff --git a/src/targets/mod.rs b/src/targets/mod.rs index 0cdbf0e..c783092 100644 --- a/src/targets/mod.rs +++ b/src/targets/mod.rs @@ -5,7 +5,7 @@ mod mac; mod win; #[cfg(target_os = "linux")] -mod linux; +pub(crate) mod linux; #[derive(Debug, Clone)] pub struct Window { @@ -82,7 +82,7 @@ pub fn get_main_display() -> Display { return win::get_main_display(); #[cfg(target_os = "linux")] - unreachable!(); + return linux::get_main_display(); } pub fn get_target_dimensions(target: &Target) -> (u64, u64) { @@ -93,5 +93,5 @@ pub fn get_target_dimensions(target: &Target) -> (u64, u64) { return win::get_target_dimensions(target); #[cfg(target_os = "linux")] - unreachable!(); + return linux::get_target_dimensions(target); } From 8b769c6b0264519ff32545fbf43629c0c1066ab5 Mon Sep 17 00:00:00 2001 From: Marcus Lian Hanestad Date: Tue, 29 Oct 2024 17:22:22 +0100 Subject: [PATCH 6/9] x11: better error handling --- src/capturer/engine/linux/mod.rs | 7 +- src/capturer/engine/linux/wayland/mod.rs | 7 +- src/capturer/engine/linux/x11/mod.rs | 98 ++++++++++++------------ src/targets/linux/mod.rs | 43 +++++------ 4 files changed, 73 insertions(+), 82 deletions(-) diff --git a/src/capturer/engine/linux/mod.rs b/src/capturer/engine/linux/mod.rs index 34f8c0c..25122b4 100644 --- a/src/capturer/engine/linux/mod.rs +++ b/src/capturer/engine/linux/mod.rs @@ -3,10 +3,7 @@ use std::{env, sync::mpsc}; use wayland::WaylandCapturer; use x11::X11Capturer; -use crate::{ - capturer::Options, - frame::Frame, -}; +use crate::{capturer::Options, frame::Frame}; mod error; @@ -34,7 +31,7 @@ impl LinuxCapturer { } else if env::var("DISPLAY").is_ok() { println!("[DEBUG] On X11"); return Self { - imp: Box::new(X11Capturer::new(options, tx)), + imp: Box::new(X11Capturer::new(options, tx).unwrap()), }; } else { panic!("Unsupported platform. Could not detect Wayland or X11 displays") diff --git a/src/capturer/engine/linux/wayland/mod.rs b/src/capturer/engine/linux/wayland/mod.rs index ed5164b..a322d14 100644 --- a/src/capturer/engine/linux/wayland/mod.rs +++ b/src/capturer/engine/linux/wayland/mod.rs @@ -29,9 +29,12 @@ use pw::{ stream::{StreamRef, StreamState}, }; -use crate::{capturer::Options, frame::{BGRxFrame, Frame, RGBFrame, RGBxFrame, XBGRFrame}}; +use crate::{ + capturer::Options, + frame::{BGRxFrame, Frame, RGBFrame, RGBxFrame, XBGRFrame}, +}; -use self::{portal::ScreenCastPortal}; +use self::portal::ScreenCastPortal; use super::{error::LinCapError, LinuxCapturerImpl}; diff --git a/src/capturer/engine/linux/x11/mod.rs b/src/capturer/engine/linux/x11/mod.rs index b8973b8..52a0590 100644 --- a/src/capturer/engine/linux/x11/mod.rs +++ b/src/capturer/engine/linux/x11/mod.rs @@ -3,7 +3,8 @@ use std::{ atomic::{AtomicU8, Ordering}, mpsc::Sender, Arc, - }, thread::JoinHandle + }, + thread::JoinHandle, }; use xcb::x; @@ -13,20 +14,26 @@ use crate::{capturer::Options, frame::Frame, targets::linux::get_default_x_displ use super::{error::LinCapError, LinuxCapturerImpl}; pub struct X11Capturer { - capturer_join_handle: Option>>, + capturer_join_handle: Option>>, capturer_state: Arc, } impl X11Capturer { - pub fn new(options: &Options, tx: Sender) -> Self { + pub fn new(options: &Options, tx: Sender) -> Result { let (conn, screen_num) = - xcb::Connection::connect_with_extensions(None, &[xcb::Extension::RandR], &[]).unwrap(); + xcb::Connection::connect_with_extensions(None, &[xcb::Extension::RandR], &[]) + .map_err(|e| LinCapError::new(e.to_string()))?; let setup = conn.get_setup(); - let screen = setup.roots().nth(screen_num as usize).unwrap(); + let Some(screen) = setup.roots().nth(screen_num as usize) else { + return Err(LinCapError::new(String::from("Failed to get setup root"))); + }; let target = match &options.target { Some(t) => t.clone(), - None => Target::Display(get_default_x_display(&conn, screen).unwrap()), + None => Target::Display( + get_default_x_display(&conn, screen) + .map_err(|e| LinCapError::new(e.to_string()))?, + ), }; let framerate = options.fps as f32; @@ -41,55 +48,44 @@ impl X11Capturer { let frame_time = std::time::Duration::from_secs_f32(1.0 / framerate); while capturer_state_clone.load(Ordering::Acquire) == 1 { let start = std::time::Instant::now(); - match &target { + let (x, y, width, height, window) = match &target { Target::Window(win) => { let geom_cookie = conn.send_request(&x::GetGeometry { drawable: x::Drawable::Window(win.raw_handle), }); - let geom = conn.wait_for_reply(geom_cookie).unwrap(); - - let img_cookie = conn.send_request(&x::GetImage { - format: x::ImageFormat::ZPixmap, - drawable: x::Drawable::Window(win.raw_handle), - x: 0, - y: 0, - width: geom.width(), - height: geom.height(), - plane_mask: u32::MAX, - }); - let img = conn.wait_for_reply(img_cookie).unwrap(); - - let img_data = img.data(); - - tx.send(Frame::BGRx(crate::frame::BGRxFrame { - display_time: 0, - width: geom.width() as i32, - height: geom.height() as i32, - data: img_data.to_vec(), - })).unwrap(); + let geom = conn.wait_for_reply(geom_cookie)?; + (0, 0, geom.width(), geom.height(), win.raw_handle) } - Target::Display(disp) => { - let img_cookie = conn.send_request(&x::GetImage { - format: x::ImageFormat::ZPixmap, - drawable: x::Drawable::Window(disp.raw_handle), - x: disp.x_offset, - y: disp.y_offset, - width: disp.width, - height: disp.height, - plane_mask: u32::MAX, - }); - let img = conn.wait_for_reply(img_cookie).unwrap(); - - let img_data = img.data(); + Target::Display(disp) => ( + disp.x_offset, + disp.y_offset, + disp.width, + disp.height, + disp.raw_handle, + ), + }; + + let img_cookie = conn.send_request(&x::GetImage { + format: x::ImageFormat::ZPixmap, + drawable: x::Drawable::Window(window), + x: x, + y: y, + width: width, + height: height, + plane_mask: u32::MAX, + }); + let img = conn.wait_for_reply(img_cookie)?; + + let img_data = img.data(); + + tx.send(Frame::BGRx(crate::frame::BGRxFrame { + display_time: 0, + width: width as i32, + height: height as i32, + data: img_data.to_vec(), + })) + .unwrap(); - tx.send(Frame::BGRx(crate::frame::BGRxFrame { - display_time: 0, - width: disp.width as i32, - height: disp.height as i32, - data: img_data.to_vec(), - })).unwrap(); - } - } let elapsed = start.elapsed(); if elapsed < frame_time { std::thread::sleep(frame_time - start.elapsed()); @@ -99,10 +95,10 @@ impl X11Capturer { Ok(()) }); - Self { + Ok(Self { capturer_state: capturer_state, capturer_join_handle: Some(jh), - } + }) } } diff --git a/src/targets/linux/mod.rs b/src/targets/linux/mod.rs index fedb7b2..1b071dc 100644 --- a/src/targets/linux/mod.rs +++ b/src/targets/linux/mod.rs @@ -82,10 +82,8 @@ fn decode_compound_text( } fn get_x11_targets() -> Result, xcb::Error> { - let (conn, _screen_num) = xcb::Connection::connect_with_xlib_display_and_extensions( - &[xcb::Extension::RandR], - &[], - )?; + let (conn, _screen_num) = + xcb::Connection::connect_with_xlib_display_and_extensions(&[xcb::Extension::RandR], &[])?; let setup = conn.get_setup(); let screens = setup.roots(); @@ -99,12 +97,10 @@ fn get_x11_targets() -> Result, xcb::Error> { let mut targets = Vec::new(); for screen in screens { - let window_list = - get_property(&conn, screen.root(), wm_client_list, x::ATOM_NONE, 100)?; + let window_list = get_property(&conn, screen.root(), wm_client_list, x::ATOM_NONE, 100)?; for client in window_list.value::() { - let cr = - get_property(&conn, *client, atom_net_wm_name, x::ATOM_STRING, 4096)?; + let cr = get_property(&conn, *client, atom_net_wm_name, x::ATOM_STRING, 4096)?; if !cr.value::().is_empty() { targets.push(Target::Window(crate::targets::Window { id: 0, @@ -115,22 +111,19 @@ fn get_x11_targets() -> Result, xcb::Error> { continue; } - let reply = - get_property(&conn, *client, x::ATOM_WM_NAME, x::ATOM_ANY, 4096)?; + let reply = get_property(&conn, *client, x::ATOM_WM_NAME, x::ATOM_ANY, 4096)?; let value: &[u8] = reply.value(); if !value.is_empty() { let ttype = reply.r#type(); - let title = if ttype == x::ATOM_STRING - || ttype == atom_utf8_string - || ttype == atom_text - { - String::from_utf8(reply.value().to_vec()).unwrap_or(String::from("n/a")) - } else if ttype == atom_compound_text { - decode_compound_text(&conn, value, client, ttype) - .map_err(|_| xcb::Error::Connection(xcb::ConnError::ClosedParseErr))? - } else { - String::from_utf8(reply.value().to_vec()).unwrap_or(String::from("n/a")) - }; + let title = + if ttype == x::ATOM_STRING || ttype == atom_utf8_string || ttype == atom_text { + String::from_utf8(reply.value().to_vec()).unwrap_or(String::from("n/a")) + } else if ttype == atom_compound_text { + decode_compound_text(&conn, value, client, ttype) + .map_err(|_| xcb::Error::Connection(xcb::ConnError::ClosedParseErr))? + } else { + String::from_utf8(reply.value().to_vec()).unwrap_or(String::from("n/a")) + }; targets.push(Target::Window(crate::targets::Window { id: 0, @@ -163,8 +156,7 @@ fn get_x11_targets() -> Result, xcb::Error> { config_timestamp: 0, }); let crtc_info = conn.wait_for_reply(crtc_info)?; - let title = - String::from_utf8(info.name().to_vec()).unwrap_or(String::from("n/a")); + let title = String::from_utf8(info.name().to_vec()).unwrap_or(String::from("n/a")); targets.push(Target::Display(crate::targets::Display { id: crtc.resource_id(), title, @@ -192,7 +184,10 @@ pub fn get_all_targets() -> Vec { } } -pub(crate) fn get_default_x_display(conn: &xcb::Connection, screen: &Screen) -> Result { +pub(crate) fn get_default_x_display( + conn: &xcb::Connection, + screen: &Screen, +) -> Result { let primary_display_cookie = conn.send_request(&GetOutputPrimary { window: screen.root(), }); From 61b921ec0c34d4f1e27eedc5ee7e13e8aa1b549f Mon Sep 17 00:00:00 2001 From: Marcus Lian Hanestad Date: Wed, 30 Oct 2024 12:27:49 +0100 Subject: [PATCH 7/9] x11: cursor drawing --- Cargo.toml | 2 +- src/capturer/engine/linux/x11/mod.rs | 214 +++++++++++++++++++++------ 2 files changed, 173 insertions(+), 43 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fdceab7..e2a29ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,5 +39,5 @@ objc = "0.2.7" pipewire = "0.8.0" dbus = "0.9.7" rand = "0.8.5" -xcb = { version = "1.4.0", features = ["randr", "xlib_xcb"] } +xcb = { version = "1.4.0", features = ["randr", "xlib_xcb", "xfixes"] } x11 = "2.21.0" \ No newline at end of file diff --git a/src/capturer/engine/linux/x11/mod.rs b/src/capturer/engine/linux/x11/mod.rs index 52a0590..e363ef5 100644 --- a/src/capturer/engine/linux/x11/mod.rs +++ b/src/capturer/engine/linux/x11/mod.rs @@ -7,7 +7,7 @@ use std::{ thread::JoinHandle, }; -use xcb::x; +use xcb::{x, Xid}; use crate::{capturer::Options, frame::Frame, targets::linux::get_default_x_display, Target}; @@ -18,11 +18,174 @@ pub struct X11Capturer { capturer_state: Arc, } +fn draw_cursor( + conn: &xcb::Connection, + img: &mut [u8], + win_x: i16, + win_y: i16, + win_width: i16, + win_height: i16, + is_win: bool, + win: &xcb::x::Window, +) -> Result<(), xcb::Error> { + let cursor_image_cookie = conn.send_request(&xcb::xfixes::GetCursorImage {}); + let cursor_image = conn.wait_for_reply(cursor_image_cookie)?; + + let win_x = win_x as i32; + let win_y = win_y as i32; + + let win_width = win_width as i32; + let win_height = win_height as i32; + + let mut cursor_x = cursor_image.x() as i32 - cursor_image.xhot() as i32; + let mut cursor_y = cursor_image.y() as i32 - cursor_image.yhot() as i32; + if is_win { + let disp = conn.get_raw_dpy(); + let mut ncursor_x = 0; + let mut ncursor_y = 0; + let mut child_return = 0; + if unsafe { + x11::xlib::XTranslateCoordinates( + disp, + x11::xlib::XDefaultRootWindow(disp), + win.resource_id() as u64, + cursor_image.x() as i32, + cursor_image.y() as i32, + &mut ncursor_x, + &mut ncursor_y, + &mut child_return, + ) + } == 0 + { + return Ok(()); + } + cursor_x = ncursor_x as i32 - cursor_image.xhot() as i32; + cursor_y = ncursor_y as i32 - cursor_image.yhot() as i32; + } + + if cursor_x >= win_width + win_x + || cursor_y >= win_height + win_y + || cursor_x < win_x + || cursor_y < win_y + { + return Ok(()); + } + + let x = cursor_x.max(win_x); + let y = cursor_y.max(win_y); + + let w = ((cursor_x + cursor_image.width() as i32).min(win_x + win_width) - x) as u32; + let h = ((cursor_y + cursor_image.height() as i32).min(win_y + win_height) - y) as u32; + + let c_off = (x - cursor_x) as u32; + let i_off: i32 = x - win_x; + + let stride: u32 = 4; + let mut cursor_idx: u32 = ((y - cursor_y) * cursor_image.width() as i32) as u32; + let mut image_idx: u32 = ((y - win_y) * win_width * stride as i32) as u32; + + for y in 0..h { + cursor_idx += c_off; + image_idx += i_off as u32 * stride; + for x in 0..w { + let cursor_pix = cursor_image.cursor_image()[cursor_idx as usize]; + let r = (cursor_pix & 0xFF) as u8; + let g = ((cursor_pix >> 8) & 0xFF) as u8; + let b = ((cursor_pix >> 16) & 0xFF) as u8; + let a = ((cursor_pix >> 24) & 0xFF); + + let i = image_idx as usize; + if a == 0xFF { + img[i] = r; + img[i + 1] = g; + img[i + 2] = b; + } else if a > 0 { + let a = 255 - a; + img[i] = r + ((img[i] as u32 * a + 255 / 2) / 255) as u8; + img[i + 1] = g + ((img[i + 1] as u32 * a + 255 / 2) / 255) as u8; + img[i + 2] = b + ((img[i + 2] as u32 * a + 255 / 2) / 255) as u8; + } + + cursor_idx += 1; + image_idx += stride; + } + cursor_idx += cursor_image.width() as u32 - w as u32 - c_off as u32; + image_idx += (win_width - w as i32 - i_off) as u32 * stride; + } + + Ok(()) +} + +fn grab(conn: &xcb::Connection, target: &Target, show_cursor: bool) -> Result { + let (x, y, width, height, window, is_win) = match &target { + Target::Window(win) => { + let geom_cookie = conn.send_request(&x::GetGeometry { + drawable: x::Drawable::Window(win.raw_handle), + }); + let geom = conn.wait_for_reply(geom_cookie)?; + (0, 0, geom.width(), geom.height(), win.raw_handle, true) + } + Target::Display(disp) => ( + disp.x_offset, + disp.y_offset, + disp.width, + disp.height, + disp.raw_handle, + false, + ), + }; + + let img_cookie = conn.send_request(&x::GetImage { + format: x::ImageFormat::ZPixmap, + drawable: x::Drawable::Window(window), + x, + y, + width, + height, + plane_mask: u32::MAX, + }); + let img = conn.wait_for_reply(img_cookie)?; + + let mut img_data = img.data().to_vec(); + + if show_cursor { + draw_cursor( + &conn, + &mut img_data, + x, + y, + width as i16, + height as i16, + is_win, + &window, + )?; + } + + Ok(Frame::BGRx(crate::frame::BGRxFrame { + display_time: 0, + width: width as i32, + height: height as i32, + data: img_data, + })) +} + +fn query_xfixes_version(conn: &xcb::Connection) -> Result<(), xcb::Error> { + let cookie = conn.send_request(&xcb::xfixes::QueryVersion { + client_major_version: xcb::xfixes::MAJOR_VERSION, + client_minor_version: xcb::xfixes::MINOR_VERSION, + }); + let _ = conn.wait_for_reply(cookie)?; + Ok(()) +} + impl X11Capturer { pub fn new(options: &Options, tx: Sender) -> Result { - let (conn, screen_num) = - xcb::Connection::connect_with_extensions(None, &[xcb::Extension::RandR], &[]) - .map_err(|e| LinCapError::new(e.to_string()))?; + let (conn, screen_num) = xcb::Connection::connect_with_xlib_display_and_extensions( + &[xcb::Extension::RandR, xcb::Extension::XFixes], + &[], + ) + .map_err(|e| LinCapError::new(e.to_string()))?; + query_xfixes_version(&conn).map_err(|e| LinCapError::new(e.to_string()))?; let setup = conn.get_setup(); let Some(screen) = setup.roots().nth(screen_num as usize) else { return Err(LinCapError::new(String::from("Failed to get setup root"))); @@ -37,6 +200,7 @@ impl X11Capturer { }; let framerate = options.fps as f32; + let show_cursor = options.show_cursor; let capturer_state = Arc::new(AtomicU8::new(0)); let capturer_state_clone = Arc::clone(&capturer_state); @@ -48,43 +212,9 @@ impl X11Capturer { let frame_time = std::time::Duration::from_secs_f32(1.0 / framerate); while capturer_state_clone.load(Ordering::Acquire) == 1 { let start = std::time::Instant::now(); - let (x, y, width, height, window) = match &target { - Target::Window(win) => { - let geom_cookie = conn.send_request(&x::GetGeometry { - drawable: x::Drawable::Window(win.raw_handle), - }); - let geom = conn.wait_for_reply(geom_cookie)?; - (0, 0, geom.width(), geom.height(), win.raw_handle) - } - Target::Display(disp) => ( - disp.x_offset, - disp.y_offset, - disp.width, - disp.height, - disp.raw_handle, - ), - }; - - let img_cookie = conn.send_request(&x::GetImage { - format: x::ImageFormat::ZPixmap, - drawable: x::Drawable::Window(window), - x: x, - y: y, - width: width, - height: height, - plane_mask: u32::MAX, - }); - let img = conn.wait_for_reply(img_cookie)?; - - let img_data = img.data(); - - tx.send(Frame::BGRx(crate::frame::BGRxFrame { - display_time: 0, - width: width as i32, - height: height as i32, - data: img_data.to_vec(), - })) - .unwrap(); + + let frame = grab(&conn, &target, show_cursor)?; + tx.send(frame).unwrap(); let elapsed = start.elapsed(); if elapsed < frame_time { @@ -96,7 +226,7 @@ impl X11Capturer { }); Ok(Self { - capturer_state: capturer_state, + capturer_state, capturer_join_handle: Some(jh), }) } From 89db0dd9b1a3574b83b1009e8df8f8f5602aec1a Mon Sep 17 00:00:00 2001 From: Marcus Lian Hanestad Date: Wed, 30 Oct 2024 13:40:13 +0100 Subject: [PATCH 8/9] x11: change inappropriate var name --- src/targets/linux/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/targets/linux/mod.rs b/src/targets/linux/mod.rs index 1b071dc..d721e6f 100644 --- a/src/targets/linux/mod.rs +++ b/src/targets/linux/mod.rs @@ -45,7 +45,7 @@ fn decode_compound_text( assert!(!display.is_null()); let c_string = CString::new(value.to_vec())?; - let mut fuck = XTextProperty { + let mut text_prop = XTextProperty { value: std::ptr::null_mut(), encoding: 0, format: 0, @@ -55,11 +55,11 @@ fn decode_compound_text( XGetTextProperty( display, client.resource_id() as u64, - &mut fuck, + &mut text_prop, x::ATOM_WM_NAME.resource_id() as u64, ) }; - if res == 0 || fuck.nitems == 0 { + if res == 0 || text_prop.nitems == 0 { return Ok(String::from("n/a")); } @@ -67,7 +67,7 @@ fn decode_compound_text( value: c_string.as_ptr() as *mut u8, encoding: ttype.resource_id() as u64, format: 8, - nitems: fuck.nitems, + nitems: text_prop.nitems, }; let mut list: *mut *mut i8 = std::ptr::null_mut(); let mut count: i32 = 0; From 8c8c0025f0a36dd605b2e3a2a069b973ca138823 Mon Sep 17 00:00:00 2001 From: Marcus Lian Hanestad Date: Sat, 2 Nov 2024 21:30:26 +0100 Subject: [PATCH 9/9] x11: set the display time for frames --- src/capturer/engine/linux/x11/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/capturer/engine/linux/x11/mod.rs b/src/capturer/engine/linux/x11/mod.rs index e363ef5..879216d 100644 --- a/src/capturer/engine/linux/x11/mod.rs +++ b/src/capturer/engine/linux/x11/mod.rs @@ -162,7 +162,10 @@ fn grab(conn: &xcb::Connection, target: &Target, show_cursor: bool) -> Result