Skip to main content

waydriver_capture_mutter/
lib.rs

1//! Mutter implementation of [`waydriver::CaptureBackend`].
2//!
3//! Creates a ScreenCast session on mutter's private D-Bus, records the
4//! virtual monitor, waits for the `PipeWireStreamAdded` signal to learn the
5//! PipeWire node id, and hands that off to the shared `waydriver::capture::grab_png`
6//! helper via the trait's default `take_screenshot` impl.
7
8use std::collections::HashMap;
9use std::path::PathBuf;
10use std::sync::Arc;
11
12use async_trait::async_trait;
13use futures_util::StreamExt;
14use zbus::zvariant::{OwnedObjectPath, Value};
15
16use waydriver::{CaptureBackend, Error, PipeWireStream, Result};
17use waydriver_compositor_mutter::MutterState;
18
19pub struct MutterCapture {
20    state: Arc<MutterState>,
21}
22
23impl MutterCapture {
24    pub fn new(state: Arc<MutterState>) -> Self {
25        Self { state }
26    }
27}
28
29#[async_trait]
30impl CaptureBackend for MutterCapture {
31    async fn start_stream(&self) -> Result<PipeWireStream> {
32        let conn = &self.state.conn;
33
34        // Step 1: Create ScreenCast session.
35        let empty_opts: HashMap<&str, Value> = HashMap::new();
36        let reply = conn
37            .call_method(
38                Some("org.gnome.Mutter.ScreenCast"),
39                "/org/gnome/Mutter/ScreenCast",
40                Some("org.gnome.Mutter.ScreenCast"),
41                "CreateSession",
42                &(empty_opts.clone(),),
43            )
44            .await
45            .map_err(|e| Error::Screenshot(format!("CreateSession: {e}")))?;
46        let session_path: OwnedObjectPath = reply
47            .body()
48            .deserialize()
49            .map_err(|e| Error::Screenshot(format!("parse session path: {e}")))?;
50
51        // Step 2: RecordMonitor on the session.
52        let reply = conn
53            .call_method(
54                Some("org.gnome.Mutter.ScreenCast"),
55                session_path.as_str(),
56                Some("org.gnome.Mutter.ScreenCast.Session"),
57                "RecordMonitor",
58                &("", empty_opts),
59            )
60            .await
61            .map_err(|e| Error::Screenshot(format!("RecordMonitor: {e}")))?;
62        let stream_path: OwnedObjectPath = reply
63            .body()
64            .deserialize()
65            .map_err(|e| Error::Screenshot(format!("parse stream path: {e}")))?;
66
67        // Step 3: Subscribe to PipeWireStreamAdded BEFORE starting.
68        // This ordering is load-bearing — mutter emits the signal synchronously
69        // during `Session.Start`, so a late subscribe misses it.
70        let stream_proxy: zbus::Proxy<'_> = zbus::proxy::Builder::new(conn)
71            .destination("org.gnome.Mutter.ScreenCast")
72            .map_err(|e| Error::Screenshot(format!("proxy destination: {e}")))?
73            .path(stream_path.as_str())
74            .map_err(|e| Error::Screenshot(format!("proxy path: {e}")))?
75            .interface("org.gnome.Mutter.ScreenCast.Stream")
76            .map_err(|e| Error::Screenshot(format!("proxy interface: {e}")))?
77            .build()
78            .await
79            .map_err(|e| Error::Screenshot(format!("build stream proxy: {e}")))?;
80
81        let mut signal_stream = stream_proxy
82            .receive_signal("PipeWireStreamAdded")
83            .await
84            .map_err(|e| Error::Screenshot(format!("receive_signal: {e}")))?;
85
86        // Step 4: Start the ScreenCast session.
87        conn.call_method(
88            Some("org.gnome.Mutter.ScreenCast"),
89            session_path.as_str(),
90            Some("org.gnome.Mutter.ScreenCast.Session"),
91            "Start",
92            &(),
93        )
94        .await
95        .map_err(|e| Error::Screenshot(format!("Start: {e}")))?;
96
97        // Step 5: Wait for PipeWireStreamAdded signal to get the node id.
98        let node_id: u32 = tokio::time::timeout(std::time::Duration::from_secs(5), async {
99            let signal = signal_stream
100                .next()
101                .await
102                .ok_or_else(|| Error::Screenshot("signal stream ended".to_string()))?;
103            signal
104                .body()
105                .deserialize::<u32>()
106                .map_err(|e| Error::Screenshot(format!("parse node_id: {e}")))
107        })
108        .await
109        .map_err(|_| Error::Screenshot("timeout waiting for PipeWireStreamAdded".to_string()))??;
110
111        tracing::debug!(node_id, "got PipeWire node id for screenshot");
112
113        Ok(PipeWireStream {
114            node_id,
115            token: Box::new(session_path),
116        })
117    }
118
119    async fn stop_stream(&self, stream: PipeWireStream) -> Result<()> {
120        let session_path = stream.token.downcast::<OwnedObjectPath>().map_err(|_| {
121            Error::Screenshot("stop_stream: token was not an OwnedObjectPath".into())
122        })?;
123        let _ = self
124            .state
125            .conn
126            .call_method(
127                Some("org.gnome.Mutter.ScreenCast"),
128                session_path.as_str(),
129                Some("org.gnome.Mutter.ScreenCast.Session"),
130                "Stop",
131                &(),
132            )
133            .await;
134        Ok(())
135    }
136
137    fn pipewire_socket(&self) -> PathBuf {
138        self.state.runtime_dir.join("pipewire-0")
139    }
140}