feat(video): mpv plays video on Linux, composited under the webview
DR-231 works. Video and audio, drawn by mpv into a framebuffer we own and
blitted into the default vbox's draw handler with `gdk_cairo_draw_from_gl()`.
The widget tree Tauri built is untouched, so nothing here can be invalidated by
a Tauri upgrade that assumes its own layout.
That settles finding 2 of playback-backend-unification.md on Linux by
demonstration rather than argument, and completes the half of G1 the spike could
not test.
Three pieces had to land together, none of which existed before:
- mpv was configured with `video: no` and no video output, so it had never
decoded a frame in this app. Now `vo=libmpv` when native video is on.
- `player_play_item` skipped loading into the native backend on Linux behind a
`#[cfg(not(target_os = "linux"))]`, because the webview always played video
there. With the webview no longer loading it, that guard meant *nothing*
played — no picture and no audio, which reads as a broken stream rather than
as a file nobody was given.
- The webview paints its own opaque background. Android clears it through a
Kotlin bridge from `enableNativeVideoCompositing()`; the CSS half of that
already ran on Linux, so only `transparent: true` on the window was missing.
Until it was, the frame was rendered correctly and covered by white.
Frame pacing is polled, not pushed. The tick callback asks mpv `has_frame()` and
draws only when the answer is yes. Both neighbouring designs were tried and both
fail, in ways that point at the wrong culprit:
- Waiting on mpv's update callback before rendering *deadlocks*: mpv does not
progress until the client renders, so if the client waits to be told, the
two hold each other. The file loads, one frame appears, and everything
stops.
- Rendering every frame-clock tick and reporting a swap each time claims a
presentation far more often than one happened. It plays, and judders badly —
which reads as a GPU or compositing limit, exactly as the spike warned.
The update callback survives as a hint and does the least it safely can from an
mpv thread: set an `AtomicBool`. It must not touch GTK — `idle_add_local*`
requires the caller to own the main context and panics from there — and it must
not hold the `Rc<RefCell<..>>` state, which is not `Send`.
Two memory-safety fixes in this file's own short history, both worth recording
because neither announced itself:
- The callback context was handed over with `Rc::into_raw` (a pointer to the
Rc's *contents*) and read back as `*const Rc<..>`, reinterpreting a RefCell
as an Rc and corrupting its refcount on the first clone. mpv invokes the
callback immediately, so this happened before anything drew. The symptom was
the process ending quietly with status 0.
- The surface was attached before the player backend was constructed, so the
mpv handle it needs had not been registered yet and it found null every
time.
Teardown (DR-232) is confirmed working on a real run: callback unregistered,
render context freed, GL objects released with the context still current, boxed
callback state reclaimed only after mpv can no longer reach it — no crash.
Still behind JELLYTAU_NATIVE_VIDEO=1 and off by default. Known open: whether
exiting the player stops mpv (reported, evidence ambiguous, needs re-checking
now the picture works), hardware decode (DR-236), and deleting the webview video
path (DR-235).
This commit is contained in:
@@ -725,18 +725,30 @@ pub async fn player_play_item(
|
||||
}
|
||||
|
||||
let controller = player.0.lock().await;
|
||||
// On Linux, video plays in the WebKitGTK HTML5 <video> element (see
|
||||
// get_player_status -> use_html5_element). The MPV backend has no embedded
|
||||
// window, so loading the stream into it would only start a redundant decode
|
||||
// (and the frontend would immediately stop it). Only load into the native
|
||||
// backend on platforms that actually render video through it (e.g. Android).
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
controller
|
||||
.play_item(media_item)
|
||||
.map_err(|e| e.to_string())?;
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Keep the queue in sync for UI/remote-transfer without starting MPV.
|
||||
// Who gets the stream depends on who is going to *render* it, which is a
|
||||
// runtime question, not a platform constant.
|
||||
//
|
||||
// Historically Linux video was always the webview's (`use_html5_element`),
|
||||
// so handing the file to MPV as well would only have started a redundant
|
||||
// decode with no window to show it in — hence a `#[cfg(not(linux))]` guard
|
||||
// and a queue-only path here. With mpv drawing the picture that inverts:
|
||||
// the webview is no longer loading anything, so if this does not load the
|
||||
// file, *nothing does*. The symptom is total silence — no picture and no
|
||||
// audio — which reads like a broken stream rather than a stream nobody was
|
||||
// given.
|
||||
//
|
||||
// This is the fifth place in this cycle where a renderer's capability was
|
||||
// written as a compile-time platform fact. Same fix as the others: ask.
|
||||
//
|
||||
// TRACES: UR-080 | DR-231, DR-235
|
||||
let renders_natively = cfg!(not(target_os = "linux")) || crate::player::native_video::enabled();
|
||||
if renders_natively {
|
||||
controller
|
||||
.play_item(media_item)
|
||||
.map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
// The webview will play it; keep the queue in sync for the UI and for a
|
||||
// remote transfer without starting a second decode.
|
||||
controller
|
||||
.set_current_item(media_item)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Reference in New Issue
Block a user