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 206b0c97..76ee9b9b 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 @@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import retrofit2.Retrofit import retrofit2.create +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -171,10 +172,10 @@ class OutputPickerController @Inject constructor( * track, set the renderer's URI, play, then pause local playback so * audio yields to the speaker. Wrapped in `runCatching` at each * step — token failure, transport-lookup failure, and SOAP failure - * each abandon the selection cleanly rather than crashing. Errors - * surface to the operator via OkHttp logging for now; spec'd UI - * error surfacing lands in a follow-up if hardware reveals it's - * needed. + * each abandon the selection cleanly rather than crashing. Failures + * log at warn level via Timber so on-device verification can find + * the cause in logcat (OkHttp's logger doesn't cover our own + * deserialize / SOAP-parse code paths). */ private suspend fun selectUpnp(route: OutputRoute) { val trackId = playerController.uiState.value.currentTrack?.id ?: return @@ -184,6 +185,8 @@ class OutputPickerController @Inject constructor( transport.setAVTransportURI(token.url) transport.play() playerController.pause() + }.onFailure { e -> + Timber.w(e, "UPnP select failed for route ${route.id}") } } diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/output/upnp/UpnpDiscoveryController.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/output/upnp/UpnpDiscoveryController.kt index 485a9301..ad0a8ebb 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/output/upnp/UpnpDiscoveryController.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/output/upnp/UpnpDiscoveryController.kt @@ -5,7 +5,6 @@ import com.fabledsword.minstrel.di.ApplicationScope import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -45,11 +44,12 @@ class UpnpDiscoveryController @Inject constructor( private val routesInternal = MutableStateFlow>(emptyList()) val routes: StateFlow> = routesInternal.asStateFlow() - private var fetchJob: Job? = null - init { ssdp.start(appScope) - fetchJob = appScope.launch(Dispatchers.IO) { + // appScope is process-lifetime (SupervisorJob + Dispatchers.Default), + // so the launched collector dies with the process — no explicit + // cancellation needed. + appScope.launch(Dispatchers.IO) { ssdp.discoveries.collect { locationUrl -> handleDiscovery(locationUrl) } } }