fix: revert MusicService debounce — it was causing the desync, not fixing it

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 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-20 23:51:15 +02:00
1 parent 52079536b6
commit f97e31178c
1 file changed
+9 -28
@@ -155,10 +155,6 @@ class MusicService : MediaSessionService() {
private inner class PlayerListener : Player.Listener { 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) { override fun onIsPlayingChanged(isPlaying: Boolean) {
val state = player.playbackState val state = player.playbackState
stateListener?.onPlaybackStateChanged( stateListener?.onPlaybackStateChanged(
@@ -188,31 +184,16 @@ class MusicService : MediaSessionService() {
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED -> "queue" Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED -> "queue"
else -> "seek" 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() 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) { override fun onTimelineChanged(timeline: Timeline, reason: Int) {