use std::collections::HashMap; use std::mem; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use serde::Deserialize; use smithay::output::Output; use zbus::fdo::RequestNameFlags; use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value}; use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext}; use super::Start; use crate::backend::IpcOutputMap; #[derive(Clone)] pub struct ScreenCast { ipc_outputs: Arc>, to_niri: calloop::channel::Sender, #[allow(clippy::type_complexity)] sessions: Arc)>>>, } #[derive(Clone)] pub struct Session { id: usize, ipc_outputs: Arc>, to_niri: calloop::channel::Sender, #[allow(clippy::type_complexity)] streams: Arc)>>>, stopped: Arc, } #[derive(Debug, Default, Deserialize, Type, Clone, Copy)] pub enum CursorMode { #[default] Hidden = 0, Embedded = 1, Metadata = 2, } #[derive(Debug, DeserializeDict, Type)] #[zvariant(signature = "dict")] struct RecordMonitorProperties { #[zvariant(rename = "cursor-mode")] cursor_mode: Option, #[zvariant(rename = "is-recording")] _is_recording: Option, } #[derive(Clone)] pub struct Stream { // FIXME: update on scale changes and whatnot. output: niri_ipc::Output, cursor_mode: CursorMode, was_started: Arc, to_niri: calloop::channel::Sender, } #[derive(Debug, SerializeDict, Type, Value)] #[zvariant(signature = "dict")] struct StreamParameters { /// Position of the stream in logical coordinates. position: (i32, i32), /// Size of the stream in logical coordinates. size: (i32, i32), } pub enum ScreenCastToNiri { StartCast { session_id: usize, output: String, cursor_mode: CursorMode, signal_ctx: SignalContext<'static>, }, StopCast { session_id: usize, }, Redraw(Output), } #[dbus_interface(name = "org.gnome.Mutter.ScreenCast")] impl ScreenCast { async fn create_session( &self, #[zbus(object_server)] server: &ObjectServer, properties: HashMap<&str, Value<'_>>, ) -> fdo::Result { if properties.contains_key("remote-desktop-session-id") { return Err(fdo::Error::Failed( "there are no remote desktop sessions".to_owned(), )); } static NUMBER: AtomicUsize = AtomicUsize::new(0); let session_id = NUMBER.fetch_add(1, Ordering::SeqCst); let path = format!("/org/gnome/Mutter/ScreenCast/Session/u{}", session_id); let path = OwnedObjectPath::try_from(path).unwrap(); let session = Session::new(session_id, self.ipc_outputs.clone(), self.to_niri.clone()); match server.at(&path, session.clone()).await { Ok(true) => { let iface = server.interface(&path).await.unwrap(); self.sessions.lock().unwrap().push((session, iface)); } Ok(false) => return Err(fdo::Error::Failed("session path already exists".to_owned())), Err(err) => { return Err(fdo::Error::Failed(format!( "error creating session object: {err:?}" ))) } } Ok(path) } #[dbus_interface(property)] async fn version(&self) -> i32 { 4 } } #[dbus_interface(name = "org.gnome.Mutter.ScreenCast.Session")] impl Session { async fn start(&self) { debug!("start"); for (stream, iface) in &*self.streams.lock().unwrap() { stream.start(self.id, iface.signal_context().clone()); } } pub async fn stop( &self, #[zbus(object_server)] server: &ObjectServer, #[zbus(signal_context)] ctxt: SignalContext<'_>, ) { debug!("stop"); if self.stopped.swap(true, Ordering::SeqCst) { // Already stopped. return; } Session::closed(&ctxt).await.unwrap(); if let Err(err) = self.to_niri.send(ScreenCastToNiri::StopCast { session_id: self.id, }) { warn!("error sending StopCast to niri: {err:?}"); } let streams = mem::take(&mut *self.streams.lock().unwrap()); for (_, iface) in streams.iter() { server .remove::(iface.signal_context().path()) .await .unwrap(); } server.remove::(ctxt.path()).await.unwrap(); } async fn record_monitor( &mut self, #[zbus(object_server)] server: &ObjectServer, connector: &str, properties: RecordMonitorProperties, ) -> fdo::Result { debug!(connector, ?properties, "record_monitor"); let Some(output) = self.ipc_outputs.lock().unwrap().get(connector).cloned() else { return Err(fdo::Error::Failed("no such monitor".to_owned())); }; if output.logical.is_none() { return Err(fdo::Error::Failed("monitor is disabled".to_owned())); } static NUMBER: AtomicUsize = AtomicUsize::new(0); let path = format!( "/org/gnome/Mutter/ScreenCast/Stream/u{}", NUMBER.fetch_add(1, Ordering::SeqCst) ); let path = OwnedObjectPath::try_from(path).unwrap(); let cursor_mode = properties.cursor_mode.unwrap_or_default(); let stream = Stream::new(output.clone(), cursor_mode, self.to_niri.clone()); match server.at(&path, stream.clone()).await { Ok(true) => { let iface = server.interface(&path).await.unwrap(); self.streams.lock().unwrap().push((stream, iface)); } Ok(false) => return Err(fdo::Error::Failed("stream path already exists".to_owned())), Err(err) => { return Err(fdo::Error::Failed(format!( "error creating stream object: {err:?}" ))) } } Ok(path) } #[dbus_interface(signal)] async fn closed(ctxt: &SignalContext<'_>) -> zbus::Result<()>; } #[dbus_interface(name = "org.gnome.Mutter.ScreenCast.Stream")] impl Stream { #[dbus_interface(signal)] pub async fn pipe_wire_stream_added(ctxt: &SignalContext<'_>, node_id: u32) -> zbus::Result<()>; #[dbus_interface(property)] async fn parameters(&self) -> StreamParameters { let logical = self.output.logical.as_ref().unwrap(); StreamParameters { position: (logical.x, logical.y), size: (logical.width as i32, logical.height as i32), } } } impl ScreenCast { pub fn new( ipc_outputs: Arc>, to_niri: calloop::channel::Sender, ) -> Self { Self { ipc_outputs, to_niri, sessions: Arc::new(Mutex::new(vec![])), } } } impl Start for ScreenCast { fn start(self) -> anyhow::Result { let conn = zbus::blocking::Connection::session()?; let flags = RequestNameFlags::AllowReplacement | RequestNameFlags::ReplaceExisting | RequestNameFlags::DoNotQueue; conn.object_server() .at("/org/gnome/Mutter/ScreenCast", self)?; conn.request_name_with_flags("org.gnome.Mutter.ScreenCast", flags)?; Ok(conn) } } impl Session { pub fn new( id: usize, ipc_outputs: Arc>, to_niri: calloop::channel::Sender, ) -> Self { Self { id, ipc_outputs, streams: Arc::new(Mutex::new(vec![])), to_niri, stopped: Arc::new(AtomicBool::new(false)), } } } impl Drop for Session { fn drop(&mut self) { let _ = self.to_niri.send(ScreenCastToNiri::StopCast { session_id: self.id, }); } } impl Stream { pub fn new( output: niri_ipc::Output, cursor_mode: CursorMode, to_niri: calloop::channel::Sender, ) -> Self { Self { output, cursor_mode, was_started: Arc::new(AtomicBool::new(false)), to_niri, } } fn start(&self, session_id: usize, ctxt: SignalContext<'static>) { if self.was_started.load(Ordering::SeqCst) { return; } let msg = ScreenCastToNiri::StartCast { session_id, output: self.output.name.clone(), cursor_mode: self.cursor_mode, signal_ctx: ctxt, }; if let Err(err) = self.to_niri.send(msg) { warn!("error sending StartCast to niri: {err:?}"); } } }