Skip to content
Open
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions README-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 7 additions & 1 deletion packages/player-core/src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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! {
Expand All @@ -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() => {
Expand Down
129 changes: 129 additions & 0 deletions packages/player/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@

#system-titlebar-buttons {
display: flex;
position: relative;
z-index: 10002;
gap: 2px;
transition:
opacity 0.3s ease-in-out,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -350,6 +469,16 @@
}
</script>
</div>
<div id="openbox-resize-handles" aria-hidden="true">
<div class="openbox-resize-handle" data-direction="top-left"></div>
<div class="openbox-resize-handle" data-direction="top"></div>
<div class="openbox-resize-handle" data-direction="top-right"></div>
<div class="openbox-resize-handle" data-direction="right"></div>
<div class="openbox-resize-handle" data-direction="bottom-right"></div>
<div class="openbox-resize-handle" data-direction="bottom"></div>
<div class="openbox-resize-handle" data-direction="bottom-left"></div>
<div class="openbox-resize-handle" data-direction="left"></div>
</div>
<div id="root"></div>
<script type="module" defer src="/src/main.tsx"></script>
</body>
Expand Down
4 changes: 4 additions & 0 deletions packages/player/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ windows = { version = "0.62", features = [
"Win32_System_Threading",
] }

[target.'cfg(target_os = "linux")'.dependencies]
libc = "0.2"
x11rb = { version = "0.13", features = ["randr"] }

[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-global-shortcut = { version = "2" }
tauri-plugin-updater = { version = "2" }
Expand Down
22 changes: 21 additions & 1 deletion packages/player/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ use crate::server::AMLLWebSocketServerWrapper;

mod db;
mod db_events;
#[cfg(target_os = "linux")]
mod linux_webview;
mod logging;
mod music_info;
mod player;
Expand Down Expand Up @@ -48,7 +50,11 @@ fn setup_app(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
let log_guard = logging::init_logging(&log_dir);
app.manage(log_guard);
info!("AMLL Player is starting!");

#[cfg(target_os = "linux")]
info!(
"Configured Linux webview backend: {}",
linux_webview::selected_backend().as_str()
);
#[cfg(target_os = "ios")]
{
use objc2::msg_send;
Expand Down Expand Up @@ -189,6 +195,11 @@ fn handle_window_event(_window: &tauri::Window, _event: &tauri::WindowEvent) {

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
#[cfg(target_os = "linux")]
if let Some(exit_code) = linux_webview::prepare() {
std::process::exit(exit_code);
}

// Install ring as the default crypto provider for rustls, because multiple providers
// (aws-lc-rs and ring) might be enabled in our dependency tree and rustls demands one to be explicitly chosen.
#[cfg(target_os = "android")]
Expand Down Expand Up @@ -245,6 +256,15 @@ pub fn run() {
server::ws_broadcast_payload,
server::ws_close_connection,
window::open_screenshot_window,
window::get_linux_webview_startup_prompt,
window::set_linux_webview_backend,
window::restart_linux_webview_with_system_backend,
window::toggle_openbox_fullscreen,
window::is_openbox_maximized,
window::toggle_openbox_maximize,
window::minimize_openbox_wrapper,
window::start_openbox_drag,
window::start_openbox_resize,
screen_capture::take_screenshot,
player::local_player_send_msg,
player::set_media_controls_enabled,
Expand Down
Loading