From f97e31178cb9c6e569e2b99efb9e1f01fb7a6f9a Mon Sep 17 00:00:00 2001 From: Honey Date: Wed, 20 May 2026 23:51:15 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20revert=20MusicService=20debounce=20?= =?UTF-8?q?=E2=80=94=20it=20was=20causing=20the=20desync,=20not=20fixing?= =?UTF-8?q?=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 120ms debounce on JS-initiated transitions (seek/setQueue) delayed updateStateSnapshot(), leaving a stale window where getState() returned the old index. If an AUTO callback fired during that window and the JS onTrackChanged handler called getState(), it would read the stale index and overwrite the correct player bar state with the wrong song. Fix: always fire onTrackChanged immediately so updateStateSnapshot() runs synchronously. The JS-side getState() reconciliation (from the previous commit) then reliably gets the settled native index every time. Co-Authored-By: Claude Sonnet 4.6 --- .../kotlin/com/novasonic/app/MusicService.kt | 37 +++++-------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/android/app/src/main/kotlin/com/novasonic/app/MusicService.kt b/android/app/src/main/kotlin/com/novasonic/app/MusicService.kt index 069e59e..260abd1 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/MusicService.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/MusicService.kt @@ -155,10 +155,6 @@ class MusicService : MediaSessionService() { private inner class PlayerListener : Player.Listener { - // Debounce JS-initiated transitions (seek / queue-replace) so rapid skips - // produce one onTrackChanged notification with the final settled state. - private var _pendingTrackChanged: Runnable? = null - override fun onIsPlayingChanged(isPlaying: Boolean) { val state = player.playbackState stateListener?.onPlaybackStateChanged( @@ -188,31 +184,16 @@ class MusicService : MediaSessionService() { Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED -> "queue" else -> "seek" } - // Always warm artwork immediately so the notification never shows a blank cover. + // Fire immediately — snapshot must always be fresh so the JS-side + // getState() reconciliation in onTrackChanged sees current native state. + stateListener?.onTrackChanged( + player.currentMediaItemIndex, + mediaItem?.mediaId ?: "", + reasonStr + ) + // Pre-warm artwork for current + next tracks so notification + // never shows a blank cover on auto-advance. warmArtworkCache() - - // Cancel any pending debounced dispatch from a previous transition. - _pendingTrackChanged?.let { handler.removeCallbacks(it) } - _pendingTrackChanged = null - - val dispatch = Runnable { - _pendingTrackChanged = null - stateListener?.onTrackChanged( - player.currentMediaItemIndex, - player.currentMediaItem?.mediaId ?: "", - reasonStr - ) - } - - if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) { - // Native auto-advance: JS didn't initiate this, notify immediately. - dispatch.run() - } else { - // JS-initiated (seek or setQueue): debounce 120 ms so rapid skips - // coalesce into one callback with the final settled index. - _pendingTrackChanged = dispatch - handler.postDelayed(dispatch, 120) - } } override fun onTimelineChanged(timeline: Timeline, reason: Int) {