fix: eliminate notification/playerbar song desync on Android
Two-pronged fix for the state drift between the system notification (ground truth) and the WebView player bar: JS (player.js): onTrackChanged now reconciles the incoming index with a synchronous NSBridge.getState() call before applying it. Stale callbacks from earlier rapid skips immediately correct themselves to the live settled index instead of overwriting the correct state. Kotlin (MusicService.kt): debounce JS-initiated transitions (seek / setQueue) by 120 ms so rapid skip sequences produce one onTrackChanged notification at the final index rather than one per skip step. Auto-advance transitions are still fired immediately since JS doesn't initiate those. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
0874fd4f9f
commit
52079536b6
2 files changed
+35
-7
No files matched your search
@@ -155,6 +155,10 @@ 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(
|
||||||
@@ -184,14 +188,31 @@ 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.
|
||||||
|
warmArtworkCache()
|
||||||
|
|
||||||
|
// Cancel any pending debounced dispatch from a previous transition.
|
||||||
|
_pendingTrackChanged?.let { handler.removeCallbacks(it) }
|
||||||
|
_pendingTrackChanged = null
|
||||||
|
|
||||||
|
val dispatch = Runnable {
|
||||||
|
_pendingTrackChanged = null
|
||||||
stateListener?.onTrackChanged(
|
stateListener?.onTrackChanged(
|
||||||
player.currentMediaItemIndex,
|
player.currentMediaItemIndex,
|
||||||
mediaItem?.mediaId ?: "",
|
player.currentMediaItem?.mediaId ?: "",
|
||||||
reasonStr
|
reasonStr
|
||||||
)
|
)
|
||||||
// Pre-warm artwork for current + next tracks so notification
|
}
|
||||||
// never shows a blank cover on auto-advance.
|
|
||||||
warmArtworkCache()
|
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) {
|
||||||
|
|||||||
+10
-3
@@ -143,9 +143,16 @@ window.NSPlayer = {
|
|||||||
try {
|
try {
|
||||||
const p = JSON.parse(payloadJson);
|
const p = JSON.parse(payloadJson);
|
||||||
if (typeof p.index !== 'number') return;
|
if (typeof p.index !== 'number') return;
|
||||||
if (p.index < 0 || p.index >= S.queue.length) return;
|
// Reconcile with the live native state so stale out-of-order callbacks
|
||||||
const indexChanged = (p.index !== S.qIdx);
|
// (e.g. from rapid skips) don't overwrite the already-correct index.
|
||||||
S.qIdx = p.index;
|
let idx = p.index;
|
||||||
|
try {
|
||||||
|
const live = JSON.parse(window.NSBridge.getState() || '{}');
|
||||||
|
if (typeof live.currentIndex === 'number') idx = live.currentIndex;
|
||||||
|
} catch {}
|
||||||
|
if (idx < 0 || idx >= S.queue.length) return;
|
||||||
|
const indexChanged = (idx !== S.qIdx);
|
||||||
|
S.qIdx = idx;
|
||||||
const s = S.queue[S.qIdx];
|
const s = S.queue[S.qIdx];
|
||||||
if (!s) return;
|
if (!s) return;
|
||||||
updatePlayerUI(s, true);
|
updatePlayerUI(s, true);
|
||||||
|
|||||||
Reference in new issue
Block a user