Add Wayland multi-monitor screen capture functionality (#12900)

* Add Wayland multi-monitor screen capture functionality

* fix wayland capture issues by reverting to CapturerPtr, the problem was that calling Display::all in get_capturer_for_display was dropping the pipewire capturer and causing the video to freeze.

* If running as AppImage or flatpak, ignore the 'multiple' argument

* Comment out warning log with unclear purpose Comment out warning log with unclear purpose

---------

Co-authored-by: fufesou <13586388+fufesou@users.noreply.github.com>
This commit is contained in:
Nathan Saslavsky
2025-09-22 07:53:14 -06:00
committed by GitHub
parent a375766ac2
commit eacb07988d
3 changed files with 112 additions and 64 deletions

View File

@@ -661,7 +661,9 @@ fn on_create_session_response(
Variant(Box::new("u3".to_string())), Variant(Box::new("u3".to_string())),
); );
// https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html // https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html
// args.insert("multiple".into(), Variant(Box::new(true))); if is_server_running() {
args.insert("multiple".into(), Variant(Box::new(true)));
}
args.insert("types".into(), Variant(Box::new(1u32))); //| 2u32))); args.insert("types".into(), Variant(Box::new(1u32))); //| 2u32)));
let path = portal.select_sources(ses.clone(), args)?; let path = portal.select_sources(ses.clone(), args)?;
@@ -725,7 +727,9 @@ fn on_select_devices_response(
Variant(Box::new("u3".to_string())), Variant(Box::new("u3".to_string())),
); );
// https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html // https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html
// args.insert("multiple".into(), Variant(Box::new(true))); if is_server_running() {
args.insert("multiple".into(), Variant(Box::new(true)));
}
args.insert("types".into(), Variant(Box::new(1u32))); //| 2u32))); args.insert("types".into(), Variant(Box::new(1u32))); //| 2u32)));
let session = session.clone(); let session = session.clone();

View File

@@ -325,7 +325,7 @@ fn get_capturer_monitor(
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
if !is_x11() { if !is_x11() {
return super::wayland::get_capturer(); return super::wayland::get_capturer_for_display(current);
} }
} }
@@ -473,11 +473,20 @@ fn run(vs: VideoService) -> ResultType<()> {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
super::wayland::ensure_inited()?; super::wayland::ensure_inited()?;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let _wayland_call_on_ret = SimpleCallOnReturn { let _wayland_call_on_ret = {
b: true, // Increment active display count when starting
f: Box::new(|| { let _display_count = super::wayland::increment_active_display_count();
super::wayland::clear();
}), SimpleCallOnReturn {
b: true,
f: Box::new(|| {
// Decrement active display count and only clear if this was the last display
let remaining_count = super::wayland::decrement_active_display_count();
if remaining_count == 0 {
super::wayland::clear();
}
}),
}
}; };
#[cfg(windows)] #[cfg(windows)]

View File

@@ -4,6 +4,7 @@ use hbb_common::{
platform::linux::{CMD_SH, DISTRO}, platform::linux::{CMD_SH, DISTRO},
}; };
use scrap::{is_cursor_embedded, set_map_err, Capturer, Display, Frame, TraitCapturer}; use scrap::{is_cursor_embedded, set_map_err, Capturer, Display, Frame, TraitCapturer};
use std::collections::HashMap;
use std::io; use std::io;
use std::process::{Command, Output}; use std::process::{Command, Output};
@@ -15,14 +16,30 @@ use crate::{
}; };
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref CAP_DISPLAY_INFO: RwLock<u64> = RwLock::new(0); static ref CAP_DISPLAY_INFO: RwLock<HashMap<usize, u64>> = RwLock::new(HashMap::new());
static ref PIPEWIRE_INITIALIZED: RwLock<bool> = RwLock::new(false);
static ref LOG_SCRAP_COUNT: Mutex<u32> = Mutex::new(0); static ref LOG_SCRAP_COUNT: Mutex<u32> = Mutex::new(0);
static ref ACTIVE_DISPLAY_COUNT: RwLock<usize> = RwLock::new(0);
} }
pub fn init() { pub fn init() {
set_map_err(map_err_scrap); set_map_err(map_err_scrap);
} }
pub(super) fn increment_active_display_count() -> usize {
let mut count = ACTIVE_DISPLAY_COUNT.write().unwrap();
*count += 1;
*count
}
pub(super) fn decrement_active_display_count() -> usize {
let mut count = ACTIVE_DISPLAY_COUNT.write().unwrap();
if *count > 0 {
*count -= 1;
}
*count
}
fn map_err_scrap(err: String) -> io::Error { fn map_err_scrap(err: String) -> io::Error {
// to-do: Handle error better, do not restart server // to-do: Handle error better, do not restart server
if err.starts_with("Did not receive a reply") { if err.starts_with("Did not receive a reply") {
@@ -70,7 +87,7 @@ impl Clone for CapturerPtr {
} }
impl TraitCapturer for CapturerPtr { impl TraitCapturer for CapturerPtr {
fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result<Frame<'a>> { fn frame<'a>(&'a mut self, timeout: std::time::Duration) -> std::io::Result<Frame<'a>> {
unsafe { (*self.0).frame(timeout) } unsafe { (*self.0).frame(timeout) }
} }
} }
@@ -93,7 +110,7 @@ pub(super) fn is_inited() -> Option<Message> {
if is_x11() { if is_x11() {
None None
} else { } else {
if *CAP_DISPLAY_INFO.read().unwrap() == 0 { if CAP_DISPLAY_INFO.read().unwrap().is_empty() {
let mut msg_out = Message::new(); let mut msg_out = Message::new();
let res = MessageBox { let res = MessageBox {
msgtype: "nook-nocancel-hasclose".to_owned(), msgtype: "nook-nocancel-hasclose".to_owned(),
@@ -126,6 +143,20 @@ fn get_max_desktop_resolution() -> Option<String> {
} }
} }
fn calculate_max_resolution_from_displays(displays: &[Display]) -> (i32, i32) {
// TODO: this doesn't work in most situations other than sharing all displays
// this is because the function only gets called with the displays being shared with pipewire
// the xrandr method does work otherwise we could get this correctly using xdg-output-unstable-v1 when xrandr isn't available
// log::warn!("using incorrect max resolution calculation uinput may not work correctly");
let (mut max_x, mut max_y) = (0, 0);
for d in displays {
let (x, y) = d.origin();
max_x = max_x.max(x + d.width() as i32);
max_y = max_y.max(y + d.height() as i32);
}
(max_x, max_y)
}
pub(super) async fn check_init() -> ResultType<()> { pub(super) async fn check_init() -> ResultType<()> {
if !is_x11() { if !is_x11() {
let mut minx = 0; let mut minx = 0;
@@ -134,13 +165,19 @@ pub(super) async fn check_init() -> ResultType<()> {
let mut maxy = 0; let mut maxy = 0;
let use_uinput = crate::input_service::wayland_use_uinput(); let use_uinput = crate::input_service::wayland_use_uinput();
if *CAP_DISPLAY_INFO.read().unwrap() == 0 { if CAP_DISPLAY_INFO.read().unwrap().is_empty() {
let mut lock = CAP_DISPLAY_INFO.write().unwrap(); let mut lock = CAP_DISPLAY_INFO.write().unwrap();
if *lock == 0 { if lock.is_empty() {
let mut all = Display::all()?; // Check if PipeWire is already initialized to prevent duplicate recorder creation
if *PIPEWIRE_INITIALIZED.read().unwrap() {
log::warn!("wayland_diag: Preventing duplicate PipeWire initialization");
return Ok(());
}
let all = Display::all()?;
*PIPEWIRE_INITIALIZED.write().unwrap() = true;
let num = all.len(); let num = all.len();
let primary = super::display_service::get_primary_2(&all); let primary = super::display_service::get_primary_2(&all);
let current = primary;
super::display_service::check_update_displays(&all); super::display_service::check_update_displays(&all);
let mut displays = super::display_service::get_sync_displays(); let mut displays = super::display_service::get_sync_displays();
for display in displays.iter_mut() { for display in displays.iter_mut() {
@@ -152,35 +189,25 @@ pub(super) async fn check_init() -> ResultType<()> {
rects.push((d.origin(), d.width(), d.height())); rects.push((d.origin(), d.width(), d.height()));
} }
let display = all.remove(current); log::debug!("#displays={}, primary={}, rects: {:?}, cpus={}/{}", num, primary, rects, num_cpus::get_physical(), num_cpus::get());
let (origin, width, height) = (display.origin(), display.width(), display.height());
log::debug!(
"#displays={}, current={}, origin: {:?}, width={}, height={}, cpus={}/{}",
num,
current,
&origin,
width,
height,
num_cpus::get_physical(),
num_cpus::get(),
);
if use_uinput { if use_uinput {
let (max_width, max_height) = match get_max_desktop_resolution() { let (max_width, max_height) = match get_max_desktop_resolution() {
Some(result) if !result.is_empty() => { Some(result) if !result.is_empty() => {
let resolution: Vec<&str> = result.split(" ").collect(); let resolution: Vec<&str> = result.split(" ").collect();
let w: i32 = resolution[0].parse().unwrap_or(origin.0 + width as i32); if let (Ok(w), Ok(h)) = (
let h: i32 = resolution[2] resolution[0].parse::<i32>(),
.trim_end_matches(",") resolution.get(2)
.parse() .unwrap_or(&"0")
.unwrap_or(origin.1 + height as i32); .trim_end_matches(",")
if w < origin.0 + width as i32 || h < origin.1 + height as i32 { .parse::<i32>()
(origin.0 + width as i32, origin.1 + height as i32) ) {
} else {
(w, h) (w, h)
} else {
calculate_max_resolution_from_displays(&all)
} }
} }
_ => (origin.0 + width as i32, origin.1 + height as i32), _ => calculate_max_resolution_from_displays(&all),
}; };
minx = 0; minx = 0;
@@ -189,19 +216,24 @@ pub(super) async fn check_init() -> ResultType<()> {
maxy = max_height; maxy = max_height;
} }
let capturer = Box::into_raw(Box::new( // Create individual CapDisplayInfo for each display with its own capturer
Capturer::new(display).with_context(|| "Failed to create capturer")?, for (idx, display) in all.into_iter().enumerate() {
)); let capturer = Box::into_raw(Box::new(
let capturer = CapturerPtr(capturer); Capturer::new(display).with_context(|| format!("Failed to create capturer for display {}", idx))?,
let cap_display_info = Box::into_raw(Box::new(CapDisplayInfo { ));
rects, let capturer = CapturerPtr(capturer);
displays,
num, let cap_display_info = Box::into_raw(Box::new(CapDisplayInfo {
primary, rects: rects.clone(),
current, displays: displays.clone(),
capturer, num,
})); primary,
*lock = cap_display_info as _; current: idx,
capturer,
}));
lock.insert(idx, cap_display_info as u64);
}
} }
} }
@@ -223,9 +255,9 @@ pub(super) async fn check_init() -> ResultType<()> {
pub(super) async fn get_displays() -> ResultType<Vec<DisplayInfo>> { pub(super) async fn get_displays() -> ResultType<Vec<DisplayInfo>> {
check_init().await?; check_init().await?;
let addr = *CAP_DISPLAY_INFO.read().unwrap(); let cap_map = CAP_DISPLAY_INFO.read().unwrap();
if addr != 0 { if let Some(addr) = cap_map.values().next() {
let cap_display_info: *const CapDisplayInfo = addr as _; let cap_display_info: *const CapDisplayInfo = *addr as _;
unsafe { unsafe {
let cap_display_info = &*cap_display_info; let cap_display_info = &*cap_display_info;
Ok(cap_display_info.displays.clone()) Ok(cap_display_info.displays.clone())
@@ -236,9 +268,9 @@ pub(super) async fn get_displays() -> ResultType<Vec<DisplayInfo>> {
} }
pub(super) fn get_primary() -> ResultType<usize> { pub(super) fn get_primary() -> ResultType<usize> {
let addr = *CAP_DISPLAY_INFO.read().unwrap(); let cap_map = CAP_DISPLAY_INFO.read().unwrap();
if addr != 0 { if let Some(addr) = cap_map.values().next() {
let cap_display_info: *const CapDisplayInfo = addr as _; let cap_display_info: *const CapDisplayInfo = *addr as _;
unsafe { unsafe {
let cap_display_info = &*cap_display_info; let cap_display_info = &*cap_display_info;
Ok(cap_display_info.primary) Ok(cap_display_info.primary)
@@ -253,26 +285,29 @@ pub fn clear() {
return; return;
} }
let mut write_lock = CAP_DISPLAY_INFO.write().unwrap(); let mut write_lock = CAP_DISPLAY_INFO.write().unwrap();
if *write_lock != 0 { for (_, addr) in write_lock.iter() {
let cap_display_info: *mut CapDisplayInfo = *write_lock as _; let cap_display_info: *mut CapDisplayInfo = *addr as _;
unsafe { unsafe {
let _box_capturer = Box::from_raw((*cap_display_info).capturer.0); let _box_capturer = Box::from_raw((*cap_display_info).capturer.0);
let _box_cap_display_info = Box::from_raw(cap_display_info); let _box_cap_display_info = Box::from_raw(cap_display_info);
*write_lock = 0;
} }
} }
write_lock.clear();
// Reset PipeWire initialization flag to allow recreation on next init
*PIPEWIRE_INITIALIZED.write().unwrap() = false;
} }
pub(super) fn get_capturer() -> ResultType<super::video_service::CapturerInfo> { pub(super) fn get_capturer_for_display(display_idx: usize) -> ResultType<super::video_service::CapturerInfo> {
if is_x11() { if is_x11() {
bail!("Do not call this function if not wayland"); bail!("Do not call this function if not wayland");
} }
let addr = *CAP_DISPLAY_INFO.read().unwrap(); let cap_map = CAP_DISPLAY_INFO.read().unwrap();
if addr != 0 { if let Some(addr) = cap_map.get(&display_idx) {
let cap_display_info: *const CapDisplayInfo = addr as _; let cap_display_info: *const CapDisplayInfo = *addr as _;
unsafe { unsafe {
let cap_display_info = &*cap_display_info; let cap_display_info = &*cap_display_info;
let rect = cap_display_info.rects[cap_display_info.current]; let rect = cap_display_info.rects[cap_display_info.current];
Ok(super::video_service::CapturerInfo { Ok(super::video_service::CapturerInfo {
origin: rect.0, origin: rect.0,
width: rect.1, width: rect.1,
@@ -285,7 +320,7 @@ pub(super) fn get_capturer() -> ResultType<super::video_service::CapturerInfo> {
}) })
} }
} else { } else {
bail!("Failed to get capturer display info"); bail!("Failed to get capturer display info for display {}", display_idx);
} }
} }