diff --git a/Cargo.lock b/Cargo.lock index b9847e5e..ffd42b48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,6 +103,7 @@ dependencies = [ "infer", "jni 0.21.1", "jwalk", + "libc", "md5", "memmap2", "mimalloc", @@ -143,6 +144,7 @@ dependencies = [ "webview2-com", "windows 0.62.2", "ws-protocol", + "x11rb", "zip 8.6.0", ] diff --git a/README-CN.md b/README-CN.md index 3959c94f..526aa818 100644 --- a/README-CN.md +++ b/README-CN.md @@ -39,6 +39,31 @@ pnpm tauri build # 发行构建 pnpm tauri dev # 开发模式 ``` +### Linux 显示后端 + +首次在可使用包装层的 NVIDIA Wayland 会话中启动时,AMLL Player 会询问是否切换到嵌套的 `Xephyr + Openbox` WebView 会话。Xephyr 会作为普通应用窗口交给桌面环境管理,Openbox 则负责内层 X11 WebView,从而绕过 WebKitGTK 显式同步崩溃,并恢复宿主侧的缩放、最大化、全屏、聚焦和关闭行为。 + +在 Arch Linux 上安装包装层依赖: + +```bash +sudo pacman -S openbox xorg-server-xephyr +``` + +如果缺少任一依赖或包装层启动失败,AMLL Player 会自动回退到现有的 XWayland 显示。Xephyr 不会向嵌套客户端提供 DRI3,因此包装层会增加一次 CPU 拷贝/合成;WebKitGTK 自身仍可使用 GPU 渲染。直接 `x11` 回退可避免这层嵌套服务器开销。 + +包装层默认让 Xephyr 使用 60 FPS,避免不必要的嵌套合成开销。需要高刷新率时可设置不高于宿主刷新率的 `AMLL_XEPHYR_FPS`,例如 `AMLL_XEPHYR_FPS=120`;WebKitGTK 在嵌套 X11 会话中仍可能把动画限制在约 60 FPS,因此不保证应用渲染帧率能够穿透。 + +可以通过 `AMLL_LINUX_WEBVIEW_BACKEND` 覆盖自动选择: + +- `auto`:NVIDIA Wayland 环境依次尝试 Openbox 包装、直接 XWayland 和软件 Wayland +- `system`:保留桌面环境和 `GDK_BACKEND` 选择的后端 +- `openbox`:请求 Xephyr + Openbox 包装,并保留相同的自动回退 +- `x11`:优先使用 X11;没有可用 X 显示时回退到软件 Wayland +- `wayland`:强制使用原生 Wayland 渲染 +- `wayland-software`:强制使用 Wayland,并禁用 WebKitGTK DMA-BUF 渲染器 + +首次选择会保存到 `~/.config/net.stevexmh.amllplayer/linux-webview.json`(或 `$XDG_CONFIG_HOME/net.stevexmh.amllplayer/linux-webview.json`)。可直接修改其中的 `backend` 为上述任一选项;`AMLL_LINUX_WEBVIEW_BACKEND` 的优先级高于该配置。在 `auto` 模式下,NVIDIA Wayland 会话可能覆盖 `GDK_BACKEND=wayland` 以绕过已知崩溃,其他显式配置的 GDK 后端仍会保留。 + ### 鸣谢 - [woshizja/sound-processor](https://github.com/woshizja/sound-processor) diff --git a/README.md b/README.md index 839052fa..9f0dcc8f 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,31 @@ pnpm tauri build # Production build pnpm tauri dev # Development mode ``` +### Linux display backend + +On the first NVIDIA Wayland launch where the wrapper is available, AMLL Player asks before switching its webview to a nested `Xephyr + Openbox` session. Xephyr is exposed to the desktop as a normal application window while Openbox manages the inner X11 webview. This avoids the WebKitGTK explicit-sync crash and restores host-side resize, maximize, fullscreen, focus, and close behavior. + +Install the wrapper dependencies on Arch Linux with: + +```bash +sudo pacman -S openbox xorg-server-xephyr +``` + +If either dependency cannot be found or the wrapper fails during startup, AMLL Player automatically falls back to the existing XWayland display. Xephyr does not expose DRI3 to its nested clients, so the wrapper adds a CPU copy/compositing step; WebKitGTK can still use the GPU for its own rendering. The direct `x11` fallback avoids that nested-server overhead. + +The wrapper defaults Xephyr to 60 FPS to avoid unnecessary nested compositing work. Set `AMLL_XEPHYR_FPS` to a rate no higher than the host refresh rate to opt into high refresh, for example `AMLL_XEPHYR_FPS=120`; WebKitGTK may still limit animation to about 60 FPS in a nested X11 session, so this does not guarantee rendering-rate passthrough. + +Set `AMLL_LINUX_WEBVIEW_BACKEND` to override the automatic selection: + +- `auto`: Openbox wrapper on NVIDIA Wayland, then direct XWayland, then software Wayland +- `system`: keep the backend selected by the desktop environment and `GDK_BACKEND` +- `openbox`: request the Xephyr + Openbox wrapper with the same automatic fallbacks +- `x11`: prefer X11, with a software Wayland fallback when no X display is available +- `wayland`: force native Wayland rendering +- `wayland-software`: force Wayland and disable the WebKitGTK DMA-BUF renderer + +The first-run choice is stored in `~/.config/net.stevexmh.amllplayer/linux-webview.json` (or `$XDG_CONFIG_HOME/net.stevexmh.amllplayer/linux-webview.json`). Edit its `backend` value to any option above. `AMLL_LINUX_WEBVIEW_BACKEND` takes precedence over the saved value. In `auto` mode, an NVIDIA Wayland session may override `GDK_BACKEND=wayland` to avoid the known crash; other explicitly configured GDK backends are preserved. + ### Acknowledgements - [woshizja/sound-processor](https://github.com/woshizja/sound-processor) diff --git a/packages/player-core/src/player.rs b/packages/player-core/src/player.rs index c0f36e6c..dd4e5903 100644 --- a/packages/player-core/src/player.rs +++ b/packages/player-core/src/player.rs @@ -246,6 +246,7 @@ impl AudioPlayer { pub async fn run(mut self) { let mut check_end_interval = tokio::time::interval(Duration::from_millis(50)); + let mut media_controls_events_open = true; loop { tokio::select! { @@ -258,11 +259,16 @@ impl AudioPlayer { } } else { break; } }, - msg = self.npc_event_rx.recv() => { + msg = self.npc_event_rx.recv(), if media_controls_events_open => { if let Some(event) = msg { self.media_manager .handle_event(event, &self.handler(), &self.evt_sender) .await; + } else { + // A failed MPRIS/media-controls initialization drops its sender. + // Polling the closed channel would otherwise keep this biased select! + // loop runnable and consume a CPU core. + media_controls_events_open = false; } }, _ = check_end_interval.tick() => { diff --git a/packages/player/index.html b/packages/player/index.html index e25a2b52..2a4d69c6 100644 --- a/packages/player/index.html +++ b/packages/player/index.html @@ -103,6 +103,8 @@ #system-titlebar-buttons { display: flex; + position: relative; + z-index: 10002; gap: 2px; transition: opacity 0.3s ease-in-out, @@ -192,6 +194,106 @@ display: none; } + #openbox-resize-handles { + display: none; + } + + html[data-amll-openbox-wrapper="true"][data-amll-openbox-maximized="false"] + #openbox-resize-handles { + display: block; + } + + html[data-amll-openbox-wrapper="true"][data-amll-openbox-maximized="true"] + #openbox-resize-handles { + display: none; + } + + .openbox-resize-handle { + position: fixed; + z-index: 10001; + } + + .openbox-resize-handle[data-direction="top"], + .openbox-resize-handle[data-direction="bottom"] { + left: 6px; + width: calc(100% - 12px); + height: 5px; + } + + .openbox-resize-handle[data-direction="top"] { + right: 148px; + width: auto; + } + + .openbox-resize-handle[data-direction="left"], + .openbox-resize-handle[data-direction="right"] { + top: 6px; + width: 5px; + height: calc(100% - 12px); + } + + .openbox-resize-handle[data-direction="top"] { + top: 0; + cursor: ns-resize; + } + + .openbox-resize-handle[data-direction="right"] { + top: 32px; + right: 0; + height: calc(100% - 38px); + cursor: ew-resize; + } + + .openbox-resize-handle[data-direction="bottom"] { + bottom: 0; + cursor: ns-resize; + } + + .openbox-resize-handle[data-direction="left"] { + left: 0; + cursor: ew-resize; + } + + .openbox-resize-handle[data-direction="top-left"], + .openbox-resize-handle[data-direction="top-right"], + .openbox-resize-handle[data-direction="bottom-left"], + .openbox-resize-handle[data-direction="bottom-right"] { + width: 8px; + height: 8px; + } + + .openbox-resize-handle[data-direction^="top"] { + top: 0; + } + + .openbox-resize-handle[data-direction="top-right"] { + right: 140px; + } + + .openbox-resize-handle[data-direction^="bottom"] { + bottom: 0; + } + + .openbox-resize-handle[data-direction$="left"] { + left: 0; + } + + .openbox-resize-handle[data-direction$="right"]:not( + [data-direction="top-right"] + ) { + right: 0; + } + + .openbox-resize-handle[data-direction="top-left"], + .openbox-resize-handle[data-direction="bottom-right"] { + cursor: nwse-resize; + } + + .openbox-resize-handle[data-direction="top-right"], + .openbox-resize-handle[data-direction="bottom-left"] { + cursor: nesw-resize; + } + @media (prefers-color-scheme: dark) { #system-titlebar.windows .system-button:not(#system-titlebar-close):hover { @@ -237,6 +339,23 @@ resizeBtn.addEventListener("click", () => { window.dispatchEvent(new Event("on-system-titlebar-click-resize")); }); + if (document.documentElement.dataset.amllOpenboxWrapper === "true") { + titlebar.removeAttribute("data-tauri-drag-region"); + titlebar.addEventListener("mousedown", (event) => { + if (event.button === 0 && !event.target.closest(".system-button")) { + event.preventDefault(); + window.__TAURI__.core.invoke("start_openbox_drag"); + } + }); + document.addEventListener("mousedown", (event) => { + const handle = event.target.closest(".openbox-resize-handle"); + if (!handle || event.button !== 0) return; + event.preventDefault(); + window.__TAURI__.core.invoke("start_openbox_resize", { + direction: handle.dataset.direction, + }); + }); + } window.SystemTitlebarAppearance = { Windows: "windows", MacOS: "macos", @@ -350,6 +469,16 @@ } +