diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/output/OutputPickerController.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/output/OutputPickerController.kt index 130b0bbf..1160653f 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/output/OutputPickerController.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/output/OutputPickerController.kt @@ -1,3 +1,4 @@ +@file:Suppress("TooManyFunctions") // 5 MediaRouter.Callback overrides inflate the count package com.fabledsword.minstrel.player.output import android.content.Context @@ -11,7 +12,6 @@ import com.fabledsword.minstrel.player.StreamTokenProvider import com.fabledsword.minstrel.player.output.upnp.RenderingControlClient import com.fabledsword.minstrel.player.output.upnp.SoapClient import com.fabledsword.minstrel.player.output.upnp.UpnpDiscoveryController -import com.fabledsword.minstrel.player.output.upnp.sonos.SonosZoneGroup import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -20,6 +20,8 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import okhttp3.OkHttpClient import timber.log.Timber import javax.inject.Inject @@ -86,17 +88,17 @@ class OutputPickerController @Inject constructor( private val systemRoutesInternal = MutableStateFlow(snapshotFromRouter()) private val selectedUpnpRouteIdInternal = MutableStateFlow(null) - private var lastKnownRemotePositionMs: Long = 0L + private val selectUpnpMutex = Mutex() val routesState: StateFlow = combine( systemRoutesInternal, upnpDiscovery.routes, upnpDiscovery.sonosTopology, selectedUpnpRouteIdInternal, - ) { sys, upnp, topology, upnpSelected -> - val suppressed = nonCoordinatorMemberUdns(topology) + ) { sys, upnp, _, upnpSelected -> + val suppressed = upnpDiscovery.nonCoordinatorMemberUdns() val visibleUpnp = upnp - .filter { it.id.removePrefix("uuid:") !in suppressed } + .filter { it.id.removePrefix("uuid:") !in suppressed } // suppressed set is bare UDNs .map { OutputRoute.fromUpnpRoute(it) } val merged = sys.available + visibleUpnp val current = if (upnpSelected != null) { @@ -107,12 +109,6 @@ class OutputPickerController @Inject constructor( RouteSnapshot(current = current, available = sortRoutes(merged)) }.stateIn(scope, SharingStarted.Eagerly, systemRoutesInternal.value) - private fun nonCoordinatorMemberUdns(topology: List): Set = - topology.flatMap { g -> - g.members.map { it.udn.removePrefix("uuid:") } - .filter { it != g.coordinatorUdn.removePrefix("uuid:") } - }.toSet() - private val callback = object : MediaRouter.Callback() { override fun onRouteAdded(router: MediaRouter, route: MediaRouter.RouteInfo) = refresh() @@ -189,14 +185,14 @@ class OutputPickerController @Inject constructor( val wasUpnp = selectedUpnpRouteIdInternal.value if (wasUpnp != null) { val active = activeUpnpHolder.active.value - lastKnownRemotePositionMs = remoteState.positionMs + val capturedPositionMs = remoteState.positionMs val wasPlayingRemote = remoteState.isPlaying scope.launch { runCatching { active?.avTransport?.stop() } .onFailure { Timber.w(it, "UPnP Stop failed during disconnect") } activeUpnpHolder.set(null) selectedUpnpRouteIdInternal.value = null - playerController.seekTo(lastKnownRemotePositionMs) + playerController.seekTo(capturedPositionMs) if (wasPlayingRemote) playerController.play() } } @@ -214,11 +210,11 @@ class OutputPickerController @Inject constructor( * the cause in logcat (OkHttp's logger doesn't cover our own * deserialize / SOAP-parse code paths). */ - private suspend fun selectUpnp(route: OutputRoute) { + private suspend fun selectUpnp(route: OutputRoute) = selectUpnpMutex.withLock { val trackId = playerController.uiState.value.currentTrack?.id if (trackId == null) { Timber.w("UPnP select skipped: no currentTrack (start playback first)") - return + return@withLock } // Honor Sonos topology: pick the coordinator's route when the user // tapped a group row. Suppression in routesState already keeps the @@ -232,7 +228,7 @@ class OutputPickerController @Inject constructor( "UPnP select skipped: no transport for route id=${effectiveRoute.id} " + "(route disappeared or id mismatch with discovery list)", ) - return + return@withLock } val rendering = renderingClientFor(effectiveRoute.id) selectedUpnpRouteIdInternal.value = effectiveRoute.id