diff --git a/CHANGELOG.md b/CHANGELOG.md index f45a9519..d7fb565c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,50 @@ 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.7.0 + +### ✨ Changes + +- **Native Android video is now the default.** Video decodes on the device's + hardware decoder instead of the built-in web player, which is easier on the + battery and lets picture-in-picture show the video rather than the app. The + default had been held back deliberately since the picture defects were fixed, + because returning from background audio left playback dead on that path; both + blockers below are fixed and verified on a device, which is the standard this + default has been held to since it last shipped early. The Settings toggle + remains, now as the fallback to the web player, and an explicit choice still + wins in both directions — anyone who turned it off keeps it off. + (UR-003, UR-004 → DR-188) + +### 🐛 Fixes + +- **The letterbox bars stop showing things that are no longer there.** With + native video on, the padding around the picture kept whatever had last been + drawn in it: the previous frame flashing on rotation, a ghost copy of the + control bar stranded at the top of the screen, each new clock digit drawn over + the one before it, and the sleep-timer and quality menus leaving their imprint + after closing. One cause under all of it — nothing painted those bars. The + window surface is opaque, and for an opaque surface Android's renderer skips + clearing the damaged region and assumes the view hierarchy covers every pixel; + the video view covers only the letterboxed rect, so the bars were the window + background's alone to paint, and enabling compositing had cleared that + background to transparent. Three earlier attempts missed because they aimed at + the window's rotation animation and at video-frame retention — which is also + why the artefact reproduced standing still, with no rotation involved. + (UR-003, UR-066 → DR-194) + +- **Returning from background audio brings the picture back.** On the native + path, coming back from the lockscreen left a black screen: a play overlay + pinned at 0:00 and a play button that did nothing. Nothing had crashed — the + transition was simply dropped. The two render paths resume by different means, + and only one of them was performed: the web player reloads from its stream URL, + while the native player owns no element and nothing watches that URL on its + behalf, so it has to be handed the item again explicitly. It now is, at the + position the audio reached. (UR-040, UR-003 → DR-196) + +- **Next Up stops repeating what Continue Watching already shows.** The same + episode could occupy both home rows at once. (UR-023 → DR-197) + ## v0.6.0 ### 🐛 Fixes diff --git a/docs/requirements.md b/docs/requirements.md index 42fc5d2c..5a5d498d 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -347,7 +347,7 @@ Internal architecture, components, and application logic. | DR-185 | The app shell stops painting over the video surface. `app.css` clears the page's opaque layers for native video through three selectors, and one of them — `html[data-native-video="active"] [data-app-shell]` — was written against an attribute **no component has ever set, in any commit**. The shell is `+layout.svelte`'s root `div`, which paints `--color-background` across the entire viewport; VideoPlayer is `fixed inset-0 z-50` and correctly makes *itself* transparent on the native path, but it stacks *above* the shell, so the WebView still composited the shell's opaque background over the whole screen and the SurfaceView behind it could never be seen. This is the missing half of the compositing DR-172 went looking for: the spec's own layer table lists this layer as "cleared by `data-native-video` → app.css", which was written but never wired, and `html`/`body` being genuinely transparent made the CSS look correct in isolation. The failure is invisible three ways over — the CSS is valid, the selector is plausible, and a rule matching nothing looks exactly like a rule matching something already transparent — while the symptom (black screen, audio fine) is identical to a real compositing failure, which is how it survived DR-150 through DR-172. Fixed by setting the attribute the rule was written for, and guarded by asserting the *relationship* rather than the rule: every attribute the compositing block targets must be set somewhere in the app, so a selector aimed at nothing fails the suite instead of failing silently on a device | UI | UR-003, UR-004, UR-041 | Done | | DR-186 | The play overlay comes down when the backend plays. `isPlaying` was assigned once from the `player_play_item` response and thereafter only by the `player://state-changed` listener — a channel the backend never emits, the same dead wire that DR-182's first fix was mistakenly hung on. On the native path the flag therefore froze at whatever the initial response said: with ExoPlayer playing, the UI still believed it was paused, so the `bg-black/30` play-button overlay stayed raised across the whole video area and the transport button kept showing ▶. The video was simultaneously dimmed and covered while it played, which reads as "the overlay never goes away" and is easily mistaken for a second compositing fault. The mirror reads the same `player` store `playerEvents.ts` feeds, which is what the architecture already says is authoritative — the player reports state, the UI consumes it — and is gated to the native path so HTML5 keeps its element-event wiring, which is authoritative there | UI | UR-003, UR-005 | Done | | DR-187 | The system bars go away with the player, not only with the fullscreen button. `enterImmersive()` had exactly one caller, `toggleFullscreen()`, so opening the player left the status and navigation bars painted over it until the user pressed a button most never press. On the native path this is worse than cosmetic: the SurfaceView fills the content view, so the bars sit directly on top of the video. The player is a full-screen surface by construction — `fixed inset-0 z-50` over a `MATCH_PARENT` surface — so entry is the right moment. Called synchronously in `onMount` before any `await`, per the native-mode pitfall, and paired with the `exitImmersive()` already unconditional in `onDestroy`, so a player torn down while immersive cannot leave the rest of the app without bars | UI | UR-066, UR-003 | Done | -| DR-188 | Native Android video is **ready to be the default except for the background-audio handoff**, and the flip therefore waits. The picture defects behind DR-172 are all found, fixed and device-verified — DR-185 (the app shell painted over the surface through a CSS rule targeting an attribute nothing set), DR-182 (nothing could lift the poster card on a path with no `