Files
jellytau/docs/native-player-verification.md
T
dtourolle fd8273824a
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 42s
🏗️ Build and Test JellyTau / Android Compile Check (push) Skipped
🏗️ Build and Test JellyTau / Supply Chain (push) Successful in 3m46s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m53s
Traceability Validation / Check Requirement Traces (push) Successful in 16s
Build & Release / Run Tests (push) Successful in 15m15s
Build & Release / Build Linux (push) Successful in 21m14s
Build & Release / Build Windows (push) Successful in 15m53s
Build & Release / Build Android (push) Successful in 31m34s
Build & Release / Create Release (push) Waiting to run
test(player): drive an engine that answers badly
Fair criticism: hardware time went into writing a checklist describing what the
tablet found, when it should have gone into making the suites able to find it.
A checklist decays and depends on someone following it. A test does not.

The gap was specific. Every engine the conformance suite drives reports sane
numbers, so it stayed green while a real one took the backend down. The old
PlayerBackend contract is a plain f64 — it never promised finite, never
promised positive, and nothing enforced it.

UT-223 adds the engine that was missing: a HostileBackend answering with
C.TIME_UNSET as seconds, NaN, both infinities, a negative and a zero. Reading a
snapshot must yield no duration and a zero position rather than panicking.
Against the adapter as originally written it fails with

    cannot convert float seconds to Duration: value is negative

which is the exact panic that produced a black screen on the tablet — now
reproduced in 0.00s on a laptop instead of by backgrounding an app.

UT-224 pins the other hardware-only finding: stopping clears an active
background-audio handoff, flag and base offset both. That was verified by
listening to a device, which is not a test.

Both were confirmed to fail against the pre-fix code before being kept.

The verification plan now says to prefer moving cases out of it and into tests,
and that what remains should be what genuinely needs eyes, ears or a display —
not what merely has not been automated yet.

793 Rust tests.
2026-08-23 10:54:49 +02:00

9.3 KiB

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, 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 originally verified only engines that behave, which is why both regressions passed them. That gap is now partly closed in code rather than in this document: UT-223 drives a deliberately hostile engine — C.TIME_UNSET, NaN, infinities, negatives — through the adapter, and fails with the exact panic that produced a black screen on a tablet. UT-224 pins the handoff clearing that was previously verified by listening to a device.

Prefer moving cases out of this file and into tests. Anything here that could fail automatically should; a checklist depends on someone remembering to follow it, and the two defects it was written for cost hardware time that would have been better spent making the suites realistic. What is left below is what genuinely needs eyes, ears, or a display — not what merely has not been automated yet.

1. Automated gates

Cheap, fast, and non-negotiable. Run from the worktree.

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

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:

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.

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:

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.
  • 🔴 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.