feat(player): native video on Linux, and one contract for every player (v0.11.0)
mpv now decodes video on Linux, drawn into a framebuffer we own and blitted
into the default vbox's draw handler. Tauri's widget tree is untouched, so an
upgrade that assumes its own layout cannot invalidate this. Direct play means
the original file, hardware decoding, and no server transcode at all — where
previously every desktop video was re-encoded to h264 for the browser engine,
whatever the file actually was. Off by default: JELLYTAU_NATIVE_VIDEO=1.
That settles finding 2 of playback-backend-unification.md — "native video
cannot be composited with a Tauri webview" — by demonstration rather than
argument, on X11 and Wayland both.
Turning it on exposed nine defects, none of them mpv's. Each was the same
mistake in a different place: a capability written down as a compile-time fact
about the platform, or a state asserted instead of confirmed.
DR-238/246 a seek routed by the stream's container rather than by what the
engine could do with it - correct only while one player handled
those streams, silent the moment another did
DR-239 a property handled but never observed, so the play/pause button
waited for an event that could not arrive
DR-240 fullscreen expanding the document while the window stayed put
DR-241 a seek issued before the engine had a file, failed, and discarded
- which is why resume began at zero
DR-247 a Linux-only gate outliving the caller that made it Linux-only,
breaking the Android build outright
DR-250 a stop aimed at whichever renderer bookkeeping believed was in
charge, missing the one actually making sound
DR-251 a duration of zero believed, leaving the seek bar no scale
DR-252 a junk float converted to a Duration, panicking the backend the
instant a length-less stream appeared
So the MediaPlayer contract (DR-242 … DR-247): `open` carries a start position,
so no caller sequences load-then-seek and none can race an engine's load;
`seek` states a destination and leaves in-place-versus-re-open to the engine;
`snapshot` is one coherent read; and `Phase::Opening` names the window where
intent used to be lost. One conformance suite runs against every engine —
FakePlayer and mpv under cargo test, ExoPlayer instrumented on a device — so an
engine is either correct or visibly failing.
Two of the nine were introduced during this work and caught on hardware, not by
any suite: an over-broad capability that grouped ExoPlayer with mpv, and the
Duration panic. The suites test engines that behave. That is recorded in
docs/native-player-verification.md, which asks for the exact action sequences
that found them.
Verified: all automated gates, conformance (mpv 9/9, legacy 8/9 by design,
ExoPlayer 7/7 on device), and manual desktop and Android passes on real
hardware.
Known open and deliberately shipped: resume reads local progress and never the
server's; the background-audio handoff still declares a state swap it does not
confirm (the symptom is now impossible, the race is not); and `bun run
android:dev` builds an APK carrying the release application id, whose failure
message advises an uninstall that would destroy app data. Fix that last one
before anyone else builds for Android.
Squashed from worktree-linux-native-video, which keeps the per-defect history.
This commit is contained in:
@@ -9,6 +9,97 @@ generated trace matrix lives in [docs/traceability.md](docs/traceability.md).
|
||||
For how long each fixed defect had been shipping before it was found, see
|
||||
[docs/defect-windows.md](docs/defect-windows.md).
|
||||
|
||||
## v0.11.0
|
||||
|
||||
Video can play through the native renderer on Linux, and the machinery every
|
||||
platform's playback goes through was rebuilt around one contract. Nine defects
|
||||
fell out of doing it — each one a capability the code had written down as a
|
||||
fact about the platform rather than asking the thing that would know.
|
||||
|
||||
### ✨ Changes
|
||||
|
||||
- **Video can decode natively on Linux, without the server re-encoding it.**
|
||||
Until now every video played on the desktop was transcoded by Jellyfin to
|
||||
h264 and handed to the browser engine, whatever the file actually was — so the
|
||||
server burned CPU on every play, and quality was capped by that conversion.
|
||||
mpv can now draw the picture directly, composited beneath the interface so the
|
||||
controls, subtitles and overlays still sit on top of it. Direct play means the
|
||||
original file, hardware decoding, and no server work at all. This is off by
|
||||
default while it settles: set `JELLYTAU_NATIVE_VIDEO=1` to try it. The browser
|
||||
path is untouched and remains what you get otherwise. (UR-080 → DR-231 …
|
||||
DR-237)
|
||||
|
||||
- **Playback speaks one language across every player.** Linux, Android and
|
||||
Windows each drove their engine through a different set of calls, and a rule
|
||||
learned on one did not reach the others — which is why several of the fixes
|
||||
below existed on one platform and not another. All three now go through a
|
||||
single contract, and one suite of behaviours runs against every engine,
|
||||
including ExoPlayer on a real device. An engine is either correct or visibly
|
||||
failing. Nothing about this is visible while it works, which is the point.
|
||||
(UR-081 → DR-242 … DR-247)
|
||||
|
||||
### 🐛 Fixes
|
||||
|
||||
- **Resuming a film starts where you left it, instead of at the beginning.**
|
||||
Asking a player to open a file and asking it to start at a position were two
|
||||
separate steps, and the second was issued before the first had finished — so
|
||||
it failed, was discarded, and playback began at zero. It affected resume and
|
||||
any skip on a stream the server was converting. The position is now part of
|
||||
opening the file, so there is no gap for it to fall into. (DR-241)
|
||||
|
||||
- **Skipping works on films the server is converting.** A skip was routed by the
|
||||
*shape* of the stream rather than by what the player could do with it. That
|
||||
happened to be right while one particular player handled those streams and
|
||||
became wrong the moment another did — after which skipping simply did nothing,
|
||||
silently. Players now say what they can do and are asked. (DR-238, DR-246)
|
||||
|
||||
- **The play and pause button follows the player again.** The code that reacted
|
||||
to pausing was never subscribed to the event it was waiting for, so the button
|
||||
stayed where it was while playback did something else. (DR-239)
|
||||
|
||||
- **Fullscreen fills the screen.** It expanded the page rather than the window,
|
||||
which was invisible while the picture was drawn inside the page and obvious as
|
||||
soon as it was not. (DR-240)
|
||||
|
||||
- **The seek bar knows how long the film is.** A player that had not yet worked
|
||||
out the duration reported zero, and zero was believed — leaving the bar with
|
||||
no scale and nothing to drag against, even though the length had been known
|
||||
since the library listed it. (DR-251)
|
||||
|
||||
- **Leaving the player stops the sound.** The stop was aimed at whichever
|
||||
renderer the app believed was in charge. Enabling background audio hands over
|
||||
to a different one, so afterwards the app stopped something that was no longer
|
||||
playing and the film carried on as an audio track in the mini player. Closing
|
||||
now stops everything, regardless of who was in charge. (DR-250)
|
||||
|
||||
- **Coming back from background audio no longer leaves a black screen.** The
|
||||
stream that plays while the app is hidden has no fixed length, and the value a
|
||||
player uses to say so is a very large negative number. Converting it crashed
|
||||
the playback engine outright, which looked like a dead player with no
|
||||
controls. (DR-252)
|
||||
|
||||
- **Android builds again.** A rule that only applied to Linux stayed attached to
|
||||
code that had stopped being Linux-only, and the Android build had not compiled
|
||||
since. (DR-247)
|
||||
|
||||
### 🧹 Under the hood
|
||||
|
||||
- The conformance suite can be run on its own: `bun run test:player` for the
|
||||
desktop engines, `bun run test:player:android` for ExoPlayer on a connected
|
||||
device. Both build a test fixture rather than carrying media in the
|
||||
repository.
|
||||
|
||||
- [docs/native-player-verification.md](docs/native-player-verification.md)
|
||||
records what to check before a release, including the exact sequences that
|
||||
found two of the defects above — both of which passed every automated test.
|
||||
|
||||
### Known limitations
|
||||
|
||||
- Resume reads progress saved on the device, not from the server, so a fresh
|
||||
install or a second device will not offer to resume something watched
|
||||
elsewhere.
|
||||
- Native video on Linux is opt-in and is not yet the default.
|
||||
|
||||
## v0.10.1
|
||||
|
||||
A single fix, for something that had been quietly overriding a choice you made.
|
||||
|
||||
Reference in New Issue
Block a user