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
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.
37        let empty_opts: HashMap<&str, Value> = HashMap::new();
38        let reply = conn
39            .call_method(
40                Some("org.gnome.Mutter.ScreenCast"),
41                "/org/gnome/Mutter/ScreenCast",
42                Some("org.gnome.Mutter.ScreenCast"),
43                "CreateSession",
44                &(empty_opts.clone(),),
45            )
46            .await
47            .map_err(|e| Error::Screenshot(format!("CreateSession: {e}")))?;
48        let session_path: OwnedObjectPath = reply
49            .body()
50            .deserialize()
51            .map_err(|e| Error::Screenshot(format!("parse session path: {e}")))?;
52
53        // Step 2: RecordMonitor on the session.
54        let reply = conn
55            .call_method(
56                Some("org.gnome.Mutter.ScreenCast"),
57                session_path.as_str(),
58                Some("org.gnome.Mutter.ScreenCast.Session"),
59                "RecordMonitor",
60                &("", empty_opts),
61            )
62            .await
63            .map_err(|e| Error::Screenshot(format!("RecordMonitor: {e}")))?;
64        let stream_path: OwnedObjectPath = reply
65            .body()
66            .deserialize()
67            .map_err(|e| Error::Screenshot(format!("parse stream path: {e}")))?;
68
69        // Step 3: Subscribe to PipeWireStreamAdded BEFORE starting.
70        // This ordering is load-bearing — mutter emits the signal synchronously
71        // during `Session.Start`, so a late subscribe misses it.
72        let stream_proxy: zbus::Proxy<'_> = zbus::proxy::Builder::new(conn)
73            .destination("org.gnome.Mutter.ScreenCast")
74            .map_err(|e| Error::Screenshot(format!("proxy destination: {e}")))?
75            .path(stream_path.as_str())
76            .map_err(|e| Error::Screenshot(format!("proxy path: {e}")))?
77            .interface("org.gnome.Mutter.ScreenCast.Stream")
78            .map_err(|e| Error::Screenshot(format!("proxy interface: {e}")))?
79            .build()
80            .await
81            .map_err(|e| Error::Screenshot(format!("build stream proxy: {e}")))?;
82
83        let mut signal_stream = stream_proxy
84            .receive_signal("PipeWireStreamAdded")
85            .await
86            .map_err(|e| Error::Screenshot(format!("receive_signal: {e}")))?;
87
88        // Step 4: Start the ScreenCast session.
89        conn.call_method(
90            Some("org.gnome.Mutter.ScreenCast"),
91            session_path.as_str(),
92            Some("org.gnome.Mutter.ScreenCast.Session"),
93            "Start",
94            &(),
95        )
96        .await
97        .map_err(|e| Error::Screenshot(format!("Start: {e}")))?;
98
99        // Step 5: Wait for PipeWireStreamAdded signal to get the node id.
100        let node_id: u32 = tokio::time::timeout(std::time::Duration::from_secs(5), async {
101            let signal = signal_stream
102                .next()
103                .await
104                .ok_or_else(|| Error::Screenshot("signal stream ended".to_string()))?;
105            signal
106                .body()
107                .deserialize::<u32>()
108                .map_err(|e| Error::Screenshot(format!("parse node_id: {e}")))
109        })
110        .await
111        .map_err(|_| Error::Screenshot("timeout waiting for PipeWireStreamAdded".to_string()))??;
112
113        tracing::debug!(node_id, "got PipeWire node id for screenshot");
114
115        Ok(PipeWireStream {
116            node_id,
117            token: Box::new(session_path),
118        })
119    }
120
121    async fn stop_stream(&self, stream: PipeWireStream) -> Result<()> {
122        let session_path = stream.token.downcast::<OwnedObjectPath>().map_err(|_| {
123            Error::Screenshot("stop_stream: token was not an OwnedObjectPath".into())
124        })?;
125        let _ = self
126            .state
127            .conn
128            .call_method(
129                Some("org.gnome.Mutter.ScreenCast"),
130                session_path.as_str(),
131                Some("org.gnome.Mutter.ScreenCast.Session"),
132                "Stop",
133                &(),
134            )
135            .await;
136        Ok(())
137    }
138
139    fn pipewire_socket(&self) -> PathBuf {
140        self.state.runtime_dir.join("pipewire-0")
141    }
142}