Two faults, both present since v0.0.1, both found and confirmed on a device. Audio track (DR-258). Jellyfin builds a transcode around one AudioStreamIndex, so the alternate tracks are not in the stream that arrives — but the native path only ever called setAudioTrack(n), which indexes ExoPlayer's audio track *groups*. On Android that is the common case, since any source whose default audio codec the device cannot decode is transcoded: logcat showed ExoPlayer holding `Audio tracks: 1` while the menu listed every track in the file, so each selection warned `Invalid audio track index` and was dropped, leaving the default track playing with nothing in the UI saying so. determine_audio_track_switch_strategy now decides by whether the stream in front of the engine carries the track at all — a direct play still selects in place, a transcode is re-negotiated at the chosen index and resumed. Where it resumes is the player's answer rather than the UI's: the native path has no <video> element to read, so it sends no position, and defaulting that to zero re-opened the film at the beginning (caught on device before it shipped). Subtitles (DR-259). The URL was missing its `Stream.` route segment, so every sideloaded subtitle 404ed; since media3 1.5 a sideloaded text track only becomes a track group once its file is parsed, so 42 failed fetches left ExoPlayer with no text tracks and selection warned `available: 0`. Verified against a live server: the built URL answers 404, the corrected one 200. The tests that should have caught this asserted the shape of a mock helper that restated the format string instead of the URL the app requests — so the new test drives the repository itself, and failed red on the old URL.
156 lines
10 KiB
Markdown
156 lines
10 KiB
Markdown
# Defect windows — which bugs were present when
|
|
|
|
For each fixed defect, the releases it was actually present in. Companion to
|
|
[CHANGELOG.md](../CHANGELOG.md), which says what changed; this says how long each
|
|
fault had been shipping before it did.
|
|
|
|
**"Present since"** is the first *release* containing the defective code, not the
|
|
first release where a user could hit it — those differ, sometimes by months, and
|
|
the gap is called out where it matters. **"How dated"** records the evidence, so a
|
|
row can be re-checked or disputed:
|
|
|
|
| Method | Meaning |
|
|
|--------|---------|
|
|
| `pickaxe` | `git log -S<token>` on the defective token — the commit that introduced the exact string, then the earliest tag containing it. Strongest evidence. |
|
|
| `feature` | The defect is inseparable from a feature that landed whole (bad rung in a new algorithm, missing caller in new plumbing), dated to that feature's release. |
|
|
| `absence` | The fix *adds* something that was never there. Dated to when the surrounding code was built, since there is no introducing commit to find. Weakest — treat as "no later than". |
|
|
|
|
## Present since the first release
|
|
|
|
Fifteen defects date to the initial proof of concept (v0.0.1, 2026-06-23) and
|
|
shipped for between two weeks and two months before anyone hit them.
|
|
That is the dominant pattern here: not regressions, but original assumptions that
|
|
went unexercised until a later feature leaned on them.
|
|
|
|
| Defect | Present since | Fixed in | Shipped broken for | How dated |
|
|
|---|---|---|---|---|
|
|
| `AudioStreamIndex=0` pinned the video stream as the audio track (DR-140) | v0.0.1 | **v0.4.6** | ~7 weeks | pickaxe |
|
|
| Download URL spelled `videoBitrate`, which Jellyfin does not bind (DR-123) | v0.0.1 | **v0.5.1** | ~7 weeks | pickaxe |
|
|
| `pause_download` / `resume_download` were no-ops (DR-168) | v0.0.1 | **v0.5.3** | ~7.5 weeks | pickaxe |
|
|
| `.part` sidecar named by `with_extension`, so no cleanup path matched it (DR-169) | v0.0.1 | **v0.5.3** | ~7.5 weeks | pickaxe |
|
|
| `Range` sent on every retry regardless of the response (DR-170) | v0.0.1 | **v0.5.3** | ~7.5 weeks | pickaxe |
|
|
| `/Items/Latest` requested with the default `GroupItems=false` | v0.0.1 | **v0.5.1** | ~7 weeks | pickaxe |
|
|
| `SubtitleStreamIndex` omitted from PlaybackInfo, letting the server burn in (DR-176) | v0.0.1 | **v0.5.5** | ~8 weeks | pickaxe |
|
|
| No `PlaySessionId`, and one hardcoded `DeviceId`, on every stream URL (DR-177) | v0.0.1 | **v0.5.5** | ~8 weeks | pickaxe |
|
|
| `download_item` never recorded `media_type`; NULL read as `'audio'` (DR-135) | v0.0.1 | **v0.4.6** | ~7 weeks | pickaxe |
|
|
| `download_album` read its track list from the local cache (DR-173) | v0.0.1 | **v0.5.5** | ~8 weeks | pickaxe |
|
|
|
|
| Device profile carried no `MaxAudioChannels` (DR-141) | v0.0.1 | **v0.4.6** | ~7 weeks | absence |
|
|
| Streaming ceiling fixed at 20 Mbps with no way to lower it (UR-074) | v0.0.1 | **v0.5.3** (as a feature) | ~7.5 weeks | pickaxe |
|
|
| Hero banner auto-rotation never restarted after a manual swipe (DR-038) | v0.0.1 | **v0.9.1** | ~8.5 weeks | pickaxe |
|
|
| Audio-track change asked the player to select a track the transcode never carried (DR-258) | v0.0.1 | **v0.11.1** | ~2 months | pickaxe |
|
|
| Subtitle URL missing its `Stream.` route segment, so every fetch 404ed (DR-259) | v0.0.1 | **v0.11.1** | ~2 months | pickaxe |
|
|
|
|
### Why they took so long to surface
|
|
|
|
Four of these were **latent until a later feature exercised them**, which is why
|
|
the fix lands so far from the cause:
|
|
|
|
- The `videoBitrate` casing was harmless while every download was `original`. It
|
|
became visible only once a quality picker existed to select against — and then
|
|
produced no error, just a full-size file, because Jellyfin discards an unbound
|
|
query key silently.
|
|
- The unconditional `Range` header was inert for the same reason: `original` is
|
|
the one rung served with a `Content-Length` and real byte-range support. It
|
|
started corrupting files in **v0.5.1**, the moment the casing fix made
|
|
transcoded downloads actually transcode. So the *code* dates to v0.0.1 and the
|
|
*corruption* to v0.5.1 — a one-release window for the visible symptom.
|
|
- The missing `PlaySessionId` only bites when a stream is re-opened for the same
|
|
item. Nothing re-opened one until quality switching, transcoded seek and
|
|
audio-track switching existed.
|
|
- The omitted `SubtitleStreamIndex` only bites on sources whose own default
|
|
subtitle track is image-based, since that is what forces the server from
|
|
sidecar to burn-in.
|
|
|
|
Two were **masked by soft failure**: the asset protocol being disabled (DR-134)
|
|
was hidden by the thumbnail cache falling back to the server copy whenever the
|
|
server was reachable, and `AudioStreamIndex=0` was hidden by servers that
|
|
silently correct an out-of-range index — which is exactly why it was reported as
|
|
"*some* videos have no audio" rather than as a bug in the client.
|
|
|
|
## Introduced by a feature, fixed later
|
|
|
|
| Defect | Present since | Fixed in | How dated |
|
|
|---|---|---|---|
|
|
| Native-path resume position never applied (both layers assumed the other seeked) | v0.0.9/v0.0.10 | **v0.5.1** | feature (`PlayerAdapter` contract) |
|
|
| `get_downloaded_items` matched "this library exists" rather than constraining the item to it (DR-167) | v0.0.17 | **v0.5.3** | feature (browsable downloaded library) |
|
|
| `SCOPE_ITEM_TYPES` — the frontend/backend boundary leak (DR-063) | v0.0.17 | **v0.2.1** | pickaxe |
|
|
| `check:boundary` anchored to the query site, blind to a named const (DR-094) | v0.0.17 | **v0.2.1** | feature (tripwire landed with the leak it missed) |
|
|
| Coverage gate divided by hardcoded denominators, reporting 158% (DR-093) | v0.0.1 | **v0.2.1** | pickaxe |
|
|
| Tap deferral raced the WebView's synthesized click (DR-092 → DR-098) | v0.1.5 | **v0.2.7** | feature (the deferral itself) |
|
|
| Transport for webview media decided from `el.paused` in the DOM (DR-097) | v0.0.9/v0.0.10 | **v0.2.7** | feature (`Html5PlayerAdapter`) |
|
|
| `pick_current_episode` rung 3 returned the first *gap*, not the furthest watched | v0.3.0 | **v0.5.1** | feature |
|
|
| `mirror_user_data` mirrored `is_favorite` alone and returned early (DR-155) | v0.4.0 | **v0.5.1** | pickaxe |
|
|
| Stop-report path never fed the sync queue that existed for it (DR-154) | v0.4.6 | **v0.5.1** | feature (queue + drain landed with no producer) |
|
|
| Background-audio base applied in two display-only places (DR-159) | v0.2.9 | **v0.5.3** | pickaxe |
|
|
| Positions reported as 0 before the first tick, and always 0 for webview media (DR-178/179/180) | v0.5.3 | **v0.5.5** | feature (DR-159's tick boundary) |
|
|
| Length-less handoff transcode left to the player's own load-error retry, which can only restart it (DR-203) | v0.0.16 | **v0.8.2** | feature (the handoff's progressive-mp3 choice) |
|
|
|
|
Three of these are worth separating out, because the defect is not a mistake in
|
|
the code so much as **plumbing that was built and never connected**:
|
|
|
|
- `repository_get_next_up_episodes` accepted a `series_id` from the day it was
|
|
written, and no caller passed one until v0.3.0.
|
|
- The sync queue and its drain were built, tested and running in v0.4.6 with
|
|
neither of its two would-be producers ever called.
|
|
- Both halves of the watched-state backend existed with no caller before v0.5.3.
|
|
|
|
An automated check cannot see any of these — the code is present, tested and
|
|
reachable in principle. Only tracing a requirement to a *call site* catches it.
|
|
|
|
## Short windows (one release or less)
|
|
|
|
| Defect | Present since | Fixed in | Note |
|
|
|---|---|---|---|
|
|
| `experimentalNativeVideo` defaulted on, shipping audio with a blank screen (DR-161 → DR-172) | v0.5.3 | **v0.5.4** | One release. The decode path was fine; the compositing step never ran. |
|
|
| Webview-shaped audio profile insufficient — server ignores a profile's audio codec (DR-149) | v0.4.7 | **v0.4.8** | The v0.4.7 fix for DR-148 was necessary and not sufficient. |
|
|
| Android `versionCode` floor went stale (`minor*100` yielding less than the 5002 already in the field) | v0.5.0 | **v0.5.3** | Caught before a broken APK shipped; no released build was un-installable. |
|
|
| Subtitle sidecar work reverted by a commit assembled from a stale tree | v0.5.5 | **v0.5.5** | Never released broken — both commits are in v0.5.5. |
|
|
|
|
## Fixed twice / never actually broken
|
|
|
|
- **Autoplay time reset (v0.0.2).** Two commit objects carry this identical
|
|
change: `dcf08f30` (merged via Gitea PR #3, tagged v0.0.2) and `fa7cb6e9` (the
|
|
local original). Both have the same parent `674c8e5c` and the same diff. A merge
|
|
chain pulled `fa7cb6e9` and its follow-up `1e599627` into master's history
|
|
during v0.5.5, so `git log v0.5.4..v0.5.5` lists an autoplay fix that changed no
|
|
file in that release — `nextEpisodeService.ts` is byte-identical across the tag
|
|
boundary. The fix shipped in **v0.0.2** and has not regressed.
|
|
|
|
This is the one case where reading the changelog off `git log` subjects would
|
|
have produced a false entry, and it is a good argument for the project's
|
|
practice of deriving release notes from TRACES rather than commit subjects.
|
|
|
|
## Recurring shapes
|
|
|
|
Four causes account for most of the table:
|
|
|
|
1. **An omitted parameter is not a neutral default.** `SubtitleStreamIndex`,
|
|
`AudioStreamIndex`, `GroupItems` and `MaxAudioChannels` all had a server-side
|
|
default that was actively wrong, and in three of the four the server's choice
|
|
was more expensive than the one intended — burn-in forcing a full re-encode
|
|
being the extreme case.
|
|
2. **Silent binding failures.** `videoBitRate` produced no error, no warning and a
|
|
plausible-looking file. So did an unbound `Range`, and so did the coverage gate
|
|
dividing by a stale denominator.
|
|
3. **Two layers each assuming the other acts.** Native resume (adapter recorded
|
|
the position, backend never seeked), end-of-playback dispatch (two paths, one
|
|
unreachable), and the surface/attach split in v0.5.0's native video.
|
|
4. **A guard keyed on state that moves.** The tap deferral keyed suppression on a
|
|
timer handle the callback had already cleared; the HTML5 toggle keyed
|
|
play-vs-pause on `el.paused`, which flips while buffering.
|
|
|
|
## Reproducing this
|
|
|
|
The pickaxe rows can be re-derived directly:
|
|
|
|
```bash
|
|
git log --oneline --reverse -S'<defective token>' -- src-tauri/src # introducing commit
|
|
git tag --contains <sha> | sort -V | head -1 # first release with it
|
|
```
|
|
|
|
Blaming the lines a fix removed (`git blame` at the fix's parent) is faster to run
|
|
across many commits but was **not** used for the rows above: it reliably lands on
|
|
whichever commit last touched the adjacent lines, which is usually not the commit
|
|
that introduced the defect. It was used only to shortlist candidates.
|