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, StreamToken};
17use waydriver_compositor_mutter::MutterState;
18
19/// Mutter ScreenCast + PipeWire capture backend.
20pub struct MutterCapture {
21    state: Arc<MutterState>,
22}
23
24impl MutterCapture {
25    /// Create a new capture backend from shared compositor state.
26    pub fn new(state: Arc<MutterState>) -> Self {
27        Self { state }
28    }
29}
30
31#[async_trait]
32impl CaptureBackend for MutterCapture {
33    async fn start_stream(&self) -> Result<PipeWireStream> {
34        let conn = self.state.conn();
35
36        // Step 1: Create ScreenCast session, linking it to the existing
37        // RemoteDesktop session so absolute pointer motion works (mutter
38        // routes NotifyPointerMotionAbsolute through the linked stream).
39        let empty_opts: HashMap<&str, Value> = HashMap::new();
40        let mut create_opts: HashMap<&str, Value> = HashMap::new();
41        create_opts.insert(
42            "remote-desktop-session-id",
43            Value::from(self.state.rd_session_id()),
44        );
45        let reply = conn
46            .call_method(
47                Some("org.gnome.Mutter.ScreenCast"),
48                "/org/gnome/Mutter/ScreenCast",
49                Some("org.gnome.Mutter.ScreenCast"),
50                "CreateSession",
51                &(create_opts,),
52            )
53            .await
54            .map_err(|e| Error::screenshot_with("CreateSession", e))?;
55        let session_path: OwnedObjectPath = reply
56            .body()
57            .deserialize()
58            .map_err(|e| Error::screenshot_with("parse session path", e))?;
59
60        // Step 2: RecordMonitor on the session.
61        let reply = conn
62            .call_method(
63                Some("org.gnome.Mutter.ScreenCast"),
64                session_path.as_str(),
65                Some("org.gnome.Mutter.ScreenCast.Session"),
66                "RecordMonitor",
67                &("", empty_opts),
68            )
69            .await
70            .map_err(|e| Error::screenshot_with("RecordMonitor", e))?;
71        let stream_path: OwnedObjectPath = reply
72            .body()
73            .deserialize()
74            .map_err(|e| Error::screenshot_with("parse stream path", e))?;
75
76        // Step 3: Subscribe to PipeWireStreamAdded BEFORE starting.
77        // This ordering is load-bearing — mutter emits the signal synchronously
78        // during `Session.Start`, so a late subscribe misses it.
79        let stream_proxy: zbus::Proxy<'_> = zbus::proxy::Builder::new(conn)
80            .destination("org.gnome.Mutter.ScreenCast")
81            .map_err(|e| Error::screenshot_with("proxy destination", e))?
82            .path(stream_path.as_str())
83            .map_err(|e| Error::screenshot_with("proxy path", e))?
84            .interface("org.gnome.Mutter.ScreenCast.Stream")
85            .map_err(|e| Error::screenshot_with("proxy interface", e))?
86            .build()
87            .await
88            .map_err(|e| Error::screenshot_with("build stream proxy", e))?;
89
90        let mut signal_stream = stream_proxy
91            .receive_signal("PipeWireStreamAdded")
92            .await
93            .map_err(|e| Error::screenshot_with("receive_signal", e))?;
94
95        // Step 4: Start the ScreenCast session — via either the SC
96        // interface (standalone) or the linked RD session. When the SC
97        // session is linked to an RD session, mutter requires
98        // `RemoteDesktop.Session.Start` to drive it; calling
99        // `ScreenCast.Session.Start` directly yields
100        // "Must be started from remote desktop session". Starting RD
101        // also unlocks `NotifyPointerMotionAbsolute` on the input
102        // backend. Only the first stream triggers RD.Start; subsequent
103        // streams share the same RD session and skip.
104        let should_start_rd = {
105            let mut guard = self.state.rd_started_lock()?;
106            if *guard {
107                false
108            } else {
109                *guard = true;
110                true
111            }
112        };
113        if should_start_rd {
114            conn.call_method(
115                Some("org.gnome.Mutter.RemoteDesktop"),
116                self.state.rd_session_path(),
117                Some("org.gnome.Mutter.RemoteDesktop.Session"),
118                "Start",
119                &(),
120            )
121            .await
122            .map_err(|e| Error::screenshot_with("RemoteDesktop Start", e))?;
123        } else {
124            conn.call_method(
125                Some("org.gnome.Mutter.ScreenCast"),
126                session_path.as_str(),
127                Some("org.gnome.Mutter.ScreenCast.Session"),
128                "Start",
129                &(),
130            )
131            .await
132            .map_err(|e| Error::screenshot_with("Start", e))?;
133        }
134
135        // Step 5: Wait for PipeWireStreamAdded signal to get the node id.
136        let node_id: u32 = tokio::time::timeout(std::time::Duration::from_secs(5), async {
137            let signal = signal_stream
138                .next()
139                .await
140                .ok_or_else(|| Error::screenshot("signal stream ended"))?;
141            signal
142                .body()
143                .deserialize::<u32>()
144                .map_err(|e| Error::screenshot_with("parse node_id", e))
145        })
146        .await
147        .map_err(|_| Error::screenshot("timeout waiting for PipeWireStreamAdded"))??;
148
149        tracing::debug!(node_id, "got PipeWire node id for screenshot");
150
151        // Publish the stream object path so MutterInput can route
152        // NotifyPointerMotionAbsolute at the correct monitor.
153        *self.state.active_stream_path_lock()? = Some(stream_path.to_string());
154
155        Ok(PipeWireStream {
156            node_id,
157            // The session_path is an `OwnedObjectPath`; `stop_stream`
158            // below downcasts back to it. `StreamToken::downcast`
159            // returns a typed error if a future change ever feeds a
160            // different value here, instead of a `()` from
161            // `Box::downcast`.
162            token: StreamToken::new(session_path),
163        })
164    }
165
166    async fn stop_stream(&self, stream: PipeWireStream) -> Result<()> {
167        let session_path = stream.token.downcast::<OwnedObjectPath>()?;
168        let _ = self
169            .state
170            .conn()
171            .call_method(
172                Some("org.gnome.Mutter.ScreenCast"),
173                session_path.as_str(),
174                Some("org.gnome.Mutter.ScreenCast.Session"),
175                "Stop",
176                &(),
177            )
178            .await;
179        *self.state.active_stream_path_lock()? = None;
180        Ok(())
181    }
182
183    fn pipewire_socket(&self) -> PathBuf {
184        self.state.runtime_dir().join("pipewire-0")
185    }
186}