fix(android): UPnP survives screen-off + cursor catches up to Sonos
android / Build + lint + test (push) Has been cancelled
android / Build + lint + test (push) Has been cancelled
This commit is contained in:
+30
-10
@@ -264,16 +264,19 @@ class MinstrelForwardingPlayer(
|
||||
}
|
||||
|
||||
/**
|
||||
* One poll tick: read position + transport state from Sonos and apply to
|
||||
* [remoteState]. The local cursor is NOT synced to Sonos's Track index --
|
||||
* during the 17-second queue load Sonos reports Track=1 (the first added
|
||||
* track) which would silently walk the local cursor to 0, then SetAV+Seek
|
||||
* lands and Sonos jumps to the user's actual track, racing with the
|
||||
* sync. Local + Sonos cursors stay in sync because every override
|
||||
* (seekToNext/Prev/seekTo) advances both sides by the same delta.
|
||||
* Hardware-button or Sonos-app driven advances ARE missed until the
|
||||
* user takes a transport action -- accepted tradeoff pending GENA
|
||||
* event subscriptions.
|
||||
* One poll tick: read position + transport state from Sonos, apply to
|
||||
* [remoteState], and forward-sync the local cursor to Sonos's Track
|
||||
* index when not in queue load.
|
||||
*
|
||||
* Cursor sync is gated on `holder.target == null` (= not loading)
|
||||
* because during load Sonos reports Track=1 while we're still
|
||||
* appending, and syncing would race the SetAV+Seek that lands
|
||||
* after. Outside load, forward sync catches Sonos auto-advances
|
||||
* (queue end-of-track), Sonos-app driven Next presses, and any
|
||||
* drift after a brief poll-failure burst that didn't trip the
|
||||
* drop threshold. Forward-only because a Next override we just
|
||||
* issued can race with a poll still reporting the prior Track --
|
||||
* the next poll catches up safely.
|
||||
*/
|
||||
private suspend fun pollOnce(active: ActiveUpnp) {
|
||||
val info = active.avTransport.getPositionInfo()
|
||||
@@ -283,6 +286,7 @@ class MinstrelForwardingPlayer(
|
||||
trackUri = info.trackUri,
|
||||
trackNumber = info.track,
|
||||
)
|
||||
maybeSyncLocalCursor(info.track)
|
||||
val transport = active.avTransport.getTransportInfo()
|
||||
when (transport.state) {
|
||||
TransportState.PLAYING -> {
|
||||
@@ -305,6 +309,22 @@ class MinstrelForwardingPlayer(
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeSyncLocalCursor(sonosTrack: Int) {
|
||||
if (holder.target.value != null) return
|
||||
if (sonosTrack <= 0) return
|
||||
val sonosIdx = sonosTrack - 1
|
||||
handler.post {
|
||||
val localIdx = delegate.currentMediaItemIndex
|
||||
if (sonosIdx > localIdx && sonosIdx < delegate.mediaItemCount) {
|
||||
Timber.w(
|
||||
"UPnP cursor catch-up: local=%d -> sonos=%d",
|
||||
localIdx, sonosIdx,
|
||||
)
|
||||
delegate.seekTo(sonosIdx, 0L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val POLL_INTERVAL_MS = 1_000L
|
||||
const val NON_PLAYING_CONFIRM = 2
|
||||
|
||||
@@ -67,6 +67,11 @@ class RemotePlayerState @Inject constructor() {
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val DROP_THRESHOLD = 3
|
||||
// ~30 seconds of consecutive poll failures before declaring the route
|
||||
// dropped. Bumped from 3 because screen-off WiFi sleep / brief Doze
|
||||
// can stall socket I/O for several seconds without the renderer
|
||||
// actually being unreachable -- a 3-failure drop kicked us back to
|
||||
// local audio every time the phone went into a pocket.
|
||||
const val DROP_THRESHOLD = 30
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user