fix: suppress spurious onQueueEnded after reconnect; handle STATE_ENDED on reopen; add isEnded to snapshot
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
1277913ac1
commit
21d55d8ab0
2 files changed
+20
No files matched your search
@@ -249,6 +249,7 @@ class MainActivity : AppCompatActivity(), MusicService.StateListener {
|
|||||||
val json = JSONObject().apply {
|
val json = JSONObject().apply {
|
||||||
put("isPlaying", ctrl.isPlaying)
|
put("isPlaying", ctrl.isPlaying)
|
||||||
put("isBuffering", ctrl.playbackState == Player.STATE_BUFFERING)
|
put("isBuffering", ctrl.playbackState == Player.STATE_BUFFERING)
|
||||||
|
put("isEnded", ctrl.playbackState == Player.STATE_ENDED)
|
||||||
put("currentIndex", ctrl.currentMediaItemIndex)
|
put("currentIndex", ctrl.currentMediaItemIndex)
|
||||||
put("currentId", ctrl.currentMediaItem?.mediaId ?: "")
|
put("currentId", ctrl.currentMediaItem?.mediaId ?: "")
|
||||||
put("positionMs", ctrl.currentPosition)
|
put("positionMs", ctrl.currentPosition)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ let _nativePos = 0; // ms
|
|||||||
let _nativePlaying = false;
|
let _nativePlaying = false;
|
||||||
let _lastPositionMs = 0; // ms — set by onPositionUpdate, used for prev() decision
|
let _lastPositionMs = 0; // ms — set by onPositionUpdate, used for prev() decision
|
||||||
let _lastDurationMs = 0; // ms — set by onPositionUpdate, used for seek calculations
|
let _lastDurationMs = 0; // ms — set by onPositionUpdate, used for seek calculations
|
||||||
|
let _readyTs = 0; // timestamp of last onReady — used to suppress spurious onQueueEnded
|
||||||
|
|
||||||
// ─── SCROBBLE ─────────────────────────────────────────────────────────────
|
// ─── SCROBBLE ─────────────────────────────────────────────────────────────
|
||||||
let _scrobbleTimer = null;
|
let _scrobbleTimer = null;
|
||||||
@@ -84,12 +85,18 @@ window.NSPlayer = {
|
|||||||
|
|
||||||
// Called when MediaController connects (first load or WebView reload).
|
// Called when MediaController connects (first load or WebView reload).
|
||||||
onReady() {
|
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();
|
||||||
|
|
||||||
if (HAS_BRIDGE && S.queue.length > 0) {
|
if (HAS_BRIDGE && S.queue.length > 0) {
|
||||||
try {
|
try {
|
||||||
const raw = window.NSBridge.getState();
|
const raw = window.NSBridge.getState();
|
||||||
const state = raw ? (JSON.parse(raw) || {}) : {};
|
const state = raw ? (JSON.parse(raw) || {}) : {};
|
||||||
const nativeLen = state.queueLength || 0;
|
const nativeLen = state.queueLength || 0;
|
||||||
const nativePlaying = state.isPlaying || false;
|
const nativePlaying = state.isPlaying || false;
|
||||||
|
const nativeEnded = state.isEnded || false;
|
||||||
const nativeId = state.currentId || '';
|
const nativeId = state.currentId || '';
|
||||||
|
|
||||||
// Single-track model: native index is always 0, meaningless for queue position.
|
// Single-track model: native index is always 0, meaningless for queue position.
|
||||||
@@ -123,6 +130,15 @@ window.NSPlayer = {
|
|||||||
S.qIdx = 0;
|
S.qIdx = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (nativeLen > 0 && nativeEnded && idMatch) {
|
||||||
|
// Song finished while backgrounded and ExoPlayer is in STATE_ENDED.
|
||||||
|
// Reset to position 0 so the user can press play to replay — do NOT advance queue.
|
||||||
|
_nativePlaying = false;
|
||||||
|
_nativePos = 0;
|
||||||
|
if (currentSong) { updatePlayerUI(currentSong, true); onNpSongChange(currentSong.id); }
|
||||||
|
setPlayIcon(false);
|
||||||
|
renderQueue();
|
||||||
|
_nsBridge('seekTo', 0);
|
||||||
} else if (nativeLen > 0 && !nativePlaying && idMatch) {
|
} else if (nativeLen > 0 && !nativePlaying && idMatch) {
|
||||||
// Paused, song matches — re-sync UI state only.
|
// Paused, song matches — re-sync UI state only.
|
||||||
_nativePlaying = false;
|
_nativePlaying = false;
|
||||||
@@ -200,6 +216,9 @@ window.NSPlayer = {
|
|||||||
|
|
||||||
// Single track finished — JS drives what plays next.
|
// Single track finished — JS drives what plays next.
|
||||||
onQueueEnded() {
|
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;
|
||||||
_nativePlaying = false;
|
_nativePlaying = false;
|
||||||
setPlayIcon(false);
|
setPlayIcon(false);
|
||||||
onEnded(); // handles repeat, radio top-up, and queue advance
|
onEnded(); // handles repeat, radio top-up, and queue advance
|
||||||
|
|||||||
Reference in new issue
Block a user