655 lines
50 KiB
HTML
655 lines
50 KiB
HTML
<!DOCTYPE HTML>
|
||
<html lang="en" class="navy" dir="ltr">
|
||
<head>
|
||
<!-- Book generated using mdBook -->
|
||
<meta charset="UTF-8">
|
||
<title>Linux Native Video Spike - 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" class="active"><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"><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/build-provenance.html"><strong aria-hidden="true">21.</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">22.</strong> Build & Release</a></li><li class="chapter-item expanded "><a href="../release-checklist.html"><strong aria-hidden="true">23.</strong> Release Checklist</a></li><li class="chapter-item expanded "><a href="../build/build-desktop-packages.html"><strong aria-hidden="true">24.</strong> Desktop Packaging</a></li><li class="chapter-item expanded "><a href="../build/build-windows.html"><strong aria-hidden="true">25.</strong> Windows Build</a></li><li class="chapter-item expanded "><a href="../defect-windows.html"><strong aria-hidden="true">26.</strong> Defect Windows</a></li><li class="chapter-item expanded "><a href="../build/docker.html"><strong aria-hidden="true">27.</strong> Docker</a></li><li class="chapter-item expanded "><a href="../build/build-builder-image.html"><strong aria-hidden="true">28.</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/linux-native-video-spike.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-linux-native-video--bounded-compositing-spike"><a class="header" href="#spec-linux-native-video--bounded-compositing-spike">Spec: Linux native video — bounded compositing spike</a></h1>
|
||
<p><strong>Status:</strong> <strong>Run 2026-08-21 — compositing works; G5 carries an open crash.</strong>
|
||
The compositing claim it set out to test is falsified on Linux. See "Result".
|
||
This file stays open until the implementation spec exists; ABR is unresolved.
|
||
<strong>Requirements:</strong> none allocated. This spike produces a decision record, not
|
||
product code — same shape as
|
||
<a href="playback-backend-unification.html">playback-backend-unification.md</a>, which is
|
||
Accepted with no requirement ids of its own. Ids are allocated by the
|
||
<em>implementation</em> spec that follows a green result.
|
||
<strong>UX spec:</strong> n/a
|
||
<strong>Supersedes / revises:</strong> re-opens finding 2 of
|
||
<a href="playback-backend-unification.html">playback-backend-unification.md</a> on Linux only.
|
||
Its findings 3, 4, 5 and 6 stand unchallenged and are <strong>not</strong> in scope here.</p>
|
||
<p><strong>Destination on completion:</strong>
|
||
<a href="../architecture/05-platform-backends.html">05-platform-backends.md</a> — a "Native
|
||
Video Compositing (Linux)" section alongside the existing Android one. The
|
||
durable half is the mechanism and the two traps below; the gates and phases are
|
||
disposable.</p>
|
||
<h2 id="summary"><a class="header" href="#summary">Summary</a></h2>
|
||
<p>Test one falsifiable claim: <em>a native video surface cannot be composited with a
|
||
Tauri webview on Linux.</em> The claim is load-bearing — it is why Linux video goes
|
||
through an h264 HLS transcode into a WebKitGTK <code><video></code> element instead of
|
||
decoding directly in the mpv instance we already run. The spike renders one mpv
|
||
frame beneath the webview, on both X11 and Wayland, and stops. It ships no
|
||
product code and flips no defaults.</p>
|
||
<p>A green result does <strong>not</strong> authorise native video on Linux; it authorises
|
||
writing the spec that would.</p>
|
||
<h2 id="motivation"><a class="header" href="#motivation">Motivation</a></h2>
|
||
<p><a href="playback-backend-unification.html">playback-backend-unification.md</a> finding 2
|
||
concluded that native video cannot be composited with a Tauri webview, on
|
||
evidence from <code>tauri-plugin-libmpv</code>'s platform table, wry#284, tauri#6343, and a
|
||
Tauri maintainer's 2024 statement that a GTK widget as a child X11 window is
|
||
"a bit hacky and it is not possible on Wayland at all."</p>
|
||
<p>Two things have changed since that was written, and one thing was never tested.</p>
|
||
<p><strong>1. The general claim has already been falsified on one platform — by us.</strong>
|
||
Android now renders ExoPlayer video on a TextureView at index 0 <em>behind a
|
||
transparent Tauri WebView</em>, with the Svelte controls drawn over it, on by
|
||
default. See
|
||
<a href="../architecture/05-platform-backends.html#native-video-compositing-android">05-platform-backends.md</a>.
|
||
That is exactly the composition finding 2 said was impossible, shipped. What
|
||
survives of the finding is a narrower, WebKitGTK-specific claim — which is worth
|
||
testing on its own terms rather than inheriting.</p>
|
||
<p><strong>2. A Tauri app now ships Linux native mpv as an active platform.</strong>
|
||
<a href="https://github.com/MaxMB15/MaxVideoPlayer">MaxVideoPlayer</a> (354 commits) embeds
|
||
libmpv via <strong>EGL + X11 child window / Wayland subsurface</strong>, with Linux and macOS
|
||
active and Windows only planned — the inverse of the plugin matrix finding 2
|
||
sampled. Its existence does not prove our case works, but it does mean the
|
||
Wayland half of the maintainer quote is out of date.</p>
|
||
<p><strong>3. The render API was never tested.</strong> Every source in finding 2 describes
|
||
<em>foreign-window embedding</em>: <code>--wid</code>, child windows, a second toplevel
|
||
position-synced to a <code>getBoundingClientRect()</code> div. That is a different mechanism
|
||
from mpv's render API, where <strong>we</strong> own the GL context and mpv draws into an FBO
|
||
we hand it (<code>mpv_render_context_create</code> / <code>mpv_render_context_render</code>, with an
|
||
upstream <a href="https://github.com/mpv-player/mpv-examples/pull/44/files">GTK example</a>).
|
||
Tauri v2 exposes <code>WebviewWindow::gtk_window()</code> and <code>default_vbox()</code>, so the
|
||
target is a widget inside Tauri's own GTK tree — not a foreign window, not a
|
||
second toplevel, and therefore not the thing that was found broken.</p>
|
||
<p>The prize is direct play: no h264 transcode, hardware decode, libass subtitles,
|
||
and no server CPU burned on every Linux play.</p>
|
||
<h2 id="the-blocker-a-green-spike-does-not-clear"><a class="header" href="#the-blocker-a-green-spike-does-not-clear">The blocker a green spike does not clear</a></h2>
|
||
<p>🔴 <strong>Read this before treating a green result as a green light.</strong></p>
|
||
<p>Finding 3 of the unification spec stands: <strong>mpv has no adaptive bitrate.</strong> It
|
||
delegates HLS to FFmpeg's demuxer, which picks one variant at open and never
|
||
adapts. The webview path has real ABR via hls.js. Compositing is necessary for
|
||
native video on Linux; it is not sufficient.</p>
|
||
<p>There is a plausible answer, and this spike exists partly to make it testable:
|
||
<strong>ABR only matters on the transcode path.</strong> A direct-played file has no variant
|
||
ladder to adapt between — the adaptation the server offers <em>is</em> the transcode.
|
||
So "mpv when the stream is direct-play, HTML5 + hls.js when the server
|
||
transcodes" would sidestep finding 3 rather than fight it, and it maps onto a
|
||
decision Rust already makes when it builds the stream URL.</p>
|
||
<p>That is a <strong>hypothesis, not a conclusion.</strong> It is out of scope here. Record it in
|
||
the spike's decision note so the follow-up spec starts from it.</p>
|
||
<h2 id="layer-assignment"><a class="header" href="#layer-assignment">Layer assignment</a></h2>
|
||
<p>The spike introduces no product logic. The table below is the assignment the
|
||
<em>follow-up</em> would inherit, written now so a green result cannot drift into
|
||
frontend decisions during implementation.</p>
|
||
<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>Which backend renders video on this platform (<code>use_html5_element</code>, <code>supports_native_video</code>)</td><td>Rust</td><td>Already there — <code>get_player_status</code> in <code>commands/player/mod.rs</code> computes it from a <code>cfg!</code>. The spike would widen that <code>cfg!</code>, not relocate the decision. The frontend already consumes it via <code>createAdapter</code>.</td></tr>
|
||
<tr><td>Whether <em>this stream</em> is direct-play or transcoded, and therefore whether mpv or hls.js renders it</td><td>Rust</td><td>Domain. It depends on Jellyfin's <code>PlaybackInfo</code> response, container/codec support, and the bitrate cap — all of which change when Jellyfin's API or our quality ladder changes. The frontend must never re-derive it from a URL shape.</td></tr>
|
||
<tr><td>Creating, sizing, and destroying the GL surface; the mpv render context</td><td>Rust</td><td>Owns the backend and the GTK window handle. There is no presentation decision in it.</td></tr>
|
||
<tr><td>Where controls, subtitles, and the mini-player sit above the video, and the letterbox/poster treatment</td><td>Frontend</td><td>Pure presentation; changes only if the UI is redesigned. Precisely the split the Android path already uses.</td></tr>
|
||
<tr><td>Reserving the video rectangle in layout and marking the shell transparent</td><td>Frontend</td><td>Presentation. <code>nativeVideo.ts</code> + the <code>[data-native-video="active"]</code> rule in <code>app.css</code> already do this for Android and are platform-agnostic.</td></tr>
|
||
</tbody></table>
|
||
</div>
|
||
<p>Borderline row, stated with its tie-breaker: <em>"is the surface currently
|
||
attached?"</em> reads like view state, but the Android work found that a surface left
|
||
in the hierarchy outlives its player (DR-184). Attachment is backend lifecycle →
|
||
<strong>Rust</strong>, with the frontend told about it, not asked.</p>
|
||
<h2 id="design"><a class="header" href="#design">Design</a></h2>
|
||
<p>A throwaway branch. No merge to <code>master</code> except the decision note.</p>
|
||
<h3 id="what-gets-built"><a class="header" href="#what-gets-built">What gets built</a></h3>
|
||
<p>One <code>#[cfg(target_os = "linux")]</code> experiment behind a feature flag, in a scratch
|
||
binary or an ignored test — <strong>not</strong> in <code>MpvBackend</code>'s constructor path:</p>
|
||
<ol>
|
||
<li>From <code>app.get_webview_window(...)</code>, take <code>gtk_window()</code> and <code>default_vbox()</code>.</li>
|
||
<li>Reparent the webview into a <code>gtk::Overlay</code>: <code>GLArea</code> as the main child, the
|
||
webview as the overlay child.</li>
|
||
<li>Set the webview background to fully transparent (wry does this when
|
||
<code>"transparent": true</code>; verify it reaches <code>webkit_web_view_set_background_color</code>).</li>
|
||
<li>In the <code>GLArea</code>'s <code>render</code> signal, drive
|
||
<code>mpv_render_context_render</code> with <code>MPV_RENDER_PARAM_OPENGL_FBO</code> pointing at the
|
||
FBO GTK bound for us.</li>
|
||
<li>Play one local file. Draw an opaque HTML element over the video area.</li>
|
||
</ol>
|
||
<p><code>video = no</code> and <code>audio-display = no</code> are set in
|
||
<a href="../../src-tauri/src/player/mpv_backend.rs#L135-L141">mpv_backend.rs:135-141</a>;
|
||
the spike overrides them on its own <code>Mpv</code> handle rather than editing that path.</p>
|
||
<h3 id="bindings"><a class="header" href="#bindings">Bindings</a></h3>
|
||
<p>The current pin is <code>libmpv = { git = "…/libmpv-rs", branch = "master" }</code> — the
|
||
dead pin <a href="libmpv2-migration.html">libmpv2-migration.md</a> exists to replace. The
|
||
render API lives in <code>libmpv2-sys</code> (<code>mpv_render_context_render</code>); the safe wrapper
|
||
was only ever a PR against the old crate. <strong>Use <code>libmpv2-sys</code> raw FFI directly in
|
||
the spike.</strong> Do not block the spike on the migration, and do not let the spike
|
||
half-perform it — if the spike goes green the migration becomes a hard
|
||
prerequisite of the implementation, which is the ordering
|
||
<a href="windows-native-audio-backend.html">windows-native-audio-backend.md</a> already sits
|
||
in.</p>
|
||
<h3 id="ipc"><a class="header" href="#ipc">IPC</a></h3>
|
||
<p>None. The spike crosses no boundary. If it goes green, the follow-up changes only
|
||
the <em>value</em> of the existing <code>useHtml5Element</code> / <code>supportsNativeVideo</code> fields — no
|
||
new wire shapes, no <code>bindings.ts</code> regeneration.</p>
|
||
<h2 id="gates"><a class="header" href="#gates">Gates</a></h2>
|
||
<p>Each is pass/fail with a named failure. Stop at the first red and write it up —
|
||
a red result is a successful spike.</p>
|
||
<div class="table-wrapper"><table><thead><tr><th>#</th><th>Question</th><th>Fails if</th></tr></thead><tbody>
|
||
<tr><td>G1</td><td>Can a custom GTK widget join Tauri's widget tree and survive the window's lifetime?</td><td><code>default_vbox()</code> is absent/unusable, or reparenting the webview breaks input or crashes.</td></tr>
|
||
<tr><td>G2</td><td>Does the webview still paint, with a transparent backdrop, over that widget?</td><td>The backdrop renders opaque black (<a href="https://github.com/tauri-apps/wry/issues/1540">wry#1540</a>) or the webview stops repainting (<a href="https://github.com/tauri-apps/tauri/issues/12800">tauri#12800</a>). <strong>This is the highest-risk gate.</strong></td></tr>
|
||
<tr><td>G3</td><td>Does mpv render a frame into our FBO?</td><td>The render context refuses GTK's context, or frames land in the wrong buffer.</td></tr>
|
||
<tr><td>G4</td><td>Does HTML drawn over the video area actually appear over it?</td><td>Video covers the controls — the exact failure wry#284 and tauri#6343 report. Without this, the whole thing is worthless: our controls, subtitles and mini-player all sit over the video.</td></tr>
|
||
<tr><td>G5</td><td>Does it survive resize, fullscreen, and SPA navigation away and back?</td><td>Flicker on resize, or a surface that outlives its route.</td></tr>
|
||
<tr><td>G6</td><td>Does it hold on <strong>both</strong> X11 and Wayland?</td><td>Either session backend fails. Wayland is the one the 2024 maintainer quote says is impossible — test it first, not last.</td></tr>
|
||
</tbody></table>
|
||
</div>
|
||
<p>G6 is not a nice-to-have. A result that only holds on X11 is red for a project
|
||
shipping to current desktops.</p>
|
||
<h3 id="time-box"><a class="header" href="#time-box">Time box</a></h3>
|
||
<p>If G1–G4 are not all green, stop and write the result up. The value of this spike
|
||
is a dated, method-specific answer — including "still no, and here is the
|
||
mechanism" — not a working player.</p>
|
||
<h2 id="result-2026-08-21"><a class="header" href="#result-2026-08-21">Result (2026-08-21)</a></h2>
|
||
<p>Run on GNOME, kernel 7.1.8, libmpv 2.5.0 (mpv 0.41.0), GTK 3.24.52, WebKitGTK
|
||
2.52.6, wry 0.53.5 — the versions <code>src-tauri/Cargo.lock</code> resolves. Spike source:
|
||
a ~250-line standalone crate using wry + gtk + <code>libmpv2-sys</code> raw FFI, driving
|
||
mpv's render API with an update callback, frame-gated repaints and
|
||
<code>report_swap</code>.</p>
|
||
<div class="table-wrapper"><table><thead><tr><th>Gate</th><th>Result</th><th>Observed mechanism</th></tr></thead><tbody>
|
||
<tr><td>G1 widget in GTK tree</td><td>🟡 <strong>partial</strong></td><td><code>GtkOverlay</code> with <code>GtkGLArea</code> as main child and the wry webview as overlay child works, built directly. <strong>Tauri's own <code>default_vbox()</code> was not exercised</strong> — see below.</td></tr>
|
||
<tr><td>G2 webview paints transparently over it</td><td>✅ green</td><td><code>with_transparent(true)</code> alone. No window-level transparency was used or needed.</td></tr>
|
||
<tr><td>G3 mpv renders into our FBO</td><td>✅ green</td><td><code>vo=libmpv</code> + <code>mpv_render_context_create</code> with <code>MPV_RENDER_PARAM_OPENGL_FBO</code> into the FBO GTK binds.</td></tr>
|
||
<tr><td>G4 HTML over video</td><td>✅ green</td><td>Opaque panel and a translucent control bar both drew over moving video.</td></tr>
|
||
<tr><td>G5 resize / drag / fullscreen</td><td>🟡 <strong>green on appearance, suspect underneath</strong></td><td>No flicker, gap or misalignment, and smooth once frame pacing was correct (trap 3). But the only crash observed came from the only session where fullscreen was exercised — see "What is still open".</td></tr>
|
||
<tr><td>G6 X11 <strong>and</strong> Wayland</td><td>✅ green</td><td>Identical on both; <code>GDK_BACKEND</code> flipped between runs.</td></tr>
|
||
</tbody></table>
|
||
</div>
|
||
<p><strong>Finding 2 of <a href="playback-backend-unification.html">playback-backend-unification.md</a>
|
||
is false on Linux</strong> when tested by the render API rather than by foreign-window
|
||
embedding. Wayland — the half the 2024 maintainer quote called impossible — is
|
||
green.</p>
|
||
<p>Better than the gate asked for: the translucent bar composited <em>alpha</em> against
|
||
the video, not merely opaque-over. Scrims, gradient fades and subtitle backdrops
|
||
therefore work, which is most of how a player UI actually looks. mpv also painted
|
||
the letterbox bars black on its own — the Android equivalent was a shipped defect
|
||
(DR-194).</p>
|
||
<h3 id="three-traps-each-of-which-cost-a-debugging-cycle"><a class="header" href="#three-traps-each-of-which-cost-a-debugging-cycle">Three traps, each of which cost a debugging cycle</a></h3>
|
||
<p>Carry these into the implementation; each produced a failure that looked like a
|
||
platform limitation and was not.</p>
|
||
<ol>
|
||
<li><strong><code>LC_NUMERIC</code> must be reset <em>after</em> <code>gtk::init()</code>, not before.</strong> mpv refuses
|
||
to start under a non-C numeric locale. <code>mpv_backend.rs</code> already handles this,
|
||
but it has no GTK init in front of it; on this path <code>gtk::init()</code> applies the
|
||
user's locale afterwards and <code>mpv_create</code> returns null.</li>
|
||
<li><strong>libepoxy exports GL entry points as <em>data</em> symbols.</strong> There is no <code>glFoo</code>
|
||
function to resolve — there is <code>epoxy_glFoo</code>, a variable holding a lazily
|
||
resolving function pointer. <code>get_proc_address</code> must return the pointer <em>stored
|
||
at</em> that symbol; returning the symbol's own address makes mpv jump into
|
||
non-executable data and take SIGSEGV/SEGV_ACCERR on the first GL call. The
|
||
<code>epoxy</code> crate does this correctly but is unusable — its <code>gl_generator</code>
|
||
dependency pulls a yanked <code>xml-rs</code>.</li>
|
||
<li><strong>Frame pacing is not optional, and its symptom is misleading.</strong> Driving
|
||
<code>queue_render()</code> off the widget's frame clock on every tick, without calling
|
||
<code>mpv_render_context_report_swap</code> after each render, leaves mpv with nothing to
|
||
time against. Playback looks fine in a window and <strong>judders at fullscreen</strong> —
|
||
which reads as a compositing or GPU limit and is neither. The fix is to
|
||
register <code>mpv_render_context_set_update_callback</code>, redraw only when it says a
|
||
frame is ready, and report the swap afterwards. Fullscreen was smooth
|
||
immediately once both were in place.</li>
|
||
</ol>
|
||
<h3 id="hardware-decode-through-the-render-api"><a class="header" href="#hardware-decode-through-the-render-api">Hardware decode through the render API</a></h3>
|
||
<p>Tested by asking mpv what it actually selected (<code>hwdec-current</code>), not what it was
|
||
asked for. All three ran 20s clean at a steady 30 fps.</p>
|
||
<div class="table-wrapper"><table><thead><tr><th><code>hwdec</code></th><th><code>hwdec-current</code></th><th>Note</th></tr></thead><tbody>
|
||
<tr><td><code>vaapi</code></td><td><code>no</code></td><td><strong>Did not engage</strong> on this box — silently fell back to software. <code>vainfo</code> is not installed, so the libva driver for the Iris Xe iGPU is likely absent. No render-API error; this looks like a missing driver package, not a compositing limit.</td></tr>
|
||
<tr><td><code>auto</code></td><td><code>nvdec-copy</code></td><td>Hardware decode <strong>does</strong> work through the render API, on the discrete RTX 3050. Copy-back rather than zero-copy interop.</td></tr>
|
||
<tr><td><code>no</code></td><td><code>no</code></td><td>Software. Clean baseline.</td></tr>
|
||
</tbody></table>
|
||
</div>
|
||
<p>The load-bearing result is the middle row: <strong>hardware decode is compatible with
|
||
mpv's render API</strong>, so the direct-play prize is real and not traded away for
|
||
software decoding. Which decoder to prefer is an implementation question — on a
|
||
hybrid Intel+NVIDIA laptop <code>auto</code> reached for the discrete GPU in copy-back mode,
|
||
which is the least efficient hardware path. An implementation should evaluate
|
||
zero-copy VA-API on the iGPU (after confirming the driver is installed) before
|
||
accepting <code>auto</code>.</p>
|
||
<p><code>hwdec=auto-safe</code> probes Vulkan video decode, which this GPU does not support.
|
||
It logs two <code>Failed setup for format vulkan</code> / <code>no frame!</code> pairs at start-up and
|
||
then settles on <code>nvdec-copy</code> — the same place <code>auto</code> lands. A first reading of
|
||
these logs mistook the start-up pair for a per-frame flood; <strong>it is not</strong>. Every
|
||
run, clean or crashed, contains exactly two. <code>auto-safe</code> is not implicated in
|
||
anything.</p>
|
||
<h3 id="what-is-still-open"><a class="header" href="#what-is-still-open">What is still open</a></h3>
|
||
<ul>
|
||
<li>
|
||
<p><strong>The Tauri half of G1.</strong> The spike built its own <code>GtkOverlay</code>. The app must
|
||
instead reach <code>WebviewWindow::gtk_window()</code> / <code>default_vbox()</code> and reparent
|
||
Tauri's existing webview into an overlay. Low risk — the same widgets, one
|
||
extra reparent — but unproven, and it is the only place Tauri-specific
|
||
behaviour could still bite.</p>
|
||
</li>
|
||
<li>
|
||
<p>🔴 <strong>ABR — finding 3's premise is in doubt.</strong> Finding 3 says mpv would regress
|
||
streaming quality because "the webview path already has real ABR via hls.js".
|
||
Three pieces of evidence in this repo suggest that is <strong>not true of the URLs we
|
||
actually build</strong>:</p>
|
||
<ol>
|
||
<li><code>get_video_stream_url</code> (<code>repository/online.rs</code>) requests a <em>single</em>
|
||
rendition — one <code>VideoBitrate</code>, one <code>MaxStreamingBitrate</code>, one <code>MaxHeight</code>.
|
||
Jellyfin transcodes to what it is asked for; it does not build a ladder.</li>
|
||
<li>The frontend contains <strong>no level-handling code at all</strong> — no <code>hls.levels</code>,
|
||
no <code>LEVEL_SWITCH</code>, no <code>currentLevel</code>. The <code>abrEwma*</code> options in
|
||
<code>VideoPlayer.svelte</code> are default tuning with nothing to act on. hls.js is
|
||
serving as an HLS <em>demuxer</em> (WebKitGTK cannot play HLS natively), not as an
|
||
adaptation engine.</li>
|
||
<li>That function's own comment describes a quality switch as <strong>rebuilding the
|
||
URL</strong> — "every path that re-opens a stream (quality switch, transcoded seek,
|
||
audio-track switch)". Manual selection by stream re-open is what you build
|
||
when there is no adaptation, and mpv can do the same thing.</li>
|
||
</ol>
|
||
<p><strong>The decisive test has not been run</strong> and needs a live server plus an API key:
|
||
count <code>#EXT-X-STREAM-INF</code> lines in a real <code>master.m3u8</code>. One line means there
|
||
is no ABR to lose and this blocker disappears. More than one means finding 3
|
||
stands and the work below applies.</p>
|
||
<p>If ABR does turn out to be real, it belongs in <strong>Rust</strong>, not in mpv, and there
|
||
are three designs in increasing cost: pick the variant at open; re-open at a
|
||
new bitrate on sustained throughput drops (this is the quality-switch path the
|
||
app already has, so it is nearly free); or run a local proxy serving mpv a
|
||
synthesized single-variant playlist while swapping renditions underneath. The
|
||
middle option is almost certainly sufficient.</p>
|
||
<p>Either way the <strong>direct-play path still does not exist</strong> — every video play
|
||
currently goes through the HLS transcode endpoint. Building it is the real
|
||
project; the compositing work proven above is the smaller half.</p>
|
||
</li>
|
||
<li>
|
||
<p>🔴 <strong>One unexplained SIGSEGV.</strong> A ~180s
|
||
run died in a <em>decoder</em> thread (libavcodec -> <code>av_log</code> -> libmpv's log handler
|
||
-> libc). No Tauri, wry, WebKitGTK, GTK or GL frame appears anywhere in the
|
||
stack, so the fault is on the mpv/ffmpeg side of the process rather than in the
|
||
compositing seam.</p>
|
||
<p>Three hypotheses were tested and <strong>none reproduced it</strong>:</p>
|
||
<div class="table-wrapper"><table><thead><tr><th>Hypothesis</th><th>Test</th><th>Result</th></tr></thead><tbody>
|
||
<tr><td><code>hwdec=auto-safe</code>'s Vulkan failures</td><td>300s soak on <code>auto-safe</code></td><td>Survived. Also based on a misreading — the failures are 2 per run at start-up, not per-frame. Dead.</td></tr>
|
||
<tr><td>Fullscreen transitions recreating the GL context under mpv's render context</td><td>240s soak, ~120 automated transitions</td><td>Survived, no core dumped.</td></tr>
|
||
<tr><td>Continuous resize thrashing the GL framebuffer</td><td>240s soak, ~2000 resizes</td><td>Survived, no core dumped.</td></tr>
|
||
</tbody></table>
|
||
</div>
|
||
<p><strong>The crash is therefore unexplained.</strong> It was observed exactly once, in the
|
||
only session a human interacted with, and did not recur in ~13 minutes of
|
||
targeted stress across the three most plausible causes. It is recorded here
|
||
rather than dismissed precisely because nothing explains it: an intermittent
|
||
fault that nobody can reproduce is worse to inherit than a deterministic one,
|
||
not better.</p>
|
||
<p>The underlying concern stands regardless of which test eventually reproduces
|
||
it. A SIGSEGV in an unrelated thread is characteristic of memory corruption,
|
||
and this spike never calls <code>mpv_render_context_free</code> and never tears down on
|
||
<code>unrealize</code> — it has no defence against the GL context being recreated beneath
|
||
the render context. That is DR-184 on Android restated: a surface outliving its
|
||
player. An implementation must bind the two lifetimes together whether or not
|
||
this particular crash is ever explained.</p>
|
||
<p><strong>Therefore G5 is recorded green on appearance only</strong>, and this crash is the
|
||
single largest piece of unfinished business in the spike. Do not read the green
|
||
gates above as "safe to build on" until it is explained or a long soak clears
|
||
it.</p>
|
||
</li>
|
||
<li>
|
||
<p>Long-run stability, seeking, track switching, HDR, and multi-window were not
|
||
exercised at all.</p>
|
||
</li>
|
||
</ul>
|
||
<h2 id="out-of-scope"><a class="header" href="#out-of-scope">Out of scope</a></h2>
|
||
<ul>
|
||
<li>Any change to the shipping Linux video path. <code>experimentalNativeVideo</code> in
|
||
<code>adapters/index.ts</code> is a <strong>suppressor, never a promoter</strong>; the spike must not
|
||
change that.</li>
|
||
<li>Adaptive bitrate. See "The blocker a green spike does not clear".</li>
|
||
<li>Windows and macOS — different mechanisms, and <strong>Windows is the easier case, not
|
||
the endangered one</strong>. See below.</li>
|
||
<li>Android. Already shipped; it is the precedent, not the target.</li>
|
||
<li>Crossfade, the libmpv2 migration, and the audio-parity work.</li>
|
||
</ul>
|
||
<h3 id="why-windows-is-unaffected-and-cheaper"><a class="header" href="#why-windows-is-unaffected-and-cheaper">Why Windows is unaffected, and cheaper</a></h3>
|
||
<p>Nothing here can regress Windows. <code>use_html5_element</code> is already a per-platform
|
||
<code>cfg!</code> in <code>get_player_status</code> — Android native, everything else HTML5 — so
|
||
divergent video paths are the existing design rather than something this
|
||
introduces. Windows keeps <code><video></code> + hls.js whatever this spike returns.</p>
|
||
<p>The mechanism does not port: <code>default_vbox()</code>, <code>GtkOverlay</code> and <code>GtkGLArea</code> are
|
||
GTK3/WebKitGTK concepts. But the <em>question</em> is already answered more favourably
|
||
there. Both mpv plugins list Windows as <strong>fully tested</strong> and Linux as broken,
|
||
because WebView2 honours a transparent background — the "native surface beneath a
|
||
transparent webview" approach that fails on WebKitGTK is the one that works on
|
||
Windows. That asymmetry is why
|
||
<a href="windows-native-audio-backend.html">windows-native-audio-backend.md</a> can call
|
||
Windows "the cleanest available win".</p>
|
||
<p>Windows' cost is packaging, not compositing: the build cross-compiles with MSVC +
|
||
<code>cargo-xwin</code>, so libmpv arrives as a bundled prebuilt DLL (the ⚠️ in finding 5's
|
||
comparison table). That cost is already committed for <em>audio</em>. Once the DLL ships
|
||
to replace <code>WebviewAudioBackend</code>, Windows video is largely a follow-on.</p>
|
||
<p>Sequencing, if native video is ever pursued on both:</p>
|
||
<ol>
|
||
<li><a href="libmpv2-migration.html">libmpv2-migration.md</a> — prerequisite for either.</li>
|
||
<li><a href="windows-native-audio-backend.html">windows-native-audio-backend.md</a> — already
|
||
specced; lands the DLL and a real Windows backend.</li>
|
||
<li>Windows native video — cheap once 2 exists, and does not need this spike.</li>
|
||
<li>Linux native video — needs this spike, and runs independently of 1–3.</li>
|
||
</ol>
|
||
<h3 id="does-this-add-a-backend"><a class="header" href="#does-this-add-a-backend">Does this add a backend?</a></h3>
|
||
<p>No — and the trajectory is convergence, not proliferation.</p>
|
||
<p><code>create_player_backend</code> in <code>lib.rs</code> already selects between four
|
||
<code>PlayerBackend</code> impls by <code>cfg!</code>: <code>MpvBackend</code> (Linux), <code>ExoPlayerBackend</code>
|
||
(Android), <code>WebviewAudioBackend</code> (Windows and anything else), and <code>NullBackend</code>
|
||
as the graceful-init fallback. The HTML5 video path is not among them — it is a
|
||
frontend adapter reporting through <code>player_report_*</code>, not a <code>PlayerBackend</code>.</p>
|
||
<p>This spike adds none of these. <code>MpvBackend</code> already exists and already runs on
|
||
Linux; it merely sets <code>video = no</code> at construction. Giving it video widens an
|
||
existing backend rather than introducing an engine.</p>
|
||
<p>Following the sequence above, the count goes <strong>down</strong>: replacing
|
||
<code>WebviewAudioBackend</code> with mpv on Windows leaves two native engines — mpv
|
||
(Linux + Windows) and ExoPlayer (Android) — with native video riding on both.</p>
|
||
<p>Two is the floor, for a reason worth stating so nobody re-litigates it: Android
|
||
cannot drop ExoPlayer even if libmpv runs there, because the foreground service,
|
||
<code>MediaSessionCompat</code> and lockscreen control are built on it (finding 7 puts the
|
||
cost at that rewrite, not at the bindings). The HTML5 path does not go away
|
||
either — it is the transcode/ABR route and the fallback.</p>
|
||
<p>The trait surface converges too: <code>ExoPlayerBackend</code> already implements the
|
||
video-surface lifecycle for Android native compositing, so teaching <code>MpvBackend</code>
|
||
video follows a path already walked rather than opening a second one.</p>
|
||
<ul>
|
||
<li>Adopting <code>tauri-plugin-libmpv</code> or <code>tauri-plugin-mpv</code> as dependencies. Both
|
||
report Linux window embedding as not working and are small projects
|
||
(20 and ~70 commits); read them, do not depend on them.</li>
|
||
</ul>
|
||
<h2 id="acceptance-criteria"><a class="header" href="#acceptance-criteria">Acceptance criteria</a></h2>
|
||
<p>The deliverable is a decision, not a feature.</p>
|
||
<ul>
|
||
<li><input disabled="" type="checkbox"/>
|
||
Each of G1–G6 recorded green/red <strong>with the observed mechanism</strong>, not just
|
||
the verdict.</li>
|
||
<li><input disabled="" type="checkbox"/>
|
||
X11 and Wayland results reported separately, each naming the compositor
|
||
and WebKitGTK version tested.</li>
|
||
<li><input disabled="" type="checkbox"/>
|
||
The direct-play/transcode ABR hypothesis recorded as open, with whatever
|
||
the spike learned about it.</li>
|
||
<li><input disabled="" type="checkbox"/>
|
||
<code>docs/specs/README.md</code> updated — this spec listed, and its row moved or
|
||
deleted per the result.</li>
|
||
<li><input disabled="" type="checkbox"/>
|
||
The Linux claim in the <code>createAdapter</code> doc comment
|
||
(<a href="../../src/lib/player/adapters/index.ts#L12-L13">adapters/index.ts:12-13</a>)
|
||
corrected either way: if red, cite this spike instead of asserting it; if
|
||
green, it is wrong and must be rewritten.</li>
|
||
<li><input disabled="" type="checkbox"/>
|
||
On <strong>red</strong>: finding 2 of
|
||
<a href="playback-backend-unification.html">playback-backend-unification.md</a> gains a
|
||
dated note naming the render-API method as also tested, and this file is
|
||
deleted. The verdict lives in the design-authority spec, not in a second
|
||
file that contradicts nothing.</li>
|
||
<li><input disabled="" type="checkbox"/>
|
||
On <strong>green</strong>: an implementation spec exists, allocating ids from
|
||
<strong>UR-077 / IR-033 / DR-216</strong> (re-check <code>requirements.md</code> — the README's
|
||
"next free DR-215" is stale, DR-215 landed), and it must answer ABR before
|
||
being accepted.</li>
|
||
<li><input disabled="" type="checkbox"/>
|
||
No spike code on <code>master</code>. If any lands, the standard gates apply:
|
||
<code>bun run check</code>, <code>bun run test</code>, <code>bun run check:boundary</code>, <code>cargo fmt</code>,
|
||
<code>cargo clippy</code>, <code>bun run test:rust</code>.</li>
|
||
</ul>
|
||
<h2 id="testing"><a class="header" href="#testing">Testing</a></h2>
|
||
<p>No automated tests. A compositing result is a visual, per-session-backend
|
||
observation and cannot be asserted in <code>cargo test</code> or vitest — pretending
|
||
otherwise would produce a test that passes on a headless runner and tells us
|
||
nothing.</p>
|
||
<p>Capture a screenshot per gate. G4 specifically: an opaque HTML element over the
|
||
video area, photographed showing the video <em>behind</em> it.</p>
|
||
<p>If it goes green, the implementation spec inherits the testable surface the
|
||
Android work already established — <code>nativeVideoLayers.test.ts</code> asserts the
|
||
<code>app.css</code> selector list and the <code>data-native-video</code> contract, and both are
|
||
platform-agnostic.</p>
|
||
<h2 id="traces"><a class="header" href="#traces">TRACES</a></h2>
|
||
<p>None. No requirement-implementing code is produced. The implementation spec that
|
||
follows a green result allocates from DR-216 and tags there.</p>
|
||
<h2 id="notes-for-the-implementer"><a class="header" href="#notes-for-the-implementer">Notes for the implementer</a></h2>
|
||
<ul>
|
||
<li><strong>Read <a href="playback-backend-unification.html">playback-backend-unification.md</a>
|
||
first, in full.</strong> This spike disputes exactly one of its six findings, on one
|
||
platform, by one method it did not try. Everything else in it is still binding
|
||
— particularly finding 3.</li>
|
||
<li>Test <strong>Wayland first</strong>. It is the gate most likely to be red and the one that
|
||
makes the rest moot.</li>
|
||
<li>The frontend plumbing already exists from the Android work: <code>createAdapter</code>,
|
||
<code>NativePlayerAdapter</code>, <code>nativeVideo.ts</code>, <code>videoSurface.ts</code>, and the
|
||
<code>[data-native-video="active"]</code> rule. A green spike is far cheaper to implement
|
||
than it would have been a year ago — which is itself part of why the question
|
||
is worth re-asking.</li>
|
||
<li>The Android record in
|
||
<a href="../architecture/05-platform-backends.html#native-video-compositing-android">05-platform-backends.md</a>
|
||
lists six shipped defects from getting this right on one platform. Expect the
|
||
Linux equivalents (the surface outliving its player, the shell painting over
|
||
it, unpainted letterbox bars) rather than rediscovering them.</li>
|
||
<li>A parallel Claude session may be active in this repo — <code>git diff</code> before
|
||
"repairing" unexpected changes.</li>
|
||
</ul>
|
||
|
||
</main>
|
||
|
||
<nav class="nav-wrapper" aria-label="Page navigation">
|
||
<!-- Mobile navigation buttons -->
|
||
<a rel="prev" href="../specs/playback-backend-unification.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/backend-owned-stream-selection.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/playback-backend-unification.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/backend-owned-stream-selection.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>
|