fix(android): debounce onReady; pre-warm artwork before playback starts
Song restart: onPageFinished can fire multiple times mid-session (hash/state changes in WebView). Added 3s debounce so onReady is not re-delivered spuriously while a song is playing. Notification artwork: warmArtworkCache was called from onTimelineChanged where player.currentMediaItemIndex hadn't updated yet (lags until prepare()), so it warmed the wrong tracks. WebBridge.setQueue now calls MusicService.publicWarmArtwork(startIndex) immediately after setMediaItems and before prepare()/play(), giving the bitmap loader a head start with the correct index. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
ac4f09c8b8
commit
494361191a
3 files changed
+33
-8
No files matched your search
@@ -30,6 +30,9 @@ class MainActivity : AppCompatActivity(), MusicService.StateListener {
|
||||
private var controllerFuture: ListenableFuture<MediaController>? = null
|
||||
private var mediaController: MediaController? = null
|
||||
private var pendingOpenNowPlaying = false
|
||||
// Debounce onReady: WebView can fire onPageFinished multiple times (hash changes,
|
||||
// certain JS navigations). Only deliver onReady once per 3-second window.
|
||||
private var lastOnReadyMs = 0L
|
||||
|
||||
// ─── Back-press handling ──────────────────────────────────────────────────
|
||||
|
||||
@@ -108,7 +111,10 @@ class MainActivity : AppCompatActivity(), MusicService.StateListener {
|
||||
controllerFuture = future
|
||||
future.addListener({
|
||||
mediaController = try { future.get() } catch (_: Exception) { null }
|
||||
if (mediaController != null) callJs("onReady")
|
||||
if (mediaController != null) {
|
||||
lastOnReadyMs = System.currentTimeMillis()
|
||||
callJs("onReady")
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(this))
|
||||
}
|
||||
|
||||
@@ -136,11 +142,16 @@ class MainActivity : AppCompatActivity(), MusicService.StateListener {
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest) = false
|
||||
|
||||
// Re-announce ready + current state after a WebView reload so JS can re-sync
|
||||
// Re-announce ready + current state after a WebView reload so JS can re-sync.
|
||||
// Debounced: WebView can fire onPageFinished for hash/state changes mid-session.
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
if (mediaController != null) {
|
||||
updateStateSnapshot()
|
||||
callJs("onReady")
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastOnReadyMs > 3_000) {
|
||||
lastOnReadyMs = now
|
||||
updateStateSnapshot()
|
||||
callJs("onReady")
|
||||
}
|
||||
}
|
||||
if (pendingOpenNowPlaying) {
|
||||
pendingOpenNowPlaying = false
|
||||
|
||||
@@ -37,6 +37,8 @@ class MusicService : MediaSessionService() {
|
||||
companion object {
|
||||
// Set by MainActivity before connecting; cleared on destroy.
|
||||
@Volatile var stateListener: StateListener? = null
|
||||
// Service instance — lets WebBridge trigger artwork pre-warming directly.
|
||||
@Volatile var instance: MusicService? = null
|
||||
}
|
||||
|
||||
// ─── Fields ───────────────────────────────────────────────────────────────
|
||||
@@ -61,6 +63,7 @@ class MusicService : MediaSessionService() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
|
||||
val httpFactory = DefaultHttpDataSource.Factory()
|
||||
.setUserAgent("NovaSonic-Android/1.0")
|
||||
@@ -118,6 +121,7 @@ class MusicService : MediaSessionService() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
instance = null
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
mediaSession.release()
|
||||
player.release()
|
||||
@@ -128,11 +132,12 @@ class MusicService : MediaSessionService() {
|
||||
|
||||
// Loads the next `count` tracks' artwork into the CacheBitmapLoader so that
|
||||
// onMediaItemTransition never stalls waiting for a fresh HTTP fetch.
|
||||
private fun warmArtworkCache(count: Int = 3) {
|
||||
val currentIdx = player.currentMediaItemIndex
|
||||
// fromIndex: explicit start index; -1 means use player.currentMediaItemIndex.
|
||||
private fun warmArtworkCache(count: Int = 3, fromIndex: Int = -1) {
|
||||
val startIdx = if (fromIndex >= 0) fromIndex else player.currentMediaItemIndex
|
||||
val total = player.mediaItemCount
|
||||
for (offset in 0 until count) {
|
||||
val idx = currentIdx + offset
|
||||
val idx = startIdx + offset
|
||||
if (idx >= total) break
|
||||
val item = player.getMediaItemAt(idx)
|
||||
if (item.mediaMetadata.artworkUri != null) {
|
||||
@@ -141,6 +146,11 @@ class MusicService : MediaSessionService() {
|
||||
}
|
||||
}
|
||||
|
||||
// Called by WebBridge right after setMediaItems so artwork fetch starts
|
||||
// before prepare()/play() — avoids the race where the notification renders
|
||||
// before the bitmap is in the cache.
|
||||
fun publicWarmArtwork(startIndex: Int) = warmArtworkCache(count = 5, fromIndex = startIndex)
|
||||
|
||||
// ─── Player.Listener ─────────────────────────────────────────────────────
|
||||
|
||||
private inner class PlayerListener : Player.Listener {
|
||||
|
||||
@@ -27,7 +27,11 @@ class WebBridge(private val controllerProvider: () -> MediaController?) {
|
||||
if (items.isEmpty()) return
|
||||
runOnMain {
|
||||
val ctrl = controllerProvider() ?: return@runOnMain
|
||||
ctrl.setMediaItems(items, startIndex.coerceAtLeast(0), 0L)
|
||||
val idx = startIndex.coerceAtLeast(0)
|
||||
ctrl.setMediaItems(items, idx, 0L)
|
||||
// Pre-warm artwork before prepare()/play() so the notification bitmap
|
||||
// cache is populated before Media3 renders the first notification.
|
||||
MusicService.instance?.publicWarmArtwork(idx)
|
||||
ctrl.prepare()
|
||||
if (autoPlay) ctrl.play()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user