From 20e683d705a0601972d8ef30bc1fba9d58a62cf7 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sat, 22 Aug 2026 21:32:59 +0200 Subject: [PATCH] feat(android): conformance on a device, and a start position for ExoPlayer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DR-247. The desktop suite cannot reach ExoPlayer: it needs an Android Context and a Looper, so it exists only inside an app process. These are the same behaviours, asserted against the engine itself. Writing them forced the same gap open that mpv had. JellyTauPlayer.load(url, mediaId) had no way to express a start position, so every caller loaded and then seeked — the test could not even be written against the old signature, which is a stronger statement than a failing assertion. The position now goes to ExoPlayer with the media item via setMediaItem(item, startPositionMs), and the two-argument form delegates to it, so nothing else had to change. Running one suite against both engines settled something guesswork could not: seekWhileOpeningIsHonoured passes on ExoPlayer with no fix ExoPlayer already queues a seek issued before prepare() completes. So the lost-seek half of DR-241 was mpv-specific, and only the missing vocabulary for a start position was shared. That is the difference between "both engines have this bug" and knowing which one does. All seven cases pass on device (ROD2-W09, arm64). The fixture is a silent WAV synthesised in the cache directory at setup rather than committed or pushed: no binary in the repo, no adb step, and an exact duration, which the seek assertions depend on. Also adds the instrumentation runner to defaultConfig and teaches sync-android-sources.sh to mirror src/androidTest, the way it already mirrors src/test — so the canonical tree stays the only place tests are edited. Run: ./gradlew :app:connectedUniversalDebugAndroidTest -x :app:rustBuildUniversalDebug --- docs/requirements.md | 2 + scripts/sync-android-sources.sh | 13 + src-tauri/android/app/build.gradle.kts | 4 + .../jellytau/player/PlayerConformanceTest.kt | 252 ++++++++++++++++++ .../jellytau/player/JellyTauPlayer.kt | 22 +- 5 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 src-tauri/android/src/androidTest/java/com/dtourolle/jellytau/player/PlayerConformanceTest.kt diff --git a/docs/requirements.md b/docs/requirements.md index d4469f0f..35737f06 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -441,6 +441,7 @@ Internal architecture, components, and application logic. | DR-243 | Every engine passes one conformance suite, and a `FakePlayer` implements the contract deterministically. The suite is written before the second engine so it cannot encode whatever the first happened to do, and it drives readiness through a harness rather than sleeping. `FakePlayer` models the one behaviour that matters — opening is not instantaneous — so the load/seek race can be expressed on purpose, and lets the controller, queue, autoplay and session logic be tested with no engine at all | Player | UR-081 | In Progress | | DR-244 | `MpvPlayer` implements `MediaPlayer` over libmpv, applying the start position at load time via mpv's own `start` option rather than seeking after an asynchronous `loadfile`, and holding a seek that arrives during `Opening` until the file loads. A standalone `player-conformance` binary runs the suite against it with audio and video routed to null, so a wrapper is verifiable without building or launching the app | Player | UR-081, UR-040 | Done | | DR-245 | `LegacyPlayer` drives the old `PlayerBackend` through the `MediaPlayer` contract, so engines not yet ported keep working during the migration and the two designs can be compared on one engine and one file. It reproduces the old load-then-play-then-seek sequence faithfully rather than a fixed-up version, because making it pass would defeat its purpose | Player | UR-081 | In Progress | +| DR-247 | ExoPlayer can be told where to start. `JellyTauPlayer.load(url, mediaId)` had no way to express a start position, so every caller loaded and then seeked; the position is now handed to ExoPlayer with the media item via `setMediaItem(item, startPositionMs)`, and the two-argument form delegates to it. Running the conformance cases on a device also settled which half of DR-241 was engine-specific: ExoPlayer already queues a seek issued before `prepare()` completes, so it never had the lost-seek defect mpv did — only the missing vocabulary for a start position | Player | UR-081, UR-005 | Done | | DR-198 | The webview runs under a real Content-Security-Policy, and the asset protocol is scoped to the one directory it still serves. `csp` was `null`, which disables CSP entirely: any script that reached the web layer — through a future `{@html}`, a dependency, or a devtools paste — would have inherited the whole IPC surface, and with it the user's session. `script-src 'self'` (Tauri injects a nonce for SvelteKit's inline bootstrap script at build time, so no `'unsafe-inline'` is needed) plus `object-src`/`frame-src 'none'` and `base-uri 'self'` is the part that is genuinely restrictive. `img-src`/`media-src`/`connect-src` cannot be: the Jellyfin origin is typed in by the user at run time and is commonly plain `http` on a LAN, so they allow `http:`/`https:` — a wide grant for *data*, but one that still bars `file:`, `filesystem:` and scripting schemes, and leaves `script-src` untouched. `style-src` keeps `'unsafe-inline'` because Svelte compiles `style="…"` attributes (including `app.html`'s `display: contents` wrapper) into markup; this is safe only while no `