diff --git a/src/player.js b/src/player.js index 40e061f..1d027bd 100644 --- a/src/player.js +++ b/src/player.js @@ -15,6 +15,7 @@ let _nativePlaying = false; let _lastPositionMs = 0; // ms — set by onPositionUpdate, used for prev() decision let _lastDurationMs = 0; // ms — set by onPositionUpdate, used for seek calculations let _readyTs = 0; // timestamp of last onReady — used to suppress spurious onQueueEnded +let _bridgeReady = false; // true once onReady has fired; onQueueEnded is ignored before this // ─── SCROBBLE ───────────────────────────────────────────────────────────── let _scrobbleTimer = null; @@ -85,10 +86,10 @@ window.NSPlayer = { // Called when MediaController connects (first load or WebView reload). onReady() { - // Guard window: ignore onQueueEnded for 1.5s after reconnect. - // ExoPlayer can deliver a stale STATE_ENDED callback right after the MediaController - // reconnects to a session whose last track had already finished. - _readyTs = Date.now(); + // Mark bridge as ready: onQueueEnded fired before this point came from a previous + // session (song finished while backgrounded) and must not advance the queue. + _bridgeReady = true; + _readyTs = Date.now(); if (HAS_BRIDGE && S.queue.length > 0) { try { @@ -216,9 +217,10 @@ window.NSPlayer = { // Single track finished — JS drives what plays next. onQueueEnded() { - // Suppress if fired within 1.5s of onReady — ExoPlayer can replay a stale STATE_ENDED - // callback right after the MediaController reconnects, which would skip the queue. - if (Date.now() - _readyTs < 1500) return; + // Ignore if onReady hasn't fired yet: the song ended while the app was backgrounded + // and the callback arrived before the new page session is fully connected. + // Also ignore the brief window right after onReady (stale STATE_ENDED replay). + if (!_bridgeReady || Date.now() - _readyTs < 1500) return; _nativePlaying = false; setPlayIcon(false); onEnded(); // handles repeat, radio top-up, and queue advance