Files
jellytau/docs/native-player-verification.md
T
dtourolle 40d358ab52 docs: a verification plan for the native player
Not a generic smoke test. Every case exists because something specific went
wrong, and most were found on hardware after the suites were already green.

The sequences are load-bearing. Two defects this cycle only appeared in a
particular order of actions — play, enable background audio, background,
foreground, exit — and testing the same features in any other order found
neither. So the plan asks for that order explicitly rather than listing
"background audio" as a feature to try.

It also states plainly that a green conformance run is not sufficient evidence
to ship, because both regressions introduced during this work passed
conformance and were caught by a person using the app.

Includes a symptom-to-cause table, because none of these presented as their
cause: a dead play/pause button was an unobserved property, a black screen was
a float that could not become a Duration, and a scrub bar with no scale was a
duration of zero being believed.

"Known open" lists what is deliberately unfixed so each gets a decision rather
than a surprise — device-local resume, the unconfirmed handoff swap, and the
broken side-by-side debug install whose own error message advises an uninstall
that would destroy the real app's data.
2026-08-23 10:39:37 +02:00

192 lines
8.6 KiB
Markdown

# Native player — verification plan
What to check before the `MediaPlayer` contract and Linux native video reach
`master`.
This is not a generic smoke test. Every case below exists because something
specific went wrong, and most of them were found on hardware **after** the
automated suites were green. Treat the sequences as load-bearing: several
defects only appeared in a particular order of actions, and testing the same
features in a different order missed them entirely.
Companion to [release-checklist.md](release-checklist.md), which covers the
release mechanics. This covers whether the player is fit to release at all.
## What is risky about this change
- `PlayerController` now talks to a `MediaPlayer` contract instead of
`PlayerBackend`. Every engine reaches it through an adapter that did not exist
before (DR-245).
- mpv decodes video on Linux for the first time, composited under the webview
(DR-231).
- Seek strategy is driven by an ability each engine declares rather than by a
truth table (DR-246).
- Two regressions were introduced during this work and caught only on a device:
a wrong capability for ExoPlayer (DR-246 follow-up) and a `Duration` panic
(DR-252). Both were invisible to the test suites.
The suites verify engines that behave. **The manual passes exist to catch
engines that do not.**
## 1. Automated gates
Cheap, fast, and non-negotiable. Run from the worktree.
```bash
bun run check # 0 errors, 0 warnings
bun run test # frontend
bun run test:rust # Rust
bun run format:check
bun run lint # 0 errors; warnings at or below the CI ratchet
bun run check:boundary
bun run traces:validate
bun run traces:coverage # at or above MIN_THRESHOLD
cd src-tauri && cargo fmt --check && cargo clippy --all-targets -- -D warnings
cargo clippy --all-targets --features conformance -- -D warnings
```
The eslint warning count is a **ratchet**: equal to the CI limit is a pass, one
over fails the build. Going one over is how a piece of dead state was found
during this work — do not raise the limit to get past it.
## 2. Engine conformance
```bash
bun run test:player # mpv + legacy, desktop
bun run test:player:android # ExoPlayer, on a connected device
```
Expected, and each deviation is meaningful rather than noise:
| Engine | Result | If it differs |
|---|---|---|
| `MpvPlayer` | 9/9 | A real regression. Stop. |
| `LegacyPlayer` | 8/9 | The one failure is `transport_settings_round_trip`: the old trait has no mute or rate. Any *other* failure is a regression. |
| ExoPlayer (device) | 7/7 | Two cases are absent because the Kotlin player exposes no mute or rate. |
A green conformance run is **not** sufficient evidence to ship. Both regressions
introduced during this work passed conformance.
## 3. Desktop (Linux)
Run with native video on, since that is what is new:
```bash
JELLYTAU_NATIVE_VIDEO=1 bun run tauri dev
```
- [ ] **Direct play** — a file the server does not transcode. Picture and sound.
- [ ] **Transcoded play** — something the server must re-encode (4K, HEVC, or an
audio codec the renderer cannot take).
- [ ] **Resume** — an item watched previously *on this install*. The prompt
appears and playback starts at the offered position, not at zero.
*(Resume is device-local — see "Known open".)*
- [ ] **Scrub** on a direct-play item; position lands and playback continues.
- [ ] **Scrub on a transcoded item.** Separate case on purpose: it takes a
different path, and it silently did nothing for months (DR-238).
- [ ] **Pause and resume** — the button follows the player. It stopped doing so
when a property was handled but never observed (DR-239).
- [ ] **Fullscreen** — the window really fills the display. Measure it if
unsure: the log prints `rendering WxH`, and a height short of the panel
means the document went fullscreen and the window did not (DR-240).
- [ ] **Exit the player** — audio stops. Listen; do not assume.
- [ ] **Audio-only playback** still works: mini player, queue, next/previous.
- [ ] Nothing in the log matches `PANIC` or `ERROR`.
## 4. Android
The tablet needs the *side-by-side* build. **Do not uninstall the release app**
to make an install succeed — see "Known open" for why the normal command is
currently wrong.
```bash
bun run android:build --device
./scripts/sync-android-sources.sh
cd src-tauri/gen/android && ANDROID_HOME="$HOME/Android/Sdk" ./gradlew \
:app:assembleUniversalDebug -x :app:rustBuildUniversalDebug \
-x :app:rustBuildArm64Debug -x :app:rustBuildArmDebug \
-x :app:rustBuildX86Debug -x :app:rustBuildX86_64Debug
adb install -r app/build/outputs/apk/universal/debug/app-universal-debug.apk
```
Confirm the package is `com.dtourolle.jellytau.debug` before installing:
```bash
aapt2 dump packagename <apk>
```
If it says `com.dtourolle.jellytau`, the suffix was lost — **stop**, re-sync and
re-assemble. Installing it would try to replace the real app.
Then, with `adb logcat` capturing:
- [ ] Play a video. Picture, sound, and controls.
- [ ] **Scrub.** The bar has a scale — a duration of `0.0` means the seek bar has
nothing to scrub against (DR-251).
- [ ] Transcoded seek lands rather than restarting the stream. ExoPlayer seeks a
transcode in place; declaring otherwise re-opened it (DR-246).
- [ ] PiP.
- [ ] Lockscreen: controls respond and position tracks.
- [ ] **The handoff sequence, in this exact order:**
1. play a video
2. enable background audio
3. background the app — audio continues
4. foreground the app — **video returns**
5. exit the player — **everything stops**
Steps 4 and 5 are where two separate defects lived (DR-250, DR-252). Doing
the same actions in another order finds neither.
- [ ] `grep -c 'PANIC at' <logcat>` returns 0.
## 5. Regression checks with a named cause
Each of these presented as something other than its cause, which is why they are
listed separately from the feature passes above.
| Symptom to look for | Was actually | Ref |
|---|---|---|
| Skip on a transcoded item does nothing, or jumps to zero | Seek strategy keyed on the container, not the engine | DR-238, DR-246 |
| Play/pause button does not follow the player | A property handled but never observed, so the event never arrived | DR-239 |
| Fullscreen leaves a strip of desktop | The document went fullscreen, the window did not | DR-240 |
| Resume plays from the beginning | A seek issued before the engine had a file was discarded | DR-241 |
| Scrub bar has no scale | Duration reported as `0.0` and believed | DR-251 |
| Black screen, no controls, after a background-audio round trip | A junk duration converted to a `Duration` panicked the backend | DR-252 |
| Audio still playing after leaving the player | The stop was aimed at whichever renderer bookkeeping believed was active | DR-250 |
## Known open — decide, do not discover
None of these are fixed. Each needs an explicit ship / do-not-ship call rather
than being met with surprise during testing.
- **Resume is device-local.** Progress is read from the local database and
nothing consults the server's `UserData`. A fresh install, a second device or
a reinstall offers no resume even though the server knows the position. Not a
regression — it has always been so.
- **The background-audio handoff is an unconfirmed state swap.**
`exit_background_audio` marks the video element the player again the moment it
is called, while the element has not reloaded. DR-250 makes the visible
symptom impossible; the race is intact and can still misdirect a lockscreen
command or a position read. See
[media-player-controller.md](specs/media-player-controller.md).
- 🔴 **The side-by-side debug install is broken.** `bun run android:dev`
produces an APK with the *release* application id, because the Tauri build
regenerates `gen/build.gradle.kts` after the sync drops the `.debug` suffix in.
It then fails on signatures, and its own error message advises uninstalling —
which would destroy the real app's data. **Fix this before anyone else builds
for Android.**
- **`PlayerBackend` still exists** behind `LegacyPlayer`, and the frontend still
carries some playback state. DR-248 and DR-249 are not started.
## Ship criteria
Ship when:
1. Every automated gate in §1 passes.
2. Conformance matches §2 exactly, deviations included.
3. §3 and §4 are complete, on real hardware, by a person.
4. §5 shows no symptom returning.
5. Every item in "Known open" has a recorded decision.
Do not ship on green suites alone. Both regressions introduced during this work
passed every suite and were caught by a person using the app.