Skip to content

feat: Linux playback reliability — bundled libmpv, idle inhibit, WebView transparency#58

Merged
MaxMB15 merged 3 commits intodevfrom
feature/investigate-linux-playback
Apr 7, 2026
Merged

feat: Linux playback reliability — bundled libmpv, idle inhibit, WebView transparency#58
MaxMB15 merged 3 commits intodevfrom
feature/investigate-linux-playback

Conversation

@MaxMB15
Copy link
Copy Markdown
Owner

@MaxMB15 MaxMB15 commented Apr 7, 2026

Summary

  • Build & bundle libmpv from source: CI now builds libmpv from source with ALSA + PulseAudio audio outputs compiled in, instead of relying on system libmpv-dev. AppImage ships fully self-contained with all audio/video/subtitle deps bundled.
  • deb/rpm audio dependencies: Added explicit libpulse0 | libasound2 (deb) and pulseaudio-libs (rpm) package dependencies, plus libass for subtitle rendering
  • Package updater dependency resolution: deb updates now use apt-get install (resolves deps automatically) instead of raw dpkg -i
  • Idle/sleep inhibit: Prevent display from dimming/sleeping during video playback (macOS: IOPMAssertion, Linux: D-Bus ScreenSaver.Inhibit / GNOME SessionManager.Inhibit)
  • Native WebView transparency: Set WebView background to transparent via Tauri's set_background_color API — fixes black screen on Linux compositors (Pop!_OS, COSMIC) where CSS-only transparency is insufficient
  • MPV log forwarding: Enable terminal=yes + msg-level=all=status so mpv internal messages appear in stderr for diagnostics
  • Audio output: Changed from hardcoded ao=pipewire,pulse,alsa to ao=auto for broader compatibility

Context

A user on Pop!_OS (Wayland, Intel UHD 630) reported black screen with no audio. Investigation revealed:

  1. WebView was opaque, obscuring the video surface — fixed by native transparency
  2. Their system libmpv was compiled without audio output plugins — fixed by bundling a source-built libmpv with audio support
  3. mpv log forwarding was needed to diagnose the audio backend failure

Key changes

  • scripts/build-libmpv.sh: Linux build now explicitly enables ALSA/PulseAudio/PipeWire when dev headers are available
  • scripts/bundle-libmpv-linux.sh: Prefers source build, recursively bundles transitive deps, verifies audio libs are present
  • crates/tauri-plugin-mpv/build.rs: Prefers libs/linux/ (source build) over system pkg-config
  • .github/workflows/{build,release}.yml: Install audio/ffmpeg dev packages, build libmpv from source
  • apps/desktop/src-tauri/tauri.conf.json: Audio + subtitle deps for deb/rpm packages
  • apps/desktop/src-tauri/src/commands.rs: apt-get install for deb updates with dpkg -i fallback

Test plan

  • CI: verify build-linux job passes with source-built libmpv
  • AppImage: verify bundled audio libraries (libpulse, libasound) are present
  • deb: verify package installs cleanly and pulls audio dependencies
  • macOS: verify display does not dim during video playback
  • Linux (Wayland): verify video is visible through the WebView
  • Linux: verify audio playback works out of the box

🤖 Generated with Claude Code

…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>
Copilot AI review requested due to automatic review settings April 7, 2026 21:35
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 IdleInhibitor and 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.

Comment on lines 48 to +55
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
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +66
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");
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +102
// 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);
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +209 to +226
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)
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 489 to 494
// 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"),
]
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
MaxMB15 and others added 2 commits April 7, 2026 23:44
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>
@MaxMB15 MaxMB15 merged commit 40a6368 into dev Apr 7, 2026
6 checks passed
@MaxMB15 MaxMB15 changed the title feat: idle inhibit, native WebView transparency, mpv log forwarding feat: Linux playback reliability — bundled libmpv, idle inhibit, WebView transparency Apr 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants