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 1989f904..cbf22ddc 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 @@ -17,6 +17,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.OkHttpClient import okhttp3.Request import timber.log.Timber +import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton @@ -45,6 +46,15 @@ class UpnpDiscoveryController @Inject constructor( ) { private val ssdp = SsdpDiscovery(context) + // Transport commands (play/pause/seek/next) and the 1 Hz poll go through + // this client. A dead renderer should fail fast so OutputPickerController's + // drop-recovery reverts to the phone in ~2 s instead of the shared client's + // 10 s connect timeout. Derived via newBuilder() so the connection pool + + // dispatcher stay shared with the app client. + private val controlHttp: OkHttpClient = okHttp.newBuilder() + .connectTimeout(CONTROL_CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .build() + private val routesInternal = MutableStateFlow>(emptyList()) val routes: StateFlow> = routesInternal.asStateFlow() @@ -91,7 +101,7 @@ class UpnpDiscoveryController @Inject constructor( fun transportFor(routeId: String): AVTransportClient? { val route = routesInternal.value.firstOrNull { it.id == routeId } ?: return null return AVTransportClient( - loggingSoapClient(okHttp, route.name), + loggingSoapClient(controlHttp, route.name), route.avTransportControlUrl, ) } @@ -225,6 +235,11 @@ class UpnpDiscoveryController @Inject constructor( // devices append. Anchored to end-of-string so it never eats // a legitimate parenthetical inside a name. val IP_SUFFIX_REGEX = Regex("""\s*\(\d+\.\d+\.\d+\.\d+\)$""") + + // Aggressive connect timeout for transport SOAP so a dead renderer + // trips the drop-recovery quickly. Conservative enough not to false-drop + // a slow-but-alive LAN; read timeout still inherits the shared client. + const val CONTROL_CONNECT_TIMEOUT_SECONDS = 2L } }