diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index 01cb82429..f20b976c0 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -9,6 +9,9 @@ license = "MIT" authors = ["Ram "] edition = "2018" +[features] +wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing"] + [dependencies] block = "0.1" cfg-if = "1.0" @@ -18,7 +21,7 @@ num_cpus = "1.13" [dependencies.winapi] version = "0.3" default-features = true -features = ["dxgi", "dxgi1_2", "dxgi1_5", "d3d11"] +features = ["dxgi", "dxgi1_2", "dxgi1_5", "d3d11", "winuser"] [dev-dependencies] repng = "0.2" @@ -30,3 +33,10 @@ quest = "0.3" [build-dependencies] target_build_utils = "0.3" bindgen = "0.53" + +[target.'cfg(target_os = "linux")'.dependencies] +dbus = { version = "0.9", optional = true } +tracing = { version = "0.1", optional = true } +gstreamer = { version = "0.16", optional = true } +gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true } +gstreamer-video = { version = "0.16", optional = true } diff --git a/libs/scrap/examples/screenshot.rs b/libs/scrap/examples/screenshot.rs index dee410b0d..e2da3b3d8 100644 --- a/libs/scrap/examples/screenshot.rs +++ b/libs/scrap/examples/screenshot.rs @@ -23,6 +23,10 @@ fn record(i: usize) { let one_second = Duration::new(1, 0); let one_frame = one_second / 60; + for d in Display::all().unwrap() { + println!("{:?} {} {}", d.origin(), d.width(), d.height()); + } + let display = get_display(i); let mut capturer = Capturer::new(display, false).expect("Couldn't begin capture."); let (w, h) = (capturer.width(), capturer.height()); diff --git a/libs/scrap/src/common/linux.rs b/libs/scrap/src/common/linux.rs new file mode 100644 index 000000000..50bab092c --- /dev/null +++ b/libs/scrap/src/common/linux.rs @@ -0,0 +1,117 @@ +use crate::common::{ + wayland, + x11::{self, Frame}, +}; +use std::io; + +pub enum Capturer { + X11(x11::Capturer), + WAYLAND(wayland::Capturer), +} + +impl Capturer { + pub fn new(display: Display, yuv: bool) -> io::Result { + Ok(match display { + Display::X11(d) => Capturer::X11(x11::Capturer::new(d, yuv)?), + Display::WAYLAND(d) => Capturer::WAYLAND(wayland::Capturer::new(d, yuv)?), + }) + } + + pub fn width(&self) -> usize { + match self { + Capturer::X11(d) => d.width(), + Capturer::WAYLAND(d) => d.width(), + } + } + + pub fn height(&self) -> usize { + match self { + Capturer::X11(d) => d.height(), + Capturer::WAYLAND(d) => d.height(), + } + } + + pub fn frame<'a>(&'a mut self, timeout_ms: u32) -> io::Result> { + match self { + Capturer::X11(d) => d.frame(timeout_ms), + Capturer::WAYLAND(d) => d.frame(timeout_ms), + } + } +} + +pub enum Display { + X11(x11::Display), + WAYLAND(wayland::Display), +} + +#[inline] +fn is_wayland() -> bool { + std::env::var("IS_WAYLAND").is_ok() + || std::env::var("XDG_SESSION_TYPE") == Ok("wayland".to_owned()) +} + +impl Display { + pub fn primary() -> io::Result { + Ok(if is_wayland() { + Display::WAYLAND(wayland::Display::primary()?) + } else { + Display::X11(x11::Display::primary()?) + }) + } + + pub fn all() -> io::Result> { + Ok(if is_wayland() { + wayland::Display::all()? + .drain(..) + .map(|x| Display::WAYLAND(x)) + .collect() + } else { + x11::Display::all()? + .drain(..) + .map(|x| Display::X11(x)) + .collect() + }) + } + + pub fn width(&self) -> usize { + match self { + Display::X11(d) => d.width(), + Display::WAYLAND(d) => d.width(), + } + } + + pub fn height(&self) -> usize { + match self { + Display::X11(d) => d.height(), + Display::WAYLAND(d) => d.height(), + } + } + + pub fn origin(&self) -> (i32, i32) { + match self { + Display::X11(d) => d.origin(), + Display::WAYLAND(d) => d.origin(), + } + } + + pub fn is_online(&self) -> bool { + match self { + Display::X11(d) => d.is_online(), + Display::WAYLAND(d) => d.is_online(), + } + } + + pub fn is_primary(&self) -> bool { + match self { + Display::X11(d) => d.is_primary(), + Display::WAYLAND(d) => d.is_primary(), + } + } + + pub fn name(&self) -> String { + match self { + Display::X11(d) => d.name(), + Display::WAYLAND(d) => d.name(), + } + } +} diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 2671c0bd0..ef709c0af 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -5,8 +5,17 @@ cfg_if! { mod quartz; pub use self::quartz::*; } else if #[cfg(x11)] { + cfg_if! { + if #[cfg(feature="wayland")] { + mod linux; + mod wayland; mod x11; - pub use self::x11::*; + pub use self::linux::*; + } else { + mod x11; + pub use self::x11::*; + } + } } else if #[cfg(dxgi)] { mod dxgi; pub use self::dxgi::*; diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs new file mode 100644 index 000000000..ff6bf8022 --- /dev/null +++ b/libs/scrap/src/common/wayland.rs @@ -0,0 +1,81 @@ +use crate::common::x11::Frame; +use crate::wayland::{capturable::*, *}; +use std::io; + +pub struct Capturer(Display, Box, bool, Vec); + +fn map_err(err: E) -> io::Error { + io::Error::new(io::ErrorKind::Other, err.to_string()) +} + +impl Capturer { + pub fn new(display: Display, yuv: bool) -> io::Result { + let r = display.0.recorder(false).map_err(map_err)?; + Ok(Capturer(display, r, yuv, Default::default())) + } + + pub fn width(&self) -> usize { + self.0.width() + } + + pub fn height(&self) -> usize { + self.0.height() + } + + pub fn frame<'a>(&'a mut self, timeout_ms: u32) -> io::Result> { + match self.1.capture(timeout_ms as _).map_err(map_err)? { + PixelProvider::BGR0(w, h, x) => Ok(Frame(if self.2 { + crate::common::bgra_to_i420(w as _, h as _, &x, &mut self.3); + &self.3[..] + } else { + x + })), + PixelProvider::NONE => Err(std::io::ErrorKind::WouldBlock.into()), + _ => Err(map_err("Invalid data")), + } + } +} + +pub struct Display(pipewire::PipeWireCapturable); + +impl Display { + pub fn primary() -> io::Result { + let mut all = Display::all()?; + if all.is_empty() { + return Err(io::ErrorKind::NotFound.into()); + } + Ok(all.remove(0)) + } + + pub fn all() -> io::Result> { + Ok(pipewire::get_capturables(false) + .map_err(map_err)? + .drain(..) + .map(|x| Display(x)) + .collect()) + } + + pub fn width(&self) -> usize { + self.0.size.0 + } + + pub fn height(&self) -> usize { + self.0.size.1 + } + + pub fn origin(&self) -> (i32, i32) { + self.0.position + } + + pub fn is_online(&self) -> bool { + true + } + + pub fn is_primary(&self) -> bool { + false + } + + pub fn name(&self) -> String { + "".to_owned() + } +} diff --git a/libs/scrap/src/common/x11.rs b/libs/scrap/src/common/x11.rs index 3eab43c1c..e9640cbbf 100644 --- a/libs/scrap/src/common/x11.rs +++ b/libs/scrap/src/common/x11.rs @@ -21,7 +21,7 @@ impl Capturer { } } -pub struct Frame<'a>(&'a [u8]); +pub struct Frame<'a>(pub(crate) &'a [u8]); impl<'a> ops::Deref for Frame<'a> { type Target = [u8]; diff --git a/libs/scrap/src/lib.rs b/libs/scrap/src/lib.rs index 6274a0801..8db2a5788 100644 --- a/libs/scrap/src/lib.rs +++ b/libs/scrap/src/lib.rs @@ -14,6 +14,9 @@ pub mod quartz; #[cfg(x11)] pub mod x11; +#[cfg(all(x11, feature="wayland"))] +pub mod wayland; + #[cfg(dxgi)] pub mod dxgi; diff --git a/libs/scrap/src/wayland.rs b/libs/scrap/src/wayland.rs new file mode 100644 index 000000000..82b219306 --- /dev/null +++ b/libs/scrap/src/wayland.rs @@ -0,0 +1,3 @@ +pub mod pipewire; +mod pipewire_dbus; +pub mod capturable; diff --git a/libs/scrap/src/wayland/README.md b/libs/scrap/src/wayland/README.md new file mode 100644 index 000000000..a3ca53344 --- /dev/null +++ b/libs/scrap/src/wayland/README.md @@ -0,0 +1,10 @@ +# About + +Derived from https://github.com/H-M-H/Weylus/tree/master/src/capturable with the author's consent, https://github.com/rustdesk/rustdesk/issues/56#issuecomment-882727967 + +# Dep + +Works fine on Ubuntu 21.04 with pipewire 3 and xdg-desktop-portal 1.8 +` +apt install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev +` diff --git a/libs/scrap/src/wayland/capturable.rs b/libs/scrap/src/wayland/capturable.rs new file mode 100644 index 000000000..05a5ec71d --- /dev/null +++ b/libs/scrap/src/wayland/capturable.rs @@ -0,0 +1,58 @@ +use std::boxed::Box; +use std::error::Error; + +pub enum PixelProvider<'a> { + // 8 bits per color + RGB(usize, usize, &'a [u8]), + BGR0(usize, usize, &'a [u8]), + // width, height, stride + BGR0S(usize, usize, usize, &'a [u8]), + NONE, +} + +impl<'a> PixelProvider<'a> { + pub fn size(&self) -> (usize, usize) { + match self { + PixelProvider::RGB(w, h, _) => (*w, *h), + PixelProvider::BGR0(w, h, _) => (*w, *h), + PixelProvider::BGR0S(w, h, _, _) => (*w, *h), + PixelProvider::NONE => (0, 0), + } + } +} + +pub trait Recorder { + fn capture(&mut self, timeout_ms: u64) -> Result>; +} + +pub trait BoxCloneCapturable { + fn box_clone(&self) -> Box; +} + +impl BoxCloneCapturable for T +where + T: Clone + Capturable + 'static, +{ + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } +} + +pub trait Capturable: Send + BoxCloneCapturable { + /// Name of the Capturable, for example the window title, if it is a window. + fn name(&self) -> String; + /// Return x, y, width, height of the Capturable as floats relative to the absolute size of the + /// screen. For example x=0.5, y=0.0, width=0.5, height=1.0 means the right half of the screen. + fn geometry_relative(&self) -> Result<(f64, f64, f64, f64), Box>; + /// Callback that is called right before input is simulated. + /// Useful to focus the window on input. + fn before_input(&mut self) -> Result<(), Box>; + /// Return a Recorder that can record the current capturable. + fn recorder(&self, capture_cursor: bool) -> Result, Box>; +} + +impl Clone for Box { + fn clone(&self) -> Self { + self.box_clone() + } +} diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs new file mode 100644 index 000000000..844e8eead --- /dev/null +++ b/libs/scrap/src/wayland/pipewire.rs @@ -0,0 +1,530 @@ +use std::collections::HashMap; +use std::error::Error; +use std::os::unix::io::AsRawFd; +use std::sync::{atomic::AtomicBool, Arc, Mutex}; +use std::time::Duration; +use tracing::{debug, trace, warn}; + +use dbus::{ + arg::{OwnedFd, PropMap, RefArg, Variant}, + blocking::{Proxy, SyncConnection}, + message::{MatchRule, MessageType}, + Message, +}; + +use gstreamer as gst; +use gstreamer::prelude::*; +use gstreamer_app::AppSink; + +use super::capturable::PixelProvider; +use super::capturable::{Capturable, Recorder}; + +use super::pipewire_dbus::{OrgFreedesktopPortalRequestResponse, OrgFreedesktopPortalScreenCast}; + +#[derive(Debug, Clone, Copy)] +struct PwStreamInfo { + path: u64, + source_type: u64, + position: (i32, i32), + size: (usize, usize), +} + +#[derive(Debug)] +pub struct DBusError(String); + +impl std::fmt::Display for DBusError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self(s) = self; + write!(f, "{}", s) + } +} + +impl Error for DBusError {} + +#[derive(Debug)] +pub struct GStreamerError(String); + +impl std::fmt::Display for GStreamerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self(s) = self; + write!(f, "{}", s) + } +} + +impl Error for GStreamerError {} + +#[derive(Clone)] +pub struct PipeWireCapturable { + // connection needs to be kept alive for recording + dbus_conn: Arc, + fd: OwnedFd, + path: u64, + source_type: u64, + pub position: (i32, i32), + pub size: (usize, usize), +} + +impl PipeWireCapturable { + fn new(conn: Arc, fd: OwnedFd, stream: PwStreamInfo) -> Self { + Self { + dbus_conn: conn, + fd, + path: stream.path, + source_type: stream.source_type, + position: stream.position, + size: stream.size, + } + } +} + +impl std::fmt::Debug for PipeWireCapturable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "PipeWireCapturable {{dbus: {}, fd: {}, path: {}, source_type: {}}}", + self.dbus_conn.unique_name(), + self.fd.as_raw_fd(), + self.path, + self.source_type + ) + } +} + +impl Capturable for PipeWireCapturable { + fn name(&self) -> String { + let type_str = match self.source_type { + 1 => "Desktop", + 2 => "Window", + _ => "Unknow", + }; + format!("Pipewire {}, path: {}", type_str, self.path) + } + + fn geometry_relative(&self) -> Result<(f64, f64, f64, f64), Box> { + Ok((0.0, 0.0, 1.0, 1.0)) + } + + fn before_input(&mut self) -> Result<(), Box> { + Ok(()) + } + + fn recorder(&self, _capture_cursor: bool) -> Result, Box> { + Ok(Box::new(PipeWireRecorder::new(self.clone())?)) + } +} + +pub struct PipeWireRecorder { + buffer: Option>, + buffer_cropped: Vec, + is_cropped: bool, + pipeline: gst::Pipeline, + appsink: AppSink, + width: usize, + height: usize, +} + +impl PipeWireRecorder { + pub fn new(capturable: PipeWireCapturable) -> Result> { + let pipeline = gst::Pipeline::new(None); + + let src = gst::ElementFactory::make("pipewiresrc", None)?; + src.set_property("fd", &capturable.fd.as_raw_fd())?; + src.set_property("path", &format!("{}", capturable.path))?; + + // For some reason pipewire blocks on destruction of AppSink if this is not set to true, + // see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982 + src.set_property("always-copy", &true)?; + + let sink = gst::ElementFactory::make("appsink", None)?; + sink.set_property("drop", &true)?; + sink.set_property("max-buffers", &1u32)?; + + pipeline.add_many(&[&src, &sink])?; + src.link(&sink)?; + let appsink = sink + .dynamic_cast::() + .map_err(|_| GStreamerError("Sink element is expected to be an appsink!".into()))?; + appsink.set_caps(Some(&gst::Caps::new_simple( + "video/x-raw", + &[("format", &"BGRx")], + ))); + + pipeline.set_state(gst::State::Playing)?; + Ok(Self { + pipeline, + appsink, + buffer: None, + width: 0, + height: 0, + buffer_cropped: vec![], + is_cropped: false, + }) + } +} + +impl Recorder for PipeWireRecorder { + fn capture(&mut self, timeout_ms: u64) -> Result> { + if let Some(sample) = self + .appsink + .try_pull_sample(gst::ClockTime::from_mseconds(timeout_ms)) + { + let cap = sample + .get_caps() + .ok_or("Failed get caps")? + .get_structure(0) + .ok_or("Failed to get structure")?; + let w: i32 = cap.get_value("width")?.get_some()?; + let h: i32 = cap.get_value("height")?.get_some()?; + let w = w as usize; + let h = h as usize; + let buf = sample + .get_buffer_owned() + .ok_or_else(|| GStreamerError("Failed to get owned buffer.".into()))?; + let mut crop = buf + .get_meta::() + .map(|m| m.get_rect()); + // only crop if necessary + if Some((0, 0, w as u32, h as u32)) == crop { + crop = None; + } + let buf = buf + .into_mapped_buffer_readable() + .map_err(|_| GStreamerError("Failed to map buffer.".into()))?; + let buf_size = buf.get_size(); + // BGRx is 4 bytes per pixel + if buf_size != (w * h * 4) { + // for some reason the width and height of the caps do not guarantee correct buffer + // size, so ignore those buffers, see: + // https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/985 + trace!( + "Size of mapped buffer: {} does NOT match size of capturable {}x{}@BGRx, \ + dropping it!", + buf_size, + w, + h + ); + } else { + // Copy region specified by crop into self.buffer_cropped + // TODO: Figure out if ffmpeg provides a zero copy alternative + if let Some((x_off, y_off, w_crop, h_crop)) = crop { + let x_off = x_off as usize; + let y_off = y_off as usize; + let w_crop = w_crop as usize; + let h_crop = h_crop as usize; + self.buffer_cropped.clear(); + let data = buf.as_slice(); + // BGRx is 4 bytes per pixel + self.buffer_cropped.reserve(w_crop * h_crop * 4); + for y in y_off..(y_off + h_crop) { + let i = 4 * (w * y + x_off); + self.buffer_cropped.extend(&data[i..i + 4 * w_crop]); + } + self.width = w_crop; + self.height = h_crop; + } else { + self.width = w; + self.height = h; + } + self.is_cropped = crop.is_some(); + self.buffer = Some(buf); + } + } else { + return Ok(PixelProvider::NONE); + } + if self.buffer.is_none() { + return Err(Box::new(GStreamerError("No buffer available!".into()))); + } + Ok(PixelProvider::BGR0( + self.width, + self.height, + if self.is_cropped { + self.buffer_cropped.as_slice() + } else { + self.buffer.as_ref().unwrap().as_slice() + }, + )) + } +} + +impl Drop for PipeWireRecorder { + fn drop(&mut self) { + if let Err(err) = self.pipeline.set_state(gst::State::Null) { + warn!("Failed to stop GStreamer pipeline: {}.", err); + } + } +} + +fn handle_response( + conn: &SyncConnection, + path: dbus::Path<'static>, + mut f: F, + failure_out: Arc, +) -> Result +where + F: FnMut( + OrgFreedesktopPortalRequestResponse, + &SyncConnection, + &Message, + ) -> Result<(), Box> + + Send + + Sync + + 'static, +{ + let mut m = MatchRule::new(); + m.path = Some(path); + m.msg_type = Some(MessageType::Signal); + m.sender = Some("org.freedesktop.portal.Desktop".into()); + m.interface = Some("org.freedesktop.portal.Request".into()); + conn.add_match(m, move |r: OrgFreedesktopPortalRequestResponse, c, m| { + debug!("Response from DBus: response: {:?}, message: {:?}", r, m); + match r.response { + 0 => {} + 1 => { + warn!("DBus response: User cancelled interaction."); + failure_out.store(true, std::sync::atomic::Ordering::Relaxed); + return true; + } + c => { + warn!("DBus response: Unknown error, code: {}.", c); + failure_out.store(true, std::sync::atomic::Ordering::Relaxed); + return true; + } + } + if let Err(err) = f(r, c, m) { + warn!("Error requesting screen capture via dbus: {}", err); + failure_out.store(true, std::sync::atomic::Ordering::Relaxed); + } + true + }) +} + +fn get_portal(conn: &SyncConnection) -> Proxy<&SyncConnection> { + conn.with_proxy( + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + Duration::from_millis(1000), + ) +} + +fn streams_from_response(response: OrgFreedesktopPortalRequestResponse) -> Vec { + (move || { + Some( + response + .results + .get("streams")? + .as_iter()? + .next()? + .as_iter()? + .filter_map(|stream| { + let mut itr = stream.as_iter()?; + let path = itr.next()?.as_u64()?; + let (keys, values): (Vec<(usize, &dyn RefArg)>, Vec<(usize, &dyn RefArg)>) = + itr.next()? + .as_iter()? + .enumerate() + .partition(|(i, _)| i % 2 == 0); + let attributes = keys + .iter() + .filter_map(|(_, key)| Some(key.as_str()?.to_owned())) + .zip( + values + .iter() + .map(|(_, arg)| *arg) + .collect::>(), + ) + .collect::>(); + let mut info = PwStreamInfo { + path, + source_type: attributes + .get("source_type") + .map_or(Some(0), |v| v.as_u64())?, + position: (0, 0), + size: (0, 0), + }; + let v = attributes + .get("size")? + .as_iter()? + .filter_map(|v| { + Some( + v.as_iter()? + .map(|x| x.as_i64().unwrap_or(0)) + .collect::>(), + ) + }) + .next(); + if let Some(v) = v { + if v.len() == 2 { + info.size.0 = v[0] as _; + info.size.1 = v[1] as _; + } + } + let v = attributes + .get("position")? + .as_iter()? + .filter_map(|v| { + Some( + v.as_iter()? + .map(|x| x.as_i64().unwrap_or(0)) + .collect::>(), + ) + }) + .next(); + if let Some(v) = v { + if v.len() == 2 { + info.position.0 = v[0] as _; + info.position.1 = v[1] as _; + } + } + Some(info) + }) + .collect::>(), + ) + })() + .unwrap_or_default() +} + +static mut INIT: bool = false; + +// mostly inspired by https://gitlab.gnome.org/snippets/19 +fn request_screen_cast( + capture_cursor: bool, +) -> Result<(SyncConnection, OwnedFd, Vec), Box> { + unsafe { + if !INIT { + gstreamer::init()?; + INIT = true; + } + } + let conn = SyncConnection::new_session()?; + let portal = get_portal(&conn); + let mut args: PropMap = HashMap::new(); + let fd: Arc>> = Arc::new(Mutex::new(None)); + let fd_res = fd.clone(); + let streams: Arc>> = Arc::new(Mutex::new(Vec::new())); + let streams_res = streams.clone(); + let failure = Arc::new(AtomicBool::new(false)); + let failure_res = failure.clone(); + args.insert( + "session_handle_token".to_string(), + Variant(Box::new("u1".to_string())), + ); + args.insert( + "handle_token".to_string(), + Variant(Box::new("u1".to_string())), + ); + let path = portal.create_session(args)?; + handle_response( + &conn, + path, + move |r: OrgFreedesktopPortalRequestResponse, c, _| { + let portal = get_portal(c); + let mut args: PropMap = HashMap::new(); + args.insert( + "handle_token".to_string(), + Variant(Box::new("u2".to_string())), + ); + // https://flatpak.github.io/xdg-desktop-portal/portal-docs.html#gdbus-method-org-freedesktop-portal-ScreenCast.SelectSources + args.insert("multiple".into(), Variant(Box::new(true))); + args.insert("types".into(), Variant(Box::new(1u32))); //| 2u32))); + + let cursor_mode = if capture_cursor { 2u32 } else { 1u32 }; + let plasma = std::env::var("DESKTOP_SESSION").map_or(false, |s| s.contains("plasma")); + if plasma && capture_cursor { + // Warn the user if capturing the cursor is tried on kde as this can crash + // kwin_wayland and tear down the plasma desktop, see: + // https://bugs.kde.org/show_bug.cgi?id=435042 + warn!("You are attempting to capture the cursor under KDE Plasma, this may crash your \ + desktop, see https://bugs.kde.org/show_bug.cgi?id=435042 for details! \ + You have been warned."); + } + args.insert("cursor_mode".into(), Variant(Box::new(cursor_mode))); + let session: dbus::Path = r + .results + .get("session_handle") + .ok_or_else(|| { + DBusError(format!( + "Failed to obtain session_handle from response: {:?}", + r + )) + })? + .as_str() + .ok_or_else(|| DBusError("Failed to convert session_handle to string.".into()))? + .to_string() + .into(); + let path = portal.select_sources(session.clone(), args)?; + let fd = fd.clone(); + let streams = streams.clone(); + let failure = failure.clone(); + let failure_out = failure.clone(); + handle_response( + c, + path, + move |_: OrgFreedesktopPortalRequestResponse, c, _| { + let portal = get_portal(c); + let mut args: PropMap = HashMap::new(); + args.insert( + "handle_token".to_string(), + Variant(Box::new("u3".to_string())), + ); + let path = portal.start(session.clone(), "", args)?; + let session = session.clone(); + let fd = fd.clone(); + let streams = streams.clone(); + let failure = failure.clone(); + let failure_out = failure.clone(); + handle_response( + c, + path, + move |r: OrgFreedesktopPortalRequestResponse, c, _| { + streams + .clone() + .lock() + .unwrap() + .append(&mut streams_from_response(r)); + let portal = get_portal(c); + fd.clone().lock().unwrap().replace( + portal.open_pipe_wire_remote(session.clone(), HashMap::new())?, + ); + Ok(()) + }, + failure_out, + )?; + Ok(()) + }, + failure_out, + )?; + Ok(()) + }, + failure_res.clone(), + )?; + // wait 3 minutes for user interaction + for _ in 0..1800 { + conn.process(Duration::from_millis(100))?; + // Once we got a file descriptor we are done! + if fd_res.lock().unwrap().is_some() { + break; + } + + if failure_res.load(std::sync::atomic::Ordering::Relaxed) { + break; + } + } + let fd_res = fd_res.lock().unwrap(); + let streams_res = streams_res.lock().unwrap(); + if fd_res.is_some() && !streams_res.is_empty() { + Ok((conn, fd_res.clone().unwrap(), streams_res.clone())) + } else { + Err(Box::new(DBusError( + "Failed to obtain screen capture.".into(), + ))) + } +} + +pub fn get_capturables(capture_cursor: bool) -> Result, Box> { + let (conn, fd, streams) = request_screen_cast(capture_cursor)?; + let conn = Arc::new(conn); + Ok(streams + .into_iter() + .map(|s| PipeWireCapturable::new(conn.clone(), fd.clone(), s)) + .collect()) +} diff --git a/libs/scrap/src/wayland/pipewire_dbus.rs b/libs/scrap/src/wayland/pipewire_dbus.rs new file mode 100644 index 000000000..3349ec8a9 --- /dev/null +++ b/libs/scrap/src/wayland/pipewire_dbus.rs @@ -0,0 +1,144 @@ +// This code was autogenerated with `dbus-codegen-rust -c blocking -m None`, see https://github.com/diwic/dbus-rs +use dbus; +#[allow(unused_imports)] +use dbus::arg; +use dbus::blocking; + +pub 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,), + ) + .map(|r: (dbus::Path<'static>,)| 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), + ) + .map(|r: (dbus::Path<'static>,)| 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), + ) + .map(|r: (dbus::Path<'static>,)| 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), + ) + .map(|r: (arg::OwnedFd,)| 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", + ) + } +} + +pub trait OrgFreedesktopPortalRequest { + fn close(&self) -> Result<(), dbus::Error>; +} + +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", ()) + } +} + +#[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"; +}