fix(android): UPnP forwarding -- route mint failures through drop, no double drop
android / Build + lint + test (push) Failing after 1m25s

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 17:18:53 -04:00
parent ab6c3a1354
commit 85cea8d559
3 changed files with 22 additions and 9 deletions
@@ -101,18 +101,30 @@ class MinstrelForwardingPlayer(
override fun seekToNextMediaItem() {
val active = holder.active.value
super.seekToNextMediaItem()
if (active != null) {
scope.launch { syncCurrentItemToRemote(active) }
if (active == null) {
super.seekToNextMediaItem()
return
}
// Advance the local cursor first so syncCurrentItemToRemote can read
// the new currentMediaItem on the application looper. ExoPlayer stays
// paused while UPnP is active (playWhenReady=false invariant), so this
// does not produce local audio.
super.seekToNextMediaItem()
scope.launch { syncCurrentItemToRemote(active) }
}
override fun seekToPreviousMediaItem() {
val active = holder.active.value
super.seekToPreviousMediaItem()
if (active != null) {
scope.launch { syncCurrentItemToRemote(active) }
if (active == null) {
super.seekToPreviousMediaItem()
return
}
// Advance the local cursor first so syncCurrentItemToRemote can read
// the new currentMediaItem on the application looper. ExoPlayer stays
// paused while UPnP is active (playWhenReady=false invariant), so this
// does not produce local audio.
super.seekToPreviousMediaItem()
scope.launch { syncCurrentItemToRemote(active) }
}
override fun getCurrentPosition(): Long =
@@ -135,8 +147,8 @@ class MinstrelForwardingPlayer(
private suspend fun syncCurrentItemToRemote(active: ActiveUpnp) {
val mediaId = handlerEvaluate { delegate.currentMediaItem?.mediaId } ?: return
val token = runCatching { tokens.mint(mediaId) }.getOrNull() ?: return
runCatching {
val token = tokens.mint(mediaId)
active.avTransport.setAVTransportURIWithMetadata(
uri = token.url,
mime = token.mime,
@@ -148,6 +160,7 @@ class MinstrelForwardingPlayer(
}
private fun handleSoapFailure(active: ActiveUpnp, t: Throwable) {
pollJob?.cancel()
Timber.w(t, "UPnP transport call failed on %s", active.routeName)
remoteState.applyError(t)
handler.post { onDrop(active.routeName) }
@@ -22,7 +22,7 @@ class RemotePlayerState {
@Volatile var currentTrackUri: String = ""; private set
@Volatile var lastError: Throwable? = null; private set
private var consecutivePollFailures: Int = 0
@Volatile private var consecutivePollFailures: Int = 0
fun applyPositionInfo(positionMs: Long, durationMs: Long, trackUri: String) {
this.positionMs = positionMs
@@ -45,7 +45,7 @@ data class RouteSnapshot(
* - [OutputRoute.Protocol.SYSTEM] — MediaRouter.selectRoute (built-in,
* wired, Bluetooth)
* - [OutputRoute.Protocol.UPNP] — mint a signed stream token via
* [CastApi.streamToken], drive the discovered renderer with
* [StreamTokenProvider.mint], drive the discovered renderer with
* AVTransport.SetAVTransportURI + Play, pause local playback so
* audio yields to the network speaker
* - [OutputRoute.Protocol.CAST] / [OutputRoute.Protocol.SONOS] —