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,
|
ReturnError,
|
||||||
/// Request information about the overview.
|
/// Request information about the overview.
|
||||||
OverviewState,
|
OverviewState,
|
||||||
|
/// Request information about screencasts.
|
||||||
|
Casts,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reply from niri to client.
|
/// Reply from niri to client.
|
||||||
@@ -161,6 +163,8 @@ pub enum Response {
|
|||||||
OutputConfigChanged(OutputConfigChanged),
|
OutputConfigChanged(OutputConfigChanged),
|
||||||
/// Information about the overview.
|
/// Information about the overview.
|
||||||
OverviewState(Overview),
|
OverviewState(Overview),
|
||||||
|
/// Information about screencasts.
|
||||||
|
Casts(Vec<Cast>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Overview information.
|
/// Overview information.
|
||||||
@@ -1473,6 +1477,52 @@ pub struct LayerSurface {
|
|||||||
pub keyboard_interactivity: LayerSurfaceKeyboardInteractivity,
|
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.
|
/// A compositor event.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
#[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).
|
/// be converted to a `String` (e.g. contained invalid UTF-8 bytes).
|
||||||
path: Option<String>,
|
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 {
|
impl From<Duration> for Timestamp {
|
||||||
|
|||||||
+37
-1
@@ -9,7 +9,7 @@
|
|||||||
use std::collections::hash_map::Entry;
|
use std::collections::hash_map::Entry;
|
||||||
use std::collections::HashMap;
|
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.
|
/// Part of the state communicated via the event stream.
|
||||||
pub trait EventStreamStatePart {
|
pub trait EventStreamStatePart {
|
||||||
@@ -46,6 +46,9 @@ pub struct EventStreamState {
|
|||||||
|
|
||||||
/// State of the config.
|
/// State of the config.
|
||||||
pub config: ConfigState,
|
pub config: ConfigState,
|
||||||
|
|
||||||
|
/// State of screencasts.
|
||||||
|
pub casts: CastsState,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The workspaces state communicated over the event stream.
|
/// The workspaces state communicated over the event stream.
|
||||||
@@ -83,6 +86,13 @@ pub struct ConfigState {
|
|||||||
pub failed: bool,
|
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 {
|
impl EventStreamStatePart for EventStreamState {
|
||||||
fn replicate(&self) -> Vec<Event> {
|
fn replicate(&self) -> Vec<Event> {
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
@@ -91,6 +101,7 @@ impl EventStreamStatePart for EventStreamState {
|
|||||||
events.extend(self.keyboard_layouts.replicate());
|
events.extend(self.keyboard_layouts.replicate());
|
||||||
events.extend(self.overview.replicate());
|
events.extend(self.overview.replicate());
|
||||||
events.extend(self.config.replicate());
|
events.extend(self.config.replicate());
|
||||||
|
events.extend(self.casts.replicate());
|
||||||
events
|
events
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +111,7 @@ impl EventStreamStatePart for EventStreamState {
|
|||||||
let event = self.keyboard_layouts.apply(event)?;
|
let event = self.keyboard_layouts.apply(event)?;
|
||||||
let event = self.overview.apply(event)?;
|
let event = self.overview.apply(event)?;
|
||||||
let event = self.config.apply(event)?;
|
let event = self.config.apply(event)?;
|
||||||
|
let event = self.casts.apply(event)?;
|
||||||
Some(event)
|
Some(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -285,3 +297,27 @@ impl EventStreamStatePart for ConfigState {
|
|||||||
None
|
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,
|
RequestError,
|
||||||
/// Print the overview state.
|
/// Print the overview state.
|
||||||
OverviewState,
|
OverviewState,
|
||||||
|
/// List screencasts.
|
||||||
|
Casts,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, clap::ValueEnum)]
|
#[derive(Clone, Debug, clap::ValueEnum)]
|
||||||
|
|||||||
+56
-2
@@ -7,8 +7,8 @@ use anyhow::{anyhow, bail, Context};
|
|||||||
use niri_config::OutputName;
|
use niri_config::OutputName;
|
||||||
use niri_ipc::socket::Socket;
|
use niri_ipc::socket::Socket;
|
||||||
use niri_ipc::{
|
use niri_ipc::{
|
||||||
Action, Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Overview,
|
Action, Cast, CastTarget, Event, KeyboardLayouts, LogicalOutput, Mode, Output,
|
||||||
Request, Response, Transform, Window, WindowLayout,
|
OutputConfigChanged, Overview, Request, Response, Transform, Window, WindowLayout,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
@@ -48,6 +48,7 @@ pub fn handle_msg(mut msg: Msg, json: bool) -> anyhow::Result<()> {
|
|||||||
Msg::EventStream => Request::EventStream,
|
Msg::EventStream => Request::EventStream,
|
||||||
Msg::RequestError => Request::ReturnError,
|
Msg::RequestError => Request::ReturnError,
|
||||||
Msg::OverviewState => Request::OverviewState,
|
Msg::OverviewState => Request::OverviewState,
|
||||||
|
Msg::Casts => Request::Casts,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut socket = Socket::connect().context("error connecting to the niri socket")?;
|
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 ");
|
let description = parts.join(" and ");
|
||||||
println!("Screenshot captured: {description}");
|
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.");
|
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(())
|
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 {
|
fn fmt_rounded(x: f64) -> String {
|
||||||
let r = x.round();
|
let r = x.round();
|
||||||
if (r - x).abs() <= 0.005 {
|
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;
|
let is_open = state.overview.is_open;
|
||||||
Response::OverviewState(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)
|
Ok(response)
|
||||||
@@ -793,6 +798,74 @@ impl State {
|
|||||||
server.send_event(event);
|
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) {
|
pub fn ipc_config_loaded(&mut self, failed: bool) {
|
||||||
let Some(server) = &self.niri.ipc_server else {
|
let Some(server) = &self.niri.ipc_server else {
|
||||||
return;
|
return;
|
||||||
|
|||||||
+23
@@ -592,6 +592,27 @@ impl CastTarget {
|
|||||||
pub fn matches_output(&self, weak: &WeakOutput) -> bool {
|
pub fn matches_output(&self, weak: &WeakOutput) -> bool {
|
||||||
matches!(self, CastTarget::Output { output, .. } if output == weak)
|
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.
|
/// Pending update to a window's focus timestamp.
|
||||||
@@ -795,6 +816,8 @@ impl State {
|
|||||||
// screencasts.
|
// screencasts.
|
||||||
#[cfg(feature = "xdp-gnome-screencast")]
|
#[cfg(feature = "xdp-gnome-screencast")]
|
||||||
self.niri.refresh_mapped_cast_window_rules();
|
self.niri.refresh_mapped_cast_window_rules();
|
||||||
|
#[cfg(feature = "xdp-gnome-screencast")]
|
||||||
|
self.ipc_refresh_casts();
|
||||||
|
|
||||||
self.niri.refresh_window_rules();
|
self.niri.refresh_window_rules();
|
||||||
self.refresh_ipc_outputs();
|
self.refresh_ipc_outputs();
|
||||||
|
|||||||
Reference in New Issue
Block a user