refactor: single-track ExoPlayer model — JS owns queue entirely
Root cause of all Android player bugs: two competing queue managers.
ExoPlayer held all tracks and auto-advanced with its own index while JS
tracked S.qIdx separately. seekToIndex() is async via runOnMain, so
skips would interleave with ExoPlayer's own advances — causing desync,
wrong-song display, and skips that silently did nothing.
Fix: ExoPlayer always holds exactly ONE MediaItem. JS is the single
source of truth for queue order, shuffle, and repeat.
JS changes (player.js):
- playQueue/playCurrent/nextSong/prevSong/toggleShuffle: replace
setQueue(allTracks, idx) + seekToIndex with setQueue([single], 0)
- _pushQueueIfBridge: send only current track
- nativeInsertAt: no-op (radio songs land in S.queue; JS picks them up)
- fillRadioQueue: remove addToQueue native call
- onTrackChanged: simplified to just schedule scrobble + radio top-up
- onPlaybackStateChanged: remove index-reconciliation block
- onPositionUpdate: remove reconciliation tick (no queue drift possible)
- onQueueEnded: call onEnded() — JS drives what plays next
- toggleRepeat: setRepeatMode('one') only for repeat=one, else 'off'
- Add onNotificationNext / onNotificationPrev to NSPlayer
- Remove _skipping/_skipTimer/_reconTick/_setSkipping (all dead code)
Android changes:
- MusicService: add onSkipToNext/onSkipToPrevious to StateListener;
add MediaSession.Callback to intercept notification skip buttons and
delegate them to JS instead of letting ExoPlayer handle them
- MainActivity: implement new StateListener methods → callJs()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
85d8e8eae7
commit
d539a43307
3 files changed
+81
-141
No files matched your search
@@ -215,6 +215,9 @@ class MainActivity : AppCompatActivity(), MusicService.StateListener {
|
||||
|
||||
override fun onError(message: String) = callJs("onError", message)
|
||||
|
||||
override fun onSkipToNext() = callJs("onNotificationNext")
|
||||
override fun onSkipToPrevious() = callJs("onNotificationPrev")
|
||||
|
||||
// ─── JS bridge helpers ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.media3.session.CacheBitmapLoader
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class MusicService : MediaSessionService() {
|
||||
@@ -32,6 +33,8 @@ class MusicService : MediaSessionService() {
|
||||
fun onPositionUpdate(positionMs: Long, durationMs: Long)
|
||||
fun onQueueEnded()
|
||||
fun onError(message: String)
|
||||
fun onSkipToNext()
|
||||
fun onSkipToPrevious()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -106,8 +109,22 @@ class MusicService : MediaSessionService() {
|
||||
)
|
||||
|
||||
mediaSession = MediaSession.Builder(this, player)
|
||||
.setBitmapLoader(bitmapLoader) // use cached loader for notification artwork
|
||||
.setBitmapLoader(bitmapLoader)
|
||||
.setSessionActivity(notifTapPending)
|
||||
.setCallback(object : MediaSession.Callback {
|
||||
// Intercept notification next/prev so JS (not ExoPlayer) handles queue logic.
|
||||
override fun onSkipToNextMediaItem(
|
||||
session: MediaSession, controller: MediaSession.ControllerInfo
|
||||
) = Futures.immediateFuture(androidx.media3.session.SessionResult(
|
||||
androidx.media3.session.SessionResult.RESULT_SUCCESS
|
||||
)).also { stateListener?.onSkipToNext() }
|
||||
|
||||
override fun onSkipToPreviousMediaItem(
|
||||
session: MediaSession, controller: MediaSession.ControllerInfo
|
||||
) = Futures.immediateFuture(androidx.media3.session.SessionResult(
|
||||
androidx.media3.session.SessionResult.RESULT_SUCCESS
|
||||
)).also { stateListener?.onSkipToPrevious() }
|
||||
})
|
||||
.build()
|
||||
|
||||
player.addListener(PlayerListener())
|
||||
|
||||
Reference in new issue
Block a user