ashpd/desktop/
camera.rs

1//! Check if a camera is available, request access to it and open a PipeWire
2//! remote stream.
3//!
4//! ### Examples
5//!
6//! ```rust,no_run
7//! use ashpd::desktop::camera::Camera;
8//!
9//! pub async fn run() -> ashpd::Result<()> {
10//!     let camera = Camera::new().await?;
11//!     if camera.is_present().await? {
12//!         camera.request_access().await?;
13//!         let remote_fd = camera.open_pipe_wire_remote().await?;
14//!         // pass the remote fd to GStreamer for example
15//!     }
16//!     Ok(())
17//! }
18//! ```
19//! An example on how to connect with Pipewire can be found [here](https://github.com/bilelmoussaoui/ashpd/blob/master/examples/screen_cast_pw.rs).
20//! Although the example's primary focus is screen casting, stream connection
21//! logic remains the same -- with one accessibility change:
22//! ```rust,ignore
23//! let stream = pw::stream::Stream::new(
24//!    &core,
25//!    "video-test",
26//!    properties! {
27//!        *pw::keys::MEDIA_TYPE => "Video",
28//!        *pw::keys::MEDIA_CATEGORY => "Capture",
29//!        *pw::keys::MEDIA_ROLE => "Screen", // <-- make this 'Camera'
30//!    },
31//! )?;
32//! ```
33
34use std::{collections::HashMap, os::fd::OwnedFd};
35
36#[cfg(feature = "pipewire")]
37use pipewire::{context::Context, main_loop::MainLoop};
38use zbus::zvariant::{self, SerializeDict, Type, Value};
39
40use super::{HandleToken, Request};
41use crate::{proxy::Proxy, Error};
42
43#[derive(SerializeDict, Type, Debug, Default)]
44#[zvariant(signature = "dict")]
45struct CameraAccessOptions {
46    handle_token: HandleToken,
47}
48
49/// The interface lets sandboxed applications access camera devices, such as web
50/// cams.
51///
52/// Wrapper of the DBus interface: [`org.freedesktop.portal.Camera`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Camera.html).
53#[derive(Debug)]
54#[doc(alias = "org.freedesktop.portal.Camera")]
55pub struct Camera<'a>(Proxy<'a>);
56
57impl<'a> Camera<'a> {
58    /// Create a new instance of [`Camera`].
59    pub async fn new() -> Result<Camera<'a>, Error> {
60        let proxy = Proxy::new_desktop("org.freedesktop.portal.Camera").await?;
61        Ok(Self(proxy))
62    }
63
64    /// Requests an access to the camera.
65    ///
66    /// # Specifications
67    ///
68    /// See also [`AccessCamera`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Camera.html#org-freedesktop-portal-camera-accesscamera).
69    #[doc(alias = "AccessCamera")]
70    #[doc(alias = "xdp_portal_access_camera")]
71    pub async fn request_access(&self) -> Result<Request<()>, Error> {
72        let options = CameraAccessOptions::default();
73        self.0
74            .empty_request(&options.handle_token, "AccessCamera", &options)
75            .await
76    }
77
78    /// Open a file descriptor to the PipeWire remote where the camera nodes are
79    /// available.
80    ///
81    /// # Returns
82    ///
83    /// File descriptor of an open PipeWire remote.
84    ///
85    /// # Specifications
86    ///
87    /// See also [`OpenPipeWireRemote`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Camera.html#org-freedesktop-portal-camera-openpipewireremote).
88    #[doc(alias = "OpenPipeWireRemote")]
89    #[doc(alias = "xdp_portal_open_pipewire_remote_for_camera")]
90    pub async fn open_pipe_wire_remote(&self) -> Result<OwnedFd, Error> {
91        // `options` parameter doesn't seems to be used yet
92        // see https://github.com/flatpak/xdg-desktop-portal/blob/1.20.0/src/camera.c#L273
93        let options: HashMap<&str, Value<'_>> = HashMap::new();
94        let fd = self
95            .0
96            .call::<zvariant::OwnedFd>("OpenPipeWireRemote", &options)
97            .await?;
98        Ok(fd.into())
99    }
100
101    /// A boolean stating whether there is any cameras available.
102    ///
103    /// # Specifications
104    ///
105    /// See also [`IsCameraPresent`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Camera.html#org-freedesktop-portal-camera-iscamerapresent).
106    #[doc(alias = "IsCameraPresent")]
107    #[doc(alias = "xdp_portal_is_camera_present")]
108    pub async fn is_present(&self) -> Result<bool, Error> {
109        self.0.property("IsCameraPresent").await
110    }
111}
112
113impl<'a> std::ops::Deref for Camera<'a> {
114    type Target = zbus::Proxy<'a>;
115
116    fn deref(&self) -> &Self::Target {
117        &self.0
118    }
119}
120
121#[cfg(feature = "pipewire")]
122/// A PipeWire camera stream returned by [`pipewire_streams`].
123#[derive(Debug)]
124pub struct Stream {
125    node_id: u32,
126    properties: HashMap<String, String>,
127}
128
129#[cfg(feature = "pipewire")]
130impl Stream {
131    /// The id of the PipeWire node.
132    pub fn node_id(&self) -> u32 {
133        self.node_id
134    }
135
136    /// The node properties.
137    pub fn properties(&self) -> HashMap<String, String> {
138        self.properties.clone()
139    }
140}
141
142#[cfg(feature = "pipewire")]
143fn pipewire_streams_inner<F: Fn(Stream) + Clone + 'static, G: FnOnce() + Clone + 'static>(
144    fd: OwnedFd,
145    callback: F,
146    done_callback: G,
147) -> Result<(), pipewire::Error> {
148    let mainloop = MainLoop::new(None)?;
149    let context = Context::new(&mainloop)?;
150    let core = context.connect_fd(fd, None)?;
151    let registry = core.get_registry()?;
152
153    let pending = core.sync(0).expect("sync failed");
154
155    let loop_clone = mainloop.clone();
156    let _listener_reg = registry
157        .add_listener_local()
158        .global(move |global| {
159            if let Some(props) = &global.props {
160                if props.get("media.role") == Some("Camera") {
161                    #[cfg(feature = "tracing")]
162                    tracing::info!("found camera: {:#?}", props);
163
164                    let mut properties = HashMap::new();
165                    for (key, value) in props.iter() {
166                        properties.insert(key.to_string(), value.to_string());
167                    }
168                    let node_id = global.id;
169
170                    let stream = Stream {
171                        node_id,
172                        properties,
173                    };
174                    callback.clone()(stream);
175                }
176            }
177        })
178        .register();
179    let _listener_core = core
180        .add_listener_local()
181        .done(move |id, seq| {
182            if id == pipewire::core::PW_ID_CORE && seq == pending {
183                loop_clone.quit();
184                done_callback.clone()();
185            }
186        })
187        .register();
188
189    mainloop.run();
190
191    Ok(())
192}
193
194/// A helper to get a list of PipeWire streams to use with the camera file
195/// descriptor returned by [`Camera::open_pipe_wire_remote`].
196///
197/// Currently, the camera portal only gives us a file descriptor. Not passing a
198/// node id may cause the media session controller to auto-connect the client to
199/// an incorrect node.
200///
201/// The method looks for the available output streams of a `media.role` type of
202/// `Camera` and return a list of `Stream`s.
203///
204/// *Note* The socket referenced by `fd` must not be used while this function is
205/// running.
206#[cfg(feature = "pipewire")]
207#[cfg_attr(docsrs, doc(cfg(feature = "pipewire")))]
208pub async fn pipewire_streams(fd: OwnedFd) -> Result<Vec<Stream>, pipewire::Error> {
209    let (sender, receiver) = futures_channel::oneshot::channel();
210    let (streams_sender, mut streams_receiver) = futures_channel::mpsc::unbounded();
211
212    let sender = std::sync::Arc::new(std::sync::Mutex::new(Some(sender)));
213    let streams_sender = std::sync::Arc::new(std::sync::Mutex::new(streams_sender));
214
215    std::thread::spawn(move || {
216        let inner_sender = sender.clone();
217
218        if let Err(err) = pipewire_streams_inner(
219            fd,
220            move |stream| {
221                let inner_streams_sender = streams_sender.clone();
222                if let Ok(mut sender) = inner_streams_sender.lock() {
223                    let _result = sender.start_send(stream);
224                };
225            },
226            move || {
227                if let Ok(mut guard) = inner_sender.lock() {
228                    if let Some(inner_sender) = guard.take() {
229                        let _result = inner_sender.send(Ok(()));
230                    }
231                }
232            },
233        ) {
234            #[cfg(feature = "tracing")]
235            tracing::error!("Failed to get pipewire streams {:#?}", err);
236            let mut guard = sender.lock().unwrap();
237            if let Some(sender) = guard.take() {
238                let _ = sender.send(Err(err));
239            }
240        }
241    });
242
243    receiver.await.unwrap()?;
244
245    let mut streams = vec![];
246    while let Ok(Some(stream)) = streams_receiver.try_next() {
247        streams.push(stream);
248    }
249
250    Ok(streams)
251}
252
253#[cfg(not(feature = "pipewire"))]
254#[cfg_attr(docsrs, doc(cfg(not(feature = "pipewire"))))]
255/// Request access to the camera and return a file descriptor if one is
256/// available.
257pub async fn request() -> Result<Option<OwnedFd>, Error> {
258    let proxy = Camera::new().await?;
259    proxy.request_access().await?;
260    if proxy.is_present().await? {
261        Ok(Some(proxy.open_pipe_wire_remote().await?))
262    } else {
263        Ok(None)
264    }
265}
266
267#[cfg(feature = "pipewire")]
268#[cfg_attr(docsrs, doc(cfg(feature = "pipewire")))]
269/// Request access to the camera and return a file descriptor and a list of the
270/// available streams, one per camera.
271pub async fn request() -> Result<Option<(OwnedFd, Vec<Stream>)>, Error> {
272    let proxy = Camera::new().await?;
273    proxy.request_access().await?;
274    if proxy.is_present().await? {
275        let fd = proxy.open_pipe_wire_remote().await?;
276        let streams = pipewire_streams(fd.try_clone()?).await?;
277        Ok(Some((fd, streams)))
278    } else {
279        Ok(None)
280    }
281}