Conversation
…arding - Prevent display dimming/sleep during playback (macOS: IOPMAssertion, Linux: D-Bus ScreenSaver/GNOME SessionManager) - Set WebView background transparent natively via Tauri API to fix black screen on Linux compositors where CSS transparency is insufficient - Forward mpv internal logs to stderr for audio/video diagnostics - Reset per-session frame counters on each attach() for clean diagnostics Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds playback-quality-of-life and diagnostics improvements to the Tauri MPV integration by preventing system idle sleep during playback, ensuring native WebView transparency for embedded video on Linux compositors, and forwarding mpv’s internal logs to stderr for easier debugging.
Changes:
- Introduce an
IdleInhibitorand wire it into MPV load/play/pause/stop flows. - Set WebView native background color to transparent (macOS/Linux) to avoid embedded video being obscured.
- Enable mpv terminal log forwarding options and reset Linux per-session frame diagnostics counters on attach.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
crates/tauri-plugin-mpv/src/mpv.rs |
Integrates idle inhibit lifecycle into playback control paths. |
crates/tauri-plugin-mpv/src/macos.rs |
Adds mpv options to forward internal logs to stderr. |
crates/tauri-plugin-mpv/src/linux.rs |
Resets frame diagnostics per attach; adds mpv stderr log forwarding options. |
crates/tauri-plugin-mpv/src/lib.rs |
Registers the new idle_inhibit module. |
crates/tauri-plugin-mpv/src/idle_inhibit.rs |
New platform-specific idle inhibition implementation (macOS IOKit, Linux D-Bus via gdbus). |
apps/desktop/src-tauri/src/lib.rs |
Sets WebView background to transparent from Rust to support embedded video visibility on Linux compositors. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| self.inner.lock().map_err(|e| e.to_string())?.stop(); | ||
| self.fallback_active.store(false, Ordering::Release); | ||
|
|
||
| self.load_impl(url, app) | ||
| let result = self.load_impl(url, app); | ||
| if result.is_ok() { | ||
| self.idle_inhibitor.inhibit(); | ||
| } | ||
| result |
There was a problem hiding this comment.
load() stops the current playback but never calls idle_inhibitor.uninhibit() before attempting the new load. If load_impl() returns Err, the previous idle-inhibit state can remain active even though playback was stopped, keeping the display awake unexpectedly. Consider uninhibiting immediately after stop() (or reusing self.stop() here), then re-inhibiting only after a successful load/play.
| pub fn inhibit(&self) { | ||
| let mut state = match self.inner.lock() { | ||
| Ok(s) => s, | ||
| Err(_) => return, | ||
| }; | ||
| if state.active { | ||
| return; | ||
| } | ||
| if self.platform_inhibit(&mut state) { | ||
| state.active = true; | ||
| tracing::debug!("[idle-inhibit] display sleep inhibited"); | ||
| } | ||
| } | ||
|
|
||
| /// Allow the display to sleep again. No-op if not inhibited. | ||
| pub fn uninhibit(&self) { | ||
| let mut state = match self.inner.lock() { | ||
| Ok(s) => s, | ||
| Err(_) => return, | ||
| }; | ||
| if !state.active { | ||
| return; | ||
| } | ||
| self.platform_uninhibit(&mut state); | ||
| state.active = false; | ||
| tracing::debug!("[idle-inhibit] display sleep uninhibited"); | ||
| } |
There was a problem hiding this comment.
IdleInhibitor returns early on a poisoned mutex (Err(_) => return). That can prevent uninhibit() from running in Drop, potentially leaving an idle-inhibit assertion active until the process exits. Consider recovering from poisoning (e.g., lock().unwrap_or_else(|e| e.into_inner())) so cleanup still happens.
| // kIOPMAssertionTypePreventUserIdleDisplaySleep | ||
| let assertion_type = cfstring("PreventUserIdleDisplaySleep"); | ||
| let reason = cfstring("MaxVideoPlayer: video playback active"); | ||
| let mut assertion_id: u32 = 0; | ||
|
|
||
| // kIOPMAssertionLevelOn = 255 | ||
| let ret = unsafe { | ||
| IOPMAssertionCreateWithName( | ||
| assertion_type, | ||
| 255, | ||
| reason, | ||
| &mut assertion_id, | ||
| ) | ||
| }; | ||
|
|
||
| unsafe { | ||
| CFRelease(reason); | ||
| CFRelease(assertion_type); | ||
| } |
There was a problem hiding this comment.
On macOS, cfstring() can return a null pointer on allocation failure. The current code unconditionally passes the returned pointers to IOPMAssertionCreateWithName and then calls CFRelease on them, which risks a crash if either pointer is null. Consider checking for null and bailing out (or only releasing non-null values) before calling into IOKit.
| fn dbus_call_u32(bus: &str, path: &str, method: &str, args: &[&str]) -> Option<u32> { | ||
| let mut cmd = std::process::Command::new("gdbus"); | ||
| cmd.arg("call") | ||
| .arg("--session") | ||
| .arg("--dest").arg(bus) | ||
| .arg("--object-path").arg(path) | ||
| .arg("--method").arg(method); | ||
| for arg in args { | ||
| cmd.arg(arg); | ||
| } | ||
| let output = cmd.output().ok()?; | ||
| if !output.status.success() { | ||
| return None; | ||
| } | ||
| // gdbus output format: "(uint32 12345,)\n" | ||
| let stdout = String::from_utf8_lossy(&output.stdout); | ||
| extract_u32_from_gdbus(&stdout) | ||
| } |
There was a problem hiding this comment.
Linux idle-inhibit uses gdbus via Command::output() with no timeout. If the session bus is slow/hung, this can block the calling thread (potentially on a playback control path) indefinitely. Consider adding a gdbus call --timeout ... option (if available) or implementing a process timeout to avoid hangs.
| // Keep the last frame visible at EOF instead of going idle. | ||
| // This lets the frontend detect EOF via position proximity and show controls. | ||
| ("keep-open", "yes"), | ||
| ("terminal", "yes"), | ||
| ("msg-level", "all=status"), | ||
| ] |
There was a problem hiding this comment.
PR description says mpv log forwarding is enabled “on all platforms”, but the option changes here are only in the macOS/Linux option sets. If other targets are supported (e.g. iOS/Android/non-macOS/Linux builds via engine.create(&[])), either extend the change to those targets or update the PR description to match the actual scope.
The IOPMAssertion* and CFString* functions require linking against the IOKit and CoreFoundation frameworks respectively. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Uninhibit before re-load so failed load_impl doesn't leave stale assertion - Recover from poisoned mutex in inhibit/uninhibit so Drop cleanup always runs - Guard against null CFString pointers on macOS - Add 3-second timeout to gdbus calls to avoid blocking on a hung D-Bus session Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
libmpv-dev. AppImage ships fully self-contained with all audio/video/subtitle deps bundled.libpulse0 | libasound2(deb) andpulseaudio-libs(rpm) package dependencies, pluslibassfor subtitle renderingapt-get install(resolves deps automatically) instead of rawdpkg -iIOPMAssertion, Linux: D-BusScreenSaver.Inhibit/GNOME SessionManager.Inhibit)set_background_colorAPI — fixes black screen on Linux compositors (Pop!_OS, COSMIC) where CSS-only transparency is insufficientterminal=yes+msg-level=all=statusso mpv internal messages appear in stderr for diagnosticsao=pipewire,pulse,alsatoao=autofor broader compatibilityContext
A user on Pop!_OS (Wayland, Intel UHD 630) reported black screen with no audio. Investigation revealed:
libmpvwas compiled without audio output plugins — fixed by bundling a source-built libmpv with audio supportKey changes
scripts/build-libmpv.sh: Linux build now explicitly enables ALSA/PulseAudio/PipeWire when dev headers are availablescripts/bundle-libmpv-linux.sh: Prefers source build, recursively bundles transitive deps, verifies audio libs are presentcrates/tauri-plugin-mpv/build.rs: Preferslibs/linux/(source build) over systempkg-config.github/workflows/{build,release}.yml: Install audio/ffmpeg dev packages, build libmpv from sourceapps/desktop/src-tauri/tauri.conf.json: Audio + subtitle deps for deb/rpm packagesapps/desktop/src-tauri/src/commands.rs:apt-get installfor deb updates withdpkg -ifallbackTest plan
build-linuxjob passes with source-built libmpv🤖 Generated with Claude Code