From 60e89fe3b6baf3973ab479ef704b6bd0f6a299da Mon Sep 17 00:00:00 2001 From: Honey Date: Mon, 8 Jun 2026 20:08:56 +0200 Subject: [PATCH] fix(android): instant cold-start resume + play button icon flicker WebBridge: skip setMediaItems/prepare when the track is already buffered from the cold-start pre-warm, so resuming the last song is instant (~0s) instead of re-buffering from scratch (~10s). Player: show pause icon during buffering (isPlaying=false, isBuffering=true) so the button doesn't flash back to "play" between tracks. Also optimistically set _nativePlaying=true and call setPlayIcon immediately on playCurrent so the icon updates before the native callback round-trip. Co-Authored-By: Claude Sonnet 4.6 --- .../kotlin/com/novasonic/app/WebBridge.kt | 19 ++++++++++++++----- src/player/player.js | 9 +++++++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/kotlin/com/novasonic/app/WebBridge.kt b/android/app/src/main/kotlin/com/novasonic/app/WebBridge.kt index dbb0e93..8d4da59 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/WebBridge.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/WebBridge.kt @@ -28,11 +28,20 @@ class WebBridge(private val controllerProvider: () -> MediaController?) { runOnMain { val ctrl = controllerProvider() ?: return@runOnMain 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 ExoPlayer already has this exact single track buffered from the cold-start + // pre-warm, skip setMediaItems+prepare so we don't throw away the buffered data. + // Just call play() — this makes the "resume last track" instant instead of ~10s. + val alreadyReady = items.size == 1 && + ctrl.mediaItemCount == 1 && + ctrl.currentMediaItem?.mediaId == items[0].mediaId && + ctrl.playbackState != Player.STATE_IDLE + if (!alreadyReady) { + 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() } } diff --git a/src/player/player.js b/src/player/player.js index 2808d56..431e276 100644 --- a/src/player/player.js +++ b/src/player/player.js @@ -189,9 +189,12 @@ window.NSPlayer = { _nativePlaying = state.isPlaying; if (!state.isPlaying && !state.isBuffering) _cancelScrobble(); if (state.isPlaying && S.previewCurrent) stopPreview(false); + // Show pause icon while buffering (isPlaying=false but isBuffering=true) so the + // button doesn't flash "play" during the brief re-buffer between tracks. + const showPlaying = state.isPlaying || state.isBuffering; if (state.isPlaying !== prev) { - setPlayIcon(state.isPlaying); - document.getElementById('player-art')?.classList.toggle('spinning', state.isPlaying); + setPlayIcon(showPlaying); + document.getElementById('player-art')?.classList.toggle('spinning', showPlaying); if (navigator.mediaSession) navigator.mediaSession.playbackState = state.isPlaying ? 'playing' : 'paused'; } @@ -374,8 +377,10 @@ export function playCurrent() { if (HAS_BRIDGE) { S._trackChanging = true; + _nativePlaying = true; _resetProgressUI(); updatePlayerUI(s, true); + setPlayIcon(true); onNpSongChange(s.id); renderQueue(); try { window.NSBridge.setQueue(_nativeSingle(s), 0, true); } catch(_) {}