mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-23 02:05:33 +07:00
ipc: Add screencast request and events for PipeWire casts
Allows desktop bars to show when screen recording is active.
This commit is contained in:
@@ -117,6 +117,8 @@ pub enum Request {
|
||||
ReturnError,
|
||||
/// Request information about the overview.
|
||||
OverviewState,
|
||||
/// Request information about screencasts.
|
||||
Casts,
|
||||
}
|
||||
|
||||
/// Reply from niri to client.
|
||||
@@ -161,6 +163,8 @@ pub enum Response {
|
||||
OutputConfigChanged(OutputConfigChanged),
|
||||
/// Information about the overview.
|
||||
OverviewState(Overview),
|
||||
/// Information about screencasts.
|
||||
Casts(Vec<Cast>),
|
||||
}
|
||||
|
||||
/// Overview information.
|
||||
@@ -1473,6 +1477,52 @@ pub struct LayerSurface {
|
||||
pub keyboard_interactivity: LayerSurfaceKeyboardInteractivity,
|
||||
}
|
||||
|
||||
/// A screencast.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub struct Cast {
|
||||
/// Stream ID of the screencast that uniquely identifies it.
|
||||
pub stream_id: u64,
|
||||
/// Session ID of the screencast.
|
||||
///
|
||||
/// A session can have multiple screencast streams. Then multiple `Cast`s will have the same
|
||||
/// `session_id`. Though, usually there's only one stream per session.
|
||||
///
|
||||
/// Do not confuse `session_id` with [`stream_id`](Self::stream_id).
|
||||
pub session_id: u64,
|
||||
/// Target being captured.
|
||||
pub target: CastTarget,
|
||||
/// Whether this is a Dynamic Cast Target screencast.
|
||||
///
|
||||
/// Meaning that actions like `SetDynamicCastWindow` will act on this screencast.
|
||||
///
|
||||
/// Keep in mind that the target can change even if this is `false`.
|
||||
pub is_dynamic_target: bool,
|
||||
/// Whether the cast is currently streaming frames.
|
||||
///
|
||||
/// This can be `false` for example when switching away to a different scene in OBS, which
|
||||
/// pauses the stream.
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
/// Target of a screencast.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum CastTarget {
|
||||
/// The target is not yet set, or was cleared.
|
||||
Nothing {},
|
||||
/// Casting an output.
|
||||
Output {
|
||||
/// Name of the screencasted output.
|
||||
name: String,
|
||||
},
|
||||
/// Casting a window.
|
||||
Window {
|
||||
/// ID of the screencasted window.
|
||||
id: u64,
|
||||
},
|
||||
}
|
||||
|
||||
/// A compositor event.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
@@ -1595,6 +1645,24 @@ pub enum Event {
|
||||
/// be converted to a `String` (e.g. contained invalid UTF-8 bytes).
|
||||
path: Option<String>,
|
||||
},
|
||||
/// The screencasts have changed.
|
||||
CastsChanged {
|
||||
/// The new screencast information.
|
||||
///
|
||||
/// This configuration completely replaces the previous configuration. I.e. if any casts
|
||||
/// are missing from here, then they were stopped.
|
||||
casts: Vec<Cast>,
|
||||
},
|
||||
/// A screencast started, or an existing cast changed.
|
||||
CastStartedOrChanged {
|
||||
/// The cast that started or changed.
|
||||
cast: Cast,
|
||||
},
|
||||
/// A screencast stopped.
|
||||
CastStopped {
|
||||
/// Stream ID of the stopped screencast.
|
||||
stream_id: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<Duration> for Timestamp {
|
||||
|
||||
+37
-1
@@ -9,7 +9,7 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{Event, KeyboardLayouts, Window, Workspace};
|
||||
use crate::{Cast, Event, KeyboardLayouts, Window, Workspace};
|
||||
|
||||
/// Part of the state communicated via the event stream.
|
||||
pub trait EventStreamStatePart {
|
||||
@@ -46,6 +46,9 @@ pub struct EventStreamState {
|
||||
|
||||
/// State of the config.
|
||||
pub config: ConfigState,
|
||||
|
||||
/// State of screencasts.
|
||||
pub casts: CastsState,
|
||||
}
|
||||
|
||||
/// The workspaces state communicated over the event stream.
|
||||
@@ -83,6 +86,13 @@ pub struct ConfigState {
|
||||
pub failed: bool,
|
||||
}
|
||||
|
||||
/// The casts state communicated over the event stream.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CastsState {
|
||||
/// Map from a stream id to the screencast.
|
||||
pub casts: HashMap<u64, Cast>,
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for EventStreamState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
let mut events = Vec::new();
|
||||
@@ -91,6 +101,7 @@ impl EventStreamStatePart for EventStreamState {
|
||||
events.extend(self.keyboard_layouts.replicate());
|
||||
events.extend(self.overview.replicate());
|
||||
events.extend(self.config.replicate());
|
||||
events.extend(self.casts.replicate());
|
||||
events
|
||||
}
|
||||
|
||||
@@ -100,6 +111,7 @@ impl EventStreamStatePart for EventStreamState {
|
||||
let event = self.keyboard_layouts.apply(event)?;
|
||||
let event = self.overview.apply(event)?;
|
||||
let event = self.config.apply(event)?;
|
||||
let event = self.casts.apply(event)?;
|
||||
Some(event)
|
||||
}
|
||||
}
|
||||
@@ -285,3 +297,27 @@ impl EventStreamStatePart for ConfigState {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for CastsState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
let casts = self.casts.values().cloned().collect();
|
||||
vec![Event::CastsChanged { casts }]
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Event) -> Option<Event> {
|
||||
match event {
|
||||
Event::CastsChanged { casts } => {
|
||||
self.casts = casts.into_iter().map(|c| (c.stream_id, c)).collect();
|
||||
}
|
||||
Event::CastStartedOrChanged { cast } => {
|
||||
self.casts.insert(cast.stream_id, cast);
|
||||
}
|
||||
Event::CastStopped { stream_id } => {
|
||||
let cast = self.casts.remove(&stream_id);
|
||||
cast.expect("stopped cast was missing from the map");
|
||||
}
|
||||
event => return Some(event),
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,8 @@ pub enum Msg {
|
||||
RequestError,
|
||||
/// Print the overview state.
|
||||
OverviewState,
|
||||
/// List screencasts.
|
||||
Casts,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, clap::ValueEnum)]
|
||||
|
||||
+56
-2
@@ -7,8 +7,8 @@ use anyhow::{anyhow, bail, Context};
|
||||
use niri_config::OutputName;
|
||||
use niri_ipc::socket::Socket;
|
||||
use niri_ipc::{
|
||||
Action, Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Overview,
|
||||
Request, Response, Transform, Window, WindowLayout,
|
||||
Action, Cast, CastTarget, Event, KeyboardLayouts, LogicalOutput, Mode, Output,
|
||||
OutputConfigChanged, Overview, Request, Response, Transform, Window, WindowLayout,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -48,6 +48,7 @@ pub fn handle_msg(mut msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
Msg::EventStream => Request::EventStream,
|
||||
Msg::RequestError => Request::ReturnError,
|
||||
Msg::OverviewState => Request::OverviewState,
|
||||
Msg::Casts => Request::Casts,
|
||||
};
|
||||
|
||||
let mut socket = Socket::connect().context("error connecting to the niri socket")?;
|
||||
@@ -496,6 +497,15 @@ pub fn handle_msg(mut msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
let description = parts.join(" and ");
|
||||
println!("Screenshot captured: {description}");
|
||||
}
|
||||
Event::CastsChanged { casts } => {
|
||||
println!("Casts changed: {casts:?}");
|
||||
}
|
||||
Event::CastStartedOrChanged { cast } => {
|
||||
println!("Cast started or changed: {cast:?}");
|
||||
}
|
||||
Event::CastStopped { stream_id } => {
|
||||
println!("Cast stopped: stream id {stream_id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -518,6 +528,28 @@ pub fn handle_msg(mut msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
println!("Overview is closed.");
|
||||
}
|
||||
}
|
||||
Msg::Casts => {
|
||||
let Response::Casts(mut casts) = response else {
|
||||
bail!("unexpected response: expected Casts, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let casts = serde_json::to_string(&casts).context("error formatting response")?;
|
||||
println!("{casts}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if casts.is_empty() {
|
||||
println!("No screencasts.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
casts.sort_by_key(|c| (c.session_id, c.stream_id));
|
||||
for cast in casts {
|
||||
print_cast(&cast);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -706,6 +738,28 @@ fn print_window(window: &Window) {
|
||||
);
|
||||
}
|
||||
|
||||
fn print_cast(cast: &Cast) {
|
||||
let active = if cast.is_active { "" } else { " (inactive)" };
|
||||
println!("Cast stream ID {}:{active}", cast.stream_id);
|
||||
println!(" Session ID: {}", cast.session_id);
|
||||
|
||||
match &cast.target {
|
||||
CastTarget::Nothing {} => {
|
||||
println!(" Target: nothing (cleared)");
|
||||
}
|
||||
CastTarget::Output { name } => {
|
||||
println!(" Target: output \"{name}\"");
|
||||
}
|
||||
CastTarget::Window { id } => {
|
||||
println!(" Target: window {id}");
|
||||
}
|
||||
}
|
||||
|
||||
if cast.is_dynamic_target {
|
||||
println!(" Dynamic cast target");
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_rounded(x: f64) -> String {
|
||||
let r = x.round();
|
||||
if (r - x).abs() <= 0.005 {
|
||||
|
||||
@@ -450,6 +450,11 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
let is_open = state.overview.is_open;
|
||||
Response::OverviewState(Overview { is_open })
|
||||
}
|
||||
Request::Casts => {
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
let casts = state.casts.casts.values().cloned().collect();
|
||||
Response::Casts(casts)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
@@ -793,6 +798,74 @@ impl State {
|
||||
server.send_event(event);
|
||||
}
|
||||
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
pub fn ipc_refresh_casts(&mut self) {
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _span = tracy_client::span!("State::ipc_refresh_casts");
|
||||
|
||||
let mut state = server.event_stream_state.borrow_mut();
|
||||
let state = &mut state.casts;
|
||||
|
||||
let mut events = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
// Check pending dynamic casts.
|
||||
for pending in &self.niri.casting.pending_dynamic_casts {
|
||||
let stream_id = pending.stream_id.get();
|
||||
seen.insert(stream_id);
|
||||
|
||||
// Pending dynamic casts don't change any properties, so we only need to check if it's
|
||||
// missing from the state.
|
||||
if !state.casts.contains_key(&stream_id) {
|
||||
let cast = niri_ipc::Cast {
|
||||
session_id: pending.session_id.get(),
|
||||
stream_id,
|
||||
target: niri_ipc::CastTarget::Nothing {},
|
||||
is_dynamic_target: true,
|
||||
is_active: false,
|
||||
};
|
||||
events.push(Event::CastStartedOrChanged { cast });
|
||||
}
|
||||
}
|
||||
|
||||
// Check active casts.
|
||||
for cast in &self.niri.casting.casts {
|
||||
let stream_id = cast.stream_id.get();
|
||||
seen.insert(stream_id);
|
||||
|
||||
if state.casts.get(&stream_id).is_none_or(|existing| {
|
||||
// Only these properties can change.
|
||||
existing.is_active != cast.is_active() || !cast.target.matches(&existing.target)
|
||||
}) {
|
||||
let cast = niri_ipc::Cast {
|
||||
session_id: cast.session_id.get(),
|
||||
stream_id,
|
||||
target: cast.target.make_ipc(),
|
||||
is_dynamic_target: cast.dynamic_target,
|
||||
is_active: cast.is_active(),
|
||||
};
|
||||
events.push(Event::CastStartedOrChanged { cast });
|
||||
}
|
||||
}
|
||||
|
||||
// Check for stopped casts.
|
||||
for stream_id in state.casts.keys() {
|
||||
if !seen.contains(stream_id) {
|
||||
events.push(Event::CastStopped {
|
||||
stream_id: *stream_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for event in events {
|
||||
state.apply(event.clone());
|
||||
server.send_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ipc_config_loaded(&mut self, failed: bool) {
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
|
||||
+23
@@ -592,6 +592,27 @@ impl CastTarget {
|
||||
pub fn matches_output(&self, weak: &WeakOutput) -> bool {
|
||||
matches!(self, CastTarget::Output { output, .. } if output == weak)
|
||||
}
|
||||
|
||||
pub fn matches(&self, ipc: &niri_ipc::CastTarget) -> bool {
|
||||
use CastTarget::*;
|
||||
match (self, ipc) {
|
||||
(Nothing, niri_ipc::CastTarget::Nothing {}) => true,
|
||||
(Output { name, .. }, niri_ipc::CastTarget::Output { name: ipc_name }) => {
|
||||
name == ipc_name
|
||||
}
|
||||
(Window { id }, niri_ipc::CastTarget::Window { id: ipc_id }) => id == ipc_id,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_ipc(&self) -> niri_ipc::CastTarget {
|
||||
use CastTarget::*;
|
||||
match self {
|
||||
Nothing => niri_ipc::CastTarget::Nothing {},
|
||||
Output { name, .. } => niri_ipc::CastTarget::Output { name: name.clone() },
|
||||
Window { id } => niri_ipc::CastTarget::Window { id: *id },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pending update to a window's focus timestamp.
|
||||
@@ -795,6 +816,8 @@ impl State {
|
||||
// screencasts.
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
self.niri.refresh_mapped_cast_window_rules();
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
self.ipc_refresh_casts();
|
||||
|
||||
self.niri.refresh_window_rules();
|
||||
self.refresh_ipc_outputs();
|
||||
|
||||
Reference in New Issue
Block a user