One fix since v0.11.4: a video in a picture-in-picture window no longer drops to audio-only partway through, replaying from wherever the picture had been when the window opened. Two independent faults had to line up for it. The player's position variable is written only by a requestAnimationFrame loop while playing, and RAF stops for a document that is not being rendered -- which an Android activity behind a PiP window is not; the `timeupdate` handler that would have covered the gap had gated itself on `!isPlaying` since the first commit. And PiP and the background-audio handoff, nominally alternatives, could both be armed at once, with a single `isInPictureInPictureMode` sample taken inside onStop() standing between them. The frozen position was not confined to the handoff: the seek bar, resume points and the progress reported to Jellyfin all read the same variable, so all three stood still for as long as a PiP window was open. Version stamped with scripts/set-version.sh. defect-windows.md records DR-265 under the present-since/reachable-since gap: defective since v0.0.1, but only hittable once PiP started working on the HTML5 path in v0.5.3.
11 KiB
Defect windows — which bugs were present when
For each fixed defect, the releases it was actually present in. Companion to 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
Sixteen 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.
DR-265 is the clearest example of the "present since" / "reachable since" gap
this file warns about: the !isPlaying gate has been there since the first
commit, but nothing paused the document underneath a playing <video> until PiP
started working on the HTML5 path in v0.5.3. Defective for ~9 weeks, hittable
for ~2.
| 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 |
| timeupdate gated on !isPlaying, so a paused activity froze the position (DR-265) | v0.0.1 | v0.11.5 | ~9 weeks | 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
videoBitratecasing was harmless while every download wasoriginal. 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
Rangeheader was inert for the same reason:originalis the one rung served with aContent-Lengthand 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
PlaySessionIdonly 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
SubtitleStreamIndexonly 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) |
Recently Added trusted the server to group new tracks — GroupItems=true only groups a track whose parent chain resolves a MusicAlbum, and older servers ignore it |
v0.5.1 | v0.11.4 | feature (the v0.5.1 fix for the same symptom) |
PiP and the background-audio handoff both armable, decided by one isInPictureInPictureMode sample (DR-266) |
v0.5.3 | v0.11.5 | feature (PiP on the HTML5 path, beside a toggle that had shipped in v0.0.16) |
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_episodesaccepted aseries_idfrom 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) andfa7cb6e9(the local original). Both have the same parent674c8e5cand the same diff. A merge chain pulledfa7cb6e9and its follow-up1e599627into master's history during v0.5.5, sogit log v0.5.4..v0.5.5lists an autoplay fix that changed no file in that release —nextEpisodeService.tsis 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 logsubjects 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:
- An omitted parameter is not a neutral default.
SubtitleStreamIndex,AudioStreamIndex,GroupItemsandMaxAudioChannelsall 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. - Silent binding failures.
videoBitRateproduced no error, no warning and a plausible-looking file. So did an unboundRange, and so did the coverage gate dividing by a stale denominator. - 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.
- 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:
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.