422 lines
32 KiB
HTML
422 lines
32 KiB
HTML
<!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><audio></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><audio></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(&mut self, volume: f32) -> Result<(), PlayerError> {
|
||
// ...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); ~26–30 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><audio></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>
|