Support isolated live window ScreenCast streams

Render selected toplevels through a private portal-only capture protocol, preserve logical snapshot geometry, and renegotiate PipeWire video on window resizing. Bound outstanding snapshots and retry temporary contention; stop on source closure and session locking.

Validation: portal unit tests (31), capture budget unit, private-bus portal contracts, and real nested/PipeWire isolation, workspace, resize, close and paused-lock tests pass. Independent review cleared. Monitor restore integration timed out on both this build and the unchanged previous compositor under the current nested host.
This commit is contained in:
Samuel Onoja committed 2026-09-30 04:02:33 +01:00
1 parent 56a888adb3
commit baca852ee9
26 files changed
+1307 -139

No files matched your search

+1
View File
@@ -14,6 +14,7 @@ name = "ferese-record"
path = "src/record_main.rs"
[dependencies]
ferese-protocols = { path = "../crates/ferese-protocols", default-features = false, features = ["client"] }
ferese-ipc = { path = "../crates/ferese-ipc" }
url = "2.5"
png = "0.18"
+178 -23
View File
@@ -78,6 +78,7 @@ struct Session {
phase: Phase,
multiple: bool,
cursor: bool,
source_types: u32,
bar_controlled: bool,
persist_mode: u32,
restore: Option<crate::restore::Restore>,
@@ -184,7 +185,7 @@ fn error(message: &str) -> zbus::fdo::Error {
zbus::fdo::Error::InvalidArgs(message.into())
}
fn source_options(options: &Options) -> zbus::fdo::Result<(bool, bool)> {
fn source_options(options: &Options) -> zbus::fdo::Result<(bool, bool, u32)> {
let number = |key: &str, default| {
options
.get(key)
@@ -195,8 +196,8 @@ fn source_options(options: &Options) -> zbus::fdo::Result<(bool, bool)> {
};
let types = number("types", 1)?;
let cursor = number("cursor_mode", 1)?;
if types & 1 == 0 {
return Err(error("Only monitor sharing is supported"));
if types == 0 || types & !3 != 0 {
return Err(error("Unsupported source types"));
}
if cursor != 1 && cursor != 2 {
return Err(error("Unsupported cursor mode"));
@@ -208,7 +209,7 @@ fn source_options(options: &Options) -> zbus::fdo::Result<(bool, bool)> {
.map_err(|_| error("Invalid multiple option"))?
.unwrap_or(false);
Ok((multiple, cursor == 2))
Ok((multiple, cursor == 2, types))
}
impl Backend {
@@ -241,7 +242,7 @@ impl Backend {
#[zbus(property)]
fn available_source_types(&self) -> u32 {
1
3
}
#[zbus(property)]
@@ -294,6 +295,7 @@ impl Backend {
phase: Phase::Created,
multiple: false,
cursor: false,
source_types: 1,
bar_controlled: false,
persist_mode: 0,
restore: None,
@@ -313,8 +315,13 @@ impl Backend {
#[zbus(header)] header: Header<'_>,
) -> zbus::fdo::Result<(u32, Options)> {
let owner = authorize(connection, &header).await?;
let settings = source_options(&options).and_then(|(multiple, cursor)| {
Ok((multiple, cursor, crate::restore::persist_mode(&options)?))
let settings = source_options(&options).and_then(|(multiple, cursor, types)| {
Ok((
multiple,
cursor,
types,
crate::restore::persist_mode(&options)?,
))
});
let mut sessions = self.sessions.lock().await;
let session = sessions
@@ -329,11 +336,12 @@ impl Backend {
}
match settings {
Ok((multiple, cursor, persist_mode)) => {
Ok((multiple, cursor, types, persist_mode)) => {
session.persist_mode = persist_mode;
session.restore = crate::restore::Restore::read(&options);
session.multiple = multiple;
session.cursor = cursor;
session.source_types = types;
session.phase = Phase::Selected;
Ok((0, Options::new()))
}
@@ -365,7 +373,7 @@ impl Backend {
return Err(error("Invalid request path"));
}
let (multiple, cursor, bar_controlled, persist_mode, restore, cancel) = {
let (multiple, cursor, source_types, bar_controlled, persist_mode, restore, cancel) = {
let mut sessions = self.sessions.lock().await;
let session = sessions
.get_mut(session_handle.as_str())
@@ -379,6 +387,7 @@ impl Backend {
(
session.multiple,
session.cursor,
session.source_types,
session.bar_controlled,
session.persist_mode,
session.restore.clone(),
@@ -412,6 +421,7 @@ impl Backend {
&parent_window,
multiple,
cursor,
source_types,
persist_mode,
restore,
bar_controlled,
@@ -559,6 +569,7 @@ async fn start_streams(
parent: &str,
multiple: bool,
cursor: bool,
source_types: u32,
requested_persistence: u32,
restore: Option<crate::restore::Restore>,
bar_controlled: bool,
@@ -566,7 +577,15 @@ async fn start_streams(
let mut discovery = tokio::task::spawn_blocking(|| Capture::connect(&AtomicBool::new(false)))
.await
.map_err(|e| e.to_string())??;
let sources = discovery.sources();
let mut sources = if source_types & 1 != 0 {
discovery.sources()
} else {
Vec::new()
};
if source_types & 2 != 0 && discovery.supports_windows() {
let windows = crate::desktop::ipc("get-windows", serde_json::json!({})).await?;
sources.extend(crate::desktop::sharing_window_sources(&windows)?);
}
let outputs = crate::desktop::ipc("get-outputs", serde_json::json!({})).await?;
let rememberable = sources
.iter()
@@ -578,6 +597,15 @@ async fn start_streams(
.collect::<Vec<_>>();
let restored = restore.and_then(|restore| {
let selected = restore.resolve(app, cursor, multiple, &sources, &outputs)?;
let selected = validate_selection(
&sources,
&selected
.iter()
.map(|source| source.name.clone())
.collect::<Vec<_>>(),
multiple,
)
.ok()?;
Some((selected, restore.effective_mode(requested_persistence)))
});
let (selected, persist_mode) = if let Some(restored) = restored {
@@ -616,7 +644,12 @@ async fn start_streams(
let mut reply = Options::new();
let fresh_outputs = crate::desktop::ipc("get-outputs", serde_json::json!({})).await?;
if !crate::restore::selection_unchanged(&selected, &outputs, &fresh_outputs) {
let monitors = selected
.iter()
.filter(|source| source.window_id().is_none())
.cloned()
.collect::<Vec<_>>();
if !crate::restore::selection_unchanged(&monitors, &outputs, &fresh_outputs) {
return Err("A selected display changed while approval was pending".into());
}
@@ -629,7 +662,7 @@ async fn start_streams(
};
reply.insert("persist_mode".into(), effective_persistence.into());
let generations = selected
let generations = monitors
.iter()
.map(|source| {
discovery
@@ -641,14 +674,28 @@ async fn start_streams(
let mut children = Vec::new();
let mut streams = Vec::new();
for (source, (_, generation)) in selected.iter().zip(&generations) {
let mut child = child_command()?
.args([
for source in &selected {
let mut command = child_command()?;
if let Some(id) = source.window_id() {
command.args([
"--stream-window",
&id.to_string(),
if cursor { "embedded" } else { "hidden" },
]);
} else {
let generation = generations
.iter()
.find(|(name, _)| name == &source.name)
.ok_or("Missing display generation")?
.1;
command.args([
"--stream",
&source.name,
if cursor { "embedded" } else { "hidden" },
&generation.to_string(),
])
]);
}
let mut child = command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
@@ -680,7 +727,7 @@ async fn start_streams(
.map_err(|error| error.to_string())??;
let final_outputs = crate::desktop::ipc("get-outputs", serde_json::json!({})).await?;
if !crate::restore::selection_unchanged(&selected, &outputs, &final_outputs) {
if !crate::restore::selection_unchanged(&monitors, &outputs, &final_outputs) {
return Err("A selected display changed during stream startup".into());
}
@@ -691,16 +738,59 @@ async fn start_streams(
return Err("A selected stream ended during startup".into());
}
let final_windows = crate::desktop::ipc("get-windows", serde_json::json!({})).await?;
let streams = streams
.into_iter()
.map(|(name, ready)| {
monitor_metadata(&final_outputs, &name, &ready).map(|metadata| (ready.node, metadata))
let metadata = if name.starts_with("window:") {
let source = selected
.iter()
.find(|source| source.name == name)
.ok_or("Unknown window stream")?;
let id = source.window_id().ok_or("Invalid selected window")?;
if !final_windows.as_array().is_some_and(|windows| {
windows
.iter()
.any(|window| window["id"].as_u64() == Some(id))
}) {
return Err("A selected window closed during startup".to_owned());
}
window_metadata(source, &ready)
} else {
monitor_metadata(&final_outputs, &name, &ready)
};
metadata.map(|metadata| (ready.node, metadata))
})
.collect::<Result<Vec<_>, _>>()?;
Ok((children, streams, reply))
}
fn window_metadata(source: &Source, ready: &Ready) -> Result<Options, String> {
if source.window_id().is_none() || ready.width == 0 || ready.height == 0 {
return Err("Invalid window stream".into());
}
let (width, height) = ready
.logical_size
.ok_or("Missing captured window geometry")?;
let size = (
i32::try_from(width).map_err(|_| "Invalid window width")?,
i32::try_from(height).map_err(|_| "Invalid window height")?,
);
if size.0 <= 0 || size.1 <= 0 {
return Err("Invalid captured window geometry".into());
}
Ok(Options::from([
("source_type".into(), 2u32.into()),
(
"size".into(),
Value::from(size)
.try_to_owned()
.map_err(|e| e.to_string())?,
),
]))
}
fn monitor_metadata(
outputs: &serde_json::Value,
name: &str,
@@ -770,7 +860,11 @@ fn validate_selection(
names: &[String],
multiple: bool,
) -> Result<Vec<Source>, String> {
if names.is_empty() || names.len() > sources.len() || (!multiple && names.len() != 1) {
if names.is_empty()
|| names.len() > 8
|| names.len() > sources.len()
|| (!multiple && names.len() != 1)
{
return Err("Invalid display selection".into());
}
@@ -893,19 +987,78 @@ mod tests {
#[test]
fn rejects_unimplemented_sources_and_cursor_modes() {
assert_eq!(source_options(&Options::new()).unwrap(), (false, false));
for bits in [0u32, 2, 4, 6] {
assert_eq!(source_options(&Options::new()).unwrap(), (false, false, 1));
for bits in [0u32, 4, 5, 6, 7] {
assert!(source_options(&HashMap::from([("types".into(), bits.into())])).is_err());
}
for bits in [1u32, 3, 5, 7] {
for bits in [1u32, 2, 3] {
assert_eq!(
source_options(&HashMap::from([("types".into(), bits.into())])).unwrap(),
(false, false)
(false, false, bits)
);
}
assert!(source_options(&HashMap::from([("cursor_mode".into(), 4u32.into())])).is_err());
}
#[test]
fn window_metadata_uses_captured_logical_geometry_and_omits_position() {
let source = Source {
name: "window:17".into(),
label: "Editor".into(),
width: 640,
height: 480,
x: 0,
y: 0,
scale: 1,
};
let ready = Ready {
node: 1,
width: 1200,
height: 800,
logical_size: Some((600, 400)),
};
let metadata = window_metadata(&source, &ready).unwrap();
assert_eq!(u32::try_from(&metadata["source_type"]).unwrap(), 2);
assert_eq!(
<(i32, i32)>::try_from(metadata["size"].try_clone().unwrap()).unwrap(),
(600, 400)
);
assert!(!metadata.contains_key("position"));
assert!(
window_metadata(
&source,
&Ready {
logical_size: None,
..ready
}
)
.is_err()
);
}
#[test]
fn multiple_selection_is_bounded_and_accepts_displays_with_windows() {
let sources = (0..9)
.map(|id| Source {
name: format!("window:{id}"),
label: "Editor".into(),
width: 640,
height: 480,
x: 0,
y: 0,
scale: 1,
})
.collect::<Vec<_>>();
let names = sources
.iter()
.map(|source| source.name.clone())
.collect::<Vec<_>>();
assert!(validate_selection(&sources, &names[..8], true).is_ok());
assert!(validate_selection(&sources, &names, true).is_err());
assert!(validate_selection(&sources, &names[..2], false).is_err());
assert!(validate_selection(&sources, &[names[0].clone(), names[0].clone()], true).is_err());
}
#[test]
fn monitor_metadata_uses_logical_fractional_scale_geometry() {
let outputs = serde_json::json!([{"name":"display","enabled":true,"x":-1440,"y":20,"width":2560,"height":1440,"scale":1.5,"current_mode":{"width":3840,"height":2160}}]);
@@ -913,6 +1066,7 @@ mod tests {
&outputs,
"display",
&Ready {
logical_size: None,
node: 1,
width: 3840,
height: 2160,
@@ -932,6 +1086,7 @@ mod tests {
&outputs,
"missing",
&Ready {
logical_size: None,
node: 1,
width: 3840,
height: 2160
+92 -28
View File
@@ -1,4 +1,5 @@
//! Bounded, cancellable Wayland capture. No screenshots or external capture tools.
use ferese_protocols::window_capture::v1::client::ferese_window_capture_manager_v1 as window_manager;
use memmap2::MmapMut;
use serde::{Deserialize, Serialize};
use std::{
@@ -18,6 +19,8 @@ use wayland_protocols_wlr::screencopy::v1::client::{
zwlr_screencopy_frame_v1 as frame, zwlr_screencopy_manager_v1 as manager,
};
pub const BUSY: &str = "Window capture snapshot pool is busy";
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Source {
pub name: String,
@@ -29,6 +32,12 @@ pub struct Source {
pub scale: i32,
}
impl Source {
pub fn window_id(&self) -> Option<u64> {
self.name.strip_prefix("window:")?.parse().ok()
}
}
struct Output {
global: u32,
proxy: wl_output::WlOutput,
@@ -37,6 +46,7 @@ struct Output {
}
pub struct Frame {
pub logical_size: Option<(u32, u32)>,
pub width: u32,
pub height: u32,
pub stride: u32,
@@ -63,10 +73,13 @@ impl Drop for Buffer {
struct State {
outputs: Vec<Output>,
manager: Option<manager::ZwlrScreencopyManagerV1>,
window_manager: Option<window_manager::FereseWindowCaptureManagerV1>,
shm: Option<wl_shm::WlShm>,
buffer: Option<Buffer>,
result: Option<Result<(), String>>,
flipped: bool,
logical_size: Option<(u32, u32)>,
busy: bool,
}
pub struct Capture {
@@ -91,7 +104,9 @@ impl Capture {
// uninterruptible roundtrip on a stalled compositor.
this.sync(stop)?;
this.sync(stop)?;
if this.state.manager.is_none() || this.state.shm.is_none() {
if (this.state.manager.is_none() && this.state.window_manager.is_none())
|| this.state.shm.is_none()
{
return Err("Ferese screen capture is unavailable in this session".into());
}
Ok(this)
@@ -109,6 +124,10 @@ impl Capture {
Ok(())
}
pub fn supports_windows(&self) -> bool {
self.state.window_manager.is_some()
}
pub fn generation(&self, name: &str) -> Option<u32> {
self.state
.outputs
@@ -156,31 +175,53 @@ impl Capture {
stop: &AtomicBool,
mut pixels: Vec<u8>,
) -> Result<Frame, String> {
let output = self
.state
.outputs
.iter()
.find(|o| o.source.name == name)
.ok_or("Shared monitor disconnected")?;
if self
.selected_output
.is_some_and(|selected| selected != output.global)
{
return Err("Shared monitor was replaced".into());
}
self.selected_output = Some(output.global);
let transform = output.transform;
self.state.result = None;
self.state.flipped = false;
let frame = self.state.manager.as_ref().unwrap().capture_output(
cursor as i32,
&output.proxy,
&self.queue.handle(),
(),
);
self.state.logical_size = None;
self.state.busy = false;
let (frame, transform) = if let Some(id) = name
.strip_prefix("window:")
.and_then(|id| id.parse::<u64>().ok())
{
let manager = self
.state
.window_manager
.as_ref()
.ok_or("Window capture is unavailable")?;
(
manager.capture_window(
(id >> 32) as u32,
id as u32,
cursor as u32,
&self.queue.handle(),
(),
),
wl_output::Transform::Normal,
)
} else {
let output = self
.state
.outputs
.iter()
.find(|o| o.source.name == name)
.ok_or("Shared monitor disconnected")?;
if self
.selected_output
.is_some_and(|selected| selected != output.global)
{
return Err("Shared monitor was replaced".into());
}
self.selected_output = Some(output.global);
let manager = self
.state
.manager
.as_ref()
.ok_or("Monitor capture is unavailable")?;
(
manager.capture_output(cursor as i32, &output.proxy, &self.queue.handle(), ()),
output.transform,
)
};
let deadline = Instant::now() + Duration::from_secs(5);
while self.state.result.is_none() {
@@ -209,6 +250,7 @@ impl Capture {
);
Ok(Frame {
logical_size: self.state.logical_size,
width,
height,
stride: width * 4,
@@ -285,6 +327,9 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
scale: 1,
},
}),
"ferese_window_capture_manager_v1" => {
state.window_manager = Some(registry.bind(name, 1, qh, ()))
}
"wl_shm" => state.shm = Some(registry.bind(name, 1, qh, ())),
"zwlr_screencopy_manager_v1" => {
state.manager = Some(registry.bind(name, version.min(3), qh, ()))
@@ -415,10 +460,11 @@ impl Dispatch<frame::ZwlrScreencopyFrameV1, ()> for State {
}
frame::Event::Ready { .. } => state.result = Some(Ok(())),
frame::Event::Failed => {
state.result = Some(Err(
"Capture ended: session locked, monitor removed, or compositor unavailable"
.into(),
))
state.result = Some(Err(if state.busy {
BUSY.into()
} else {
"Capture ended: session locked, source closed, or compositor unavailable".into()
}))
}
frame::Event::Flags {
flags: WEnum::Value(flags),
@@ -522,6 +568,24 @@ wayland_client::delegate_noop!(State: ignore wl_shm::WlShm);
wayland_client::delegate_noop!(State: ignore wl_shm_pool::WlShmPool);
wayland_client::delegate_noop!(State: ignore wl_buffer::WlBuffer);
wayland_client::delegate_noop!(State: ignore manager::ZwlrScreencopyManagerV1);
impl Dispatch<window_manager::FereseWindowCaptureManagerV1, ()> for State {
fn event(
state: &mut Self,
_: &window_manager::FereseWindowCaptureManagerV1,
event: window_manager::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
window_manager::Event::Geometry { width, height, .. } => {
state.logical_size = Some((width, height))
}
window_manager::Event::Busy { .. } => state.busy = true,
_ => (),
}
}
}
#[cfg(test)]
mod tests {
+46
View File
@@ -462,6 +462,38 @@ fn window_sources(windows: &serde_json::Value) -> Result<Vec<crate::capture::Sou
Ok(sources)
}
pub(crate) fn sharing_window_sources(
windows: &serde_json::Value,
) -> Result<Vec<crate::capture::Source>, String> {
let windows = windows.as_array().ok_or("Invalid window list")?;
let eligible = windows
.iter()
.filter(|window| {
window["workspace"].as_u64().is_some()
&& window["capture_width"]
.as_i64()
.is_some_and(|width| width > 0)
&& window["capture_height"]
.as_i64()
.is_some_and(|height| height > 0)
&& window["app_id"].as_str() != Some("dev.ferese.ScreenShare")
})
.take(128)
.map(|window| {
let mut window = window.clone();
window["mapped"] = true.into();
window["width"] = window["capture_width"].clone();
window["height"] = window["capture_height"].clone();
window
})
.collect::<Vec<_>>();
let mut sources = window_sources(&serde_json::Value::Array(eligible))?;
for source in &mut sources {
source.name = format!("window:{}", source.name);
}
Ok(sources)
}
async fn select_window(app: &str, parent: &str) -> Result<Option<u64>, String> {
let windows = ipc("get-windows", serde_json::json!({})).await?;
let sources = window_sources(&windows)?;
@@ -754,6 +786,20 @@ fn edit_wallpaper(path: &Path, image: &Path, target: &str) -> Result<(), String>
#[cfg(test)]
mod tests {
#[test]
fn live_window_choices_include_inactive_workspaces_and_exclude_picker() {
let windows = serde_json::json!([
{"id": 1, "workspace": 2, "mapped": false, "app_id": "org.Editor", "title": "Notes", "capture_width": 640, "capture_height": 480},
{"id": 2, "workspace": 1, "mapped": true, "app_id": "dev.ferese.ScreenShare", "capture_width": 400, "capture_height": 300},
{"id": 3, "workspace": 1, "capture_width": 0, "capture_height": 0}
]);
let sources = super::sharing_window_sources(&windows).unwrap();
assert_eq!(sources.len(), 1);
assert_eq!(sources[0].window_id(), Some(1));
assert_eq!((sources[0].width, sources[0].height), (640, 480));
assert!(sources[0].label.contains("Notes"));
}
use super::*;
#[test]
+4
View File
@@ -20,6 +20,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
[flag, name, cursor] if flag == "--stream" => {
stream::run(name.clone(), cursor == "embedded", None)
}
[flag, id, cursor] if flag == "--stream-window" => {
let id: u64 = id.parse()?;
stream::run(format!("window:{id}"), cursor == "embedded", None)
}
[flag, name, cursor, generation] if flag == "--stream" => stream::run(
name.clone(),
cursor == "embedded",
+22 -10
View File
@@ -71,9 +71,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
}
let prompt: Prompt = serde_json::from_str(&input)?;
if prompt.sources.is_empty()
|| prompt.sources.len() > if prompt.window_capture { 128 } else { 16 }
{
if prompt.sources.is_empty() || prompt.sources.len() > 144 {
return Err("No shareable displays".into());
}
@@ -187,6 +185,9 @@ impl cosmic::Application for Picker {
if self.selected.contains(&index) {
self.selected.retain(|i| *i != index);
} else {
if self.prompt.multiple && self.selected.len() >= 8 {
return Task::none();
}
if !self.prompt.multiple {
self.selected.clear();
}
@@ -244,7 +245,7 @@ impl cosmic::Application for Picker {
self.text(if self.prompt.window_capture {
"Choose a window"
} else {
"Choose a display"
"Choose what to share"
})
.size(18),
self.text(format!(
@@ -264,16 +265,27 @@ impl cosmic::Application for Picker {
.align_y(Alignment::Center),
);
let mut sources = column([]).spacing(6);
let mut previous_kind = None;
for (index, source) in self.prompt.sources.iter().enumerate() {
let is_window = self.prompt.window_capture || source.window_id().is_some();
if !self.prompt.window_capture && previous_kind != Some(is_window) {
sources = sources.push(
self.text(if is_window { "Windows" } else { "Displays" })
.size(12),
);
previous_kind = Some(is_window);
}
let selected = self.selected.contains(&index);
let mut entry = row![
glyph(ferese_theme::icons::DISPLAY, 24),
column![
self.text(if self.prompt.window_capture {
&source.label
} else {
&source.name
})
self.text(
if self.prompt.window_capture || source.window_id().is_some() {
&source.label
} else {
&source.name
}
)
.size(14),
self.text(format!("{} × {}", source.width, source.height))
.size(12)
@@ -305,7 +317,7 @@ impl cosmic::Application for Picker {
self.text(if self.prompt.window_capture {
"Only the selected window will be captured."
} else {
"Everything on this display, including notifications, is visible."
"Displays share everything visible. Windows share only their own content."
})
.size(12)
.width(Length::Fill),
+81 -33
View File
@@ -15,6 +15,8 @@ use std::{
#[derive(Debug, Serialize, Deserialize)]
pub struct Ready {
#[serde(default)]
pub logical_size: Option<(u32, u32)>,
pub node: u32,
pub width: u32,
pub height: u32,
@@ -27,6 +29,7 @@ struct Data {
fresh: Arc<AtomicBool>,
announced: bool,
sequence: u64,
negotiated: (u32, u32),
}
// pipewire-rs exposes read-only metadata through Buffer. Own a dequeued raw
@@ -110,6 +113,38 @@ fn pod(object: spa::pod::Object) -> Vec<u8> {
.into_inner()
}
fn video_format(width: u32, height: u32) -> Vec<u8> {
pod(spa::pod::object!(
spa::utils::SpaTypes::ObjectParamFormat,
spa::param::ParamType::EnumFormat,
spa::pod::property!(
spa::param::format::FormatProperties::MediaType,
Id,
spa::param::format::MediaType::Video
),
spa::pod::property!(
spa::param::format::FormatProperties::MediaSubtype,
Id,
spa::param::format::MediaSubtype::Raw
),
spa::pod::property!(
spa::param::format::FormatProperties::VideoFormat,
Id,
spa::param::video::VideoFormat::BGRx
),
spa::pod::property!(
spa::param::format::FormatProperties::VideoSize,
Rectangle,
spa::utils::Rectangle { width, height }
),
spa::pod::property!(
spa::param::format::FormatProperties::VideoFramerate,
Fraction,
spa::utils::Fraction { num: 30, denom: 1 }
)
))
}
pub fn run(
name: String,
cursor: bool,
@@ -126,8 +161,18 @@ pub fn run(
if let Some(generation) = generation {
capture.pin_output(&name, generation)?;
}
let first = capture.frame(&name, cursor, &stop, Vec::new())?;
let deadline = Instant::now() + Duration::from_secs(5);
let first = loop {
match capture.frame(&name, cursor, &stop, Vec::new()) {
Err(error) if error == crate::capture::BUSY && Instant::now() < deadline => {
std::thread::sleep(Duration::from_millis(10));
}
result => break result?,
}
};
let (width, height) = (first.width, first.height);
let logical_size = first.logical_size;
let resizable = name.starts_with("window:");
let latest = Arc::new(Mutex::new(first));
let streaming = Arc::new(AtomicBool::new(false));
@@ -154,6 +199,7 @@ pub fn run(
"node.latency" => "1/30",
},
)?;
let format_frames = latest.clone();
let data = Data {
latest: latest.clone(),
stop: stop.clone(),
@@ -161,6 +207,7 @@ pub fn run(
fresh: fresh.clone(),
announced: false,
sequence: 0,
negotiated: (width, height),
};
let _listener = stream
.add_local_listener_with_user_data(data)
@@ -180,6 +227,7 @@ pub fn run(
println!(
"{}",
serde_json::to_string(&Ready {
logical_size,
node,
width,
height
@@ -206,13 +254,18 @@ pub fn run(
let Some(param) = param else { return };
let mut format = spa::param::video::VideoInfoRaw::default();
if format.parse(param).is_err()
|| format.size().width != width
|| format.size().height != height
|| format.size().width == 0
|| format.size().height == 0
|| (!resizable && (format.size().width, format.size().height) != (width, height))
|| u64::from(format.size().width) * u64::from(format.size().height) * 4
> 128 * 1024 * 1024
|| format.format() != spa::param::video::VideoFormat::BGRx
{
data.stop.store(true, Ordering::Relaxed);
return;
}
let (width, height) = (format.size().width, format.size().height);
data.negotiated = (width, height);
let buffers = pod(spa::pod::object!(
spa::utils::SpaTypes::ObjectParamBuffers,
spa::param::ParamType::Buffers,
@@ -260,6 +313,10 @@ pub fn run(
return;
}
let frame = data.latest.lock().unwrap();
if (frame.width, frame.height) != data.negotiated {
*target.chunk_mut().size_mut() = 0;
return;
}
let row = frame.width as usize * 4;
let bytes = row * frame.height as usize;
let Some(destination) = target.data() else {
@@ -283,35 +340,7 @@ pub fn run(
})
.register()?;
let format = pod(spa::pod::object!(
spa::utils::SpaTypes::ObjectParamFormat,
spa::param::ParamType::EnumFormat,
spa::pod::property!(
spa::param::format::FormatProperties::MediaType,
Id,
spa::param::format::MediaType::Video
),
spa::pod::property!(
spa::param::format::FormatProperties::MediaSubtype,
Id,
spa::param::format::MediaSubtype::Raw
),
spa::pod::property!(
spa::param::format::FormatProperties::VideoFormat,
Id,
spa::param::video::VideoFormat::BGRx
),
spa::pod::property!(
spa::param::format::FormatProperties::VideoSize,
Rectangle,
spa::utils::Rectangle { width, height }
),
spa::pod::property!(
spa::param::format::FormatProperties::VideoFramerate,
Fraction,
spa::utils::Fraction { num: 30, denom: 1 }
)
));
let format = video_format(width, height);
stream.connect(
spa::utils::Direction::Output,
@@ -334,7 +363,7 @@ pub fn run(
{
checked = Instant::now();
match capture.frame(&name, cursor, &capture_stop, std::mem::take(&mut spare)) {
Ok(frame) if frame.width == width && frame.height == height => {
Ok(frame) if resizable || (frame.width == width && frame.height == height) => {
spare = std::mem::replace(&mut *latest.lock().unwrap(), frame).pixels;
fresh.store(capture_streaming.load(Ordering::Relaxed), Ordering::SeqCst);
}
@@ -342,6 +371,7 @@ pub fn run(
eprintln!("Shared monitor changed size; start sharing again");
capture_stop.store(true, Ordering::Relaxed);
}
Err(error) if error == crate::capture::BUSY => (),
Err(error) => {
eprintln!("{error}");
capture_stop.store(true, Ordering::Relaxed);
@@ -352,8 +382,23 @@ pub fn run(
}
});
let mut next_frame = Instant::now();
let mut requested_size = (width, height);
let mut renegotiation_error = None;
while !stop.load(Ordering::Relaxed) {
let size = {
let frame = format_frames.lock().unwrap();
(frame.width, frame.height)
};
if resizable && size != requested_size {
let format = video_format(size.0, size.1);
if let Err(error) = stream.update_params(&mut [Pod::from_bytes(&format).unwrap()]) {
renegotiation_error = Some(error);
stop.store(true, Ordering::Relaxed);
break;
}
requested_size = size;
}
let active = streaming.load(Ordering::Relaxed);
let timeout = if active {
next_frame
@@ -375,6 +420,9 @@ pub fn run(
let _ = stream.disconnect();
let _ = capture_thread.join();
if let Some(error) = renegotiation_error {
return Err(error.into());
}
Ok(())
}