Investigation into unifying the playback backends (Linux/MPV, Android/ExoPlayer, Windows/webview) onto one engine with hardware acceleration. Conclusion: video cannot be unified onto a native engine; audio can. The blocker is not mpv-specific. WebKitGTK, WebView2 and Android WebView each draw into their own compositor surface, so a native video surface sits either entirely above or entirely below the webview and cannot interleave with HTML. GStreamer and libVLC fail identically. mpv would additionally regress streaming: it has no adaptive bitrate, while the current hls.js path does. Six specs added: - playback-backend-unification: the analysis and decision, with evidence - android-audio-settings-parity: set_audio_settings on ExoPlayerBackend - android-native-video-spike: timeboxed test of SurfaceView compositing - windows-native-audio-backend: replace the webview <audio> shim with libmpv - libmpv2-migration: dead libmpv git pin -> libmpv2, plus a LICENSE file - playback-docs-corrections: the requirement-status fixes applied here Corrections to requirements.md, all verified against source: - UR-031/DR-034 claimed crossfade was "Done (Linux only)". It is implemented nowhere (mpv_backend.rs has a bare TODO) and is architecturally blocked on mpv, whose single-stream audio chain cannot feed acrossfade's two inputs. - Parity matrix listed crossfade as a Linux/Android gap; it is neither. - The matrix omitted the equalizer, which has the same Linux-only shape. - The suggested ConcatenatingMediaSource is deprecated in current Media3. nativeAdapter.ts cited tauri#10152 as an upstream blocker for native Android video. That issue is a stale feature request, dead since 2024-07-01; the capability shipped in tauri 27d01834 (2024-09-02), and the related black-screen bug was fixed in wry 0.39.4 (we ship 0.55.x). What is genuinely unproven is SurfaceView-behind-WebView compositing, which the spike now tracks.
210 lines
10 KiB
Markdown
210 lines
10 KiB
Markdown
# Spec: Windows native audio backend
|
||
|
||
**Status:** Proposed
|
||
**Requirements:** UR-003, UR-027, UR-032, UR-033 → DR-030, DR-035, DR-036; new IR-030
|
||
**UX spec:** n/a — Settings › Audio already renders the controls
|
||
**Supersedes / revises:** acts on the "audio can unify, video cannot" conclusion in [playback-backend-unification.md](playback-backend-unification.md)
|
||
|
||
## Summary
|
||
|
||
Give Windows a real native audio backend instead of the current webview
|
||
`<audio>` shim. Windows is the only platform where audio playback has no decoder
|
||
of its own: `WebviewAudioBackend` hands a URL to a frontend `<audio>` element and
|
||
relays transport commands. It cannot set volume, cannot apply any audio setting,
|
||
and reports state only via DOM events.
|
||
|
||
Audio needs no rendering surface, so **none of the webview-compositing problems
|
||
that block unified video apply here.** This is the cleanest available win.
|
||
|
||
## Motivation
|
||
|
||
`WebviewAudioBackend` was a deliberate stopgap ("audio-only playback for
|
||
platforms without a native audio backend"), and it works — but it has a hard
|
||
functional gap. From `webview_audio_backend.rs`:
|
||
|
||
```rust
|
||
fn set_volume(&mut self, volume: f32) -> Result<(), PlayerError> {
|
||
// ...stores locally only; there is no ControlCommand action for volume
|
||
}
|
||
```
|
||
|
||
So volume changes never reach the element; the frontend has to observe the player
|
||
store and apply volume itself. `set_audio_settings` likewise stores values that
|
||
nothing consumes — EQ, normalization, and gapless are all inert on Windows.
|
||
|
||
Meanwhile the backend-unification investigation established that a native *audio*
|
||
engine is unproblematic on Windows specifically: `tauri-plugin-libmpv` lists
|
||
Windows as its **fully tested** platform (in contrast to Linux, where embedding
|
||
is broken — but that is a *video surface* problem, which audio does not have).
|
||
|
||
## Layer assignment
|
||
|
||
| Logic / responsibility | Layer | Why it belongs there |
|
||
|------------------------|-------|----------------------|
|
||
| Decoding and playing the audio stream | Rust | Playback is domain logic; every other platform already decodes in Rust or a native player. The webview shim is the anomaly. |
|
||
| Applying `AudioSettings` (EQ/normalize/gapless) | Rust | Same `AudioSettings` contract as MPV/ExoPlayer; band layout and presets stay canonical in `settings.rs`. |
|
||
| Position/state reporting | Rust | Restores the project's core principle — the player is the authoritative source of state. Today Windows inverts this: the DOM element is authoritative and Rust mirrors it. |
|
||
| Volume | Rust | Currently broken precisely because it is split across the boundary. |
|
||
| Rendering the player UI | Frontend | Unchanged. |
|
||
|
||
The strongest argument for this change is the third row. CLAUDE.md states
|
||
playback state is one-directional with the player authoritative; on Windows that
|
||
is currently false, and the `player_report_*` round-trip exists to paper over it.
|
||
|
||
## Design
|
||
|
||
### Engine choice
|
||
|
||
Two viable options; **libmpv is recommended** for consistency with the Linux
|
||
audio backend.
|
||
|
||
| | libmpv | GStreamer |
|
||
|---|---|---|
|
||
| Windows status | ✅ `tauri-plugin-libmpv` reports fully tested | ✅ works, but… |
|
||
| Rust bindings | `libmpv2` 6.0.0, active | `gstreamer-rs` 0.25.x, excellent |
|
||
| Cross-MSVC from Linux | ⚠️ needs prebuilt DLL + import lib | ❌ `gstreamer-sys` uses pkg-config, fights `cargo-xwin` |
|
||
| Code reuse | ✅ `MpvBackend` logic is directly reusable | ❌ a second engine to learn |
|
||
| Crossfade capable | ❌ single-stream chain | ✅ `audiomixer` |
|
||
|
||
libmpv wins on reuse: `MpvBackend`'s `set_audio_settings` — the `af` lavfi graph
|
||
built by `build_af_filter`, `eq_filter_entries`, `normalize_filter_entry` — is
|
||
platform-independent and would apply unchanged.
|
||
|
||
The one reason to prefer GStreamer is crossfade (UR-031), which mpv structurally
|
||
cannot do. If crossfade becomes a priority, revisit; it would then argue for
|
||
GStreamer on *both* Linux and Windows, which is a much larger change.
|
||
|
||
### Structure
|
||
|
||
Rename the cfg gate so `MpvBackend` is no longer Linux-only:
|
||
|
||
```rust
|
||
// src-tauri/src/player/mod.rs
|
||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||
pub mod mpv_backend;
|
||
```
|
||
|
||
`MpvBackend::new` needs one platform-specific branch: `detect_audio_system()`
|
||
currently probes `pactl`/`pw-cli`/`/proc/asound/cards` to pick an `ao`. On
|
||
Windows the equivalent is `wasapi` (mpv's default), so the detection is a
|
||
`#[cfg]` returning `"wasapi"` — no probing needed.
|
||
|
||
Everything else — the event loop, the 250ms position thread, the seek-suppression
|
||
window, the `af` filter graph — is unchanged.
|
||
|
||
`WebviewAudioBackend` stays for other targets (macOS and anything else hitting
|
||
the `not(any(...))` arm) and as the fallback if libmpv fails to initialize. The
|
||
existing `emit_backend_init_failed` path already handles that gracefully.
|
||
|
||
### Build
|
||
|
||
`libmpv2-sys` is well-suited to cross-compilation: no pkg-config, vendored
|
||
headers, pregenerated bindings (no libclang). It emits `cargo:rustc-link-lib=mpv`
|
||
unconditionally, so the build must supply a linkable import library for
|
||
`x86_64-pc-windows-msvc`.
|
||
|
||
Keep the `build_libmpv` feature **off** — its Unix path shells out to mpv-build
|
||
and explicitly rejects cross-compilation.
|
||
|
||
🔴 Per CLAUDE.md, the prebuilt libmpv **must be added to the builder image**
|
||
(`Dockerfile.builder` → rebuild + push via `scripts/build-builder-image.sh`), not
|
||
installed at CI job time. `libmpv-2.dll` must also be bundled into the NSIS
|
||
installer via `tauri.conf.json`'s resources.
|
||
|
||
### Verified build mechanics
|
||
|
||
The cross-compile path was tested hands-on from Linux (July 2026), not inferred:
|
||
|
||
- Neither shinchiro nor zhongfly ships an `mpv.def` or MSVC `mpv.lib` — only a
|
||
MinGW `libmpv.dll.a`. (Several online sources claim otherwise; they are wrong.)
|
||
- An MSVC-style import lib can be generated locally with LLVM tools only:
|
||
`llvm-readobj --coff-exports libmpv-2.dll` → synthesize `mpv.def` →
|
||
`llvm-dlltool -m i386:x86-64 -d mpv.def -l mpv.lib`. `llvm-lib /def:` produces a
|
||
byte-identical result.
|
||
- A real `lld-link` link against that import lib **succeeds**, and the resulting
|
||
import table resolves `mpv_client_api_version` from `libmpv-2.dll`. `lld-link`
|
||
is the linker `cargo-xwin` uses, so this is the load-bearing step.
|
||
- Linking directly against the shipped MinGW `libmpv.dll.a` **also** succeeds, so
|
||
def-generation may be skippable — but that relies on lld's GNU-archive
|
||
tolerance rather than a documented contract. Keep `llvm-dlltool` as the
|
||
fallback.
|
||
- MinGW origin is not an ABI problem: libmpv exports a pure C ABI, and the x86-64
|
||
Windows calling convention is platform-defined. The upstream note that MSVC
|
||
cannot *build* mpv is frequently misread as "MSVC cannot *link* libmpv" — that
|
||
is not what it says.
|
||
- 🔴 Never free/realloc across the DLL boundary — use `mpv_free`.
|
||
|
||
Build wiring is ordinary: `cargo:rustc-link-lib=dylib=mpv` plus
|
||
`cargo:rustc-link-search`. Nothing about libmpv conflicts with `cargo-xwin`.
|
||
|
||
### Size and shipping
|
||
|
||
Measured uncompressed: **93 MiB** (zhongfly `mpv-dev-lgpl-x86_64`) vs **112 MiB**
|
||
(shinchiro, full GPL build); ~26–30 MB compressed in the `.7z`.
|
||
|
||
**Ship the zhongfly LGPL build** — smaller, and there is no reason to pull the
|
||
GPL variant in for an audio-only use.
|
||
|
||
Import-table inspection confirms **no companion DLLs are needed**: every
|
||
dependency is a system DLL (`KERNEL32`, `USER32`, `d2d1`, `DWrite`, `OPENGL32`,
|
||
`vulkan-1`, UCRT `api-ms-win-*`). One file to bundle.
|
||
|
||
93 MiB is still substantial against a Tauri app's usual few MB. Since we use mpv
|
||
audio-only, investigate whether a pruned build (no video decoders, no libplacebo)
|
||
is worth producing for the builder image — but treat that as an optimization,
|
||
not a blocker.
|
||
|
||
## Out of scope
|
||
|
||
- Windows *video*. Stays in WebView2 + hls.js — it works and has ABR.
|
||
- Crossfade (UR-031/DR-034) — not implemented anywhere; needs its own spec.
|
||
- Replacing `WebviewAudioBackend` for macOS.
|
||
- MPRIS/SMTC media-key integration — worth a follow-up, not this spec.
|
||
|
||
## Acceptance criteria
|
||
|
||
- [ ] Windows build produces a `MpvBackend`-backed player; `backend-init-failed` is emitted (not a crash) if libmpv is unavailable.
|
||
- [ ] Volume control works from the UI — the current hard gap.
|
||
- [ ] EQ, normalization, and gapless audibly take effect on Windows.
|
||
- [ ] Position/state originate in Rust; the `<audio>` element is no longer in the audio path.
|
||
- [ ] Seek, next/previous, and queue advance work; sleep timer stops playback.
|
||
- [ ] `libmpv-2.dll` ships in the NSIS installer and the app runs on a clean Windows VM with no mpv installed.
|
||
- [ ] Builder image carries the Windows libmpv artefacts; **no toolchain install added to any CI step**.
|
||
- [ ] `bun run check`, `bun run test`, `bun run check:boundary` pass.
|
||
- [ ] `cargo fmt` clean, `cargo clippy` clean, `bun run test:rust` passes.
|
||
- [ ] New requirement-implementing code carries `// TRACES:` comments.
|
||
|
||
## Testing
|
||
|
||
**Rust**: the existing `mpv_backend_test.rs` and the `build_af_filter` /
|
||
`normalize_filter_entry` / `eq_filter_entries` unit tests already cover the
|
||
filter-graph logic and are platform-independent — they should pass unchanged
|
||
under a Windows `cargo check`/test. Add a test asserting `detect_audio_system()`
|
||
returns `wasapi` under `cfg(windows)`.
|
||
|
||
**Manual, on Windows**: volume, EQ preset change, normalization toggle, gapless
|
||
between two tracks, seek, queue advance, sleep timer. Then the packaging test —
|
||
install the NSIS output on a clean VM and confirm it launches and plays.
|
||
|
||
Per CLAUDE.md, the volume gap is a *bug fix*: write a failing test for
|
||
"`set_volume` reaches the backend" before implementing.
|
||
|
||
## TRACES
|
||
|
||
- Windows `MpvBackend` construction in `create_player_backend` → `// TRACES: UR-003 | IR-030`
|
||
- `detect_audio_system` Windows branch → `IR-030`
|
||
- Existing `set_audio_settings` gains Windows coverage → `UR-027, UR-032, UR-033 | DR-030, DR-035, DR-036`
|
||
- Allocate **IR-030** in `requirements.md` ("libmpv integration for Windows audio playback").
|
||
|
||
## Notes for the implementer
|
||
|
||
- Do this **after** [libmpv2-migration.md](libmpv2-migration.md) — porting the
|
||
current dead `libmpv` git pin to a second platform would double the migration
|
||
work.
|
||
- `libmpv2` has broken its API in every major release (4.0 removed command
|
||
helpers, 5.0 removed `mpv_node`, 6.0 changed `RenderContext` ownership). Pin an
|
||
exact version.
|
||
- Only the `render`-feature parts of `libmpv2` concern video; audio-only use does
|
||
not need it, and disabling the default `render` feature may shrink the build.
|
||
- A parallel Claude session may be active — `git diff` first.
|