🏗️ Build and Test JellyTau / Run Tests (push) Successful in 6m55s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m21s
Traceability Validation / Check Requirement Traces (push) Successful in 15s
Build & Release / Run Tests (push) Successful in 7m36s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 2m57s
Build & Release / Build Linux (push) Successful in 20m4s
Build & Release / Build Windows (push) Successful in 8m42s
Build & Release / Build Android (push) Successful in 30m30s
Build & Release / Create Release (push) Successful in 17s
The version lived in four files — package.json, tauri.conf.json, Cargo.toml and
Cargo.lock — that had to be hand-edited in lockstep, and the release workflow
rewrote exactly one of them. A tagged build therefore produced an installer
named for the tag wrapped around package metadata naming the previous release,
and the Linux job, which had no version step at all, shipped whatever happened
to be committed.
scripts/set-version.sh now writes all four from one argument and is the only
thing that does. Every release job calls it with the tag, including the Linux
job that was missing one. The committed versions become a placeholder for dev
builds rather than something to maintain by hand.
The Android versionCode moves into the same script, unchanged in formula
(1000 + major*10000 + minor*100 + patch). It stays inline-documented because the
reasoning is not obvious: builds already in the field shipped code 1000, and
Android refuses an update whose code is lower than the installed one, so a
formula that can emit a smaller number for a newer release bricks updates
irreversibly. UT-150 asserts that property directly — monotonic across an
upgrade sequence, and always above the floor.
Two edge cases the previous inline version got wrong:
- A prerelease tag (v0.6.0-rc1) made $(( 0-rc1 )) abort the step under set -e.
The suffix is stripped before the arithmetic; the manifests keep it.
- CI passes "${GITHUB_REF#refs/tags/}" unconditionally, which on a branch build
is still a full ref. That reached the validator verbatim and would have failed
every untagged Android build; a non-tag ref now falls back to git describe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
267 lines
14 KiB
Markdown
267 lines
14 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.5.0
|
||
|
||
### ✨ Features
|
||
|
||
- **Android video can render on the device's own video surface.** Settings →
|
||
Video Playback → **Native Video** (experimental, off by default) hands
|
||
decoding to ExoPlayer, which draws into a surface composited *behind* a
|
||
transparent WebView, with the player controls layered on top of it.
|
||
|
||
The backend had reported "this platform has a native video surface" on Android
|
||
all along, but the frontend threw that answer away in two separate places, so
|
||
the path had never actually run. Both are lifted. The setting can only ever
|
||
*suppress* the backend's choice, never override it upward: turning it off
|
||
forces the web player even where native is available, and turning it on does
|
||
nothing on platforms whose backend never offered it — Linux cannot composite
|
||
behind its webview, so it stays on the web player either way.
|
||
|
||
Verified playing on a physical device. Still unverified: the mini-player
|
||
transition, audio-track switching on the native path, and whether hardware
|
||
decoding measurably improves battery or CPU — so the toggle stays off by
|
||
default. (UR-003, UR-004 → DR-150)
|
||
|
||
### 🐛 Fixes
|
||
|
||
- **The video surface now reaches the screen at all.** The player built its
|
||
video surface, handed it to ExoPlayer, and then never added it to the view
|
||
hierarchy, because the Activity reference it needed was never supplied — so
|
||
native video would have decoded to a surface nobody could see, whatever else
|
||
was fixed. This also silently disabled picture-in-picture for video, which
|
||
gated on that same never-attached surface. (UR-003, UR-041 → DR-151)
|
||
|
||
- **Platform playback support is no longer guessed from the browser user
|
||
agent.** The frontend re-derived "does this platform decode audio natively" by
|
||
string-matching `navigator.userAgent` — a second copy of a decision the
|
||
backend already makes, free to drift out of step with the backends it was
|
||
describing. The backend now reports its own capabilities and the frontend
|
||
consumes them. (UR-003, UR-005 → DR-152)
|
||
|
||
### 🔧 Internal
|
||
|
||
- **The git tag is now the single source of truth for a release version.** The
|
||
version lived in four files that had to be edited in lockstep, and the release
|
||
workflow rewrote exactly one of them — so a tagged build produced an installer
|
||
named for the tag wrapped around package metadata naming the *previous*
|
||
release, and the Linux job, which had no version step at all, shipped whatever
|
||
happened to be committed. `scripts/set-version.sh` now writes all four from
|
||
one argument and every release job calls it with the tag. The Android
|
||
`versionCode` is derived in the same place, guarded by tests for the property
|
||
that actually matters: it must increase monotonically and stay above the value
|
||
already installed in the field, or Android silently refuses the update.
|
||
(DR-153)
|
||
|
||
## 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.
|