fix: wayland controlled side, cursor misalignment (#13537)
Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -6944,6 +6944,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"webm",
|
||||
"winapi 0.3.9",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -690,9 +690,20 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
|
||||
Widget _buildScrollAutoNonTextureRender(
|
||||
ImageModel m, CanvasModel c, double s) {
|
||||
double sizeScale = s;
|
||||
if (widget.ffi.ffiModel.isPeerLinux) {
|
||||
final displays = widget.ffi.ffiModel.pi.getCurDisplays();
|
||||
if (displays.isNotEmpty) {
|
||||
sizeScale = s / displays[0].scale;
|
||||
}
|
||||
}
|
||||
return CustomPaint(
|
||||
size: Size(c.size.width, c.size.height),
|
||||
painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
|
||||
painter: ImagePainter(
|
||||
image: m.image,
|
||||
x: c.x / sizeScale,
|
||||
y: c.y / sizeScale,
|
||||
scale: sizeScale),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -705,17 +716,19 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
if (rect == null) {
|
||||
return Container();
|
||||
}
|
||||
final isPeerLinux = ffiModel.isPeerLinux;
|
||||
final curDisplay = ffiModel.pi.currentDisplay;
|
||||
for (var i = 0; i < displays.length; i++) {
|
||||
final textureId = widget.ffi.textureModel
|
||||
.getTextureId(curDisplay == kAllDisplayValue ? i : curDisplay);
|
||||
if (true) {
|
||||
// both "textureId.value != -1" and "true" seems ok
|
||||
final sizeScale = isPeerLinux ? s / displays[i].scale : s;
|
||||
children.add(Positioned(
|
||||
left: (displays[i].x - rect.left) * s + offset.dx,
|
||||
top: (displays[i].y - rect.top) * s + offset.dy,
|
||||
width: displays[i].width * s,
|
||||
height: displays[i].height * s,
|
||||
width: displays[i].width * sizeScale,
|
||||
height: displays[i].height * sizeScale,
|
||||
child: Obx(() => Texture(
|
||||
textureId: textureId.value,
|
||||
filterQuality:
|
||||
|
||||
@@ -577,7 +577,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
color: MyTheme.canvasColor,
|
||||
child: Stack(children: () {
|
||||
final paints = [
|
||||
ImagePaint(),
|
||||
ImagePaint(ffiModel: gFFI.ffiModel),
|
||||
Positioned(
|
||||
top: 10,
|
||||
right: 10,
|
||||
@@ -635,7 +635,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
|
||||
Widget getBodyForDesktopWithListener() {
|
||||
final ffiModel = Provider.of<FfiModel>(context);
|
||||
var paints = <Widget>[ImagePaint()];
|
||||
var paints = <Widget>[ImagePaint(ffiModel: ffiModel)];
|
||||
if (showCursorPaint) {
|
||||
final cursor = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: 'show-remote-cursor');
|
||||
@@ -1055,11 +1055,20 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
||||
}
|
||||
|
||||
class ImagePaint extends StatelessWidget {
|
||||
final FfiModel ffiModel;
|
||||
ImagePaint({Key? key, required this.ffiModel}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final m = Provider.of<ImageModel>(context);
|
||||
final c = Provider.of<CanvasModel>(context);
|
||||
var s = c.scale;
|
||||
if (ffiModel.isPeerLinux) {
|
||||
final displays = ffiModel.pi.getCurDisplays();
|
||||
if (displays.isNotEmpty) {
|
||||
s = s / displays[0].scale;
|
||||
}
|
||||
}
|
||||
final adjust = c.getAdjustY();
|
||||
return CustomPaint(
|
||||
painter: ImagePainter(
|
||||
|
||||
@@ -159,6 +159,8 @@ class FfiModel with ChangeNotifier {
|
||||
bool get isPeerAndroid => _pi.platform == kPeerPlatformAndroid;
|
||||
bool get isPeerMobile => isPeerAndroid;
|
||||
|
||||
bool get isPeerLinux => _pi.platform == kPeerPlatformLinux;
|
||||
|
||||
bool get viewOnly => _viewOnly;
|
||||
bool get showMyCursor => _showMyCursor;
|
||||
|
||||
@@ -179,6 +181,9 @@ class FfiModel with ChangeNotifier {
|
||||
if (displays.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (isPeerLinux) {
|
||||
useDisplayScale = true;
|
||||
}
|
||||
int scale(int len, double s) {
|
||||
if (useDisplayScale) {
|
||||
return len.toDouble() ~/ s;
|
||||
@@ -1076,18 +1081,17 @@ class FfiModel with ChangeNotifier {
|
||||
if (displays.length == 1) {
|
||||
bind.sessionSetSize(
|
||||
sessionId: sessionId,
|
||||
display:
|
||||
pi.currentDisplay == kAllDisplayValue ? 0 : pi.currentDisplay,
|
||||
width: _rect!.width.toInt(),
|
||||
height: _rect!.height.toInt(),
|
||||
display: pi.currentDisplay == kAllDisplayValue ? 0 : pi.currentDisplay,
|
||||
width: displays[0].width,
|
||||
height: displays[0].height,
|
||||
);
|
||||
} else {
|
||||
for (int i = 0; i < displays.length; ++i) {
|
||||
bind.sessionSetSize(
|
||||
sessionId: sessionId,
|
||||
display: i,
|
||||
width: displays[i].width.toInt(),
|
||||
height: displays[i].height.toInt(),
|
||||
width: displays[i].width,
|
||||
height: displays[i].height,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1436,8 +1440,17 @@ class FfiModel with ChangeNotifier {
|
||||
d.cursorEmbedded = evt['cursor_embedded'] == 1;
|
||||
d.originalWidth = evt['original_width'] ?? kInvalidResolutionValue;
|
||||
d.originalHeight = evt['original_height'] ?? kInvalidResolutionValue;
|
||||
double v = (evt['scale']?.toDouble() ?? 100.0) / 100;
|
||||
d._scale = v > 1.0 ? v : 1.0;
|
||||
d._scale = 1.0;
|
||||
final scaledWidth = evt['scaled_width'];
|
||||
if (scaledWidth != null) {
|
||||
final sw = int.tryParse(scaledWidth.toString());
|
||||
if (sw != null && sw > 0 && d.width > 0) {
|
||||
d._scale = max(d.width.toDouble() / sw, 1.0);
|
||||
} else {
|
||||
debugPrint(
|
||||
"Invalid scaled_width ($scaledWidth) or width (${d.width}), using default scale 1.0");
|
||||
}
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
@@ -2438,11 +2451,6 @@ class CanvasModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
set scale(v) {
|
||||
_scale = v;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
panX(double dx) {
|
||||
_x += dx;
|
||||
if (isMobile) {
|
||||
@@ -2976,9 +2984,10 @@ class CursorModel with ChangeNotifier {
|
||||
var cx = r.center.dx;
|
||||
var cy = r.center.dy;
|
||||
var tryMoveCanvasX = false;
|
||||
final displayRect = parent.target?.ffiModel.rect;
|
||||
if (dx > 0) {
|
||||
final maxCanvasCanMove = _displayOriginX +
|
||||
(parent.target?.imageModel.image!.width ?? 1280) -
|
||||
(displayRect?.width ?? 1280) -
|
||||
r.right.roundToDouble();
|
||||
tryMoveCanvasX = _x + dx > cx && maxCanvasCanMove > 0;
|
||||
if (tryMoveCanvasX) {
|
||||
@@ -3000,7 +3009,7 @@ class CursorModel with ChangeNotifier {
|
||||
var tryMoveCanvasY = false;
|
||||
if (dy > 0) {
|
||||
final mayCanvasCanMove = _displayOriginY +
|
||||
(parent.target?.imageModel.image!.height ?? 720) -
|
||||
(displayRect?.height ?? 720) -
|
||||
r.bottom.roundToDouble();
|
||||
tryMoveCanvasY = _y + dy > cy && mayCanvasCanMove > 0;
|
||||
if (tryMoveCanvasY) {
|
||||
|
||||
@@ -10,7 +10,7 @@ authors = ["Ram <quadrupleslap@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing"]
|
||||
wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing", "zbus"]
|
||||
mediacodec = ["ndk"]
|
||||
linux-pkg-config = ["dep:pkg-config"]
|
||||
hwcodec = ["dep:hwcodec"]
|
||||
@@ -57,6 +57,7 @@ tracing = { version = "0.1", optional = true }
|
||||
gstreamer = { version = "0.16", optional = true }
|
||||
gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true }
|
||||
gstreamer-video = { version = "0.16", optional = true }
|
||||
zbus = { version = "3.15", optional = true }
|
||||
|
||||
[dependencies.hwcodec]
|
||||
git = "https://github.com/rustdesk-org/hwcodec"
|
||||
|
||||
@@ -88,6 +88,27 @@ impl Display {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scale(&self) -> f64 {
|
||||
match self {
|
||||
Display::X11(_d) => 1.0,
|
||||
Display::WAYLAND(d) => d.scale(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn logical_width(&self) -> usize {
|
||||
match self {
|
||||
Display::X11(d) => d.width(),
|
||||
Display::WAYLAND(d) => d.logical_width(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn logical_height(&self) -> usize {
|
||||
match self {
|
||||
Display::X11(d) => d.height(),
|
||||
Display::WAYLAND(d) => d.logical_height(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn origin(&self) -> (i32, i32) {
|
||||
match self {
|
||||
Display::X11(d) => d.origin(),
|
||||
|
||||
@@ -8,7 +8,6 @@ use super::x11::PixelBuffer;
|
||||
|
||||
pub struct Capturer(Display, Box<dyn Recorder>, Vec<u8>);
|
||||
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref MAP_ERR: RwLock<Option<fn(err: String)-> io::Error>> = Default::default();
|
||||
}
|
||||
@@ -61,7 +60,7 @@ impl TraitCapturer for Capturer {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Display(pipewire::PipeWireCapturable);
|
||||
pub struct Display(pub(crate) pipewire::PipeWireCapturable);
|
||||
|
||||
impl Display {
|
||||
pub fn primary() -> io::Result<Display> {
|
||||
@@ -81,11 +80,35 @@ impl Display {
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.0.size.0
|
||||
self.physical_width()
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.0.size.1
|
||||
self.physical_height()
|
||||
}
|
||||
|
||||
pub fn physical_width(&self) -> usize {
|
||||
self.0.physical_size.0
|
||||
}
|
||||
|
||||
pub fn physical_height(&self) -> usize {
|
||||
self.0.physical_size.1
|
||||
}
|
||||
|
||||
pub fn logical_width(&self) -> usize {
|
||||
self.0.logical_size.0
|
||||
}
|
||||
|
||||
pub fn logical_height(&self) -> usize {
|
||||
self.0.logical_size.1
|
||||
}
|
||||
|
||||
pub fn scale(&self) -> f64 {
|
||||
if self.logical_width() == 0 {
|
||||
1.0
|
||||
} else {
|
||||
self.physical_width() as f64 / self.logical_width() as f64
|
||||
}
|
||||
}
|
||||
|
||||
pub fn origin(&self) -> (i32, i32) {
|
||||
@@ -97,7 +120,7 @@ impl Display {
|
||||
}
|
||||
|
||||
pub fn is_primary(&self) -> bool {
|
||||
false
|
||||
self.0.primary
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod capturable;
|
||||
pub mod pipewire;
|
||||
pub mod display;
|
||||
mod screencast_portal;
|
||||
mod request_portal;
|
||||
pub mod remote_desktop_portal;
|
||||
|
||||
256
libs/scrap/src/wayland/display.rs
Normal file
256
libs/scrap/src/wayland/display.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
use hbb_common::regex::Regex;
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::Mutex;
|
||||
use std::{
|
||||
process::{Command, Output, Stdio},
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tracing::warn;
|
||||
|
||||
use hbb_common::platform::linux::{get_wayland_displays, WaylandDisplayInfo};
|
||||
|
||||
lazy_static! {
|
||||
static ref DISPLAYS: Mutex<Option<Arc<Displays>>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
const COMMAND_TIMEOUT: Duration = Duration::from_millis(1000);
|
||||
|
||||
pub struct Displays {
|
||||
pub primary: usize,
|
||||
pub displays: Vec<WaylandDisplayInfo>,
|
||||
}
|
||||
|
||||
// We need this helper to run commands with a timeout, as some commands may hang.
|
||||
// `kscreen-doctor -o` is known to hang when:
|
||||
// 1. On Archlinux, Both GNOME and KDE Plasma are installed.
|
||||
// 2. Run this command in a GNOME session.
|
||||
fn run_with_timeout(
|
||||
program: &str,
|
||||
args: &[&str],
|
||||
timeout: Duration,
|
||||
label: &str,
|
||||
) -> Option<Output> {
|
||||
let mut child = Command::new(program)
|
||||
.args(args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.ok()?;
|
||||
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
if let Ok(Some(_)) = child.try_wait() {
|
||||
break;
|
||||
}
|
||||
if start.elapsed() >= timeout {
|
||||
warn!("{} command timed out after {:?}", label, timeout);
|
||||
if let Err(e) = child.kill() {
|
||||
warn!("Failed to kill child process for '{}': {}", label, e);
|
||||
}
|
||||
if let Err(e) = child.wait() {
|
||||
warn!("Failed to wait for child process for '{}': {}", label, e);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(30));
|
||||
}
|
||||
|
||||
match child.wait_with_output() {
|
||||
Ok(output) => {
|
||||
if !output.status.success() {
|
||||
warn!("{} command failed with status: {}", label, output.status);
|
||||
return None;
|
||||
}
|
||||
Some(output)
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
// There are some limitations with xrandr method:
|
||||
// 1. It only works when XWayland is running.
|
||||
// 2. The distro may not have xrandr installed by default.
|
||||
// 3. xrandr may not report "primary" in its output. eg. openSUSE Leap 15.6 KDE Plasma.
|
||||
fn try_xrandr_primary() -> Option<String> {
|
||||
let output = Command::new("xrandr").output().ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
for line in text.lines() {
|
||||
if line.contains("primary") && line.contains("connected") {
|
||||
if let Some(name) = line.split_whitespace().next() {
|
||||
return Some(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn try_kscreen_primary() -> Option<String> {
|
||||
if !hbb_common::platform::linux::is_kde_session() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let output = run_with_timeout(
|
||||
"kscreen-doctor",
|
||||
&["-o"],
|
||||
COMMAND_TIMEOUT,
|
||||
"kscreen-doctor -o",
|
||||
)?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// Remove ANSI color codes
|
||||
let re_ansi = Regex::new(r"\x1b\[[0-9;]*m").ok()?;
|
||||
let clean_text = re_ansi.replace_all(&text, "");
|
||||
|
||||
// Split the text into blocks, each starting with "Output:".
|
||||
// The first element of the split will be empty, so we skip it.
|
||||
for block in clean_text.split("Output:").skip(1) {
|
||||
// Check if this block describes the primary monitor.
|
||||
if block.contains("priority 1") {
|
||||
// The monitor name is the second piece of text in the block, after the ID.
|
||||
// e.g., " 1 eDP-1 enabled..." -> "eDP-1"
|
||||
if let Some(name) = block.split_whitespace().nth(1) {
|
||||
return Some(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn try_gdbus_primary() -> Option<String> {
|
||||
let output = run_with_timeout(
|
||||
"gdbus",
|
||||
&[
|
||||
"call",
|
||||
"--session",
|
||||
"--dest",
|
||||
"org.gnome.Mutter.DisplayConfig",
|
||||
"--object-path",
|
||||
"/org/gnome/Mutter/DisplayConfig",
|
||||
"--method",
|
||||
"org.gnome.Mutter.DisplayConfig.GetCurrentState",
|
||||
],
|
||||
COMMAND_TIMEOUT,
|
||||
"gdbus DisplayConfig.GetCurrentState",
|
||||
)?;
|
||||
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// Match logical monitor entries with primary=true
|
||||
// Pattern: (x, y, scale, transform, true, [('connector-name', ...), ...], ...)
|
||||
// Use regex to find entries where 5th field is true, then extract connector name
|
||||
// Example matched text: "(0, 0, 1.5, 0, true, [('HDMI-1', 'MHH', 'Monitor', '0x00000000')], ...)"
|
||||
let re = Regex::new(r"\([^()]*,\s*true,\s*\[\('([^']+)'").ok()?;
|
||||
|
||||
if let Some(captures) = re.captures(&text) {
|
||||
return captures.get(1).map(|m| m.as_str().to_string());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn get_primary_monitor() -> Option<String> {
|
||||
try_xrandr_primary()
|
||||
.or_else(try_kscreen_primary)
|
||||
.or_else(try_gdbus_primary)
|
||||
}
|
||||
|
||||
pub fn get_displays() -> Arc<Displays> {
|
||||
let mut lock = DISPLAYS.lock().unwrap();
|
||||
match lock.as_ref() {
|
||||
Some(displays) => displays.clone(),
|
||||
None => match get_wayland_displays() {
|
||||
Ok(displays) => {
|
||||
let mut primary_index = None;
|
||||
if let Some(name) = get_primary_monitor() {
|
||||
for (i, display) in displays.iter().enumerate() {
|
||||
if display.name == name {
|
||||
primary_index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
if primary_index.is_none() {
|
||||
for (i, display) in displays.iter().enumerate() {
|
||||
if display.x == 0 && display.y == 0 {
|
||||
primary_index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let displays = Arc::new(Displays {
|
||||
primary: primary_index.unwrap_or(0),
|
||||
displays,
|
||||
});
|
||||
*lock = Some(displays.clone());
|
||||
displays
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Failed to get wayland displays: {}", err);
|
||||
Arc::new(Displays {
|
||||
primary: 0,
|
||||
displays: Vec::new(),
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn clear_wayland_displays_cache() {
|
||||
let _ = DISPLAYS.lock().unwrap().take();
|
||||
}
|
||||
|
||||
// Return (min_x, max_x, min_y, max_y)
|
||||
pub fn get_desktop_rect_for_uinput() -> Option<(i32, i32, i32, i32)> {
|
||||
let wayland_displays = get_displays();
|
||||
let displays = &wayland_displays.displays;
|
||||
if displays.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// For compatibility, if only one display, we use the physical size for `uinput`.
|
||||
// Otherwise, we use the logical size for `uinput`.
|
||||
if displays.len() == 1 {
|
||||
let d = &displays[0];
|
||||
return Some((d.x, d.x + d.width, d.y, d.y + d.height));
|
||||
}
|
||||
|
||||
let mut min_x = i32::MAX;
|
||||
let mut min_y = i32::MAX;
|
||||
let mut max_x = i32::MIN;
|
||||
let mut max_y = i32::MIN;
|
||||
for d in displays.iter() {
|
||||
min_x = min_x.min(d.x);
|
||||
min_y = min_y.min(d.y);
|
||||
let size = if let Some(logical_size) = d.logical_size {
|
||||
logical_size
|
||||
} else {
|
||||
// When `logical_size` is None, we cannot obtain the correct desktop rectangle.
|
||||
// This may occur if the Wayland compositor does not provide logical size information,
|
||||
// or if display information is incomplete. We fall back to physical size, which provides
|
||||
// usable dimensions, but may not always be correct depending on compositor behavior.
|
||||
warn!(
|
||||
"Display at ({}, {}) is missing logical_size; falling back to physical size ({}, {}).",
|
||||
d.x, d.y, d.width, d.height
|
||||
);
|
||||
(d.width, d.height)
|
||||
};
|
||||
max_x = max_x.max(d.x + size.0);
|
||||
max_y = max_y.max(d.y + size.1);
|
||||
}
|
||||
Some((min_x, max_x, min_y, max_y))
|
||||
}
|
||||
@@ -2,9 +2,12 @@ use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::process::Command;
|
||||
use std::sync::{atomic::AtomicBool, Arc, Mutex};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, AtomicU8, Ordering},
|
||||
Arc, Mutex,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, trace, warn};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
use dbus::{
|
||||
arg::{OwnedFd, PropMap, RefArg, Variant},
|
||||
@@ -17,23 +20,63 @@ use gstreamer as gst;
|
||||
use gstreamer::prelude::*;
|
||||
use gstreamer_app::AppSink;
|
||||
|
||||
use hbb_common::config;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use hbb_common::{bail, config, platform::linux::CMD_SH, tokio, ResultType};
|
||||
|
||||
use super::capturable::PixelProvider;
|
||||
use super::capturable::{Capturable, Recorder};
|
||||
use super::display::{clear_wayland_displays_cache, get_displays, Displays};
|
||||
use super::remote_desktop_portal::OrgFreedesktopPortalRemoteDesktop as remote_desktop_portal;
|
||||
use super::request_portal::OrgFreedesktopPortalRequestResponse;
|
||||
use super::screencast_portal::OrgFreedesktopPortalScreenCast as screencast_portal;
|
||||
use hbb_common::platform::linux::CMD_SH;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref RDP_SESSION_INFO: Mutex<Option<RdpSessionInfo>> = Mutex::new(None);
|
||||
// Maybe it's better to save this cache in config file?
|
||||
// Because "--server" process may be restarted frequently, then the cache will be lost.
|
||||
// But the users have to know where to find and delete the config file when they want to clear the cache,
|
||||
// or we have to add a UI for that.
|
||||
// For simplicity, we just keep it in memory for now.
|
||||
static ref PIPEWIRE_DISPLAY_OFFSET_CACHE: Mutex<Option<PipewireDisplayOffsetCache>> =
|
||||
Mutex::new(None);
|
||||
}
|
||||
|
||||
// For KDE Plasma only, because GNOME provides position info.
|
||||
struct PipewireDisplayOffsetCache {
|
||||
// We need to compare the displays, because:
|
||||
// 1. On Archlinux KDE Plasma
|
||||
// 2. One display, and connect, remember share choice.
|
||||
// 3. Plug in another monitor.
|
||||
// 4. The portal will reuse the restore token, no new share choice dialog, but the share screen is different.
|
||||
// The controlling side will see the new monitor.
|
||||
// All displays as one string for easy comparison
|
||||
// name1-x1-y1-width1-height1;name2-x2-y2-width2-height2;...
|
||||
display_key: String,
|
||||
restore_token: String,
|
||||
offsets: Vec<(i32, i32)>,
|
||||
}
|
||||
|
||||
// KDE Plasma may not provide position info
|
||||
static HAS_POSITION_ATTR: AtomicBool = AtomicBool::new(false);
|
||||
static IS_SERVER_RUNNING: AtomicU8 = AtomicU8::new(0); // 0: uninitialized, 1:true, 2: false
|
||||
|
||||
impl PipewireDisplayOffsetCache {
|
||||
fn displays_to_key(displays: &Arc<Displays>) -> String {
|
||||
displays
|
||||
.displays
|
||||
.iter()
|
||||
.map(|d| format!("{}-{}-{}-{}-{}", d.name, d.x, d.y, d.width, d.height))
|
||||
.collect::<Vec<String>>()
|
||||
.join(";")
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn close_session() {
|
||||
let _ = RDP_SESSION_INFO.lock().unwrap().take();
|
||||
clear_wayland_displays_cache();
|
||||
HAS_POSITION_ATTR.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -52,6 +95,8 @@ pub fn try_close_session() {
|
||||
}
|
||||
if close {
|
||||
*rdp_info = None;
|
||||
clear_wayland_displays_cache();
|
||||
HAS_POSITION_ATTR.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +120,10 @@ impl PwStreamInfo {
|
||||
pub fn get_size(&self) -> (usize, usize) {
|
||||
self.size
|
||||
}
|
||||
|
||||
pub fn get_position(&self) -> (i32, i32) {
|
||||
self.position
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -108,8 +157,10 @@ pub struct PipeWireCapturable {
|
||||
fd: OwnedFd,
|
||||
path: u64,
|
||||
source_type: u64,
|
||||
pub primary: bool,
|
||||
pub position: (i32, i32),
|
||||
pub size: (usize, usize),
|
||||
pub logical_size: (usize, usize),
|
||||
pub physical_size: (usize, usize),
|
||||
}
|
||||
|
||||
impl PipeWireCapturable {
|
||||
@@ -117,27 +168,31 @@ impl PipeWireCapturable {
|
||||
conn: Arc<SyncConnection>,
|
||||
fd: OwnedFd,
|
||||
resolution: Arc<Mutex<Option<(usize, usize)>>>,
|
||||
stream: PwStreamInfo,
|
||||
stream: &PwStreamInfo,
|
||||
) -> Self {
|
||||
// alternative to get screen resolution as stream.size is not always correct ex: on fractional scaling
|
||||
// https://github.com/rustdesk/rustdesk/issues/6116#issuecomment-1817724244
|
||||
let size = get_res(Self {
|
||||
let physical_size = get_res(Self {
|
||||
dbus_conn: conn.clone(),
|
||||
fd: fd.clone(),
|
||||
path: stream.path,
|
||||
source_type: stream.source_type,
|
||||
primary: false,
|
||||
position: stream.position,
|
||||
size: stream.size,
|
||||
logical_size: stream.size,
|
||||
physical_size: (0, 0),
|
||||
})
|
||||
.unwrap_or(stream.size);
|
||||
*resolution.lock().unwrap() = Some(size);
|
||||
*resolution.lock().unwrap() = Some(physical_size);
|
||||
Self {
|
||||
dbus_conn: conn,
|
||||
fd,
|
||||
path: stream.path,
|
||||
source_type: stream.source_type,
|
||||
primary: false,
|
||||
position: stream.position,
|
||||
size,
|
||||
logical_size: stream.size,
|
||||
physical_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,7 +269,7 @@ pub struct PipeWireRecorder {
|
||||
}
|
||||
|
||||
impl PipeWireRecorder {
|
||||
pub fn new(capturable: PipeWireCapturable) -> Result<Self, Box<dyn Error>> {
|
||||
pub fn new(capturable: PipeWireCapturable) -> ResultType<Self> {
|
||||
let pipeline = gst::Pipeline::new(None);
|
||||
|
||||
let src = gst::ElementFactory::make("pipewiresrc", None)?;
|
||||
@@ -247,7 +302,36 @@ impl PipeWireRecorder {
|
||||
));
|
||||
appsink.set_caps(Some(&caps));
|
||||
|
||||
// [Workaround]
|
||||
// Crash may occur if there are multiple pipelines started at the same time.
|
||||
// `pipeline.get_state()` can significantly reduce the probability of crashes,
|
||||
// but cannot completely resolve this issue.
|
||||
// Adding a short sleep period can also reduce the probability of crashes.
|
||||
debug!(
|
||||
"[gstreamer] Setting pipeline {} to PLAYING state...",
|
||||
capturable.fd.as_raw_fd()
|
||||
);
|
||||
pipeline.set_state(gst::State::Playing)?;
|
||||
|
||||
// Wait for the state change to actually complete before proceeding.
|
||||
// The 2000ms timeout for pipeline state change was chosen based on empirical testing.
|
||||
let state_change = pipeline.get_state(gst::ClockTime::from_mseconds(2000));
|
||||
match state_change {
|
||||
(Ok(_), gst::State::Playing, _) => {
|
||||
debug!(
|
||||
"[gstreamer] Pipeline {} state confirmed as PLAYING.",
|
||||
capturable.fd.as_raw_fd()
|
||||
);
|
||||
}
|
||||
(result, state, pending) => {
|
||||
warn!(
|
||||
"[gstreamer] Pipeline {} state change incomplete: result={:?}, state={:?}, pending={:?}",
|
||||
capturable.fd.as_raw_fd(), result, state, pending
|
||||
);
|
||||
}
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(150));
|
||||
|
||||
Ok(Self {
|
||||
pipeline,
|
||||
appsink,
|
||||
@@ -366,6 +450,8 @@ impl Drop for PipeWireRecorder {
|
||||
if let Err(err) = self.pipeline.set_state(gst::State::Null) {
|
||||
warn!("Failed to stop GStreamer pipeline: {}.", err);
|
||||
}
|
||||
// Wait for state change to complete to avoid races during PipeWire teardown.
|
||||
let _ = self.pipeline.get_state(gst::ClockTime::from_mseconds(2000));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,18 +482,18 @@ where
|
||||
0 => {}
|
||||
1 => {
|
||||
warn!("DBus response: User cancelled interaction.");
|
||||
failure_out.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
failure_out.store(true, Ordering::SeqCst);
|
||||
return true;
|
||||
}
|
||||
c => {
|
||||
warn!("DBus response: Unknown error, code: {}.", c);
|
||||
failure_out.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
failure_out.store(true, Ordering::SeqCst);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if let Err(err) = f(r, c, m) {
|
||||
warn!("Error requesting screen capture via dbus: {}", err);
|
||||
failure_out.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
failure_out.store(true, Ordering::SeqCst);
|
||||
}
|
||||
true
|
||||
})
|
||||
@@ -488,6 +574,7 @@ fn streams_from_response(response: OrgFreedesktopPortalRequestResponse) -> Vec<P
|
||||
if v.len() == 2 {
|
||||
info.position.0 = v[0] as _;
|
||||
info.position.1 = v[1] as _;
|
||||
HAS_POSITION_ATTR.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -510,16 +597,15 @@ pub fn get_available_cursor_modes() -> Result<u32, dbus::Error> {
|
||||
}
|
||||
|
||||
// mostly inspired by https://gitlab.gnome.org/-/snippets/39
|
||||
pub fn request_remote_desktop() -> Result<
|
||||
(
|
||||
SyncConnection,
|
||||
OwnedFd,
|
||||
Vec<PwStreamInfo>,
|
||||
dbus::Path<'static>,
|
||||
bool,
|
||||
),
|
||||
Box<dyn Error>,
|
||||
> {
|
||||
pub fn request_remote_desktop(
|
||||
capture_cursor: bool,
|
||||
) -> ResultType<(
|
||||
SyncConnection,
|
||||
OwnedFd,
|
||||
Vec<PwStreamInfo>,
|
||||
dbus::Path<'static>,
|
||||
bool,
|
||||
)> {
|
||||
unsafe {
|
||||
if !INIT {
|
||||
gstreamer::init()?;
|
||||
@@ -574,6 +660,7 @@ pub fn request_remote_desktop() -> Result<
|
||||
session.clone(),
|
||||
failure.clone(),
|
||||
is_support_restore_token,
|
||||
capture_cursor,
|
||||
),
|
||||
failure_res.clone(),
|
||||
)?;
|
||||
@@ -586,7 +673,7 @@ pub fn request_remote_desktop() -> Result<
|
||||
break;
|
||||
}
|
||||
|
||||
if failure_res.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
if failure_res.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -607,9 +694,7 @@ pub fn request_remote_desktop() -> Result<
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(Box::new(DBusError(
|
||||
"Failed to obtain screen capture. You may need to upgrade the PipeWire library for better compatibility. Please check https://github.com/rustdesk/rustdesk/issues/8600#issuecomment-2254720954 for more details.".into()
|
||||
)))
|
||||
bail!("Failed to obtain screen capture. You may need to upgrade the PipeWire library for better compatibility. Please check https://github.com/rustdesk/rustdesk/issues/8600#issuecomment-2254720954 for more details.")
|
||||
}
|
||||
|
||||
fn on_create_session_response(
|
||||
@@ -618,6 +703,7 @@ fn on_create_session_response(
|
||||
session: Arc<Mutex<Option<dbus::Path<'static>>>>,
|
||||
failure: Arc<AtomicBool>,
|
||||
is_support_restore_token: bool,
|
||||
capture_cursor: bool,
|
||||
) -> impl Fn(
|
||||
OrgFreedesktopPortalRequestResponse,
|
||||
&SyncConnection,
|
||||
@@ -666,6 +752,14 @@ fn on_create_session_response(
|
||||
}
|
||||
args.insert("types".into(), Variant(Box::new(1u32))); //| 2u32)));
|
||||
|
||||
if capture_cursor {
|
||||
get_available_cursor_modes().ok().map(|modes| {
|
||||
if modes & 0x2 != 0 {
|
||||
args.insert("cursor_mode".to_string(), Variant(Box::new(2u32)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let path = portal.select_sources(ses.clone(), args)?;
|
||||
handle_response(
|
||||
c,
|
||||
@@ -838,7 +932,7 @@ pub fn get_capturables() -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> {
|
||||
};
|
||||
|
||||
if rdp_connection.is_none() {
|
||||
let (conn, fd, streams, session, is_support_restore_token) = request_remote_desktop()?;
|
||||
let (conn, fd, streams, session, is_support_restore_token) = request_remote_desktop(false)?;
|
||||
let conn = Arc::new(conn);
|
||||
|
||||
let rdp_info = RdpSessionInfo {
|
||||
@@ -852,7 +946,7 @@ pub fn get_capturables() -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> {
|
||||
*rdp_connection = Some(rdp_info);
|
||||
}
|
||||
|
||||
let rdp_info = match rdp_connection.as_ref() {
|
||||
let rdp_info = match rdp_connection.as_mut() {
|
||||
Some(res) => res,
|
||||
None => {
|
||||
return Err(Box::new(DBusError("RDP response is None.".into())));
|
||||
@@ -861,8 +955,7 @@ pub fn get_capturables() -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> {
|
||||
|
||||
Ok(rdp_info
|
||||
.streams
|
||||
.clone()
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|s| {
|
||||
PipeWireCapturable::new(
|
||||
rdp_info.conn.clone(),
|
||||
@@ -883,7 +976,12 @@ pub fn get_capturables() -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> {
|
||||
//
|
||||
// `screencast_portal` supports restore_token and persist_mode if the version is greater than or equal to 4.
|
||||
// `remote_desktop_portal` does not support restore_token and persist_mode.
|
||||
fn is_server_running() -> bool {
|
||||
pub(crate) fn is_server_running() -> bool {
|
||||
let v = IS_SERVER_RUNNING.load(Ordering::SeqCst);
|
||||
if v > 0 {
|
||||
return v == 1;
|
||||
}
|
||||
|
||||
let app_name = config::APP_NAME.read().unwrap().clone().to_lowercase();
|
||||
let output = match Command::new(CMD_SH.as_str())
|
||||
.arg("-c")
|
||||
@@ -898,5 +996,525 @@ fn is_server_running() -> bool {
|
||||
|
||||
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||
let is_running = output_str.contains(&format!("{} --server", app_name));
|
||||
IS_SERVER_RUNNING.store(if is_running { 1 } else { 2 }, Ordering::SeqCst);
|
||||
is_running
|
||||
}
|
||||
|
||||
// The logical size reported by portal may be different from the size reported by `get_displays()`.
|
||||
// So we need to use the workaround here.
|
||||
// 1. openSUSE, KDE Plasma
|
||||
// 2. Kubuntu 24.04 TLS, after running `sudo apt install plasma-workspace-wayland`
|
||||
// Maybe it's a bug, and we can remove this workaround in the future.
|
||||
pub fn try_fix_logical_size(shared_displays: &mut Vec<crate::Display>) {
|
||||
if !is_server_running() {
|
||||
return;
|
||||
}
|
||||
|
||||
let wayland_displays = get_displays();
|
||||
if wayland_displays.displays.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for sd in shared_displays.iter_mut() {
|
||||
if let crate::Display::WAYLAND(d) = sd {
|
||||
let capturable = &mut d.0;
|
||||
for wd in wayland_displays.displays.iter() {
|
||||
if capturable.position.0 == wd.x && capturable.position.1 == wd.y {
|
||||
if let Some(logical_size) = wd.logical_size {
|
||||
if capturable.physical_size.0 != wd.width as usize
|
||||
|| capturable.physical_size.1 != wd.height as usize
|
||||
{
|
||||
// If "Full Workspace" is selected in the portal dialog,
|
||||
// the physical size reported by portal may not match the display info.
|
||||
debug!(
|
||||
"Physical size of capturable ({:?}) does not match display info: ({:?}) - ({:?}). Skipping logical size fix.",
|
||||
capturable.position,
|
||||
capturable.physical_size,
|
||||
(wd.width as usize, wd.height as usize)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if capturable.logical_size.0 != logical_size.0 as usize
|
||||
|| capturable.logical_size.1 != logical_size.1 as usize
|
||||
{
|
||||
warn!(
|
||||
"Fixing logical size of capturable from {:?} to {:?} based on display info {:?}.",
|
||||
capturable.logical_size,
|
||||
logical_size,
|
||||
wd
|
||||
);
|
||||
capturable.logical_size =
|
||||
(logical_size.0 as usize, logical_size.1 as usize);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill_displays(
|
||||
mouse_move_to: impl Fn(i32, i32),
|
||||
get_cursor_pos: fn() -> Option<(i32, i32)>,
|
||||
shared_displays: &mut Vec<crate::Display>,
|
||||
) -> ResultType<()> {
|
||||
if !is_server_running() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut rdp_connection = RDP_SESSION_INFO.lock().unwrap();
|
||||
let rdp_info = match rdp_connection.as_mut() {
|
||||
Some(res) => res,
|
||||
None => {
|
||||
// Unreachable
|
||||
bail!("RDP session info is None when filling display positions.");
|
||||
}
|
||||
};
|
||||
|
||||
let all_displays = get_displays();
|
||||
if !HAS_POSITION_ATTR.load(Ordering::SeqCst) {
|
||||
if all_displays.displays.len() > 1 {
|
||||
debug!("Multiple Wayland displays detected, adjusting stream positions accordingly.");
|
||||
try_fill_positions(
|
||||
mouse_move_to,
|
||||
get_cursor_pos,
|
||||
&all_displays,
|
||||
shared_displays,
|
||||
&mut rdp_info.streams,
|
||||
)?;
|
||||
}
|
||||
HAS_POSITION_ATTR.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if all_displays.displays.len() > 1 {
|
||||
sort_streams(&all_displays, shared_displays, &mut rdp_info.streams);
|
||||
}
|
||||
|
||||
shared_displays.iter_mut().next().map(|d| {
|
||||
if let crate::Display::WAYLAND(d) = d {
|
||||
d.0.primary = true;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_fill_positions(
|
||||
mouse_move_to: impl Fn(i32, i32),
|
||||
get_cursor_pos: fn() -> Option<(i32, i32)>,
|
||||
displays: &Arc<Displays>,
|
||||
shared_displays: &mut Vec<crate::Display>,
|
||||
streams: &mut Vec<PwStreamInfo>,
|
||||
) -> ResultType<()> {
|
||||
if try_fill_positions_from_cache(displays, shared_displays, streams) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut multi_matched_indices = Vec::new();
|
||||
for (i, sd) in shared_displays.iter_mut().enumerate() {
|
||||
if let crate::Display::WAYLAND(d) = sd {
|
||||
let capturable = &mut d.0;
|
||||
let mut match_count = 0;
|
||||
for wd in displays.displays.iter() {
|
||||
if capturable.physical_size.0 == wd.width as usize
|
||||
&& capturable.physical_size.1 == wd.height as usize
|
||||
{
|
||||
capturable.position = (wd.x, wd.y);
|
||||
if let Some(pw_stream) = streams.get_mut(i) {
|
||||
pw_stream.position = (wd.x, wd.y);
|
||||
}
|
||||
match_count += 1;
|
||||
}
|
||||
}
|
||||
if match_count == 0 {
|
||||
warn!(
|
||||
"No matching display found for capturable with size {:?}.",
|
||||
capturable.physical_size
|
||||
);
|
||||
} else if match_count > 1 {
|
||||
multi_matched_indices.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !multi_matched_indices.is_empty() {
|
||||
fill_multi_matched_positions(
|
||||
mouse_move_to,
|
||||
get_cursor_pos,
|
||||
displays,
|
||||
shared_displays,
|
||||
streams,
|
||||
multi_matched_indices,
|
||||
)?;
|
||||
}
|
||||
|
||||
save_positions_to_cache(displays, shared_displays);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_fill_positions_from_cache(
|
||||
displays: &Arc<Displays>,
|
||||
shared_displays: &mut Vec<crate::Display>,
|
||||
streams: &mut Vec<PwStreamInfo>,
|
||||
) -> bool {
|
||||
let mut lock = PIPEWIRE_DISPLAY_OFFSET_CACHE.lock().unwrap();
|
||||
let Some(cache) = lock.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if cache.offsets.len() != shared_displays.len() {
|
||||
let _ = lock.take();
|
||||
return false;
|
||||
}
|
||||
|
||||
let display_key = PipewireDisplayOffsetCache::displays_to_key(displays);
|
||||
if cache.display_key != display_key {
|
||||
let _ = lock.take();
|
||||
return false;
|
||||
}
|
||||
|
||||
let restore_token = config::LocalConfig::get_option(RESTORE_TOKEN_CONF_KEY);
|
||||
if cache.restore_token != restore_token {
|
||||
let _ = lock.take();
|
||||
return false;
|
||||
}
|
||||
|
||||
for (i, sd) in shared_displays.iter_mut().enumerate() {
|
||||
if let crate::Display::WAYLAND(d) = sd {
|
||||
let capturable = &mut d.0;
|
||||
if let Some((x_off, y_off)) = cache.offsets.get(i) {
|
||||
capturable.position = (*x_off, *y_off);
|
||||
if let Some(pw_stream) = streams.get_mut(i) {
|
||||
pw_stream.position = (*x_off, *y_off);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn save_positions_to_cache(displays: &Arc<Displays>, shared_displays: &Vec<crate::Display>) {
|
||||
let restore_token = config::LocalConfig::get_option(RESTORE_TOKEN_CONF_KEY);
|
||||
if restore_token.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut offsets = Vec::new();
|
||||
for sd in shared_displays.iter() {
|
||||
if let crate::Display::WAYLAND(d) = sd {
|
||||
let capturable = &d.0;
|
||||
offsets.push((capturable.position.0, capturable.position.1));
|
||||
}
|
||||
}
|
||||
|
||||
let display_key = PipewireDisplayOffsetCache::displays_to_key(displays);
|
||||
let cache = PipewireDisplayOffsetCache {
|
||||
display_key,
|
||||
restore_token,
|
||||
offsets,
|
||||
};
|
||||
|
||||
*PIPEWIRE_DISPLAY_OFFSET_CACHE.lock().unwrap() = Some(cache);
|
||||
}
|
||||
|
||||
fn compare_left_up_corner(w: usize, d1: &[u8], d2: &[u8]) -> bool {
|
||||
if w == 0 {
|
||||
return false;
|
||||
}
|
||||
if d1.len() != d2.len() {
|
||||
return false;
|
||||
}
|
||||
let bpp = 4; // BGR0/RGB0
|
||||
let stride = w.saturating_mul(bpp);
|
||||
if stride == 0 || d1.len() < stride || d2.len() < stride {
|
||||
return false;
|
||||
}
|
||||
let h = d1.len() / stride;
|
||||
if h == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let roi_w = std::cmp::min(36, w);
|
||||
let roi_h = std::cmp::min(36, h);
|
||||
let mut diff_px = 0usize;
|
||||
let total_px = roi_w * roi_h;
|
||||
// Minimum number of differing pixels required to consider images different.
|
||||
const MIN_DIFF_PIXELS: usize = 8;
|
||||
// Divisor for threshold calculation: allows up to 1/8 of ROI pixels to differ before returning true.
|
||||
const DIFF_THRESHOLD_DIVISOR: usize = 8;
|
||||
let threshold = std::cmp::max(MIN_DIFF_PIXELS, total_px / DIFF_THRESHOLD_DIVISOR);
|
||||
|
||||
for y in 0..roi_h {
|
||||
let row_off = y * stride;
|
||||
for x in 0..roi_w {
|
||||
let i = row_off + x * bpp;
|
||||
let a = &d1[i..i + bpp];
|
||||
let b = &d2[i..i + bpp];
|
||||
if a != b {
|
||||
diff_px += 1;
|
||||
if diff_px >= threshold {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn fill_multi_matched_positions(
|
||||
mouse_move_to: impl Fn(i32, i32),
|
||||
get_cursor_pos: fn() -> Option<(i32, i32)>,
|
||||
displays: &Arc<Displays>,
|
||||
shared_displays: &mut Vec<crate::Display>,
|
||||
streams: &mut Vec<PwStreamInfo>,
|
||||
multi_matched_indices: Vec<usize>,
|
||||
) -> ResultType<()> {
|
||||
debug!(
|
||||
"Multiple capturables ({:?}) match the same display size, attempting to disambiguate positions.",
|
||||
&multi_matched_indices);
|
||||
if multi_matched_indices.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let is_support_embeded_cursor = get_available_cursor_modes()
|
||||
.ok()
|
||||
.map(|modes| modes & 0x2 != 0)
|
||||
.unwrap_or(false);
|
||||
if is_support_embeded_cursor {
|
||||
fill_multi_matched_positions_cursor(
|
||||
mouse_move_to,
|
||||
get_cursor_pos,
|
||||
displays,
|
||||
shared_displays,
|
||||
streams,
|
||||
multi_matched_indices,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mouse_move_to_(
|
||||
mouse_move_to: &impl Fn(i32, i32),
|
||||
get_cursor_pos: fn() -> Option<(i32, i32)>,
|
||||
x: i32,
|
||||
y: i32,
|
||||
) {
|
||||
const MOVE_MOUSE_TIMEOUT: Duration = Duration::from_millis(150);
|
||||
let start = std::time::Instant::now();
|
||||
while start.elapsed() < MOVE_MOUSE_TIMEOUT {
|
||||
mouse_move_to(x, y);
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
if let Some((x1, y1)) = get_cursor_pos() {
|
||||
if x1 == x && y1 == y {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
warn!(
|
||||
"Failed to move mouse to ({}, {}) within timeout: {:?}.",
|
||||
x, y, &MOVE_MOUSE_TIMEOUT
|
||||
);
|
||||
}
|
||||
|
||||
fn fill_multi_matched_positions_cursor(
|
||||
mouse_move_to: impl Fn(i32, i32),
|
||||
get_cursor_pos: fn() -> Option<(i32, i32)>,
|
||||
displays: &Arc<Displays>,
|
||||
shared_displays: &mut Vec<crate::Display>,
|
||||
streams: &mut Vec<PwStreamInfo>,
|
||||
multi_matched_indices: Vec<usize>,
|
||||
) -> ResultType<()> {
|
||||
// This creates a new remote desktop session for cursor-based position detection.
|
||||
// The session is temporary, used only for disambiguation, and is dropped after detection completes.
|
||||
let (conn, fd, streams_with_cursor, _session, _is_support_restore_token) =
|
||||
request_remote_desktop(true)?;
|
||||
let conn = Arc::new(conn);
|
||||
|
||||
let mut matched_indices = Vec::new();
|
||||
const CAPTURE_TIMEOUT_MS: u64 = 1_000;
|
||||
for idx in multi_matched_indices {
|
||||
match (
|
||||
shared_displays.get_mut(idx),
|
||||
streams.get_mut(idx),
|
||||
streams_with_cursor.get(idx),
|
||||
) {
|
||||
(Some(crate::Display::WAYLAND(d)), Some(pw_stream), Some(pw_stream_with_cursor)) => {
|
||||
// Check if only one display matches the size
|
||||
let mut match_count = 0;
|
||||
for (i, wd) in displays.displays.iter().enumerate() {
|
||||
if matched_indices.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
if d.0.physical_size.0 == wd.width as usize
|
||||
&& d.0.physical_size.1 == wd.height as usize
|
||||
{
|
||||
match_count += 1;
|
||||
}
|
||||
}
|
||||
if match_count == 0 {
|
||||
error!(
|
||||
"No matching display found for capturable with size {:?}.",
|
||||
d.0.physical_size
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if match_count == 1 {
|
||||
for (i, wd) in displays.displays.iter().enumerate() {
|
||||
if matched_indices.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
if d.0.physical_size.0 == wd.width as usize
|
||||
&& d.0.physical_size.1 == wd.height as usize
|
||||
{
|
||||
d.0.position = (wd.x, wd.y);
|
||||
pw_stream.position = (wd.x, wd.y);
|
||||
matched_indices.push(i);
|
||||
debug!(
|
||||
"Disambiguated position for capturable with size {:?} to ({}, {}).",
|
||||
d.0.physical_size, wd.x, wd.y
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Move the mouse to a neutral position first,
|
||||
// to avoid interference from previous position.
|
||||
mouse_move_to_(&mouse_move_to, get_cursor_pos, 300, 300);
|
||||
|
||||
let mut rec = PipeWireRecorder::new(PipeWireCapturable {
|
||||
dbus_conn: conn.clone(),
|
||||
fd: fd.clone(),
|
||||
path: pw_stream_with_cursor.path,
|
||||
source_type: pw_stream_with_cursor.source_type,
|
||||
primary: false,
|
||||
position: pw_stream_with_cursor.position,
|
||||
logical_size: pw_stream_with_cursor.size,
|
||||
physical_size: (0, 0),
|
||||
})?;
|
||||
// Take first frame and copy owned buffer to avoid borrow across second capture
|
||||
let (is_bgr, w, first_buf): (bool, usize, Vec<u8>) =
|
||||
match rec.capture(CAPTURE_TIMEOUT_MS) {
|
||||
Ok(PixelProvider::BGR0(w, _, data1)) => (true, w, data1.to_vec()),
|
||||
Ok(PixelProvider::RGB0(w, _, data1)) => (false, w, data1.to_vec()),
|
||||
Ok(_) => {
|
||||
error!("Unexpected pixel format on first capture.");
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to capture screen for position disambiguation: {}",
|
||||
e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let matched_len = matched_indices.len();
|
||||
for (i, wd) in displays.displays.iter().enumerate() {
|
||||
if matched_indices.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if wd.width as usize == d.0.physical_size.0
|
||||
&& wd.height as usize == d.0.physical_size.1
|
||||
{
|
||||
mouse_move_to_(&mouse_move_to, get_cursor_pos, wd.x + 8, wd.y + 8);
|
||||
rec.saved_raw_data.clear();
|
||||
match rec.capture(CAPTURE_TIMEOUT_MS) {
|
||||
Ok(PixelProvider::BGR0(_, _, data2)) if is_bgr => {
|
||||
if compare_left_up_corner(w, &first_buf, data2) {
|
||||
d.0.position = (wd.x, wd.y);
|
||||
pw_stream.position = (wd.x, wd.y);
|
||||
matched_indices.push(i);
|
||||
debug!(
|
||||
"Disambiguated position for capturable with size {:?} to ({}, {}).",
|
||||
d.0.physical_size, wd.x, wd.y
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(PixelProvider::RGB0(_, _, data2)) if !is_bgr => {
|
||||
if compare_left_up_corner(w, &first_buf, data2) {
|
||||
d.0.position = (wd.x, wd.y);
|
||||
pw_stream.position = (wd.x, wd.y);
|
||||
matched_indices.push(i);
|
||||
debug!(
|
||||
"Disambiguated position for capturable with size {:?} to ({}, {}).",
|
||||
d.0.physical_size, wd.x, wd.y
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
// unreachable
|
||||
error!("Pixel format changed between captures, cannot disambiguate position.");
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to capture screen for position disambiguation: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if matched_len == matched_indices.len() {
|
||||
error!(
|
||||
"Failed to disambiguate position for capturable with size {:?}.",
|
||||
d.0.physical_size
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sort_streams(
|
||||
displays: &Arc<Displays>,
|
||||
shared_displays: &mut Vec<crate::Display>,
|
||||
streams: &mut Vec<PwStreamInfo>,
|
||||
) {
|
||||
if streams.is_empty() {
|
||||
// unreachable
|
||||
error!("No streams available to sort.");
|
||||
return;
|
||||
}
|
||||
|
||||
// put the main display first, then the rest by the order of displays
|
||||
let mut display_order: Vec<(i32, i32)> = Vec::new();
|
||||
if let Some(d) = displays.displays.get(displays.primary) {
|
||||
display_order.push((d.x, d.y));
|
||||
}
|
||||
for (i, d) in displays.displays.iter().enumerate() {
|
||||
if i != displays.primary {
|
||||
display_order.push((d.x, d.y));
|
||||
}
|
||||
}
|
||||
|
||||
let mut sorted_streams = Vec::new();
|
||||
let mut sorted_shared_displays = Vec::new();
|
||||
// Move matching items in order without cloning
|
||||
for (x, y) in display_order.into_iter() {
|
||||
for i in 0..streams.len() {
|
||||
if streams[i].position.0 == x && streams[i].position.1 == y {
|
||||
sorted_streams.push(streams.remove(i));
|
||||
// shared_displays.len() must be equal to streams.len()
|
||||
// But we still check the length to avoid panic
|
||||
if shared_displays.len() > i {
|
||||
sorted_shared_displays.push(shared_displays.remove(i));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
*streams = sorted_streams;
|
||||
*shared_displays = sorted_shared_displays;
|
||||
}
|
||||
|
||||
@@ -1755,6 +1755,13 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
thread.video_sender.send(MediaData::Reset).ok();
|
||||
}
|
||||
|
||||
let mut scale = 1.0;
|
||||
if let Some(pi) = &self.handler.lc.read().unwrap().peer_info {
|
||||
if let Some(d) = pi.displays.get(s.display as usize) {
|
||||
scale = d.scale;
|
||||
}
|
||||
}
|
||||
|
||||
if s.width > 0 && s.height > 0 {
|
||||
self.handler.set_display(
|
||||
s.x,
|
||||
@@ -1762,6 +1769,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
s.width,
|
||||
s.height,
|
||||
s.cursor_embedded,
|
||||
scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,17 +427,8 @@ impl ClipboardContext {
|
||||
// Don't use `hbb_common::platform::linux::is_kde()` here.
|
||||
// It's not correct in the server process.
|
||||
#[cfg(target_os = "linux")]
|
||||
let is_kde_x11 = {
|
||||
use hbb_common::platform::linux::CMD_SH;
|
||||
let is_kde = std::process::Command::new(CMD_SH.as_str())
|
||||
.arg("-c")
|
||||
.arg("ps -e | grep -E kded[0-9]+ | grep -v grep")
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.output()
|
||||
.map(|o| !o.stdout.is_empty())
|
||||
.unwrap_or(false);
|
||||
is_kde && crate::platform::linux::is_x11()
|
||||
};
|
||||
let is_kde_x11 = hbb_common::platform::linux::is_kde_session()
|
||||
&& crate::platform::linux::is_x11();
|
||||
#[cfg(target_os = "macos")]
|
||||
let is_kde_x11 = false;
|
||||
let clear_holder_text = if is_kde_x11 {
|
||||
|
||||
@@ -609,7 +609,22 @@ impl FlutterHandler {
|
||||
h.insert("original_width", original_resolution.width);
|
||||
h.insert("original_height", original_resolution.height);
|
||||
}
|
||||
h.insert("scale", (d.scale * 100.0f64) as i32);
|
||||
// Don't convert scale (x 100) to i32 directly.
|
||||
// (d.scale * 100.0f64) as i32 may produces inaccuracies.
|
||||
//
|
||||
// Example: GNOME Wayland with Fractional Scaling enabled:
|
||||
// - Physical resolution: 2560x1600
|
||||
// - Logical resolution: 1074x1065
|
||||
// - Scale factor: 150%
|
||||
// Passing physical dimensions and scale factor prevents accurate logical resolution calculation
|
||||
// since 2560/1.5 = 1706.666... (rounded to 1706.67) and 1600/1.5 = 1066.666... (rounded to 1066.67)
|
||||
// h.insert("scale", (d.scale * 100.0f64) as i32);
|
||||
|
||||
// Send scaled_width for accurate logical scale calculation.
|
||||
if d.scale > 0.0 {
|
||||
let scaled_width = (d.width as f64 / d.scale).round() as i32;
|
||||
h.insert("scaled_width", scaled_width);
|
||||
}
|
||||
msg_vec.push(h);
|
||||
}
|
||||
serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned())
|
||||
@@ -679,7 +694,7 @@ impl InvokeUiSession for FlutterHandler {
|
||||
}
|
||||
|
||||
/// unused in flutter, use switch_display or set_peer_info
|
||||
fn set_display(&self, _x: i32, _y: i32, _w: i32, _h: i32, _cursor_embedded: bool) {}
|
||||
fn set_display(&self, _x: i32, _y: i32, _w: i32, _h: i32, _cursor_embedded: bool, _scale: f64) {}
|
||||
|
||||
fn update_privacy_mode(&self) {
|
||||
self.push_event::<&str>("update_privacy_mode", &[], &[]);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use crate::keyboard::input_source::{change_input_source, get_cur_session_input_source};
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::platform::linux::is_x11;
|
||||
use crate::{
|
||||
client::file_trait::FileManager,
|
||||
common::{make_fd_to_json, make_vec_fd_to_json},
|
||||
@@ -1471,19 +1473,45 @@ pub fn main_get_main_display() -> SyncReturn<String> {
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
let mut display_info = "".to_owned();
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
if let Ok(displays) = crate::display_service::try_get_displays() {
|
||||
// to-do: Need to detect current display index.
|
||||
if let Some(display) = displays.iter().next() {
|
||||
display_info = serde_json::to_string(&HashMap::from([
|
||||
("w", display.width()),
|
||||
("h", display.height()),
|
||||
]))
|
||||
.unwrap_or_default();
|
||||
{
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let is_linux_wayland = false;
|
||||
#[cfg(target_os = "linux")]
|
||||
let is_linux_wayland = !is_x11();
|
||||
|
||||
if !is_linux_wayland {
|
||||
if let Ok(displays) = crate::display_service::try_get_displays() {
|
||||
// to-do: Need to detect current display index.
|
||||
if let Some(display) = displays.iter().next() {
|
||||
display_info = serde_json::to_string(&HashMap::from([
|
||||
("w", display.width()),
|
||||
("h", display.height()),
|
||||
]))
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if is_linux_wayland {
|
||||
let displays = scrap::wayland::display::get_displays();
|
||||
if let Some(display) = displays.displays.get(displays.primary) {
|
||||
let logical_size = display
|
||||
.logical_size
|
||||
.unwrap_or((display.width, display.height));
|
||||
display_info = serde_json::to_string(&HashMap::from([
|
||||
("w", logical_size.0),
|
||||
("h", logical_size.1),
|
||||
]))
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
SyncReturn(display_info)
|
||||
}
|
||||
|
||||
// No need to check if is on Wayland in this function.
|
||||
// The Flutter side gets display information on Wayland using a different method.
|
||||
pub fn main_get_displays() -> SyncReturn<String> {
|
||||
#[cfg(target_os = "ios")]
|
||||
let display_info = "".to_owned();
|
||||
|
||||
@@ -304,6 +304,12 @@ pub(super) fn get_display_info(idx: usize) -> Option<DisplayInfo> {
|
||||
// Display to DisplayInfo
|
||||
// The DisplayInfo is be sent to the peer.
|
||||
pub(super) fn check_update_displays(all: &Vec<Display>) {
|
||||
// For compatibility: if only one display, scale remains 1.0 and we use the physical size for `uinput`.
|
||||
// If there are multiple displays, we use the logical size for `uinput` by setting scale to d.scale().
|
||||
#[cfg(target_os = "linux")]
|
||||
let use_logical_scale = !is_x11()
|
||||
&& crate::is_server()
|
||||
&& scrap::wayland::display::get_displays().displays.len() > 1;
|
||||
let displays = all
|
||||
.iter()
|
||||
.map(|d| {
|
||||
@@ -315,6 +321,12 @@ pub(super) fn check_update_displays(all: &Vec<Display>) {
|
||||
{
|
||||
scale = d.scale();
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if use_logical_scale {
|
||||
scale = d.scale();
|
||||
}
|
||||
}
|
||||
let original_resolution = get_original_resolution(
|
||||
&display_name,
|
||||
((d.width() as f64) / scale).round() as usize,
|
||||
|
||||
@@ -20,7 +20,10 @@ use scrap::wayland::pipewire::RDP_SESSION_INFO;
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
ops::{Deref, DerefMut},
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc,
|
||||
},
|
||||
thread,
|
||||
time::{self, Duration, Instant},
|
||||
};
|
||||
@@ -1834,6 +1837,51 @@ pub fn wayland_use_rdp_input() -> bool {
|
||||
!crate::platform::is_x11() && !crate::is_server()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub struct TemporaryMouseMoveHandle {
|
||||
thread_handle: Option<std::thread::JoinHandle<()>>,
|
||||
tx: Option<mpsc::Sender<(i32, i32)>>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl TemporaryMouseMoveHandle {
|
||||
pub fn new() -> Self {
|
||||
let (tx, rx) = mpsc::channel::<(i32, i32)>();
|
||||
let thread_handle = std::thread::spawn(move || {
|
||||
log::debug!("TemporaryMouseMoveHandle thread started");
|
||||
for (x, y) in rx {
|
||||
ENIGO.lock().unwrap().mouse_move_to(x, y);
|
||||
}
|
||||
log::debug!("TemporaryMouseMoveHandle thread exiting");
|
||||
});
|
||||
TemporaryMouseMoveHandle {
|
||||
thread_handle: Some(thread_handle),
|
||||
tx: Some(tx),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_mouse_to(&self, x: i32, y: i32) {
|
||||
if let Some(tx) = &self.tx {
|
||||
let _ = tx.send((x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl Drop for TemporaryMouseMoveHandle {
|
||||
fn drop(&mut self) {
|
||||
log::debug!("Dropping TemporaryMouseMoveHandle");
|
||||
// Close the channel to signal the thread to exit.
|
||||
self.tx.take();
|
||||
// Wait for the thread to finish.
|
||||
if let Some(thread_handle) = self.thread_handle.take() {
|
||||
if let Err(e) = thread_handle.join() {
|
||||
log::error!("Error joining TemporaryMouseMoveHandle thread: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref MODIFIER_MAP: HashMap<i32, Key> = [
|
||||
(ControlKey::Alt, Key::Alt),
|
||||
|
||||
@@ -71,6 +71,7 @@ pub mod client {
|
||||
stream: PwStreamInfo,
|
||||
resolution: (usize, usize),
|
||||
scale: Option<f64>,
|
||||
position: (f64, f64),
|
||||
}
|
||||
|
||||
impl RdpInputMouse {
|
||||
@@ -98,12 +99,14 @@ pub mod client {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let pos = stream.get_position();
|
||||
Ok(Self {
|
||||
conn,
|
||||
session,
|
||||
stream,
|
||||
resolution,
|
||||
scale,
|
||||
position: (pos.0 as f64, pos.1 as f64),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -128,6 +131,8 @@ pub mod client {
|
||||
} else {
|
||||
y as f64
|
||||
};
|
||||
let x = x - self.position.0;
|
||||
let y = y - self.position.1;
|
||||
let portal = get_portal(&self.conn);
|
||||
let _ = remote_desktop_portal::notify_pointer_motion_absolute(
|
||||
&portal,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use super::*;
|
||||
use hbb_common::{
|
||||
allow_err,
|
||||
platform::linux::{CMD_SH, DISTRO},
|
||||
use hbb_common::{allow_err, anyhow, platform::linux::DISTRO};
|
||||
use scrap::{
|
||||
is_cursor_embedded, set_map_err,
|
||||
wayland::pipewire::{fill_displays, try_fix_logical_size},
|
||||
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::process::{Command, Output};
|
||||
|
||||
use crate::{
|
||||
client::{
|
||||
@@ -127,45 +127,28 @@ pub(super) fn is_inited() -> Option<Message> {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_max_desktop_resolution() -> Option<String> {
|
||||
// works with Xwayland
|
||||
let output: Output = Command::new(CMD_SH.as_str())
|
||||
.arg("-c")
|
||||
.arg("xrandr | awk '/current/ { print $8,$9,$10 }'")
|
||||
.output()
|
||||
.ok()?;
|
||||
|
||||
if output.status.success() {
|
||||
let result = String::from_utf8_lossy(&output.stdout);
|
||||
Some(result.trim().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
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<()> {
|
||||
if !is_x11() {
|
||||
let mut minx = 0;
|
||||
let mut maxx = 0;
|
||||
let mut miny = 0;
|
||||
let mut maxy = 0;
|
||||
let use_uinput = crate::input_service::wayland_use_uinput();
|
||||
|
||||
if CAP_DISPLAY_INFO.read().unwrap().is_empty() {
|
||||
if crate::input_service::wayland_use_uinput() {
|
||||
if let Some((minx, maxx, miny, maxy)) =
|
||||
scrap::wayland::display::get_desktop_rect_for_uinput()
|
||||
{
|
||||
log::info!(
|
||||
"update mouse resolution: ({}, {}), ({}, {})",
|
||||
minx,
|
||||
maxx,
|
||||
miny,
|
||||
maxy
|
||||
);
|
||||
allow_err!(
|
||||
input_service::update_mouse_resolution(minx, maxx, miny, maxy).await
|
||||
);
|
||||
} else {
|
||||
log::warn!("Failed to get desktop rect for uinput");
|
||||
}
|
||||
}
|
||||
|
||||
let mut lock = CAP_DISPLAY_INFO.write().unwrap();
|
||||
if lock.is_empty() {
|
||||
// Check if PipeWire is already initialized to prevent duplicate recorder creation
|
||||
@@ -173,8 +156,16 @@ pub(super) async fn check_init() -> ResultType<()> {
|
||||
log::warn!("wayland_diag: Preventing duplicate PipeWire initialization");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let all = Display::all()?;
|
||||
|
||||
let mut all = Display::all()?;
|
||||
log::debug!("Initializing displays with fill_displays()");
|
||||
{
|
||||
let temp_mouse_move_handle = input_service::TemporaryMouseMoveHandle::new();
|
||||
let move_mouse_to = |x, y| temp_mouse_move_handle.move_mouse_to(x, y);
|
||||
fill_displays(move_mouse_to, crate::get_cursor_pos, &mut all)?;
|
||||
}
|
||||
log::debug!("Attempting to fix logical size with try_fix_logical_size()");
|
||||
try_fix_logical_size(&mut all);
|
||||
*PIPEWIRE_INITIALIZED.write().unwrap() = true;
|
||||
let num = all.len();
|
||||
let primary = super::display_service::get_primary_2(&all);
|
||||
@@ -189,40 +180,23 @@ pub(super) async fn check_init() -> ResultType<()> {
|
||||
rects.push((d.origin(), d.width(), d.height()));
|
||||
}
|
||||
|
||||
log::debug!("#displays={}, primary={}, rects: {:?}, cpus={}/{}", num, primary, rects, num_cpus::get_physical(), num_cpus::get());
|
||||
|
||||
if use_uinput {
|
||||
let (max_width, max_height) = match get_max_desktop_resolution() {
|
||||
Some(result) if !result.is_empty() => {
|
||||
let resolution: Vec<&str> = result.split(" ").collect();
|
||||
if let (Ok(w), Ok(h)) = (
|
||||
resolution[0].parse::<i32>(),
|
||||
resolution.get(2)
|
||||
.unwrap_or(&"0")
|
||||
.trim_end_matches(",")
|
||||
.parse::<i32>()
|
||||
) {
|
||||
(w, h)
|
||||
} else {
|
||||
calculate_max_resolution_from_displays(&all)
|
||||
}
|
||||
}
|
||||
_ => calculate_max_resolution_from_displays(&all),
|
||||
};
|
||||
|
||||
minx = 0;
|
||||
maxx = max_width;
|
||||
miny = 0;
|
||||
maxy = max_height;
|
||||
}
|
||||
log::debug!(
|
||||
"#displays={}, primary={}, rects: {:?}, cpus={}/{}",
|
||||
num,
|
||||
primary,
|
||||
rects,
|
||||
num_cpus::get_physical(),
|
||||
num_cpus::get()
|
||||
);
|
||||
|
||||
// Create individual CapDisplayInfo for each display with its own capturer
|
||||
for (idx, display) in all.into_iter().enumerate() {
|
||||
let capturer = Box::into_raw(Box::new(
|
||||
Capturer::new(display).with_context(|| format!("Failed to create capturer for display {}", idx))?,
|
||||
));
|
||||
let capturer =
|
||||
Box::into_raw(Box::new(Capturer::new(display).with_context(|| {
|
||||
format!("Failed to create capturer for display {}", idx)
|
||||
})?));
|
||||
let capturer = CapturerPtr(capturer);
|
||||
|
||||
|
||||
let cap_display_info = Box::into_raw(Box::new(CapDisplayInfo {
|
||||
rects: rects.clone(),
|
||||
displays: displays.clone(),
|
||||
@@ -231,24 +205,11 @@ pub(super) async fn check_init() -> ResultType<()> {
|
||||
current: idx,
|
||||
capturer,
|
||||
}));
|
||||
|
||||
|
||||
lock.insert(idx, cap_display_info as u64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if use_uinput {
|
||||
if minx != maxx && miny != maxy {
|
||||
log::info!(
|
||||
"update mouse resolution: ({}, {}), ({}, {})",
|
||||
minx,
|
||||
maxx,
|
||||
miny,
|
||||
maxy
|
||||
);
|
||||
allow_err!(input_service::update_mouse_resolution(minx, maxx, miny, maxy).await);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -293,12 +254,14 @@ pub fn clear() {
|
||||
}
|
||||
}
|
||||
write_lock.clear();
|
||||
|
||||
|
||||
// Reset PipeWire initialization flag to allow recreation on next init
|
||||
*PIPEWIRE_INITIALIZED.write().unwrap() = false;
|
||||
}
|
||||
|
||||
pub(super) fn get_capturer_for_display(display_idx: usize) -> ResultType<super::video_service::CapturerInfo> {
|
||||
pub(super) fn get_capturer_for_display(
|
||||
display_idx: usize,
|
||||
) -> ResultType<super::video_service::CapturerInfo> {
|
||||
if is_x11() {
|
||||
bail!("Do not call this function if not wayland");
|
||||
}
|
||||
@@ -307,7 +270,7 @@ pub(super) fn get_capturer_for_display(display_idx: usize) -> ResultType<super::
|
||||
let cap_display_info: *const CapDisplayInfo = *addr as _;
|
||||
unsafe {
|
||||
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 {
|
||||
origin: rect.0,
|
||||
width: rect.1,
|
||||
@@ -320,7 +283,10 @@ pub(super) fn get_capturer_for_display(display_idx: usize) -> ResultType<super::
|
||||
})
|
||||
}
|
||||
} else {
|
||||
bail!("Failed to get capturer display info for display {}", display_idx);
|
||||
bail!(
|
||||
"Failed to get capturer display info for display {}",
|
||||
display_idx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ function isEnterKey(evt) {
|
||||
|
||||
function getScaleFactor() {
|
||||
if (!is_win) return 1;
|
||||
return self.toPixels(10000dip) / 10000.;
|
||||
var s = self.toPixels(10000dip) / 10000.;
|
||||
return s < 0.000001 ? 1 : s;
|
||||
}
|
||||
var scaleFactor = getScaleFactor();
|
||||
view << event resolutionchange {
|
||||
|
||||
@@ -125,8 +125,9 @@ impl InvokeUiSession for SciterHandler {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embedded: bool) {
|
||||
self.call("setDisplay", &make_args!(x, y, w, h, cursor_embedded));
|
||||
fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embedded: bool, scale: f64) {
|
||||
let scale = if scale <= 0.0 { 1.0 } else { scale };
|
||||
self.call("setDisplay", &make_args!(x, y, w, h, cursor_embedded, scale));
|
||||
// https://sciter.com/forums/topic/color_spaceiyuv-crash
|
||||
// Nothing spectacular in decoder – done on CPU side.
|
||||
// So if you can do BGRA translation on your side – the better.
|
||||
|
||||
@@ -4,10 +4,13 @@ var is_port_forward = handler.is_port_forward();
|
||||
var input_blocked = false;
|
||||
var display_width = 0;
|
||||
var display_height = 0;
|
||||
var display_remote_scale = 1;
|
||||
var display_origin_x = 0;
|
||||
var display_origin_y = 0;
|
||||
var display_cursor_embedded = false;
|
||||
var display_scale = 1;
|
||||
// the scale factor is different from `display_scale` if peer platform is Linux (Wayland).
|
||||
var cursor_scale = 1;
|
||||
var keyboard_enabled = true; // server side
|
||||
var clipboard_enabled = true; // server side
|
||||
var audio_enabled = true; // server side
|
||||
@@ -15,13 +18,15 @@ var file_enabled = true; // server side
|
||||
var restart_enabled = true; // server side
|
||||
var recording_enabled = true; // server side
|
||||
var scroll_body = $(body);
|
||||
var peer_platform = "";
|
||||
|
||||
handler.setDisplay = function(x, y, w, h, cursor_embedded) {
|
||||
handler.setDisplay = function(x, y, w, h, cursor_embedded, scale) {
|
||||
display_width = w;
|
||||
display_height = h;
|
||||
display_origin_x = x;
|
||||
display_origin_y = y;
|
||||
display_cursor_embedded = cursor_embedded;
|
||||
display_remote_scale = scale;
|
||||
adaptDisplay();
|
||||
if (recording) handler.record_screen(true, 0, w, h);
|
||||
}
|
||||
@@ -29,12 +34,24 @@ handler.setDisplay = function(x, y, w, h, cursor_embedded) {
|
||||
// in case toolbar not shown correctly
|
||||
view.windowMinSize = (scaleIt(500), scaleIt(300));
|
||||
|
||||
function get_peer_platform() {
|
||||
if (peer_platform == "") {
|
||||
peer_platform = handler.peer_platform();
|
||||
}
|
||||
return peer_platform;
|
||||
}
|
||||
|
||||
function isRemoteLinux() {
|
||||
return get_peer_platform() == "Linux";
|
||||
}
|
||||
|
||||
function adaptDisplay() {
|
||||
var w = display_width;
|
||||
var h = display_height;
|
||||
if (!w || !h) return;
|
||||
var style = handler.get_view_style();
|
||||
display_scale = 1.;
|
||||
cursor_scale = 1.;
|
||||
var (sx, sy, sw, sh) = view.screenBox(view.windowState == View.WINDOW_FULL_SCREEN ? #frame : #workarea, #rectw);
|
||||
if (sw >= w && sh > h) {
|
||||
var hh = $(header).box(#height, #border);
|
||||
@@ -71,6 +88,10 @@ function adaptDisplay() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isRemoteLinux()) {
|
||||
cursor_scale = display_scale * display_remote_scale;
|
||||
if (cursor_scale <= 0.0001) cursor_scale = 1.;
|
||||
}
|
||||
refreshCursor();
|
||||
handler.style.set {
|
||||
width: w / scaleFactor + "px",
|
||||
@@ -279,7 +300,7 @@ function handler.onMouse(evt)
|
||||
entered = false;
|
||||
stdout.println("leave");
|
||||
handler.leave(handler.get_keyboard_mode());
|
||||
if (is_left_down && handler.peer_platform() == "Android") {
|
||||
if (is_left_down && get_peer_platform() == "Android") {
|
||||
is_left_down = false;
|
||||
handler.send_mouse((1 << 3) | 2, 0, 0, evt.altKey,
|
||||
evt.ctrlKey, evt.shiftKey, evt.commandKey);
|
||||
@@ -303,8 +324,8 @@ function handler.onMouse(evt)
|
||||
resetWheel();
|
||||
}
|
||||
if (!keyboard_enabled) return false;
|
||||
x = (x / display_scale).toInteger();
|
||||
y = (y / display_scale).toInteger();
|
||||
x = (x / cursor_scale).toInteger();
|
||||
y = (y / cursor_scale).toInteger();
|
||||
// insert down between two up, osx has this behavior for triple click
|
||||
if (last_mouse_mask == 2 && mask == 2) {
|
||||
handler.send_mouse((evt.buttons << 3) | 1, 0, 0, evt.altKey,
|
||||
@@ -339,14 +360,18 @@ var cursors = {};
|
||||
var image_binded;
|
||||
|
||||
function scaleCursorImage(img) {
|
||||
var w = (img.width * display_scale).toInteger();
|
||||
var h = (img.height * display_scale).toInteger();
|
||||
var factor = cursor_scale;
|
||||
if (cursor_img.style#display != 'none') {
|
||||
factor /= scaleFactor;
|
||||
}
|
||||
var w = (img.width * factor).toInteger();
|
||||
var h = (img.height * factor).toInteger();
|
||||
cursor_img.style.set {
|
||||
width: w + "px",
|
||||
height: h + "px",
|
||||
};
|
||||
self.bindImage("in-memory:cursor", img);
|
||||
if (display_scale == 1) return img;
|
||||
if (factor == 1) return img;
|
||||
function paint(gfx) {
|
||||
gfx.drawImage(img, 0, 0, w, h);
|
||||
}
|
||||
@@ -360,7 +385,7 @@ function updateCursor(system=false) {
|
||||
if (system) {
|
||||
handler.style#cursor = undefined;
|
||||
} else if (cur_img) {
|
||||
handler.style.cursor(cur_img, (cur_hotx * display_scale).toInteger(), (cur_hoty * display_scale).toInteger());
|
||||
handler.style.cursor(cur_img, (cur_hotx * cursor_scale).toInteger(), (cur_hoty * cursor_scale).toInteger());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,14 +438,15 @@ handler.setCursorPosition = function(x, y) {
|
||||
cur_y = y - display_origin_y;
|
||||
var x = cur_x - cur_hotx;
|
||||
var y = cur_y - cur_hoty;
|
||||
x *= display_scale / scaleFactor;
|
||||
y *= display_scale / scaleFactor;
|
||||
x *= cursor_scale / scaleFactor;
|
||||
y *= cursor_scale / scaleFactor;
|
||||
cursor_img.style.set {
|
||||
left: x + "px",
|
||||
top: y + "px",
|
||||
};
|
||||
if (cursor_img.style#display == 'none') {
|
||||
cursor_img.style#display = "block";
|
||||
refreshCursor();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1658,7 +1658,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default {
|
||||
fn set_cursor_data(&self, cd: CursorData);
|
||||
fn set_cursor_id(&self, id: String);
|
||||
fn set_cursor_position(&self, cp: CursorPosition);
|
||||
fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embedded: bool);
|
||||
fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embedded: bool, scale: f64);
|
||||
fn switch_display(&self, display: &SwitchDisplay);
|
||||
fn set_peer_info(&self, peer_info: &PeerInfo); // flutter
|
||||
fn set_displays(&self, displays: &Vec<DisplayInfo>);
|
||||
@@ -1804,6 +1804,7 @@ impl<T: InvokeUiSession> Interface for Session<T> {
|
||||
current.width,
|
||||
current.height,
|
||||
current.cursor_embedded,
|
||||
current.scale,
|
||||
);
|
||||
}
|
||||
self.update_privacy_mode();
|
||||
|
||||
Reference in New Issue
Block a user