Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ viewer does not reconnect them automatically.
| `~/.config/uuremote/config.json` | user_id, device_id, client_id, system_id, machine_guid, token, csrf; optional `sign_key`, `ffmpeg_root`. `$UU_CONFIG` overrides the path |
| `~/.config/uuremote/host_settings.json` | persisted 安全 settings: auto_lock, keep_mute, mute_permanent; optional `video_encoder` (`auto`/`cpu`/`vaapi`/`nvenc`) and `video_codec` (`auto`/`h264`/`h265`), written by `set-encoder` / `set-codec` (0600, merge write). Non-`auto` beats `UU_VIDEO_ENCODER` / `UU_VIDEO_CODEC`. `set-encoder` and `set-codec` both send `SIGUSR2` to the pid in `host.pid` (same binary). Live desktop streams restart the encoder (`EncoderTask::restart`, sequence kept, first frame IDR) without dropping the peer. Codec preference is logged and applies on the next desktop stream. `SIGUSR2` does not re-probe. Hand edits: `kill -USR2 $(cat ~/.config/uuremote/host.pid)` |
| `UU_DESKTOP_BACKEND` | `auto` (default) / `x11` / `wayland` / `portal` / `kms` (experimental feature) / `none` (headless: no display; desktop offers ignored; tool peers get a nominal 1920x1080 screen + Allow permissions, which the port-mapping viewer requires) |
| `UU_X11_MULTISCREEN=1`, `UU_X11_OUTPUT` | X11: advertise each RandR monitor (`xrandr --listmonitors`, read once at start; primary or `UU_X11_OUTPUT` is id 0) through the same screen-switch path as `UU_WAYLAND_MULTISCREEN` (`wayland::outputs`/`select`). x11grab reads `display+x,y` at the monitor size, XTest/uinput absolute input adds the monitor offset, the in-process engine and `desktop_mode` resize are skipped (分辨率 scales). Needs two or more monitors |
| `UU_DESKTOP_RESIZE=0` | X11: the viewer's 分辨率 choice scales the encoded stream instead of changing the display mode. Default: `src/desktop_mode.rs` switches the primary output via xrandr (adds a CVT-RB mode when missing, re-applies once if mutter resets it), restores the original mode for resolution_type 3 and 2 s after the last desktop stream ends |
| `UU_NVENC_RGB=0` / `=1` | NVENC 4:2:0 without scaling takes packed BGRA/BGR0 directly (no CPU swscale). Default: on, except when a connected DRM connector belongs to an nvidia-bound card (desktop composited on the encoding GPU; #16 measured 82.6 vs 98 fps at 4K). `0` forces CPU conversion, `1` forces the direct path |
| `UU_INPUT_BACKEND` | `auto` (uinput on Wayland/portal when `/dev/uinput` is accessible, udev `uaccess` rule in README) / `uinput` / `native`. X11 keeps XTest (held-key autorepeat handling) |
Expand Down
8 changes: 6 additions & 2 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,19 +104,21 @@ failure falls back to SHM. The virgl VM verified fallback only: successful impor
mapping and hardware acceleration remain unverified. This is not zero-copy VAAPI;
`UU_VIDEO_ENCODER=vaapi` still uploads CPU frames through FFmpeg.

### Hyprland monitor switching (experimental)
### Monitor switching (experimental)

Native Wayland absolute pointer motion now targets the captured output by default, fixing multi-monitor click offsets. uinput may still handle keyboard, buttons and relative motion.

Set `UU_WAYLAND_MULTISCREEN=1` to select physical monitors from the phone client. `UU_WAYLAND_OUTPUT` chooses the initial output. Capture and absolute pointer targeting switch together across different resolutions, scales and desktop positions.

X11 sessions send all monitors as one combined picture by default. With `UU_X11_MULTISCREEN=1` the host lists the monitors from `xrandr --listmonitors`; the viewer selects one, only that region is captured, and absolute pointer positions are offset to that monitor. The primary monitor is the default; `UU_X11_OUTPUT` picks another (e.g. `HDMI-1`). The layout is read when the host starts; restart the service after changing it. In this mode the viewer's resolution setting scales the stream instead of changing the display mode. Experimental: unit tests and Xvfb virtual monitors only, not yet accepted with the official clients.

Switching reuses one video track and pauses input until the new source sends a frame. Capture/encoder failure or a first-frame timeout requests restoration of the previous source. The inventory uses `hyprctl` and supports unrotated outputs; restart the host after monitor hotplug or layout changes. Phone-side switching and clicks were tested on Omarchy / Hyprland with 2560×1440 at 125% and 1920×1080 at 133.33%.

## Not implemented / not supported

| Feature | Status |
|---|---|
| Switching between multiple monitors | Experimental on native Hyprland Wayland; see above |
| Switching between multiple monitors | Experimental on native Hyprland Wayland and on X11 (`UU_X11_MULTISCREEN=1`); see above |
| True color (4:4:4) | Supported (v0.1.15): the default FFmpeg pipeline with NVENC / libx264 encodes H.264 High 4:4:4; `inproc-av` native NVENC encodes H.264 High 4:4:4 / HEVC FRExt; VAAPI stays 4:2:0 and reports that to the client |
| HDR | Not implemented |
| Super screen / virtual display | Not implemented |
Expand Down Expand Up @@ -339,6 +341,8 @@ Common environment variables:
| `UU_NVENC_RGB=0` / `=1` | NVENC encodes BGRA directly (saves CPU). Off by default when a monitor is connected to the NVIDIA card (slower when the desktop is composited on the same GPU); `0` forces off, `1` forces on |
| `UU_WAYLAND_MULTISCREEN=1` | Enable Hyprland physical monitor switching (off by default) |
| `UU_WAYLAND_OUTPUT` | Initial native Wayland output name, e.g. `HDMI-A-1` |
| `UU_X11_MULTISCREEN=1` | Select and capture X11 monitors separately (experimental, off by default, needs `xrandr`) |
| `UU_X11_OUTPUT` | Initial X11 monitor name (`xrandr --listmonitors`); default: the primary |
| `UU_WAYLAND_NO_VIRTUAL_KEYBOARD=1` | Native Wayland skips the virtual keyboard (e.g. Denial reserves it for its input method); keys go through uinput. Without wlr-virtual-pointer the pointer also uses uinput (#72) |
| `UU_INPUT_BACKEND` | `auto` (default: uinput on Wayland when accessible) / `uinput` / `native` |
| `UU_TERMINAL_SHORTCUTS=0` | Disable the Ctrl+V paste rewrite in terminal windows |
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,19 +90,21 @@ GNOME Portal 的控制通道不再等待授权窗口或第一帧画面:先通
原生 Wayland 实验选项:`UU_WAYLAND_PIPELINE=0` 可关闭流水线做对照。`UU_WAYLAND_DMABUF=1` 显式尝试 dma-buf→GBM CPU 映射(默认关闭,需要运行时 libgbm;可用 `UU_WAYLAND_DMABUF_DEVICE` 指定 render node,默认 `/dev/dri/renderD128`)。仅支持显式线性修饰符、单平面 8-bit RGB,失败回退 SHM;virgl VM 中只验证了回退,成功导入/映射和硬件加速均未验证。这不是零拷贝 VAAPI 路径;选择 `UU_VIDEO_ENCODER=vaapi` 时仍由 FFmpeg 上传 CPU 帧。
首次连接时,桌面会弹窗请求「远程桌面 / 屏幕共享」授权,同意后会记住授权。

### Hyprland 多显示器切换(实验性)
### 多显示器切换(实验性)

原生 Wayland 的绝对鼠标定位默认绑定正在采集的输出,修复双屏桌面下的点击偏移。uinput 可继续用于键盘、按键和相对移动。

设置 `UU_WAYLAND_MULTISCREEN=1` 后,手机端可选择并切换物理显示器;`UU_WAYLAND_OUTPUT` 指定默认屏幕。画面与绝对鼠标定位同步切换,支持不同分辨率、缩放和桌面位置。

X11 会话默认把所有显示器拼成一张画面。设置 `UU_X11_MULTISCREEN=1` 后,主机按 `xrandr --listmonitors` 列出各个显示器,控制端可选择其中一块,只采集该区域,绝对鼠标坐标加上该屏幕在桌面中的偏移;默认屏幕为主显示器,`UU_X11_OUTPUT` 可指定其他屏幕(如 `HDMI-1`)。显示器布局在主机启动时读取,改动后需重启服务。此模式下控制端的「分辨率」改为缩放画面,不再修改显示模式。实验性:目前只有单元测试与 Xvfb 虚拟显示器实测,尚未经官方客户端实机验收。

切屏复用同一条视频轨道,过渡期间暂停输入,发送新屏首帧后恢复;采集或编码失败、等待首帧超时会请求恢复原屏。当前通过 `hyprctl` 枚举未旋转的显示器,显示器插拔或布局变化后需重启服务。已在 Omarchy / Hyprland 上用官方手机客户端实测 2560×1440(125%)与 1920×1080(133.33%)双屏切换及点击。

## 尚未实现 / 不支持

| 功能 | 状态 |
|---|---|
| 多显示器切换 | Hyprland 原生 Wayland 后端实验性支持,见上文 |
| 多显示器切换 | Hyprland 原生 Wayland 后端与 X11(`UU_X11_MULTISCREEN=1`)实验性支持,见上文 |
| 真彩(4:4:4) | 支持(v0.1.15):NVENC / libx264 的默认 FFmpeg 管线输出 H.264 High 4:4:4;`inproc-av` 原生 NVENC 输出 H.264 High 4:4:4 / HEVC FRExt;VAAPI 管线仍为 4:2:0,并如实回报给客户端 |
| HDR | 未实现 |
| 超级屏 / 虚拟显示器 | 未实现 |
Expand Down Expand Up @@ -307,6 +309,8 @@ systemctl --user restart uuremote-native.service
| `UU_NVENC_RGB=0` / `=1` | NVENC 直接编码 BGRA(省 CPU)。显示器接在 NVIDIA 卡上时默认关闭(同卡合成时直吃反而更慢),`0` 强制关,`1` 强制开 |
| `UU_WAYLAND_MULTISCREEN=1` | 启用 Hyprland 物理显示器切换(默认关闭) |
| `UU_WAYLAND_OUTPUT` | 原生 Wayland 默认采集输出名称,例如 `HDMI-A-1` |
| `UU_X11_MULTISCREEN=1` | X11 下按显示器分别选择和采集(实验性,默认关闭,需要 `xrandr`) |
| `UU_X11_OUTPUT` | X11 默认采集的显示器名称(`xrandr --listmonitors`),默认主显示器 |
| `UU_WAYLAND_NO_VIRTUAL_KEYBOARD=1` | 原生 Wayland 不创建虚拟键盘(如 Denial 只把它给自己的输入法),键盘改走 uinput。合成器没有 wlr-virtual-pointer 时鼠标也自动改走 uinput(#72) |
| `UU_INPUT_BACKEND` | `auto`(默认,Wayland 下可用时用 uinput)/ `uinput` / `native` |
| `UU_TERMINAL_SHORTCUTS=0` | 关闭终端窗口里的 Ctrl+V 粘贴转换 |
Expand Down
6 changes: 6 additions & 0 deletions src/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ pub fn x11_size() -> Option<(u32, u32)> {
}

/// The root size from the last refresh, without an X round trip (pointer mapping).
/// Display the X11 engine opened, once a capture has initialized it.
pub fn x11_display() -> Option<String> {
let name = unsafe { x11_display_name() };
if name.is_null() { None } else { Some(unsafe { std::ffi::CStr::from_ptr(name) }.to_string_lossy().into_owned()) }
}

pub fn x11_last_size() -> Option<(u32, u32)> {
let (mut w, mut h): (libc::c_int, libc::c_int) = (0, 0);
if unsafe { x11_get_dimensions(&mut w, &mut h) } != 0 || w <= 0 || h <= 0 { return None; }
Expand Down
30 changes: 29 additions & 1 deletion src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,9 +334,12 @@ impl InputInjector {
let result=crate::wayland::move_pointer(x,y,false);
if result!=-2 { return result; }
}
// X11 multi-screen: the point is inside the selected monitor; XTest and uinput span the root.
let placement=if relative || self.backend!=crate::desktop_backend::DesktopBackend::X11 {None} else {crate::wayland::x11_placement()};
let (x,y)=placement.map_or((x,y),|(ox,oy)|(x+ox,y+oy));
if let Some(uinput)=&self.uinput {
if relative { return Self::uinput_result(uinput.move_relative(x,y)); }
let (w,h)=*self.screen.lock().unwrap();
let (w,h)=placement.and_then(|_|crate::capture::x11_last_size()).unwrap_or(*self.screen.lock().unwrap());
if w > 1 && h > 1 {
let scale = |v: i32, extent: u32| (i64::from(v) * i64::from(crate::uinput::ABS_MAX) / i64::from(extent - 1)) as i32;
return Self::uinput_result(uinput.move_absolute(scale(x,w),scale(y,h)));
Expand Down Expand Up @@ -700,3 +703,28 @@ mod output_pointer_tests {
}
}
}

#[cfg(all(test,target_os="linux"))]
mod x11_monitor_pointer_tests {
use super::*;
#[test]
#[ignore="requires an X11 display with two or more RandR monitors and UU_X11_MULTISCREEN=1; moves pointer only"]
fn absolute_pointer_lands_on_the_selected_x11_monitor() {
let _capture=crate::capture::ScreenCapture::new().unwrap();
let input=InputInjector::new();
assert_eq!(input.backend,crate::desktop_backend::DesktopBackend::X11);
let outputs=crate::wayland::outputs();assert!(crate::wayland::x11_multiscreen() && outputs.len()>=2,"needs two X11 monitors");
for (id,output) in outputs.iter().enumerate().chain(outputs.iter().enumerate().rev()) {
crate::wayland::select(id).unwrap();
for (x,y) in [(0.0,0.0),(0.5,0.5),(0.999,0.999),(0.1,0.9)] {
let size=(output.width,output.height);
let pixels=crate::input_mapping::normalized_pointer(x,y,size.0,size.1).unwrap();
input.mouse_absolute_normalized(x,y,size);input.flush_pointer();
let (mut ax,mut ay,mut mask)=(0,0,0);
assert_eq!(unsafe {x11_pointer_state(&mut ax,&mut ay,&mut mask)},0);
assert_eq!((ax,ay),(output.x+pixels.0,output.y+pixels.1),"monitor={} normalized=({x},{y})",output.name);
}
}
crate::wayland::select(0).unwrap();
}
}
9 changes: 6 additions & 3 deletions src/video.rs
Original file line number Diff line number Diff line change
Expand Up @@ -814,18 +814,20 @@ async fn read_encoder_with(
settings: VideoSettings, #[cfg_attr(not(feature = "inproc-av"), allow(unused_variables))] idr: Arc<AtomicBool>, budget: Arc<Semaphore>,
selection: crate::video_select::Selection,
)->Result<(),String>{
let selected_output=if display.starts_with("wayland:") {crate::wayland::source(settings.source_id)} else {None};
let selected_output=crate::wayland::capture_source(&display,portal.is_some(),settings.source_id);
let size=selected_output.map_or(size,|s|(s.width,s.height));
let kms=display.starts_with("kms:");
let wayland=kms || display.starts_with("wayland:");
// X11 multi-screen: x11grab reads only the selected monitor's region of the root.
let x11_region=selected_output.filter(|_|!wayland);
let (permits_tx,mut permits_rx)=mpsc::channel::<OwnedSemaphorePermit>(4);
let kms_capture=if kms {
let capture=tokio::task::spawn_blocking(crate::kms::Capture::open).await.map_err(|_|"KMS worker failed")??;
if (capture.width,capture.height)!=size { return Err("KMS mode changed; restart host to refresh desktop geometry".into()); }
Some(Arc::new(capture))
} else { None };
#[cfg(feature = "inproc-av")]
if allow_inproc && native_inproc(&display, portal.is_some(), encoder) {
if allow_inproc && x11_region.is_none() && native_inproc(&display, portal.is_some(), encoder) {
let max_kbps = settings.max_kbps().unwrap_or(30_000) as i64;
let config = crate::inproc_encoder::Config {
display: display.clone(), input: size, output: settings.output, fps: u32::from(settings.fps),
Expand Down Expand Up @@ -859,8 +861,9 @@ async fn read_encoder_with(
"-framerate",&fps,"-i","pipe:0","-an"]);
} else {
// The viewer draws its local pointer; do not bake a second into video.
let input=x11_region.map_or_else(||display.clone(),|o|format!("{display}+{},{}",o.x,o.y));
command.args(["-f","x11grab","-draw_mouse","0","-framerate",&fps,
"-video_size",&size,"-i",&display,"-an"]);
"-video_size",&size,"-i",&input,"-an"]);
}
// 其他分辨率: scale the captured desktop (the host display mode is left untouched).
command.args(scale_options(&settings, encoder, Some(source_size)));
Expand Down
Loading
Loading