6b3d853442ec9c4ca588a0fd9417675b32a0939e
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
14b6a8609d |
fix(player): three defects native video exposed, and the logs to see them
Each of these was invisible while Linux video played in the webview, and each became reachable the moment mpv started rendering. DR-238 — a transcoded seek re-negotiates the stream on every renderer, not just the webview. `determine_video_seek_strategy` treated `is_hls` as a proxy for "seekable in place", which held only because hls.js was always the HLS renderer: it seeks within the VOD playlist it is handed and lets the server catch up. mpv's HLS demuxer cannot make Jellyfin transcode from a new offset, so with native video on, every transcoded seek became a backend seek that silently did nothing. One cell of the truth table changes; all four webview cells are byte-identical. DR-239 — properties the mpv event loop handles are now observed. libmpv delivers PropertyChange only for properties registered with observe_property, so the `pause` arm was unreachable code that read as implemented: StateChanged was never emitted and the play/pause control never moved. UT-218 asserts the two lists agree, so the class cannot recur. DR-240 — fullscreen moves whatever owns the pixels. requestFullscreen() fullscreens the *document*, which sufficed while the <video> element lived inside it and WebKit scaled it. A native surface is drawn behind the webview at window size, so a document-only fullscreen expanded the page and left the picture at its old size — on WebKitGTK, a maximised window with decorations still holding a strip of the screen. Measured on a 3440x1440 panel: 1361 tall before, 1440 after. DR-241 — a seek issued before mpv has a file to seek in is honoured rather than dropped. loadfile returns as soon as the command is queued, so `time-pos` does not resolve yet and setting it fails. The two callers that always hit that window are resume and a transcoded seek, both of which re-open the stream and then ask for a position; the failed seek was discarded and playback began at zero. Also adds the instrumentation that made the diagnosis possible rather than speculative: an entry log on player_stop, a render-size log that re-fires on change instead of latching once, and decoded-vs-display video geometry on file load. The last of those retired a wrong theory — a picture that does not fill an ultrawide turned out to be a 16:9 source with its letterbox baked in, not a rendering fault. |
||
|
|
d3ecd8ee91 |
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).
|
||
|
|
45144cb6b0 |
feat(video): render mpv behind the webview, and collapse duplicated helpers
DR-231 with the design the failed reparent forced. mpv's render API draws into
an FBO we own; the texture is composited by `gdk_cairo_draw_from_gl()` in the
default vbox's own `draw` handler. GTK draws a container before its children, so
the webview lands on top for free — no reparenting, no GtkOverlay, and nothing a
Tauri upgrade can invalidate by assuming its own widget layout.
Split so Windows inherits the useful half: `mpv_render` is the portable side
(render context, framebuffer, GL resolution) and `video_surface` is the GTK side
that consumes it. Nothing in the former is GTK-aware.
Three things the spike paid for, carried over rather than rediscovered:
- libepoxy exports GL entry points as *data* symbols. `dlsym("epoxy_glFoo")`
returns the address *of a function pointer*, not of code — returning it
makes mpv jump into non-executable data and take SIGSEGV on the first GL
call. The value is read out of that location instead.
- Frame pacing goes through mpv's update callback plus `report_swap`. Its
absence looks like a GPU or compositing limit (fine in a window, judders at
fullscreen) and is neither.
- The render context is created on `realize` and destroyed on `unrealize`,
with the update callback unregistered *before* the free, so a callback
cannot land on a freed pointer. That is DR-232 built in from the start
rather than retrofitted: the spike had no teardown at all, which remains the
likeliest explanation for the one SIGSEGV it could not reproduce.
Writing it also caught a bug that would have looked like severe stutter: the
update callback flagged a new frame but never asked GTK to repaint, so decoded
frames would only have reached the screen when something else happened to
invalidate the widget.
Still off by default behind JELLYTAU_NATIVE_VIDEO=1. It compiles and is wired;
no frame has been put on screen yet.
Redundant code, continued. `formatSecondsDuration` had no caller. Three
components had hand-rolled `formatDuration`: Queue's was byte-equivalent to the
shared "mm:ss", while EpisodeFocusView and the library page shared an identical
"1h 23m" shape the util did not offer — so that format joins the other two and
all three components now call one function.
A survey for exported symbols referenced only by tests returns 23 more. They are
deliberately left: spot-checking found `setLogForwarder` is the injection seam
for a lazily-initialised forwarder, and `getCachedImageUrl` is the read path of
a thumbnail cache whose management UI exists in Settings. Neither is dead — one
is test infrastructure and the other is an unwired feature, and deleting either
would remove capability while looking like tidying. The list is worth working
through deliberately, not in a playback branch.
|
||
|
|
fecd6022fe |
chore(traceability): shift this branch's ids clear of master's
Master allocated DR-224 and UT-211 while this branch was in flight — the third collision on this work. Everything here moves up by one: DR-224..236 become DR-225..237, UT-211..213 become UT-212..214. UR-079, UR-080 and IR-033 were still free and are unchanged. Mechanical, and matched on each row's own text rather than on its number, so a row cannot be shifted twice or the wrong one caught. Master's DR-224 (the background-audio toggle) and UT-211 are untouched. |
||
|
|
84cf31b929 |
feat(video): build the native video surface, and fix what running it exposed
Three things, all found by actually running the app rather than by reading it.
The surface (DR-230). A GtkGLArea as the main child of a GtkOverlay with
Tauri's own webview reparented on top — the desktop shape of what Android
already does with ExoPlayer. It attaches cleanly and is then **off by
default**, because the reparent fails the gate the spike said it would.
`tauri-runtime-wry`'s undecorated-resizing handler walks a hard-coded path on
every button press in the webview:
webview.parent() // "This one should be GtkBox"
.parent() // ...and this one the GtkWindow
.downcast::<gtk::Window>().unwrap()
Wrapping the webview makes that chain webview -> GtkOverlay -> GtkBox, the
downcast fails, and the panic is non-unwinding so it aborts the process. The
decoration check that would make the handler inert runs *after* the unwrap, so
no window configuration avoids it. The surface attaching successfully is
therefore not the gate — a click is. It lives behind JELLYTAU_NATIVE_VIDEO=1
with the mechanism written down, because the next attempt needs to keep Tauri's
two-hop shape intact and that is the whole design constraint.
Also settles a dependency question the spike left implied: the render API is
reachable from the pinned libmpv revision. Its safe `render` module is an empty
stub, but libmpv-sys carries every render symbol and `Mpv::ctx` is public, so
the context can be built over the handle the audio backend already drives. This
does not need the libmpv2 migration first.
The HLS effect re-ran on object identity. `currentSelection` is a struct, and
every reload replaces it even when the URL and transport are unchanged — so the
effect tore down hls.js and reattached for an unchanged stream, leaving the
element blank until a seek forced another cycle. The pre-DR-224 code read a
plain URL *string*, where re-assigning the same value was a no-op; the codebase
documents relying on that and swapping in a struct broke it silently. The
loader decision now takes a primitive transport tag, so the component cannot
depend on object identity — the bug is unrepresentable rather than merely
fixed.
The device profile contradicted itself. The direct-play profile claimed h264
alone on the webview path while the transcoding profile said "you may transcode
to h264 or hevc" — telling the server "I cannot play hevc, so re-encode it" and
then "re-encoding it to hevc is fine". Streams came back carrying
VideoCodec=h264,hevc with hevc-level/profile/bitdepth set. When the server took
that option the webview got something it could not decode, which presents as
video stuck on its first frame rather than as an error. Transcode targets are
now derived from the same codec list as direct play, capped to the two codecs a
Jellyfin server actually encodes so a wider decode list never asks for an av1
encode.
That is the third defect in one family: a decode capability stated in more than
one place, with the copies disagreeing. DR-233 exists to collapse them into one
renderer-derived source, and this is evidence for it rather than a preference.
Not fixed here, and worth knowing:
- The requested VideoBitrate is sized to the ceiling, not to the source — a
2.2 Mbps source was being re-encoded at 19.8 Mbps, roughly 9x. Pre-existing,
but this branch is the first thing that knows the source bitrate and so the
first that can cap it.
- The `debug` build type produces an APK with the *release* applicationId:
`applicationIdSuffix = ".debug"` is present in the canonical gradle and absent
from the generated copy, though the identical line in the `release` block
survives. Not caused by our sync, which is a plain cp. Independent of this
work; it is why the side-by-side release build is the one that installs.
|