The audio codec list sent to Jellyfin comes from MediaCodecList, which describes ExoPlayer — but video does not play through ExoPlayer. Android force-renders every video in the webview <video> element (the interim override in VideoPlayer.svelte) and Linux always has, and Chromium/WebKit decode a far narrower set than the platform does. A motorola edge 30 ships /vendor/etc/media_codecs_dolby_audio.xml, so it reported ac3,eac3; the server direct-played an E-AC-3 track with static=true and the webview built a video decoder and no audio decoder at all — full picture, no sound. The defect is triggered by capability rather than the lack of it, which is why a Fairphone and an Honor tablet play the same file on the same build: without the Dolby decoder they never claim the codec, so the server transcodes to AAC. Confirmed by A/B on the failing device — hevc+eac3 silent, hevc+aac audible, same session, same profile, same direct-play path, audio codec the only variable. video_audio_codecs narrows the platform list to the webview-decodable set for the video direct-play profile only. Audio-only playback really is the native player's, so that profile keeps the full list rather than transcoding music that plays perfectly well. A list with nothing decodable still claims aac, since a profile claiming nothing invites the server to give up instead of transcoding. The video codec list is deliberately untouched: HEVC direct-plays through the webview correctly, so the constraint is specific to audio. Test-first: the tests failed against the old behaviour before the filter existed, including the case built from the phone's real codec list. The requirement-count assertion in extract-traces.test.ts moves 280 -> 281 for the added DR, which is the deliberate edit that test exists to force. Not yet verified on device — the 0.4.7 APK was still building.
10 KiB
Changelog
All notable changes to JellyTau are documented here.
Entries are grouped by the capability they change, not by commit. Requirement IDs in parentheses point at docs/requirements.md; the generated trace matrix lives in docs/traceability.md.
v0.4.7
🐛 Fixes
- Video plays with sound on devices that ship a Dolby decoder. The audio
codec list sent to Jellyfin came from
MediaCodecList, which describes ExoPlayer — but video does not play through ExoPlayer: it renders in the webview<video>element, which decodes far less. A phone whose vendor licenses Dolby therefore advertisedac3/eac3, got a direct play, and showed full picture with no audio, while a leaner device claimed neither codec, received an AAC transcode, and played the same file correctly. The video direct-play profile is now narrowed to what the webview can decode; audio-only playback is genuinely the native player's and keeps the full list, so music is not transcoded needlessly. (UR-004 → DR-148)
v0.4.1
🐛 Fixes
- The lockscreen pause works while a video's audio plays in the background.
The handoff starts native audio and only then tears the WebView
<video>down — and that teardown fires a DOMpausethe frontend reports like any other, which left the controller believing webview media was still the player. Transport stayed aimed at it: pressing pause on the lockscreen sent a control command to a<video>that no longer existed while the native player carried on, and the element's parting position report dragged the displayed time backwards. A handoff is now tracked explicitly, so it hands transport to the native backend and ignores what the dying element still reports. A pause made from the lockscreen also survives the return to the app, instead of being undone by the play state captured when the handoff began. (UR-040, UR-005 → DR-052, DR-097)
v0.4.0
✨ Features
-
Favourites, across libraries. A
/library/favoritespage renders favourites from every library with All / Movies / Shows / Music scope tabs, reusing the standard grid so card shape still follows the media — a mixed All tab reads as posters, squares and thumbnails side by side. Home carries favourite rows below Recently Added, and a row with no items does not render at all, so a fresh install shows no empty rows. Server favourite state is mirrored into the local database as results are cached, so offline browsing sees the same favourites as the server; a toggle made offline is never overwritten by a stale server value before it has been pushed. (UR-067, UR-069 → DR-113, DR-114, DR-115, DR-117, DR-118) -
Search answers from the local index. The instant leg read only downloaded items, so with no downloads it returned nothing and every keystroke fell through to a full
Recursive=trueserver query. It now reads the whole synced catalog through the same availability CTEget_itemsuses, gated on the sameinclude_catalog_browseflag, so search and browse cannot diverge. The index also gained MusicArtist, Playlist and People — the very groups search sorts results into. Re-indexing moved from a frontend startup call to a Rust background task with a 6h TTL, so a long session no longer searches a stale catalog. (UR-065 → DR-108, DR-110, DR-111)
🐛 Fixes
-
Playback no longer restarts an episode at random on a flaky connection. Background audio-only playback of a video streams a progressive mp3 transcode over plain HTTP, which is chunked and so declares no length: when the connection dropped mid-episode, ExoPlayer saw end-of-input and reported
STATE_ENDED, indistinguishable from the real end. The app ran its end-of-episode logic mid-episode and playback parked inSTATE_ENDED, where the next play intent from the lockscreen, notification or a Bluetooth reconnect seeks an ended player to position 0 — surfacing as "the episode randomly restarted". The item's runtime is now what decides: an end reported well short of it re-opens the stream where it stopped. (UR-040 → DR-129) -
A network hiccup no longer kills playback outright. Music and video declare a length, so a cut connection reaches them as an error rather than a phantom end — and every error stopped the player. A recoverable error now gets one bounded attempt at re-opening the stream where it stopped, with a growing backoff, leaving the rest of the queue intact. On Linux, MPV additionally reconnects inside the demuxer so ordinary blips never surface at all, and
EndFile(ERROR)— previously a bare log line that left playback halted while the UI still showed "playing" — is now reported and recovered. (UR-004, UR-040 → DR-129, DR-130) -
The player no longer reads 0:00 as a track ends on Linux. MPV exposes
time-posanddurationas properties of the loaded file, so at EOF it unloads and both stop resolving — reporting zero at exactly the moment end-of-file handling asks where playback reached. The last reading seen while media was loaded is now kept and used as the fallback. (UR-005 → DR-130) -
Server-side deletions propagate to the local catalog.
DELETE FROM itemsexisted nowhere, so items removed on the server lingered locally forever. A post-crawl mark-and-sweep now removes them, scoped to crawled types, skipping downloaded items, and refusing to run after a partial crawl. Separately,items_ftsgrew a full duplicate index on every catalog pass; it is now a real upsert, with a migration rebuilding existing indexes. (DR-110) -
Android system bars and display cutout are handled correctly. (UR-066)
v0.2.0
✨ Features
-
Audio settings now work on Android. The equalizer, volume normalization and gapless playback controls in Settings › Audio previously rendered on Android and did nothing —
ExoPlayerBackendwas the only backend that never implementedset_audio_settings, and the trait's default silently reported success while applying nothing. All three now take effect:- Equalizer — the canonical 10-band ISO curve is resampled onto whatever bands the device's equalizer actually exposes (commonly 5), by nearest centre frequency.
- Volume normalization — via
LoudnessEnhancer. Note this is a gain stage, not a true EBU R128 normalizer like the Linuxdynaudnormpath, so it approximates rather than matches Linux behaviour. - Gapless playback — honours the setting via
pauseAtEndOfMediaItems(ExoPlayer is gapless by default, so this disables it when you turn it off).
The effects re-attach automatically when ExoPlayer rebuilds its audio sink on a format change, so the equalizer no longer stops applying part-way through a queue. (UR-027, UR-032, UR-033 → DR-030, DR-035, DR-036, IR-004)
⚠️ Not yet verified on a physical device.
AudioEffectavailability and band layouts vary by device and OEM ROM; where an effect is unavailable it is logged and skipped rather than crashing playback.
📋 Documentation
- Playback backend unification investigation. Six new specs in
docs/specs/ record why the playback backends cannot be unified
onto a single engine: every candidate (mpv, GStreamer, libVLC) fails the same
webview-compositing constraint, because WebKitGTK/WebView2/Android WebView each
own their compositor surface and native video cannot interleave with HTML.
Audio can unify; video cannot. Also specifies the Android native-video spike,
a Windows native audio backend, and the
libmpv2migration.
🐛 Corrected requirement statuses
These were documented as working and were not. No behaviour changed — the docs were wrong.
- Crossfade (UR-031, DR-034) was marked "Done (Linux only)". It is implemented
nowhere, and is architecturally blocked on mpv: its audio chain is
single-stream, and FFmpeg's
acrossfaderequires two inputs. Real crossfade would need two libmpv instances. - The platform parity matrix listed crossfade as a Linux/Android gap (it is neither) and omitted the equalizer (which was a genuine gap, now closed).
nativeAdapter.tscited tauri#10152 as blocking native Android video. That issue is a stale feature request; the capability shipped in September 2024. What remains unproven is SurfaceView-behind-WebView compositing, now tracked by a spec rather than asserted as an upstream blocker.
v0.1.2
✨ Features
- Search results are ordered by how well they match. A name that starts with the query now outranks one matching mid-word — typing "parks" finds "Parks and Recreation" before "Sparks of Love" — and at equal match quality a container outranks its contents, so a series lands above its own episodes. Ranking is applied to the instant cached results and to the merged cache+server list alike, so the list no longer reshuffles when server results arrive. (UR-060, DR-090)
- Separate Shows, Episodes and People result groups. The combined "TV Shows" group splits into Shows and Episodes so a show never competes with its own episodes for a slot, and a new People group means searching an actor's name reaches their bio page. Default order is Shows → Episodes → Movies → Songs → Albums → Artists → People; a group order saved before the split keeps the position it was dragged to. (UR-060, DR-091)
🐛 Bug Fixes
- The library header search bar works on every library page. It previously
searched in place and depended on
/libraryrendering results inline, so on any other/library/**route the results were fetched and never shown./searchis now the single surface that renders results, and the header bar hands its query and scope over via the URL. (UR-049, DR-063) - Video smaller than the window is scaled up to fit. Sizing only ever shrank oversized media, so a 480p source on a 1080p display played as a small picture in the middle of a black frame. The picture now fits whichever axis constrains it, in both directions, preserving aspect ratio. (UR-005)
📋 Requirements
Linux: 64-bit, GLIBC 2.29+ Android: 8.0+
v0.1.1 and earlier
Released before this file existed — see the git history and the release notes on each tag.