fix: an app update is offered only when the app changed; casting no longer sticks on "loading" when the same queue plays again (#6455, #6456)
release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m30s
release / go (push) Successful in 1m46s
release / android (push) Successful in 5m2s
release / Build signed APK (releases and dev) (push) Successful in 5m13s
release / integration (push) Successful in 5m22s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 13s
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m30s
release / go (push) Successful in 1m46s
release / android (push) Successful in 5m2s
release / Build signed APK (releases and dev) (push) Successful in 5m13s
release / integration (push) Successful in 5m22s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 13s
release / Verify release artifacts (tag releases only) (push) Skipped
ci/version.sh --apk names the APK from android/ alone, and the APK lane calls it. The app offers an update when the server's APK name differs from its own, so a name derived from the whole repo offered every phone a rebuild of the app it already had on each server-only deploy. The image keeps the union set: it bundles the APK under client/. Casting (#6456): - setMediaItems with the tracks already queued (play the queued album from another track) cleared the cast and waited for a resync that the id-keyed collector never ran. It is now a move within the queue: Seek TRACK_NR on a Sonos, start that track on a one-track renderer; the play() goes through. - A queue replaced through setMediaItems could take the Sonos incremental diff, which cleared target without restoring active, so the app thought it was back on the phone while the speaker played on, and the chosen start track was ignored. The diff now runs only while the route is still active; a replaced queue reloads in full at its start index. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+46
-10
@@ -216,24 +216,56 @@ class MinstrelForwardingPlayer(
|
||||
// drops via isLoadingUpnp() = true; the OutputPickerController
|
||||
// observes the uiState.queue change and runs the resync (re-clears
|
||||
// Sonos's native queue + AddURIToQueue the new tracks + Play).
|
||||
//
|
||||
// The SAME tracks set again (play on the album already queued, from
|
||||
// another track) is not a new queue: the renderer already holds it, and
|
||||
// the uiState collector -- which keys on the track ids -- would never see
|
||||
// a change to resync on, leaving the cast stuck "loading". It is a move
|
||||
// within the queue, so the renderer is pointed at the new track and the
|
||||
// play() that follows goes through.
|
||||
|
||||
override fun setMediaItems(mediaItems: List<MediaItem>) {
|
||||
val sameQueue = holdsSameItems(mediaItems)
|
||||
super.setMediaItems(mediaItems)
|
||||
markPendingResyncIfRemote()
|
||||
afterQueueSet(sameQueue)
|
||||
}
|
||||
|
||||
override fun setMediaItems(mediaItems: List<MediaItem>, resetPosition: Boolean) {
|
||||
val sameQueue = holdsSameItems(mediaItems)
|
||||
super.setMediaItems(mediaItems, resetPosition)
|
||||
markPendingResyncIfRemote()
|
||||
afterQueueSet(sameQueue)
|
||||
}
|
||||
|
||||
override fun setMediaItems(mediaItems: List<MediaItem>, startIndex: Int, startPositionMs: Long) {
|
||||
val sameQueue = holdsSameItems(mediaItems)
|
||||
super.setMediaItems(mediaItems, startIndex, startPositionMs)
|
||||
markPendingResyncIfRemote()
|
||||
afterQueueSet(sameQueue)
|
||||
}
|
||||
|
||||
private fun markPendingResyncIfRemote() {
|
||||
val wasActive = holder.active.value ?: return
|
||||
private fun holdsSameItems(mediaItems: List<MediaItem>): Boolean =
|
||||
mediaItems.size == delegate.mediaItemCount &&
|
||||
mediaItems.indices.all { mediaItems[it].mediaId == delegate.getMediaItemAt(it).mediaId }
|
||||
|
||||
private fun afterQueueSet(sameQueue: Boolean) {
|
||||
val active = holder.active.value ?: return
|
||||
if (!sameQueue) {
|
||||
markPendingResync(active)
|
||||
return
|
||||
}
|
||||
val index = delegate.currentMediaItemIndex
|
||||
val positionMs = delegate.currentPosition.coerceAtLeast(0L)
|
||||
Timber.w(
|
||||
"setMediaItems with the queue %s already holds -- moving to track %d",
|
||||
active.routeName,
|
||||
index + 1,
|
||||
)
|
||||
when (active.queueMode) {
|
||||
QueueMode.SINGLE_TRACK -> startSingleTrack(active, index, positionMs)
|
||||
QueueMode.NATIVE_QUEUE -> seekNativeQueue(active, index, positionMs)
|
||||
}
|
||||
}
|
||||
|
||||
private fun markPendingResync(wasActive: ActiveUpnp) {
|
||||
Timber.w(
|
||||
"setMediaItems while UPnP active (%s) -- marking pending resync",
|
||||
wasActive.routeName,
|
||||
@@ -332,11 +364,15 @@ class MinstrelForwardingPlayer(
|
||||
super.seekTo(mediaItemIndex, positionMs)
|
||||
when (active.queueMode) {
|
||||
QueueMode.SINGLE_TRACK -> playSingleTrackAt(active, mediaItemIndex, positionMs)
|
||||
QueueMode.NATIVE_QUEUE -> sendNativeQueueCommand(active) {
|
||||
active.avTransport.seekToTrack(mediaItemIndex + 1)
|
||||
if (positionMs > 0L) {
|
||||
active.avTransport.seek(positionMs)
|
||||
}
|
||||
QueueMode.NATIVE_QUEUE -> seekNativeQueue(active, mediaItemIndex, positionMs)
|
||||
}
|
||||
}
|
||||
|
||||
private fun seekNativeQueue(active: ActiveUpnp, index: Int, positionMs: Long) {
|
||||
sendNativeQueueCommand(active) {
|
||||
active.avTransport.seekToTrack(index + 1)
|
||||
if (positionMs > 0L) {
|
||||
active.avTransport.seek(positionMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-7
@@ -383,23 +383,27 @@ class OutputPickerController @Inject constructor(
|
||||
resyncSingleTrack(upnpRoute, transport, newQueue, newCurrentIndex)
|
||||
return@withLock
|
||||
}
|
||||
val handledIncrementally = runCatching {
|
||||
// The incremental diff is for a queue changed under a cast that kept
|
||||
// running (play next, an append, a removal): it leaves the playing
|
||||
// track alone. A queue REPLACED through setMediaItems has already
|
||||
// cleared active, and the listener asked for its start track and for
|
||||
// it to play -- both of which the diff would ignore, and with active
|
||||
// never restored the app would think it had gone back to the phone
|
||||
// while the speaker played on. That case reloads in full below.
|
||||
val stillActive = activeUpnpHolder.active.value?.routeId == routeId
|
||||
val handledIncrementally = stillActive && runCatching {
|
||||
sonosQueue.tryIncrementalResync(transport, oldIds, newQueue)
|
||||
}.getOrElse { e ->
|
||||
Timber.w(e, "Sonos incremental resync errored; falling back to full reload")
|
||||
false
|
||||
}
|
||||
if (handledIncrementally) {
|
||||
// Active was never cleared on the incremental path; clear any
|
||||
// target that markPendingResyncIfRemote set (it didn't, for
|
||||
// incremental cases that don't go through setMediaItems, but
|
||||
// belt-and-suspenders).
|
||||
activeUpnpHolder.setTarget(null)
|
||||
return@withLock
|
||||
}
|
||||
// Full rebuild: ensure active is cleared so transport calls drop
|
||||
// (markPendingResyncIfRemote may already have done this on the
|
||||
// setMediaItems path).
|
||||
// (the forwarding player has already done this on the setMediaItems
|
||||
// path).
|
||||
if (activeUpnpHolder.active.value != null) {
|
||||
activeUpnpHolder.set(null)
|
||||
activeUpnpHolder.setTarget(routeId)
|
||||
|
||||
Reference in New Issue
Block a user