android: swap exoplayer for libmpv#6
Merged
Merged
Conversation
prereq for the libmpv swap landing in the next commit — dev.jdtech.mpv:libmpv:1.0.0 ships kotlin 2.2 metadata and the 1.9 compiler bails with "actual metadata version is 2.2.0, compiler can read up to 2.0.0".
single audio engine across desktop/ios/android. android now runs libmpv 0.41 (+ ffmpeg 8.1, libass, libplacebo, mbedtls) packaged in the dev.jdtech.mpv:libmpv:1.0.0 maven central aar — 47mb universal, all 4 abis. minSdk bumps 24 → 26 (libmpv's floor, android 8.0, ≥99% device coverage). what this kills: - pre-download-and-swap dance for plex chunked-opus transcodes (libmpv handles mid-stream seek via the demuxer buffer + lavf reconnect chain, no more 30s pre-buffer transcode files) - system android.media.audiofx.Equalizer 5-band wrapper, plus the audioSessionId deferred-init race - suppressNextIdleEvent latch for exoplayer's mid-flight stop - the entire `mpvBufferingChange` / waveform scanning indicator (next commit) — libmpv's paused-for-cache feeds media3's STATE_BUFFERING natively - DefaultLoadControl tuning, position-poller-on-a-runnable, the PlayerListener bridge dance what this adds: - LibmpvSimplePlayer: a media3 SimpleBasePlayer wrapping MPVLib. state mutations + invalidateState() hop to applicationLooper via mainHandler (mandatory — mpv events fire on the event-loop thread). property observers cover time-pos, duration, pause, playlist-pos, idle-active, paused-for-cache. media session, foreground service, notification provider, accent tinting all unchanged — only the Player implementation behind them swaps. - 10-band lavfi EQ (50/100/200/400/800/1600/3200/6400/12800/ 16000 hz, ±12dB) identical to desktop/ios via `af=equalizer=...`. no more 5-band system fx surprise; bluetooth headphone dsp still chains on top of audiotrack output. - real `mpvGetDemuxerCacheTime` — prefetch.rs::wait_for_live_drain finally gets a non-sentinel value on android, stops hitting the 30s ceiling. - proguard keep for dev.jdtech.mpv.** because MPVLib uses jni reflection to call eventProperty/event/logMessage from native. without the keep, release builds silently SIGABRT on first property observation. settings migration: eq_bands.len() != 10 forces back to ten zeros on load (existing android users had 5 from the system fx). EQ resets to flat once, no crash, ui re-renders against the new 10-band schema.
ios was diverging on two options: `prefetch-playlist=yes` and missing `demuxer-readahead-secs`/`demuxer-max-bytes` overrides. brought ios into line with desktop's `default_mpv_options()`, which is also what the new android `LibmpvSimplePlayer` set in the previous commit. - `prefetch-playlist=no` everywhere. ramus's own reqwest prefetch worker rewrites playlist entries to file:// urls as they cache, so mpv's internal playlist prefetch would just race it. single prefetch strategy across all five platforms. - `demuxer-readahead-secs=1200` + `demuxer-max-bytes=2GiB` so the whole file lands in mpv's demuxer cache and seek-within-buffered is instant. matters most on plex chunked transcodes because `accept-ranges: none` means seek-forward outside the cache triggers a full re-get. remaining genuine ios divergence: `ao=audiounit` (the platform's AO) and `audio-exclusive=yes` (the avaudiosession category interlocks cleanly with the lock-screen widget — desktop handles exclusivity through OS-level audio session apis, not mpv). android matches desktop on everything except `ao=audiotrack,opensles`.
40d95ed added a waveform "scanning" sweep driven by an android-only `mpvBufferingChange` event because exoplayer didn't expose anything equivalent to mpv's `paused-for-cache`. with libmpv on android the event is no longer special — paused-for-cache feeds media3's STATE_BUFFERING natively, which is the same surface desktop and ios have always used. ripping the now-orphan plumbing in full so android matches the other two platforms exactly: - `MpvCallbacks.on_buffering_change` field + the lib.rs wiring - `emit_buffering_state` / `BufferingStatePayload` in events.rs - `BufferingPayload` parsing + listener registration in mpv_mobile.rs - `buffering-state` event listener in ui/src/lib/usePlaybackEvents.ts - `isBuffering` + `setBuffering` in playbackStore - the sweep RAF loop, `bufferingFraction` state, and both buffering branches of WaveformSeekBar net: one buffer model across five platforms (status === "buffering" from playback-state). seek bar shows the same thing on every host.
three findings from /ultrareview on #6: - (security, normal) `MediaItem.setMediaId(args.url)` was leaking the plex token across the MediaSession AIDL boundary. mediaId is one of the few fields media3 serialises to bound MediaController clients — any app with BIND_NOTIFICATION_LISTENER_SERVICE plus `adb dumpsys media_session` could pluck `X-Plex-Token` / `X-Plex-Headers` out of the session timeline. dropped both `setMediaId` calls; LibmpvSimplePlayer.getState falls back to `item.hashCode().toString()` for the playlist uid so the empty mediaId is harmless. previous (exoplayer) code used `MediaItem.fromUri` which left mediaId empty by default — the leak is a regression from this branch's switch to `Builder()`. - (nit) `fileLoadedEmitted` latch was only reset on idx change, so a same-position queue replace (queue=[A] → queue=[B] with B at idx 0) suppressed `mpvFileLoaded` for the new track. now tracks `lastReportedUri` alongside `lastReportedIndex` and resets on either change — metadata-only refreshes (same URI, same idx) still pass through silently. - (nit) `append-play` branch in `mpvLoadFile` didn't actually start playback from idle. media3's Player interface has no `append-play` equivalent: `addMediaItem` always maps to bare `loadfile append` (play=false in mpv), and `play()` on a SimpleBasePlayer in STATE_IDLE just flips `pause` without loading. now seeks to the appended index and plays — matches mpv's native append-play semantics. dead code in current rust callers but closes the contract gap.
1337raspberry
added a commit
that referenced
this pull request
May 15, 2026
readme/contributing/security/plugin-readme and the release.yml header comment still described the android audio stack as media3/exoplayer. since #6 it's libmpv via the dev.jdtech.mpv:libmpv aar wrapped behind a media3 SimpleBasePlayer, so the plumbing and the gotchas are different. also bump the README's android requirement from "7.0 nougat (api 24)" to "8.0 oreo (api 26)" — libmpv-android's floor. note in the download table that the apk now ships the libmpv + ffmpeg/codec stack inline (similar to the macos .app bundle). the contributing.md "single-thread contract" gotcha gets reworded in the same shape: the rule (every player.* call hits the main looper) is unchanged, the reason is now libmpv's event-loop thread + SimpleBasePlayer.applicationLooper, not exoplayer's internal threading model. the security.md mediasession-metadata bullet got broader phrasing to cover the AIDL-surface token leak class of bug (#6's ultrareview fix) without naming a specific engine.
1337raspberry
added a commit
that referenced
this pull request
May 15, 2026
the libmpv section of licenses/NOTICE.md was scoped to desktop
("ramus dynamically loads libmpv at runtime to provide audio
playback") even though ios shipped MPVKit and android shipped
Media3 well before this. now that android has joined the libmpv
boat via the dev.jdtech.mpv:libmpv aar, broadened the section to
say "every platform" and split out the per-platform substitution
story (libpath search on desktop, MPVKit SPM on ios, gradle on
android).
added an explicit table for the supporting libraries bundled
inside the android aar — ffmpeg 8.1, libplacebo, libass,
fribidi, libunibreak, harfbuzz, freetype, fontconfig, mbedtls,
dav1d, libxml2, lua — with their versions, licenses, and
upstream urls. all permissive or LGPL-2.1+, same posture
licenses/LICENSE.LGPL-2.1 already covers. version list verified
against jarnedemeulemeester/libmpv-android v1.0.0's release
notes — that's the upstream of the aar we pin in #6.
THIRD_PARTY_LICENSES.md's preamble pointer to NOTICE.md got
matching language ("libmpv on every platform, plus the
supporting libraries shipped inside the android aar") and the
file is regenerated. version field in the regen also bumps
ramus-core/ramus-tauri from 1.0.0 → 1.0.1 (drift from 397df71)
— that was just stale.
THIRD_PARTY_LICENSES.md continues to be generated from Cargo.lock
+ pnpm-lock only; android gradle deps are documented manually in
NOTICE.md, which is the cheaper and cleaner spot for them given
the AAR ships a known fixed set and the generator can't see
gradle's graph deterministically.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
android now runs libmpv via
dev.jdtech.mpv:libmpv:1.0.0behind a media3SimpleBasePlayer. single audio engine across desktop / ios / android.commits
LibmpvSimplePlayer.kt, 10-band lavfi EQ, settings migration, proguard keep)paused-for-cachefeeds media3 natively now)minSdk bumps 24 → 26 (libmpv's floor). +12 MB per ABI in apk size, not a constraint.
test plan