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