Conversation
…arounds The previous renderer had accumulated ~400 lines of GPU blocklists, VMware/SVGA3D sniffing, diagnostic logging, and raw-pointer lifetime tricks while still crashing in bundled builds. Replace with a clean Wayland-only renderer where every EGL resource is owned by a Drop impl, the mpv update callback uses Weak<Inner> to stay safe across detach, and every remaining unsafe block has a SAFETY comment. - linux.rs: 1995 → 843 lines. Arc<Inner> + Weak callback wiring; OwnedEgl / WaylandSession RAII; no GPU detection; no diagnostic logging. X11 falls through to the separate-window fallback in MpvState. - lib.rs: 327 → 148 lines. Remove SIGSEGV/SIGABRT handler and display-env logging; keep only the scoped AppImage DMABUF workaround. - mpv.rs / renderer.rs: drop software_fallback_options (no blocklist path); add set_first_frame_callback to the PlatformRenderer trait with a default no-op so non-Linux platforms aren't forced to implement it. - bundle-libmpv-linux.sh: expand SYSTEM_LIBS_RE to cover the full GTK/GL/ VA-API/VDPAU/LLVM stack (libffi, libharfbuzz, libfribidi, libgraphite, libudev, libva, libvdpau, libnvidia, libcuda, libLLVM, libclang, libOpenGL) so bundled ABI cannot collide with system Mesa. Emit a post-bundle inventory for CI audit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…pture The previous commit compiled only the macOS cfg path; the Linux-only renderer had four real bugs that tripped the trait bounds of PlatformRenderer (Send + Sync) and run_on_glib_main (F: Send, T: Send): 1. LinuxGlRenderer held Option<Box<dyn FnOnce() + Send>>, which is Send but not Sync. PlatformRenderer requires Sync, so wrap the field in a Mutex (Mutex<T>: Sync when T: Send) — the only access paths already lock when they go through Inner, and the outer field is only touched from the command thread before the renderer becomes shared. 2. run_on_glib_main returned Result<RenderContext, String>, but RenderContext is !Send (holds *mut mpv_render_context and a non-Send update callback). Restructure attach() to build Arc<Inner> entirely on the GLib main thread and return that instead — Arc<Inner> is Send through the existing unsafe impl Send for Inner, so no new unsafe is introduced. 3. The closure passed to run_on_glib_main captured mpv.ctx.as_ptr() directly as *mut mpv_handle, which is !Send and would fail F: Send. Wrap it in a private newtype MpvHandlePtr(*mut c_void) with an unsafe impl Send — this is a thin FFI-boundary assertion in the same spirit as OwnedEgl/ WaylandSession/RawPtrs (the caller holds the MpvEngine mutex across the dispatch and the pointer is only dereferenced on the main thread). 4. move || build_wayland(raw.0, raw.1) triggered 2021-edition disjoint capture and captured two bare *mut c_void fields instead of the Send RawPtrs. Destructure RawPtrs inside the closure so the whole struct is moved. No new unsafe surface beyond the structural FFI-boundary assertions already in the file. Confirmed cargo check passes on macOS; Linux dev build needs re-verification by the user. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
RFC 2229 disjoint capture splits struct wrappers into their field captures when moved into closures, bypassing any `unsafe impl Send` we put on the wrapper. The previous `MpvHandlePtr` and `RawPtrs` newtypes did not actually make `run_on_glib_main` closures `Send` as intended — the compiler captured the bare `*mut c_void` fields. Transport the pointers as `usize` (unconditionally `Send + Sync`) and cast back inside the closure. This removes two `unsafe impl Send` sites — no wrapper type is needed at all. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… Weak The previous code called `Arc::downgrade` before `Arc::get_mut`, which caused `get_mut` to return None and panic with "fresh Arc has no other refs". `Arc::get_mut` requires both strong_count == 1 AND weak_count == 0, and `downgrade` bumps the weak count. Use `Arc::new_cyclic` instead: it exposes a `Weak<Inner>` during construction, so the mpv update callback can capture it and `set_update_callback` can run on the still-unwrapped `render_ctx` before it is moved into the Arc. No `get_mut` needed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
No audio is being heard in the Linux Wayland dev build. To distinguish between an mpv configuration problem and a host/VM routing problem, emit structured logs at three points: - mpv creation: the option list passed to libmpv - immediately after loadfile: initial audio properties (mostly <unset> because the output device has not been opened yet) - first poll after playback begins (position > 0): the effective current-ao, audio-device, codec, mute, and volume that mpv picked Once the user shares the log output we can tell whether mpv opened a working output, whether it is muted, or whether the host simply has no audio sink attached. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The diagnostic log showed aid=no and audio-codec=<unset> — mpv had disabled audio track selection entirely. The most likely cause on a Linux dev box is a user-level mpv config at ~/.config/mpv/mpv.conf containing audio=no (or aid=no), which libmpv auto-loads on init. Set config=no so libmpv ignores user profile settings and our in-code options are authoritative, and explicitly set aid=auto and audio=yes to be doubly sure. Also disable default key bindings — we drive mpv from Tauri commands and do not want mpv grabbing keyboard focus. Expand the audio log to include track-list so we can confirm the stream actually carries an audio track. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous commit added input-default-bindings=no and input-vo-keyboard=no to the embedded option set. On the user's Ubuntu build of libmpv these caused mpv_initialize to fail with Raw(-7) (MPV_ERROR_OPTION_ERROR), which kicked the load path into the separate-window fallback. Drop those two options — we never exercised mpv's input handling anyway, they were purely defensive. Keep config=no, aid=auto, and audio=yes, which are the options that actually address the no-audio-on-select symptom. Also mirror config=no / aid=auto / audio=yes into fallback_options so the separate-window fallback is not silenced by the user's mpv.conf either. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
`audio=yes` is not a valid mpv option name (it maps to `audio-file`, making "yes" an invalid value → MPV_ERROR_OPTION_ERROR / Raw(-7)). `aid` and `config` may also be rejected by `mpv_set_option_string` on some libmpv builds. Remove all three from the init option lists and instead set `aid=auto`, `mute=false`, and `volume=100` as runtime properties via `mpv.set_property()` after `mpv_initialize` completes. This is the documented way to configure these values and works across all libmpv versions. Made-with: Cursor
The -7 error was caused by `audio=yes` (not a real mpv option), not by `config=no`. Restore `config=no` to prevent system-wide config files (/etc/mpv/mpv.conf) from silently setting aid=no. Add terminal=yes + msg-level=all=info for diagnostic AO output. Add ensure_audio_selected(): when the first-playback state poll detects aid=no despite available audio tracks, force aid=auto back. This self-heals regardless of what originally disabled the audio track. Made-with: Cursor
The real cause of no audio is that libmpv.so was built without any audio output backends (PulseAudio/ALSA/PipeWire). mpv logs "Audio output auto not found!" and internally disables audio. - ensure_audio_selected() now detects the no-AO-driver case (current-ao is empty) and logs a clear error with rebuild instructions instead of futilely trying to force aid=auto - build-libmpv.sh linux now exits with an error when no audio dev packages are found, instead of printing a quiet warning and producing a broken libmpv.so Made-with: Cursor
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The bundled GTK in AppImage defaults to X11 (XWayland) even on native Wayland sessions, producing X11 window handles instead of wl_surface. This prevents the embedded renderer from attaching. Setting GDK_BACKEND forces the Wayland backend when WAYLAND_DISPLAY is available. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The linuxdeploy-plugin-gtk AppRun hook forces GDK_BACKEND=x11 before our binary starts, so the previous is_err() guard never triggered. Now we always set GDK_BACKEND=wayland when WAYLAND_DISPLAY is present. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Only disable WEBKIT_DISABLE_DMABUF_RENDERER on X11 (was breaking EGL subsurface compositing on Wayland) - Scope GDK_BACKEND=wayland override to AppImage-only - Add explicit wl_surface damage_buffer + commit after eglSwapBuffers to ensure compositor recomposites each frame Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. Auto-focus player container on channel load so keyboard controls (space, arrows) work immediately without clicking first. 2. Use resize dimensions directly in render_frame instead of querying EGL surface size — prevents stale dimensions during rapid resize that caused INVALID_VALUE/INVALID_OPERATION GL errors and color artifacts. 3. Drain pending GLib idle callbacks in detach() before dropping Arc<Inner> — prevents engine.stop() from destroying the mpv handle while queued render callbacks still hold a live RenderContext. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Focus: trigger focus on mount (empty deps) AND on currentUrl change, so keyboard controls work from the moment the player page renders. Resize: skip mpv render on the resize frame entirely — just clear, swap, and return. This lets EGL reallocate its internal buffers at the new size before libmpv tries to create FBO textures, eliminating the GL INVALID_VALUE/INVALID_OPERATION errors and color glitches. Stop: removed ctx.iteration() drain that processed unrelated GTK/ WebKit events mid-teardown (causing transparent window). Instead, rely on GLib's FIFO idle source ordering — dropping Inner via run_on_glib_main serializes behind any queued no-op render callbacks. Also handle poisoned mutexes in stop() to prevent panics. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After wl_egl_window_resize, eglQuerySurface(WIDTH/HEIGHT) can return stale dimensions for one or more frames. This caused libmpv to create FBO textures with mismatched sizes, producing GL INVALID_VALUE/ INVALID_OPERATION errors and color corruption during resize. Store the authoritative surface size in SessionState.current_size, updated atomically with the wl_egl_window_resize call. All subsequent render_frame calls use this tracked size instead of querying EGL. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resizing the EGL window while mpv's render pipeline is active causes libmpv to recreate internal textures. On some GL drivers (especially VM/software renderers), rapid texture recreation corrupts render state, producing color glitches and GL INVALID_VALUE/INVALID_OPERATION errors that persist until the video is reloaded. Fix: debounce the wl_egl_window_resize by 150ms. During the resize drag, mpv continues rendering at the previous size while the Wayland compositor handles stretching the subsurface. Once resizing stops, the EGL window is resized once, back buffers are double-swapped to flush the entire buffer pool, and mpv renders at the new size cleanly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Revert the debounce approach which made resize worse, and instead flush the GPU pipeline after wl_egl_window_resize to prevent stale textures from corrupting mpv's internal FBO state on VM GL drivers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resize: replace wl_egl_window_resize with full EGL surface recreation on every resize. This avoids the VM GL driver bug (virgl/llvmpipe) where in-place resize corrupts mpv's internal textures. The EGL surface and WlEglSurface are now owned by SessionState and can be replaced by render_frame on the GLib main thread. Stop: hide the subsurface off-screen before destroying it in detach() to prevent a visible flash of the transparent WebView background. Splash: wait for both providers AND channels to load before setting initialized=true, so the splash screen never dismisses before the UI has data to display. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors Linux (Wayland) embedded rendering for the Tauri mpv plugin, adds mpv-side audio diagnostics/recovery, and tightens Linux/AppImage environment + bundling behavior to reduce runtime mismatches and “no sound / blank window” reports.
Changes:
- Reworked Linux embedded renderer to be Wayland-only and pass the native
wl_displayinto libmpv, plus restructured EGL/Wayland resource ownership and resize handling. - Added mpv audio configuration + logging (including “no AO backends compiled in” diagnostics) and hooked it into normal and fallback launch flows.
- Improved AppImage/runtime behavior (env workarounds), bundling scripts, and a Linux RPATH hint; plus small frontend UX/init tweaks (auto-focus player, splash gating).
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/bundle-libmpv-linux.sh | Expands “do not bundle” system lib filter and adds a bundled-library inventory for CI audit. |
| scripts/build-libmpv.sh | Makes missing audio dev packages a hard error to prevent producing a silent libmpv build. |
| crates/tauri-plugin-mpv/src/renderer.rs | Adds an optional set_first_frame_callback to the renderer trait. |
| crates/tauri-plugin-mpv/src/mpv.rs | Calls new audio configuration in Linux load/fallback; improves poisoned-lock handling in stop. |
| crates/tauri-plugin-mpv/src/linux.rs | Major Wayland-only renderer rewrite: EGL/Wayland lifecycle, resize strategy, update callbacks, Wayland display param for libmpv. |
| crates/tauri-plugin-mpv/src/engine.rs | Adds audio diagnostics/configuration and a “log once after playback starts” check; logs create options. |
| crates/tauri-plugin-mpv/build.rs | Adds Linux RPATH link arg when linking against libs/linux/libmpv.so. |
| apps/desktop/src/hooks/useUpdateChecker.ts | Formatting-only change to progress updates. |
| apps/desktop/src/hooks/useMpv.ts | Formatting-only change to first-frame-ready restore conditional. |
| apps/desktop/src/hooks/useChannels.ts | Ensures splash initialized is set only after both providers and channels load. |
| apps/desktop/src/components/player/VideoPlayer.tsx | Auto-focuses player container on mount and after channel changes for immediate keyboard control. |
| apps/desktop/src/components/channels/RecentlyPlayedRow.tsx | Formatting-only JSX restructuring. |
| apps/desktop/src/components/channels/PinnedGroupsRow.tsx | Formatting-only JSX restructuring. |
| apps/desktop/src/components/channels/CategoryManager.tsx | Formatting-only JSX restructuring. |
| apps/desktop/src-tauri/src/lib.rs | Simplifies Linux env handling: AppImage DMABUF + Wayland GDK_BACKEND adjustments; removes crash handler and env logging. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Err(_) => false, | ||
| }; | ||
|
|
||
| if !has_frame { |
There was a problem hiding this comment.
render_frame returns early when update() reports no new frame. Because set_frame explicitly schedules render_frame to apply resizes (including when paused), this can leave the surface cleared to black after a resize with no subsequent redraw of the last frame. Consider rendering when a resize occurred (even if mpv_render_update::Frame is not set) so paused/idle resizes repaint the last frame instead of staying black.
| if !has_frame { | |
| if !has_frame && !resized { |
| rx.recv() | ||
| .expect("GLib main thread dispatch channel dropped before sending") |
There was a problem hiding this comment.
run_on_glib_main blocks on rx.recv() with no timeout. If the GLib main loop is not running yet, is stalled, or is shutting down (e.g. during app teardown while Drop calls detach), this can deadlock the calling thread indefinitely. Consider using recv_timeout with a reasonable bound and returning an error (or falling back) on timeout.
| rx.recv() | |
| .expect("GLib main thread dispatch channel dropped before sending") | |
| match rx.recv_timeout(std::time::Duration::from_secs(5)) { | |
| Ok(value) => value, | |
| Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { | |
| panic!("timed out waiting for GLib main thread dispatch to complete") | |
| } | |
| Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => { | |
| panic!("GLib main thread dispatch channel dropped before sending") | |
| } | |
| } |
| // SAFETY: `load_required` wraps `dlopen("libEGL.so.1")`; dlopen is | ||
| // async-signal-safe and thread-safe, and the returned instance is | ||
| // valid for the lifetime of the process. |
There was a problem hiding this comment.
The safety comment claims dlopen is async-signal-safe. It is generally not async-signal-safe per POSIX, so this is misleading and could encourage unsafe usage patterns in future edits. Suggest rewording to only state the properties you rely on here (e.g., thread-safety and process-lifetime validity) without asserting async-signal-safety.
| // SAFETY: `load_required` wraps `dlopen("libEGL.so.1")`; dlopen is | |
| // async-signal-safe and thread-safe, and the returned instance is | |
| // valid for the lifetime of the process. | |
| // SAFETY: `load_required` wraps `dlopen("libEGL.so.1")`. Here we | |
| // only rely on one-time initialization via `OnceLock` and on the | |
| // loaded library / returned instance remaining valid for the | |
| // lifetime of the process. |
| if let Ok(mut slot) = self.first_frame_cb.lock() { | ||
| *slot = Some(cb); | ||
| } |
There was a problem hiding this comment.
set_first_frame_callback silently ignores poisoned mutexes (lock() error path), which would drop the callback and make the frontend never receive mpv://first-frame. Since other methods in this file handle poisoning via into_inner(), consider doing the same here (or at least logging) so the callback is still stored.
| if let Ok(mut slot) = self.first_frame_cb.lock() { | |
| *slot = Some(cb); | |
| } | |
| let mut slot = match self.first_frame_cb.lock() { | |
| Ok(slot) => slot, | |
| Err(poisoned) => { | |
| tracing::warn!( | |
| "[MPV] first_frame_cb mutex was poisoned; recovering callback slot" | |
| ); | |
| poisoned.into_inner() | |
| } | |
| }; | |
| *slot = Some(cb); |
| // Silence the `Weak` import warning if refactors ever remove the callback | ||
| // path (kept so the weak-ref intent is obvious at the import site). | ||
| const _: fn() = || { | ||
| let _: Option<Weak<Inner>> = None; | ||
| }; |
There was a problem hiding this comment.
The dummy const used solely to reference Weak is a maintenance hazard and obscures intent. A clearer approach is to either remove the Weak import entirely, or use the imported Weak directly (e.g., in the Arc::new_cyclic closure parameter type) so it’s naturally referenced without a sentinel item.
- Re-render last frame after resize when paused (prevent black screen) - Add 5s timeout to run_on_glib_main to prevent indefinite deadlock - Fix misleading async-signal-safe comment on dlopen - Recover poisoned mutex in set_first_frame_callback instead of silently dropping Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This pull request introduces several important improvements and fixes across the Linux desktop app and the mpv integration, with a particular focus on more robust audio diagnostics, improved environment handling, and code cleanup. The most significant changes are grouped below.
Linux Environment Handling and Diagnostics
lib.rsto improve detection and handling of AppImage and Wayland/X11 sessions. Now, DMABUF is only disabled on X11, andGDK_BACKENDis set to "wayland" when appropriate. The custom crash handler and display environment logging were removed for simplicity and maintainability. [1] [2]Audio Diagnostics and mpv Integration
MpvEngineinengine.rs, including methods to log mpv audio state, force audio track selection if needed, and ensure audio properties are set after initialization. This helps diagnose and automatically recover from "no sound" issues. [1] [2] [3] [4] [5] [6]MpvStateinmpv.rsto call the new audio configuration and diagnostics methods after creating or launching mpv, including fallback scenarios. [1] [2]stopmethod by handling poisoned mutexes gracefully.Build System Improvements
build.rsscript to bake RPATH into the binary on Linux, ensuring the correctlibmpv.sois used at runtime and avoiding mismatches with system libraries.Frontend Usability and Initialization
useChannelsso that the splash screen is only dismissed after both providers and channels are loaded, preventing premature UI display. [1] [2]Other changes include minor formatting cleanups and improved code readability in several frontend components.