1use bitflags::bitflags;
32use dbus::{
33 arg::{OwnedFd, RefArg, Variant},
34 blocking::{Connection, Proxy},
35 channel::Token,
36 Message, Path,
37};
38use generated::{
39 OrgFreedesktopPortalRequestResponse, OrgFreedesktopPortalScreenCast,
40 OrgFreedesktopPortalSession,
41};
42use std::{
43 collections::HashMap,
44 convert::TryInto,
45 os::unix::prelude::RawFd,
46 sync::mpsc::{self, Receiver},
47 time::Duration,
48};
49
50mod generated;
51
52#[derive(Debug)]
57pub enum PortalError {
58 Generic(String),
60 DBus(dbus::Error),
62 Parse,
64 Cancelled,
66}
67
68impl std::convert::From<String> for PortalError {
69 fn from(error_string: String) -> Self {
70 PortalError::Generic(error_string)
71 }
72}
73
74impl std::convert::From<dbus::Error> for PortalError {
75 fn from(err: dbus::Error) -> Self {
76 PortalError::DBus(err)
77 }
78}
79
80impl std::fmt::Display for PortalError {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 write!(f, "D-Bus Portal error: {0:?}", self)
83 }
84}
85
86impl std::error::Error for PortalError {}
87
88pub struct ScreenCast {
92 state: ConnectionState,
93 session: String,
94 multiple: bool,
95 source_types: Option<SourceType>,
96}
97
98impl ScreenCast {
99 pub fn new() -> Result<Self, PortalError> {
103 let state = ConnectionState::open_new()?;
104
105 let session = {
106 let request = Request::with_handler(&state, |a| {
107 a.results
108 .get("session_handle")
109 .unwrap()
110 .as_str()
111 .unwrap()
112 .to_owned()
113 })?;
114 let mut session_args = HashMap::<String, Variant<Box<dyn RefArg>>>::new();
116 session_args.insert(
117 "handle_token".into(),
118 Variant(Box::new(String::from(&request.handle))),
119 );
120 session_args.insert(
121 "session_handle_token".into(),
122 Variant(Box::new(String::from(&request.handle))),
123 );
124 state.desktop_proxy().create_session(session_args)?;
125 request.wait_response()?
126 };
127
128 Ok(ScreenCast {
129 state,
130 session,
131 multiple: false,
132 source_types: None,
133 })
134 }
135
136 pub fn source_types(&self) -> Result<SourceType, PortalError> {
138 let types = self.state.desktop_proxy().available_source_types()?;
139 Ok(SourceType::from_bits_truncate(types))
140 }
141
142 pub fn set_source_types(&mut self, types: SourceType) {
145 self.source_types = Some(types);
146 }
147
148 pub fn enable_multiple(&mut self) {
152 self.multiple = true;
153 }
154
155 pub fn start(self, parent_window: Option<&str>) -> Result<ActiveScreenCast, PortalError> {
158 let desktop_proxy = self.state.desktop_proxy();
159
160 {
161 let request = Request::new(&self.state)?;
162 let session = dbus::Path::from(&self.session);
163 let mut select_args = HashMap::<String, Variant<Box<dyn RefArg>>>::new();
164 select_args.insert(
165 "handle_token".into(),
166 Variant(Box::new(String::from(&request.handle))),
167 );
168 select_args.insert(
169 "types".into(),
170 Variant(Box::new(match self.source_types {
171 Some(types) => types.bits(),
172 None => desktop_proxy.available_source_types()?,
173 })),
174 );
175 select_args.insert("multiple".into(), Variant(Box::new(self.multiple)));
176 desktop_proxy.select_sources(session, select_args)?;
177 request.wait_response()?;
178 }
179
180 let streams = {
181 let request = Request::with_handler(&self.state, |response| {
182 if response.response != 0 {
183 return Err(PortalError::Cancelled);
184 }
185 match response.results.get("streams") {
186 Some(streams) => match streams.as_iter() {
187 Some(streams) => streams
188 .flat_map(|s| {
189 s.as_iter()
190 .into_iter()
191 .flat_map(|t| t.map(|u| u.try_into()))
192 })
193 .collect(),
194 None => Err(PortalError::Parse),
195 },
196 None => Err(PortalError::Parse),
197 }
198 })?;
199 let session = dbus::Path::from(&self.session);
200 let mut select_args = HashMap::<String, Variant<Box<dyn RefArg>>>::new();
201 select_args.insert(
202 "handle_token".into(),
203 Variant(Box::new(String::from(&request.handle))),
204 );
205 desktop_proxy.start(session, parent_window.unwrap_or(""), select_args)?;
206 request.wait_response()?
207 }?;
208
209 let pipewire_fd =
210 desktop_proxy.open_pipe_wire_remote(dbus::Path::from(&self.session), HashMap::new())?;
211
212 Ok(ActiveScreenCast {
213 state: self.state,
214 session_path: self.session,
215 pipewire_fd,
216 streams,
217 })
218 }
219}
220
221pub struct ActiveScreenCast {
224 state: ConnectionState,
225 session_path: String,
226 pipewire_fd: OwnedFd,
227 streams: Vec<ScreenCastStream>,
228}
229
230impl ActiveScreenCast {
231 pub fn pipewire_fd(&self) -> RawFd {
233 self.pipewire_fd.clone().into_fd()
234 }
235
236 pub fn streams(&self) -> impl Iterator<Item = &ScreenCastStream> {
238 self.streams.iter()
239 }
240
241 pub fn close(&self) -> Result<(), PortalError> {
243 let session = Session::open(&self.state, &self.session_path)?;
245 session.close()?;
246 Ok(())
247 }
248}
249
250impl std::ops::Drop for ActiveScreenCast {
251 fn drop(&mut self) {
252 let _ = self.close();
253 }
254}
255
256#[derive(Debug)]
257pub struct ScreenCastStream {
258 pipewire_node: u32,
259 }
261
262impl ScreenCastStream {
263 pub fn pipewire_node(&self) -> u32 {
265 self.pipewire_node
266 }
267}
268
269impl std::convert::TryFrom<&dyn RefArg> for ScreenCastStream {
270 type Error = PortalError;
271
272 fn try_from(value: &dyn RefArg) -> Result<Self, Self::Error> {
273 let mut parts_iter = value.as_iter().ok_or(PortalError::Parse)?;
274 let node_id = parts_iter
275 .next()
276 .and_then(|r| r.as_u64())
277 .map(|r| r as u32)
278 .ok_or(PortalError::Parse)?;
279 Ok(ScreenCastStream {
281 pipewire_node: node_id,
282 })
283 }
284}
285
286bitflags! {
287 pub struct SourceType : u32 {
288 const MONITOR = 0b00001;
289 const WINDOW = 0b00010;
290 }
291}
292
293struct ConnectionState {
298 connection: Connection,
299 sender_token: String,
300}
301
302impl ConnectionState {
303 pub fn open_new() -> Result<Self, dbus::Error> {
305 let connection = Connection::new_session()?;
308 let sender_token = String::from(&connection.unique_name().replace(".", "_")[1..]);
309 Ok(ConnectionState {
310 connection,
311 sender_token,
312 })
313 }
314
315 pub fn desktop_proxy(&self) -> Proxy<&Connection> {
317 self.connection.with_proxy(
318 "org.freedesktop.portal.Desktop",
319 "/org/freedesktop/portal/desktop",
320 Duration::from_secs(20),
321 )
322 }
323}
324
325struct Request<'a, Response> {
328 proxy: Proxy<'a, &'a Connection>,
330 handle: String,
332 response: Receiver<Response>,
334 match_token: Token,
336}
337
338impl<'a> Request<'a, ()> {
339 pub fn new(state: &'a ConnectionState) -> Result<Self, PortalError> {
342 Self::with_handler(state, |_| {})
343 }
344}
345
346impl<'a, Response> Request<'a, Response> {
347 pub fn with_handler<ResponseHandler>(
351 state: &'a ConnectionState,
352 mut on_response: ResponseHandler,
353 ) -> Result<Self, PortalError>
354 where
355 ResponseHandler: FnMut(OrgFreedesktopPortalRequestResponse) -> Response + Send + 'static,
356 Response: Send + 'static,
357 {
358 let handle = format!("screencap{0}", rand::random::<usize>());
359 let resp_path = Path::new(format!(
360 "/org/freedesktop/portal/desktop/request/{0}/{1}",
361 state.sender_token, handle
362 ))?;
363 let proxy = state.connection.with_proxy(
364 "org.freedesktop.portal.Desktop",
365 resp_path,
366 Duration::from_secs(20),
367 );
368 let (sender, response) = mpsc::channel();
369 let match_token = proxy.match_signal(
370 move |a: OrgFreedesktopPortalRequestResponse, _: &Connection, _: &Message| {
371 let res = on_response(a);
374 sender.send(res).is_ok()
375 },
376 )?;
377 Ok(Request {
378 proxy,
379 handle,
380 response,
381 match_token,
382 })
383 }
384
385 pub fn wait_response(&self) -> Result<Response, PortalError> {
386 loop {
388 if let Ok(data) = self.response.try_recv() {
389 return Ok(data);
390 } else {
391 self.proxy.connection.process(Duration::from_millis(100))?;
392 }
393 }
394 }
395}
396
397impl<'a, T> std::ops::Drop for Request<'a, T> {
398 fn drop(&mut self) {
399 let _ = self.proxy.match_stop(self.match_token, true);
400 }
401}
402
403struct Session<'a> {
405 proxy: Proxy<'a, &'a Connection>,
406}
407
408impl<'a> Session<'a> {
409 pub fn open(state: &'a ConnectionState, path: &str) -> Result<Self, PortalError> {
410 let path = dbus::Path::new(path)?;
411 let proxy = state.connection.with_proxy(
412 "org.freedesktop.portal.Desktop",
413 path,
414 Duration::from_secs(20),
415 );
416 Ok(Session { proxy })
417 }
418
419 pub fn close(&self) -> Result<(), PortalError> {
420 self.proxy.close()?;
421 Ok(())
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use super::SourceType;
428
429 #[test]
430 pub fn check_source_types() {
431 assert_eq!(1, SourceType::MONITOR.bits());
432 assert_eq!(2, SourceType::WINDOW.bits());
433 assert_eq!(3, (SourceType::WINDOW | SourceType::MONITOR).bits());
434 }
435}