Advertising a webview-shaped profile (DR-148) was necessary but not sufficient. Probing the server directly showed Jellyfin 10.11.5 enforces a DirectPlayProfile's Container and VideoCodec — excluding either returns SupportsDirectPlay:false with TranscodeReasons=ContainerNotSupported / VideoCodecNotSupported — but ignores its AudioCodec entirely: an E-AC-3 track is still offered for direct play against a profile listing only aac,flac,mp3,opus,vorbis. Neither a VideoAudio CodecProfile forbidding the codec nor MaxAudioChannels:2 against a 6-channel track changes the answer, so no profile the client can send fixes this and the picture plays silent. The client therefore stops delegating a question it can answer itself. The negotiated source's audio is checked against what the webview decodes, and an undecodable track forces the existing h264/aac HLS transcode regardless of the server calling direct play fine; direct_play and needs_transcoding are corrected to match so the frontend and the reporting path agree with the URL actually used. The track judged is the one that would be served — the default, else the first — since a supported track further down is not the one that plays. A source with no audio, or a codec the server did not name, is left alone rather than transcoded on a guess. Test-first: the new tests failed against the old behaviour before the decision existed. Verified on a motorola edge 30 by the audio HAL, not by ear — the same E-AC-3 episode logged isMusicActive=true once and 58 ACDB-LOADER lines under this build, against 0 and 0 on 0.4.6, where an AAC file in the same session produced 16 and 116. No FATAL EXCEPTION, so R8 on the signed release build is unaffected. Also carries in-flight subtitle-track work authored in a parallel session (subtitleTracks, VideoPlayer, player/media, bindings) at the user's request, so the tag matches the APK verified on device.
215 lines
11 KiB
Markdown
215 lines
11 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.8
|
||
|
||
### 🐛 Fixes
|
||
|
||
- **Video with an undecodable soundtrack now transcodes instead of playing
|
||
silent.** Advertising a webview-shaped profile (v0.4.7) turned out not to be
|
||
enough: Jellyfin 10.11.5 enforces a direct-play profile's container and video
|
||
codec but ignores its audio codec, offering an E-AC-3 track for direct play
|
||
against a profile listing only AAC — and no `CodecProfile` or channel limit
|
||
changes that. The client now checks the track it would actually be served
|
||
against what its renderer can decode and forces the h264/AAC HLS transcode
|
||
when it cannot, rather than trusting the negotiation.
|
||
(UR-004 → DR-149)
|
||
|
||
## 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.
|