diff --git a/app/src/main/java/com/radiola/service/PlayerController.kt b/app/src/main/java/com/radiola/service/PlayerController.kt index 3b83f02..5198673 100644 --- a/app/src/main/java/com/radiola/service/PlayerController.kt +++ b/app/src/main/java/com/radiola/service/PlayerController.kt @@ -4,6 +4,8 @@ import android.content.Context import android.media.AudioDeviceCallback import android.media.AudioDeviceInfo import android.media.AudioManager +import android.net.ConnectivityManager +import android.net.Network import android.net.Uri import androidx.media3.common.AudioAttributes import androidx.media3.common.C @@ -90,6 +92,31 @@ class PlayerController @Inject constructor( private var retryCount = 0 private var reconnectJob: Job? = null + // Намерение играть: пользователь включил станцию и не ставил паузу/стоп. + // По нему решаем, переподключаться ли после обрыва — НЕ сдаёмся навсегда. + @Volatile private var intendedToPlay = false + + // Слушатель сети: возврат/смена сети (Wi-Fi↔LTE в машине, выход из туннеля) + // мгновенно переподключает поток, не дожидаясь бэк-оффа. Главная причина, по + // которой радио раньше «не оживало» после смены сети. + private val connectivityManager = + context.getSystemService(ConnectivityManager::class.java) + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + if (!intendedToPlay) return + // Колбэк — на системном потоке; ExoPlayer трогаем на main (timerScope). + timerScope.launch { + if (!exoPlayer.isPlaying) { + retryCount = 0 + runCatching { + exoPlayer.prepare() + exoPlayer.playWhenReady = true + } + } + } + } + } + private companion object { const val FADE_MS = 20_000L // длительность плавного затухания в конце таймера const val CROSSFADE_MS = 90_000L // переход радио → звук для сна (внутри аутро) @@ -212,7 +239,10 @@ class PlayerController @Inject constructor( */ private fun scheduleReconnect() { reconnectJob?.cancel() - if (retryCount >= 10) return + // Пока пользователь хочет играть — пробуем переподключаться бесконечно + // (с бэк-оффом до 15с). Раньше сдавались навсегда после 10 попыток (~100с) → + // длинный туннель/смена сети глушили радио до ручного перезапуска. + if (!intendedToPlay) return val delayMs = (2_000L * (retryCount + 1)).coerceAtMost(15_000L) retryCount++ reconnectJob = timerScope.launch { @@ -244,6 +274,7 @@ class PlayerController @Inject constructor( init { audioManager.registerAudioDeviceCallback(audioDeviceCallback, null) + runCatching { connectivityManager?.registerDefaultNetworkCallback(networkCallback) } } fun play(url: String, stationPrefix: String, stationName: String, stationId: Int? = null) { @@ -251,6 +282,7 @@ class PlayerController @Inject constructor( // Новая станция — сбрасываем переподключение предыдущего потока. reconnectJob?.cancel() retryCount = 0 + intendedToPlay = true _currentStationId.value = stationId _icyTitle.value = null val mediaItem = MediaItem.Builder() @@ -412,17 +444,19 @@ class PlayerController @Inject constructor( } fun pause() { - // Пауза пользователем — отменяем отложенное переподключение, иначе оно - // позже само возобновит воспроизведение. + // Пауза пользователем — больше не хотим играть, отменяем переподключение. + intendedToPlay = false reconnectJob?.cancel() exoPlayer.pause() } fun play() { + intendedToPlay = true exoPlayer.play() } fun stop() { + intendedToPlay = false reconnectJob?.cancel() exoPlayer.stop() _currentStationPrefix.value = null @@ -432,6 +466,7 @@ class PlayerController @Inject constructor( fun release() { timerScope.cancel() audioManager.unregisterAudioDeviceCallback(audioDeviceCallback) + runCatching { connectivityManager?.unregisterNetworkCallback(networkCallback) } sleepSoundPlayer.stop() exoPlayer.release() }