Files
jellytau/specs/windows-native-audio-backend.html
T

422 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE HTML>
<html lang="en" class="navy" dir="ltr">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Windows Native Audio Backend - JellyTau Documentation</title>
<!-- Custom HTML head -->
<meta name="description" content="Requirements, traceability, and architecture for the JellyTau Jellyfin client.">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<link rel="icon" href="../favicon.svg">
<link rel="shortcut icon" href="../favicon.png">
<link rel="stylesheet" href="../css/variables.css">
<link rel="stylesheet" href="../css/general.css">
<link rel="stylesheet" href="../css/chrome.css">
<link rel="stylesheet" href="../css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="../fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="../highlight.css">
<link rel="stylesheet" href="../tomorrow-night.css">
<link rel="stylesheet" href="../ayu-highlight.css">
<!-- Custom theme stylesheets -->
</head>
<body class="sidebar-visible no-js">
<div id="body-container">
<!-- Provide site root to javascript -->
<script>
var path_to_root = "../";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "navy";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('navy')
html.classList.add(theme);
var body = document.querySelector('body');
body.classList.remove('no-js')
body.classList.add('js');
</script>
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed -->
<script>
var body = document.querySelector('body');
var sidebar = null;
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
sidebar_toggle.checked = sidebar === 'visible';
body.classList.remove('sidebar-visible');
body.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<ol class="chapter"><li class="chapter-item expanded affix "><a href="../index.html">Introduction</a></li><li class="chapter-item expanded affix "><li class="part-title">Requirements & Traceability</li><li class="chapter-item expanded "><a href="../requirements.html"><strong aria-hidden="true">1.</strong> Requirements Specification</a></li><li class="chapter-item expanded "><a href="../traceability.html"><strong aria-hidden="true">2.</strong> Traceability Matrix</a></li><li class="chapter-item expanded "><a href="../traceability-ci.html"><strong aria-hidden="true">3.</strong> Traceability CI</a></li><li class="chapter-item expanded "><a href="../traces-quick-ref.html"><strong aria-hidden="true">4.</strong> Traces Quick Reference</a></li><li class="chapter-item expanded affix "><li class="part-title">Architecture</li><li class="chapter-item expanded "><a href="../architecture/index.html"><strong aria-hidden="true">5.</strong> Overview</a><a class="toggle"><div></div></a></li><li><ol class="section"><li class="chapter-item "><a href="../architecture/01-rust-backend.html"><strong aria-hidden="true">5.1.</strong> Rust Backend</a></li><li class="chapter-item "><a href="../architecture/02-svelte-frontend.html"><strong aria-hidden="true">5.2.</strong> Svelte Frontend</a></li><li class="chapter-item "><a href="../architecture/03-data-flow.html"><strong aria-hidden="true">5.3.</strong> Data Flow</a></li><li class="chapter-item "><a href="../architecture/04-type-sync-and-threading.html"><strong aria-hidden="true">5.4.</strong> Type Sync & Threading</a></li><li class="chapter-item "><a href="../architecture/05-platform-backends.html"><strong aria-hidden="true">5.5.</strong> Platform Backends</a></li><li class="chapter-item "><a href="../architecture/06-downloads-and-offline.html"><strong aria-hidden="true">5.6.</strong> Downloads & Offline</a></li><li class="chapter-item "><a href="../architecture/07-connectivity.html"><strong aria-hidden="true">5.7.</strong> Connectivity</a></li><li class="chapter-item "><a href="../architecture/08-database-design.html"><strong aria-hidden="true">5.8.</strong> Database Design</a></li><li class="chapter-item "><a href="../architecture/09-security.html"><strong aria-hidden="true">5.9.</strong> Security</a></li></ol></li><li class="chapter-item expanded "><li class="part-title">UX</li><li class="chapter-item expanded "><a href="../ux-flows.html"><strong aria-hidden="true">6.</strong> UX Flows</a></li><li class="chapter-item expanded affix "><li class="part-title">Specs — Pending Work</li><li class="chapter-item expanded "><a href="../specs/index.html"><strong aria-hidden="true">7.</strong> Specs Index</a></li><li class="chapter-item expanded "><a href="../specs/SPEC-TEMPLATE.html"><strong aria-hidden="true">8.</strong> Spec Template</a></li><li class="chapter-item expanded "><a href="../specs/SPEC-REVIEW-CHECKLIST.html"><strong aria-hidden="true">9.</strong> Spec Review Checklist</a></li><li class="chapter-item expanded "><a href="../specs/playback-backend-unification.html"><strong aria-hidden="true">10.</strong> Playback Backend Unification</a></li><li class="chapter-item expanded "><a href="../specs/linux-native-video-spike.html"><strong aria-hidden="true">11.</strong> Linux Native Video Spike</a></li><li class="chapter-item expanded "><a href="../specs/backend-owned-stream-selection.html"><strong aria-hidden="true">12.</strong> Backend-Owned Stream Selection</a></li><li class="chapter-item expanded "><a href="../specs/player-facade-enforcement.html"><strong aria-hidden="true">13.</strong> Player Facade Enforcement</a></li><li class="chapter-item expanded "><a href="../specs/windows-native-audio-backend.html" class="active"><strong aria-hidden="true">14.</strong> Windows Native Audio Backend</a></li><li class="chapter-item expanded "><a href="../specs/libmpv2-migration.html"><strong aria-hidden="true">15.</strong> libmpv2 Migration</a></li><li class="chapter-item expanded "><a href="../specs/read-through-media-cache.html"><strong aria-hidden="true">16.</strong> Read-Through Media Cache</a></li><li class="chapter-item expanded "><a href="../specs/scoped-search.html"><strong aria-hidden="true">17.</strong> Scoped Search</a></li><li class="chapter-item expanded "><a href="../specs/scoped-search-boundary.html"><strong aria-hidden="true">18.</strong> Scoped Search Boundary</a></li><li class="chapter-item expanded "><a href="../specs/scoped-search-boundary-implementation.html"><strong aria-hidden="true">19.</strong> Scoped Search Boundary — Implementation</a></li><li class="chapter-item expanded "><a href="../specs/frontend-domain-model.html"><strong aria-hidden="true">20.</strong> Frontend Domain Model</a></li><li class="chapter-item expanded "><a href="../specs/desktop-native-video.html"><strong aria-hidden="true">21.</strong> Desktop Native Video</a></li><li class="chapter-item expanded "><a href="../specs/build-provenance.html"><strong aria-hidden="true">22.</strong> Build Provenance</a></li><li class="chapter-item expanded affix "><li class="part-title">Build & Release</li><li class="chapter-item expanded "><a href="../build/build-release.html"><strong aria-hidden="true">23.</strong> Build & Release</a></li><li class="chapter-item expanded "><a href="../release-checklist.html"><strong aria-hidden="true">24.</strong> Release Checklist</a></li><li class="chapter-item expanded "><a href="../native-player-verification.html"><strong aria-hidden="true">25.</strong> Native Player Verification</a></li><li class="chapter-item expanded "><a href="../build/build-desktop-packages.html"><strong aria-hidden="true">26.</strong> Desktop Packaging</a></li><li class="chapter-item expanded "><a href="../build/build-windows.html"><strong aria-hidden="true">27.</strong> Windows Build</a></li><li class="chapter-item expanded "><a href="../defect-windows.html"><strong aria-hidden="true">28.</strong> Defect Windows</a></li><li class="chapter-item expanded "><a href="../build/docker.html"><strong aria-hidden="true">29.</strong> Docker</a></li><li class="chapter-item expanded "><a href="../build/build-builder-image.html"><strong aria-hidden="true">30.</strong> Builder Image</a></li><li class="chapter-item expanded affix "><li class="spacer"></li><li class="chapter-item expanded affix "><a href="../api-redirect.html">Rust API Reference (rustdoc)</a></li></ol>
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
<div class="sidebar-resize-indicator"></div>
</div>
</nav>
<!-- Track and set sidebar scroll position -->
<script>
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
sidebarScrollbox.addEventListener('click', function(e) {
if (e.target.tagName === 'A') {
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
}
}, { passive: true });
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
sessionStorage.removeItem('sidebar-scroll');
if (sidebarScrollTop) {
// preserve sidebar scroll position when navigating via links within sidebar
sidebarScrollbox.scrollTop = sidebarScrollTop;
} else {
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
var activeSection = document.querySelector('#sidebar .active');
if (activeSection) {
activeSection.scrollIntoView({ block: 'center' });
}
}
</script>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky">
<div class="left-buttons">
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</label>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">JellyTau Documentation</h1>
<div class="right-buttons">
<a href="../print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
<a href="https://gitea.tourolle.paris/dtourolle/jellytau" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa fa-github"></i>
</a>
<a href="https://gitea.tourolle.paris/dtourolle/jellytau/_edit/master/docs/../docs/specs/windows-native-audio-backend.md" title="Suggest an edit" aria-label="Suggest an edit">
<i id="git-edit-button" class="fa fa-edit"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h1 id="spec-windows-native-audio-backend"><a class="header" href="#spec-windows-native-audio-backend">Spec: Windows native audio backend</a></h1>
<p><strong>Status:</strong> Proposed — not started. Windows still runs on
<code>WebviewAudioBackend</code>. Blocked on <a href="libmpv2-migration.html">libmpv2-migration.md</a>,
whose crate swap has not landed either.
<strong>Requirements:</strong> UR-003, UR-027, UR-032, UR-033 → DR-030, DR-035, DR-036;
⚠️ the suggested id <strong>IR-030 has since been allocated</strong> to the scheduled catalog
crawl — allocate a fresh id (IR-033 or later) on implementation
<strong>UX spec:</strong> n/a — Settings Audio already renders the controls
<strong>Supersedes / revises:</strong> acts on the "audio can unify, video cannot" conclusion in <a href="playback-backend-unification.html">playback-backend-unification.md</a></p>
<h2 id="summary"><a class="header" href="#summary">Summary</a></h2>
<p>Give Windows a real native audio backend instead of the current webview
<code>&lt;audio&gt;</code> shim. Windows is the only platform where audio playback has no decoder
of its own: <code>WebviewAudioBackend</code> hands a URL to a frontend <code>&lt;audio&gt;</code> element and
relays transport commands. It cannot set volume, cannot apply any audio setting,
and reports state only via DOM events.</p>
<p>Audio needs no rendering surface, so <strong>none of the webview-compositing problems
that block unified video apply here.</strong> This is the cleanest available win.</p>
<h2 id="motivation"><a class="header" href="#motivation">Motivation</a></h2>
<p><code>WebviewAudioBackend</code> 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 <code>webview_audio_backend.rs</code>:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>fn set_volume(&amp;mut self, volume: f32) -&gt; Result&lt;(), PlayerError&gt; {
// ...stores locally only; there is no ControlCommand action for volume
}
<span class="boring">}</span></code></pre></pre>
<p>So volume changes never reach the element; the frontend has to observe the player
store and apply volume itself. <code>set_audio_settings</code> likewise stores values that
nothing consumes — EQ, normalization, and gapless are all inert on Windows.</p>
<p>Meanwhile the backend-unification investigation established that a native <em>audio</em>
engine is unproblematic on Windows specifically: <code>tauri-plugin-libmpv</code> lists
Windows as its <strong>fully tested</strong> platform (in contrast to Linux, where embedding
is broken — but that is a <em>video surface</em> problem, which audio does not have).</p>
<h2 id="layer-assignment"><a class="header" href="#layer-assignment">Layer assignment</a></h2>
<div class="table-wrapper"><table><thead><tr><th>Logic / responsibility</th><th>Layer</th><th>Why it belongs there</th></tr></thead><tbody>
<tr><td>Decoding and playing the audio stream</td><td>Rust</td><td>Playback is domain logic; every other platform already decodes in Rust or a native player. The webview shim is the anomaly.</td></tr>
<tr><td>Applying <code>AudioSettings</code> (EQ/normalize/gapless)</td><td>Rust</td><td>Same <code>AudioSettings</code> contract as MPV/ExoPlayer; band layout and presets stay canonical in <code>settings.rs</code>.</td></tr>
<tr><td>Position/state reporting</td><td>Rust</td><td>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.</td></tr>
<tr><td>Volume</td><td>Rust</td><td>Currently broken precisely because it is split across the boundary.</td></tr>
<tr><td>Rendering the player UI</td><td>Frontend</td><td>Unchanged.</td></tr>
</tbody></table>
</div>
<p>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 <code>player_report_*</code> round-trip exists to paper over it.</p>
<h2 id="design"><a class="header" href="#design">Design</a></h2>
<h3 id="engine-choice"><a class="header" href="#engine-choice">Engine choice</a></h3>
<p>Two viable options; <strong>libmpv is recommended</strong> for consistency with the Linux
audio backend.</p>
<div class="table-wrapper"><table><thead><tr><th></th><th>libmpv</th><th>GStreamer</th></tr></thead><tbody>
<tr><td>Windows status</td><td><code>tauri-plugin-libmpv</code> reports fully tested</td><td>✅ works, but…</td></tr>
<tr><td>Rust bindings</td><td><code>libmpv2</code> 6.0.0, active</td><td><code>gstreamer-rs</code> 0.25.x, excellent</td></tr>
<tr><td>Cross-MSVC from Linux</td><td>⚠️ needs prebuilt DLL + import lib</td><td><code>gstreamer-sys</code> uses pkg-config, fights <code>cargo-xwin</code></td></tr>
<tr><td>Code reuse</td><td><code>MpvBackend</code> logic is directly reusable</td><td>❌ a second engine to learn</td></tr>
<tr><td>Crossfade capable</td><td>❌ single-stream chain</td><td><code>audiomixer</code></td></tr>
</tbody></table>
</div>
<p>libmpv wins on reuse: <code>MpvBackend</code>'s <code>set_audio_settings</code> — the <code>af</code> lavfi graph
built by <code>build_af_filter</code>, <code>eq_filter_entries</code>, <code>normalize_filter_entry</code> — is
platform-independent and would apply unchanged.</p>
<p>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 <em>both</em> Linux and Windows, which is a much larger change.</p>
<h3 id="structure"><a class="header" href="#structure">Structure</a></h3>
<p>Rename the cfg gate so <code>MpvBackend</code> is no longer Linux-only:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// src-tauri/src/player/mod.rs
#[cfg(any(target_os = "linux", target_os = "windows"))]
pub mod mpv_backend;
<span class="boring">}</span></code></pre></pre>
<p><code>MpvBackend::new</code> needs one platform-specific branch: <code>detect_audio_system()</code>
currently probes <code>pactl</code>/<code>pw-cli</code>/<code>/proc/asound/cards</code> to pick an <code>ao</code>. On
Windows the equivalent is <code>wasapi</code> (mpv's default), so the detection is a
<code>#[cfg]</code> returning <code>"wasapi"</code> — no probing needed.</p>
<p>Everything else — the event loop, the 250ms position thread, the seek-suppression
window, the <code>af</code> filter graph — is unchanged.</p>
<p><code>WebviewAudioBackend</code> stays for other targets (macOS and anything else hitting
the <code>not(any(...))</code> arm) and as the fallback if libmpv fails to initialize. The
existing <code>emit_backend_init_failed</code> path already handles that gracefully.</p>
<h3 id="build"><a class="header" href="#build">Build</a></h3>
<p><code>libmpv2-sys</code> is well-suited to cross-compilation: no pkg-config, vendored
headers, pregenerated bindings (no libclang). It emits <code>cargo:rustc-link-lib=mpv</code>
unconditionally, so the build must supply a linkable import library for
<code>x86_64-pc-windows-msvc</code>.</p>
<p>Keep the <code>build_libmpv</code> feature <strong>off</strong> — its Unix path shells out to mpv-build
and explicitly rejects cross-compilation.</p>
<p>🔴 Per CLAUDE.md, the prebuilt libmpv <strong>must be added to the builder image</strong>
(<code>Dockerfile.builder</code> → rebuild + push via <code>scripts/build-builder-image.sh</code>), not
installed at CI job time. <code>libmpv-2.dll</code> must also be bundled into the NSIS
installer via <code>tauri.conf.json</code>'s resources.</p>
<h3 id="verified-build-mechanics"><a class="header" href="#verified-build-mechanics">Verified build mechanics</a></h3>
<p>The cross-compile path was tested hands-on from Linux (July 2026), not inferred:</p>
<ul>
<li>Neither shinchiro nor zhongfly ships an <code>mpv.def</code> or MSVC <code>mpv.lib</code> — only a
MinGW <code>libmpv.dll.a</code>. (Several online sources claim otherwise; they are wrong.)</li>
<li>An MSVC-style import lib can be generated locally with LLVM tools only:
<code>llvm-readobj --coff-exports libmpv-2.dll</code> → synthesize <code>mpv.def</code>
<code>llvm-dlltool -m i386:x86-64 -d mpv.def -l mpv.lib</code>. <code>llvm-lib /def:</code> produces a
byte-identical result.</li>
<li>A real <code>lld-link</code> link against that import lib <strong>succeeds</strong>, and the resulting
import table resolves <code>mpv_client_api_version</code> from <code>libmpv-2.dll</code>. <code>lld-link</code>
is the linker <code>cargo-xwin</code> uses, so this is the load-bearing step.</li>
<li>Linking directly against the shipped MinGW <code>libmpv.dll.a</code> <strong>also</strong> succeeds, so
def-generation may be skippable — but that relies on lld's GNU-archive
tolerance rather than a documented contract. Keep <code>llvm-dlltool</code> as the
fallback.</li>
<li>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 <em>build</em> mpv is frequently misread as "MSVC cannot <em>link</em> libmpv" — that
is not what it says.</li>
<li>🔴 Never free/realloc across the DLL boundary — use <code>mpv_free</code>.</li>
</ul>
<p>Build wiring is ordinary: <code>cargo:rustc-link-lib=dylib=mpv</code> plus
<code>cargo:rustc-link-search</code>. Nothing about libmpv conflicts with <code>cargo-xwin</code>.</p>
<h3 id="size-and-shipping"><a class="header" href="#size-and-shipping">Size and shipping</a></h3>
<p>Measured uncompressed: <strong>93 MiB</strong> (zhongfly <code>mpv-dev-lgpl-x86_64</code>) vs <strong>112 MiB</strong>
(shinchiro, full GPL build); ~2630 MB compressed in the <code>.7z</code>.</p>
<p><strong>Ship the zhongfly LGPL build</strong> — smaller, and there is no reason to pull the
GPL variant in for an audio-only use.</p>
<p>Import-table inspection confirms <strong>no companion DLLs are needed</strong>: every
dependency is a system DLL (<code>KERNEL32</code>, <code>USER32</code>, <code>d2d1</code>, <code>DWrite</code>, <code>OPENGL32</code>,
<code>vulkan-1</code>, UCRT <code>api-ms-win-*</code>). One file to bundle.</p>
<p>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.</p>
<h2 id="out-of-scope"><a class="header" href="#out-of-scope">Out of scope</a></h2>
<ul>
<li>Windows <em>video</em>. Stays in WebView2 + hls.js — it works and has ABR.</li>
<li>Crossfade (UR-031/DR-034) — not implemented anywhere; needs its own spec.</li>
<li>Replacing <code>WebviewAudioBackend</code> for macOS.</li>
<li>MPRIS/SMTC media-key integration — worth a follow-up, not this spec.</li>
</ul>
<h2 id="acceptance-criteria"><a class="header" href="#acceptance-criteria">Acceptance criteria</a></h2>
<ul>
<li><input disabled="" type="checkbox"/>
Windows build produces a <code>MpvBackend</code>-backed player; <code>backend-init-failed</code> is emitted (not a crash) if libmpv is unavailable.</li>
<li><input disabled="" type="checkbox"/>
Volume control works from the UI — the current hard gap.</li>
<li><input disabled="" type="checkbox"/>
EQ, normalization, and gapless audibly take effect on Windows.</li>
<li><input disabled="" type="checkbox"/>
Position/state originate in Rust; the <code>&lt;audio&gt;</code> element is no longer in the audio path.</li>
<li><input disabled="" type="checkbox"/>
Seek, next/previous, and queue advance work; sleep timer stops playback.</li>
<li><input disabled="" type="checkbox"/>
<code>libmpv-2.dll</code> ships in the NSIS installer and the app runs on a clean Windows VM with no mpv installed.</li>
<li><input disabled="" type="checkbox"/>
Builder image carries the Windows libmpv artefacts; <strong>no toolchain install added to any CI step</strong>.</li>
<li><input disabled="" type="checkbox"/>
<code>bun run check</code>, <code>bun run test</code>, <code>bun run check:boundary</code> pass.</li>
<li><input disabled="" type="checkbox"/>
<code>cargo fmt</code> clean, <code>cargo clippy</code> clean, <code>bun run test:rust</code> passes.</li>
<li><input disabled="" type="checkbox"/>
New requirement-implementing code carries <code>// TRACES:</code> comments.</li>
</ul>
<h2 id="testing"><a class="header" href="#testing">Testing</a></h2>
<p><strong>Rust</strong>: the existing <code>mpv_backend_test.rs</code> and the <code>build_af_filter</code> /
<code>normalize_filter_entry</code> / <code>eq_filter_entries</code> unit tests already cover the
filter-graph logic and are platform-independent — they should pass unchanged
under a Windows <code>cargo check</code>/test. Add a test asserting <code>detect_audio_system()</code>
returns <code>wasapi</code> under <code>cfg(windows)</code>.</p>
<p><strong>Manual, on Windows</strong>: 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.</p>
<p>Per CLAUDE.md, the volume gap is a <em>bug fix</em>: write a failing test for
"<code>set_volume</code> reaches the backend" before implementing.</p>
<h2 id="traces"><a class="header" href="#traces">TRACES</a></h2>
<ul>
<li>Windows <code>MpvBackend</code> construction in <code>create_player_backend</code><code>// TRACES: UR-003 | IR-030</code></li>
<li><code>detect_audio_system</code> Windows branch → <code>IR-030</code></li>
<li>Existing <code>set_audio_settings</code> gains Windows coverage → <code>UR-027, UR-032, UR-033 | DR-030, DR-035, DR-036</code></li>
<li>Allocate <strong>IR-030</strong> in <code>requirements.md</code> ("libmpv integration for Windows audio playback").</li>
</ul>
<h2 id="notes-for-the-implementer"><a class="header" href="#notes-for-the-implementer">Notes for the implementer</a></h2>
<ul>
<li>Do this <strong>after</strong> <a href="libmpv2-migration.html">libmpv2-migration.md</a> — porting the
current dead <code>libmpv</code> git pin to a second platform would double the migration
work.</li>
<li><code>libmpv2</code> has broken its API in every major release (4.0 removed command
helpers, 5.0 removed <code>mpv_node</code>, 6.0 changed <code>RenderContext</code> ownership). Pin an
exact version.</li>
<li>Only the <code>render</code>-feature parts of <code>libmpv2</code> concern video; audio-only use does
not need it, and disabling the default <code>render</code> feature may shrink the build.</li>
<li>A parallel Claude session may be active — <code>git diff</code> first.</li>
</ul>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../specs/player-facade-enforcement.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../specs/libmpv2-migration.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="../specs/player-facade-enforcement.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../specs/libmpv2-migration.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<script>
window.playground_copyable = true;
</script>
<script src="../elasticlunr.min.js"></script>
<script src="../mark.min.js"></script>
<script src="../searcher.js"></script>
<script src="../clipboard.min.js"></script>
<script src="../highlight.js"></script>
<script src="../book.js"></script>
<!-- Custom JS scripts -->
</div>
</body>
</html>