diff --git a/CHANGELOG.md b/CHANGELOG.md index fbeb0c9e..33872dcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,295 @@ 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). +For how long each fixed defect had been shipping before it was found, see +[docs/defect-windows.md](docs/defect-windows.md). + +## v0.5.5 + +### ✨ Features + +- **Library artwork is laid out as a mosaic instead of cropped to one box.** The + library overview and the home shortcut strip showed three different artwork + shapes — square music covers, 16:9 backdrops, 2:3 posters — in grids that pick + one box and crop everything to it; the home strip lined its row up by cutting + the music covers down. Both surfaces now justify rows to a shared height with + each tile as wide as its own artwork, packing from the *decoded* aspect ratio + and committing one debounced batch so the grid does not reshuffle as artwork + lands. The last row is deliberately left unstretched, so one leftover tile does + not inflate into a banner. Favourites also gain a tile per category beside the + library it belongs to — which collection type maps to which category is + Jellyfin vocabulary, so it is derived in Rust rather than rebuilding the exact + leak `SearchScope::item_types` was extracted to close. + (UR-075, UR-067 → DR-163, DR-164) + +### 🐛 Fixes + +- **The server no longer burns subtitles into the picture.** Reported as + "subtitles are shown even when off", with no toggle in the app clearing them — + because they were never the app's subtitles. `PlaybackInfo` omitted + `SubtitleStreamIndex`, which does not mean "none": the server then honours the + source's own default flag, and on the reported episode that default was a PGS + bitmap track, which cannot go out as a sidecar. So it composited the track onto + every frame. The cost landed on the *video*: burn-in rules out remuxing, so an + HEVC stream that needed only its audio transcoded was re-encoded frame by + frame, which the server could not sustain — playback stalled every few seconds + and seeks took five to nine seconds to draw a frame. The negotiation and the + stream URL now both ask for `-1` and advertise every text format the app can + render as `External`, and the picker offers only subtitles the app can actually + draw, with the codec verdict decided in Rust and carried across the boundary. + Nothing is lost: the app already fetches text tracks and draws them itself. + (UR-020, UR-004 → DR-176) + +- **Switching bitrate mid-film no longer stalls playback.** Jellyfin keys a + transcode job by device and play session, but every stream URL carried the same + hardcoded `DeviceId` and no `PlaySessionId` at all — so a second stream for an + item was indistinguishable from the first and nothing ever stopped the old + ffmpeg. The server served the new playlist and then answered 400 for its + segments. Re-opening a stream is not rare: a quality switch, a transcoded seek + and an audio-track switch all do it. Each open now mints a session id and stops + the job it supersedes, in the URL builder so every re-open path is covered by + construction. Two client faults that made the same incident worse go with it: + the fatal-HLS-error handler double-counted the transcode seek offset, so past + roughly halfway through a film any transient network error read as + end-of-stream and autoplay skipped to the next item; and the HTML5 reload + primitive resolved on its own timeout, reporting success for a reload the + server never served. (UR-074, UR-004 → DR-177) + +- **Downloading an album gets the whole album.** `download_album` read its track + list from the local catalog cache, but Jellyfin does not return `AlbumId` on + every listing endpoint, so tracks cached from one of those were invisible to + the query — three albums in the reported database had it NULL on every track. + The frontend then resolved stream URLs from its *own* list and paired them with + the returned rows by position, so a row could be handed another track's URL and + anything past the end of the shorter list never started. The same missing link + hid downloaded tracks under their album offline. The operation now belongs to + Rust end to end — the server is asked what the album contains, the album link + is written onto every track queued, URLs resolve in the backend scoped to the + rows just queued, and each track gets its own file so a title repeated across + two discs stops overwriting itself. Re-tapping download on a broken album heals + it. (DR-173) + +- **Playback positions reported to Jellyfin are real ones.** Returning to the + foreground before the background-audio stream had started playing handed the + frontend 0.0s, so the episode restarted from the beginning and the stop report + wrote that zero to the server as the resume point. The same blind spot covered + webview-rendered media, whose native position is a permanent 0 — 14 of 14 stop + reports in a 35-minute trace were zeroes, one landing 40s after the frontend + had correctly reported 15:22 for the same episode. Position is now the maximum + of the backend's reading, the last position webview media reported and the + handoff base (at most one is ever meaningful); zero-position stop reports are + withheld, since a zero is never information and only ever destroys a real + resume point; and progress is reported from the controller's own ticks — + `/Sessions/Playing/Progress` had previously been requested zero times in those + 35 minutes. A finished audio-only episode is also reported stopped at its + runtime so Jellyfin's 90% rule marks it played, which nothing else could do + once the webview was suspended. (UR-005, UR-025, UR-040, UR-071 → DR-178, + DR-179, DR-180) + +- **The streaming quality button uses a speedometer icon**, not the + cloud-download glyph that read as a download action. + + + + +## v0.5.4 + +### 🐛 Fixes + +- **Native Android video is opt-in again — enabling it by default shipped sound + with a blank screen.** The decode path was never at fault: ExoPlayer ran and + fed a live SurfaceView the whole time, behind an opaque page. The step that + clears the layers above it never took effect — the WebView was logged going + transparent `= false` and never `= true`. This is precisely what the flag + existed to contain, and v0.5.3 had turned it on so picture-in-picture would + have a real surface to shrink. Reverting costs nothing that matters: PiP drives + from the WebView `