use std::{
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use dbus::{
MessageType, Path,
arg::{Iter, OwnedFd, PropMap, ReadAll, RefArg, TypeMismatchError, Variant},
blocking::Connection,
message::MatchRule,
strings::{BusName, Interface},
};
use parking_lot::Mutex;
use crate::error::{CaptureError, PortalCall, PortalError, Result};
pub const SOURCE_TYPE_MONITOR: u32 = 1;
const CURSOR_MODE_EMBEDDED: u32 = 2;
struct Request {
token: String,
path: Path<'static>,
slot: Arc<Mutex<Option<Response>>>,
}
#[derive(Debug)]
pub struct Response {
pub response: u32,
pub results: PropMap,
}
impl ReadAll for Response {
fn read(i: &mut Iter) -> std::result::Result<Self, TypeMismatchError> {
Ok(Response {
response: i.read()?,
results: i.read()?,
})
}
}
pub struct Session {
pub node_id: u32,
pub fd: OwnedFd,
}
const PORTAL_BUS_NAME: &str = "org.freedesktop.portal.Desktop";
pub struct DbusScreen {
connection: Connection,
portal_gone: Arc<AtomicBool>,
}
impl DbusScreen {
pub fn new() -> Result<Self> {
let connection = Connection::new_session()?;
let portal_gone = Arc::new(AtomicBool::new(false));
let mut rule = MatchRule::new_signal("org.freedesktop.DBus", "NameOwnerChanged");
rule.sender = Some(BusName::from("org.freedesktop.DBus"));
connection.add_match(rule, {
let portal_gone = Arc::clone(&portal_gone);
move |(name, old, _new): (String, String, String), _c, _msg| {
if name == PORTAL_BUS_NAME && !old.is_empty() {
portal_gone.store(true, Ordering::Relaxed);
}
true
}
})?;
Ok(Self {
connection,
portal_gone,
})
}
pub fn start(&self, wanted_source_types: u32) -> Result<Session> {
let proxy = self.connection.with_proxy(
PORTAL_BUS_NAME,
"/org/freedesktop/portal/desktop",
Duration::from_secs(3),
);
let request = self.watch("create")?;
let mut map = PropMap::new();
map.insert(
"handle_token".to_string(),
Variant(Box::new(request.token.clone())),
);
map.insert(
"session_handle_token".to_string(),
Variant(Box::new(request.token.clone())),
);
let path = proxy
.method_call("org.freedesktop.portal.ScreenCast", "CreateSession", (map,))
.map(|r: (Path<'static>,)| r.0)?;
let resp = self.recv_resp(&request, path, PortalCall::CreateSession)?;
check_response(&resp, PortalCall::CreateSession)?;
let handle = resp
.results
.get("session_handle")
.and_then(|handle| handle.0.as_str())
.ok_or(PortalError::MalformedReply {
call: PortalCall::CreateSession,
})?;
let handle = Path::from(handle.to_string());
let available = proxy
.method_call(
"org.freedesktop.DBus.Properties",
"Get",
("org.freedesktop.portal.ScreenCast", "AvailableSourceTypes"),
)
.map(|r: (Variant<u32>,)| (r.0).0)?;
let source_type = if wanted_source_types == 0 {
available
} else {
let wanted = available & wanted_source_types;
if wanted == 0 {
return Err(PortalError::NoMatchingSource {
wanted: wanted_source_types,
available,
}
.into());
}
wanted
};
let request = self.watch("select")?;
let mut map = PropMap::new();
map.insert(
String::from("handle_token"),
Variant(Box::new(request.token.clone())),
);
map.insert(String::from("types"), Variant(Box::new(source_type)));
map.insert(String::from("multiple"), Variant(Box::new(false)));
let cursor_modes = proxy
.method_call(
"org.freedesktop.DBus.Properties",
"Get",
("org.freedesktop.portal.ScreenCast", "AvailableCursorModes"),
)
.map_or(0, |r: (Variant<u32>,)| (r.0).0);
if cursor_modes & CURSOR_MODE_EMBEDDED != 0 {
map.insert(
String::from("cursor_mode"),
Variant(Box::new(CURSOR_MODE_EMBEDDED)),
);
}
let path = proxy
.method_call(
"org.freedesktop.portal.ScreenCast",
"SelectSources",
(handle.clone(), map),
)
.map(|r: (Path<'static>,)| r.0)?;
let resp = self.recv_resp(&request, path, PortalCall::SelectSources)?;
check_response(&resp, PortalCall::SelectSources)?;
let request = self.watch("start")?;
let mut map = PropMap::new();
map.insert(
String::from("handle_token"),
Variant(Box::new(request.token.clone())),
);
let path = proxy
.method_call(
"org.freedesktop.portal.ScreenCast",
"Start",
(handle.clone(), "", map),
)
.map(|r: (Path<'static>,)| r.0)?;
let resp = self.recv_resp(&request, path, PortalCall::Start)?;
check_response(&resp, PortalCall::Start)?;
let pipewire_node_id = resp
.results
.get("streams")
.and_then(|streams| streams.as_iter())
.and_then(|mut iter| iter.next())
.and_then(|item| item.as_iter())
.and_then(|mut iter| iter.next())
.and_then(|item| item.as_iter())
.and_then(|mut iter| iter.next())
.and_then(|item| item.as_u64())
.ok_or(PortalError::MalformedReply {
call: PortalCall::Start,
})?;
let fd = proxy
.method_call(
"org.freedesktop.portal.ScreenCast",
"OpenPipeWireRemote",
(handle, PropMap::new()),
)
.map(|r: (OwnedFd,)| r.0)?;
Ok(Session {
node_id: pipewire_node_id as u32,
fd,
})
}
fn watch(&self, name: &str) -> Result<Request> {
let sender = self
.connection
.unique_name()
.trim_start_matches(':')
.replace('.', "_");
let token = format!("scrcap_{name}");
let path = Path::from(format!(
"/org/freedesktop/portal/desktop/request/{sender}/{token}"
));
let slot = Arc::new(Mutex::new(None));
self.add_response_match(path.clone(), Arc::clone(&slot))?;
Ok(Request { token, path, slot })
}
fn add_response_match(
&self,
path: Path<'static>,
slot: Arc<Mutex<Option<Response>>>,
) -> Result<()> {
let mut rule = MatchRule::new();
rule.path = Some(path);
rule.msg_type = Some(MessageType::Signal);
rule.sender = Some(BusName::from(PORTAL_BUS_NAME));
rule.interface = Some(Interface::from("org.freedesktop.portal.Request"));
self.connection
.add_match(rule, move |res: Response, _c, _msg| {
*slot.lock() = Some(res);
false
})?;
Ok(())
}
fn recv_resp(
&self,
request: &Request,
actual: Path<'static>,
call: PortalCall,
) -> Result<Response> {
if actual != request.path {
self.add_response_match(actual, Arc::clone(&request.slot))?;
}
loop {
self.connection.process(Duration::from_millis(100))?;
if let Some(resp) = request.slot.lock().take() {
return Ok(resp);
}
if self.portal_gone.load(Ordering::Relaxed) {
return Err(PortalError::Vanished { call }.into());
}
}
}
}
fn check_response(resp: &Response, call: PortalCall) -> Result<()> {
match resp.response {
0 => Ok(()),
1 => Err(CaptureError::Cancelled),
response => Err(PortalError::Refused { call, response }.into()),
}
}