From 75d256eda5150f65b2fe0be6ceea9c2a96258db7 Mon Sep 17 00:00:00 2001 From: nk Date: Thu, 11 Jun 2026 23:22:39 +0300 Subject: [PATCH] =?UTF-8?q?fix(player):=20=D0=B2=D0=BE=D1=81=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=82=D0=BE=D0=BA=D0=B0=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5?= =?UTF-8?q?=20=D1=81=D0=BC=D0=B5=D0=BD=D1=8B/=D0=BF=D1=80=D0=BE=D0=BF?= =?UTF-8?q?=D0=B0=D0=B6=D0=B8=20=D1=81=D0=B5=D1=82=D0=B8=20(=D0=BC=D0=B0?= =?UTF-8?q?=D1=88=D0=B8=D0=BD=D0=B0,=20=D1=82=D1=83=D0=BD=D0=BD=D0=B5?= =?UTF-8?q?=D0=BB=D0=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Причина «падений через 10-15 мин в машине»: при смене сети (Wi-Fi↔LTE) или долгом обрыве поток рвался, а переподключение СДАВАЛОСЬ навсегда после 10 попыток (~100с), и не было реакции на возврат сети → радио не оживало, foreground-сервис отваливался, процесс убивала система (это выглядело как «падение», хотя крэша не было). - scheduleReconnect больше не сдаётся: переподключается, пока пользователь хочет играть (флаг intendedToPlay; пауза/стоп его снимают). - Добавлен ConnectivityManager.registerDefaultNetworkCallback: при возврате/смене сети мгновенно re-prepare потока, не дожидаясь бэк-оффа. Co-Authored-By: Claude Opus 4.8 --- .../com/radiola/service/PlayerController.kt | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) 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() }