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.
201 lines
10 KiB
Markdown
201 lines
10 KiB
Markdown
# 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](docs/requirements.md); the
|
||
generated trace matrix lives in [docs/traceability.md](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 advertised `ac3`/`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 DOM `pause` the 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/favorites` page 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=true` server query. It now reads the whole synced
|
||
catalog through the same availability CTE `get_items` uses, gated on the same
|
||
`include_catalog_browse` flag, 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 in `STATE_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-pos` and `duration` as 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 items`
|
||
existed 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_fts` grew 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 — `ExoPlayerBackend` was the only backend that never
|
||
implemented `set_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 Linux `dynaudnorm` path, 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.** `AudioEffect` availability 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/](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 `libmpv2` migration.
|
||
|
||
### 🐛 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 `acrossfade` requires 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.ts` cited 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.
|
||
|
||
<!--
|
||
Note: v0.1.3–v0.1.5 have no entries here. Their changes are in the git log
|
||
and docs/traceability.md.
|
||
-->
|
||
|
||
## 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 `/library` rendering results inline, so on
|
||
any other `/library/**` route the results were fetched and never shown.
|
||
`/search` is 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.
|