use bitflags::bitflags;
use dbus::{
arg::{OwnedFd, RefArg, Variant},
blocking::{Connection, Proxy},
channel::Token,
Message, Path,
};
use generated::{
OrgFreedesktopPortalRequestResponse, OrgFreedesktopPortalScreenCast,
OrgFreedesktopPortalSession,
};
use std::{
collections::HashMap,
convert::TryInto,
os::unix::prelude::RawFd,
sync::mpsc::{self, Receiver},
time::Duration,
};
mod generated;
#[derive(Debug)]
pub enum PortalError {
Generic(String),
DBus(dbus::Error),
Parse,
Cancelled,
}
impl std::convert::From<String> for PortalError {
fn from(error_string: String) -> Self {
PortalError::Generic(error_string)
}
}
impl std::convert::From<dbus::Error> for PortalError {
fn from(err: dbus::Error) -> Self {
PortalError::DBus(err)
}
}
impl std::fmt::Display for PortalError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "D-Bus Portal error: {0:?}", self)
}
}
impl std::error::Error for PortalError {}
pub struct ScreenCast {
state: ConnectionState,
session: String,
multiple: bool,
source_types: Option<SourceType>,
}
impl ScreenCast {
pub fn new() -> Result<Self, PortalError> {
let state = ConnectionState::open_new()?;
let session = {
let request = Request::with_handler(&state, |a| {
a.results
.get("session_handle")
.unwrap()
.as_str()
.unwrap()
.to_owned()
})?;
let mut session_args = HashMap::<String, Variant<Box<dyn RefArg>>>::new();
session_args.insert(
"handle_token".into(),
Variant(Box::new(String::from(&request.handle))),
);
session_args.insert(
"session_handle_token".into(),
Variant(Box::new(String::from(&request.handle))),
);
state.desktop_proxy().create_session(session_args)?;
request.wait_response()?
};
Ok(ScreenCast {
state,
session,
multiple: false,
source_types: None,
})
}
pub fn source_types(&self) -> Result<SourceType, PortalError> {
let types = self.state.desktop_proxy().available_source_types()?;
Ok(SourceType::from_bits_truncate(types))
}
pub fn set_source_types(&mut self, types: SourceType) {
self.source_types = Some(types);
}
pub fn enable_multiple(&mut self) {
self.multiple = true;
}
pub fn start(self, parent_window: Option<&str>) -> Result<ActiveScreenCast, PortalError> {
let desktop_proxy = self.state.desktop_proxy();
{
let request = Request::new(&self.state)?;
let session = dbus::Path::from(&self.session);
let mut select_args = HashMap::<String, Variant<Box<dyn RefArg>>>::new();
select_args.insert(
"handle_token".into(),
Variant(Box::new(String::from(&request.handle))),
);
select_args.insert(
"types".into(),
Variant(Box::new(match self.source_types {
Some(types) => types.bits(),
None => desktop_proxy.available_source_types()?,
})),
);
select_args.insert("multiple".into(), Variant(Box::new(self.multiple)));
desktop_proxy.select_sources(session, select_args)?;
request.wait_response()?;
}
let streams = {
let request = Request::with_handler(&self.state, |response| {
if response.response != 0 {
return Err(PortalError::Cancelled);
}
match response.results.get("streams") {
Some(streams) => match streams.as_iter() {
Some(streams) => streams
.flat_map(|s| {
s.as_iter()
.into_iter()
.flat_map(|t| t.map(|u| u.try_into()))
})
.collect(),
None => Err(PortalError::Parse),
},
None => Err(PortalError::Parse),
}
})?;
let session = dbus::Path::from(&self.session);
let mut select_args = HashMap::<String, Variant<Box<dyn RefArg>>>::new();
select_args.insert(
"handle_token".into(),
Variant(Box::new(String::from(&request.handle))),
);
desktop_proxy.start(session, parent_window.unwrap_or(""), select_args)?;
request.wait_response()?
}?;
let pipewire_fd =
desktop_proxy.open_pipe_wire_remote(dbus::Path::from(&self.session), HashMap::new())?;
Ok(ActiveScreenCast {
state: self.state,
session_path: self.session,
pipewire_fd,
streams,
})
}
}
pub struct ActiveScreenCast {
state: ConnectionState,
session_path: String,
pipewire_fd: OwnedFd,
streams: Vec<ScreenCastStream>,
}
impl ActiveScreenCast {
pub fn pipewire_fd(&self) -> RawFd {
self.pipewire_fd.clone().into_fd()
}
pub fn streams(&self) -> impl Iterator<Item = &ScreenCastStream> {
self.streams.iter()
}
pub fn close(&self) -> Result<(), PortalError> {
let session = Session::open(&self.state, &self.session_path)?;
session.close()?;
Ok(())
}
}
impl std::ops::Drop for ActiveScreenCast {
fn drop(&mut self) {
let _ = self.close();
}
}
#[derive(Debug)]
pub struct ScreenCastStream {
pipewire_node: u32,
}
impl ScreenCastStream {
pub fn pipewire_node(&self) -> u32 {
self.pipewire_node
}
}
impl std::convert::TryFrom<&dyn RefArg> for ScreenCastStream {
type Error = PortalError;
fn try_from(value: &dyn RefArg) -> Result<Self, Self::Error> {
let mut parts_iter = value.as_iter().ok_or(PortalError::Parse)?;
let node_id = parts_iter
.next()
.and_then(|r| r.as_u64())
.map(|r| r as u32)
.ok_or(PortalError::Parse)?;
Ok(ScreenCastStream {
pipewire_node: node_id,
})
}
}
bitflags! {
pub struct SourceType : u32 {
const MONITOR = 0b00001;
const WINDOW = 0b00010;
}
}
struct ConnectionState {
connection: Connection,
sender_token: String,
}
impl ConnectionState {
pub fn open_new() -> Result<Self, dbus::Error> {
let connection = Connection::new_session()?;
let sender_token = String::from(&connection.unique_name().replace(".", "_")[1..]);
Ok(ConnectionState {
connection,
sender_token,
})
}
pub fn desktop_proxy(&self) -> Proxy<&Connection> {
self.connection.with_proxy(
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
Duration::from_secs(20),
)
}
}
struct Request<'a, Response> {
proxy: Proxy<'a, &'a Connection>,
handle: String,
response: Receiver<Response>,
match_token: Token,
}
impl<'a> Request<'a, ()> {
pub fn new(state: &'a ConnectionState) -> Result<Self, PortalError> {
Self::with_handler(state, |_| {})
}
}
impl<'a, Response> Request<'a, Response> {
pub fn with_handler<ResponseHandler>(
state: &'a ConnectionState,
mut on_response: ResponseHandler,
) -> Result<Self, PortalError>
where
ResponseHandler: FnMut(OrgFreedesktopPortalRequestResponse) -> Response + Send + 'static,
Response: Send + 'static,
{
let handle = format!("screencap{0}", rand::random::<usize>());
let resp_path = Path::new(format!(
"/org/freedesktop/portal/desktop/request/{0}/{1}",
state.sender_token, handle
))?;
let proxy = state.connection.with_proxy(
"org.freedesktop.portal.Desktop",
resp_path,
Duration::from_secs(20),
);
let (sender, response) = mpsc::channel();
let match_token = proxy.match_signal(
move |a: OrgFreedesktopPortalRequestResponse, _: &Connection, _: &Message| {
let res = on_response(a);
sender.send(res).is_ok()
},
)?;
Ok(Request {
proxy,
handle,
response,
match_token,
})
}
pub fn wait_response(&self) -> Result<Response, PortalError> {
loop {
if let Ok(data) = self.response.try_recv() {
return Ok(data);
} else {
self.proxy.connection.process(Duration::from_millis(100))?;
}
}
}
}
impl<'a, T> std::ops::Drop for Request<'a, T> {
fn drop(&mut self) {
let _ = self.proxy.match_stop(self.match_token, true);
}
}
struct Session<'a> {
proxy: Proxy<'a, &'a Connection>,
}
impl<'a> Session<'a> {
pub fn open(state: &'a ConnectionState, path: &str) -> Result<Self, PortalError> {
let path = dbus::Path::new(path)?;
let proxy = state.connection.with_proxy(
"org.freedesktop.portal.Desktop",
path,
Duration::from_secs(20),
);
Ok(Session { proxy })
}
pub fn close(&self) -> Result<(), PortalError> {
self.proxy.close()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::SourceType;
#[test]
pub fn check_source_types() {
assert_eq!(1, SourceType::MONITOR.bits());
assert_eq!(2, SourceType::WINDOW.bits());
assert_eq!(3, (SourceType::WINDOW | SourceType::MONITOR).bits());
}
}