fix(player): не глохнуть в фоне (wake mode) + авто-переподключение потока
Симптом: по Bluetooth в машине с выключенным экраном радио через время замолкало. Причины и фиксы: - setWakeMode(C.WAKE_MODE_NETWORK) + право WAKE_LOCK — ExoPlayer держит partial wakelock + wifilock во время игры. Без этого система усыпляла CPU/Wi-Fi при выключенном экране → буфер пустел → поток глох (главная причина). - onPlayerError → scheduleReconnect(): при обрыве сети (туннели, край соты) поток пере-готавливается с нарастающей задержкой (2с→15с, до 10 попыток), а не замолкает навсегда. Счётчик сбрасывается при успешном старте; переподключение отменяется при ручной паузе/стопе/смене станции.
This commit is contained in:
@@ -11,6 +11,7 @@ import androidx.media3.common.ForwardingPlayer
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Metadata
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import android.util.Log
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
@@ -85,6 +86,10 @@ class PlayerController @Inject constructor(
|
||||
private val timerScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
private var sleepJob: Job? = null
|
||||
|
||||
// Переподключение при обрыве потока (дорога/туннели).
|
||||
private var retryCount = 0
|
||||
private var reconnectJob: Job? = null
|
||||
|
||||
private companion object {
|
||||
const val FADE_MS = 20_000L // длительность плавного затухания в конце таймера
|
||||
const val CROSSFADE_MS = 90_000L // переход радио → звук для сна (внутри аутро)
|
||||
@@ -148,11 +153,24 @@ class PlayerController @Inject constructor(
|
||||
true
|
||||
)
|
||||
.setHandleAudioBecomingNoisy(true)
|
||||
// Держим CPU + Wi-Fi активными, пока играем (partial wakelock + wifilock).
|
||||
// Без этого при выключенном экране система усыпляет сеть → буфер пустеет →
|
||||
// радио глохнет (главная причина «обрыва» в машине по Bluetooth).
|
||||
.setWakeMode(C.WAKE_MODE_NETWORK)
|
||||
.build()
|
||||
.apply {
|
||||
addListener(object : Player.Listener {
|
||||
override fun onIsPlayingChanged(playing: Boolean) {
|
||||
_isPlaying.value = playing
|
||||
// Успешно играем — сбрасываем счётчик попыток переподключения.
|
||||
if (playing) retryCount = 0
|
||||
}
|
||||
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
// В дороге сигнал рвётся (туннели, край соты). Не глушим радио
|
||||
// навсегда — пере-готовим поток с нарастающей задержкой.
|
||||
Log.w("PlayerController", "Ошибка плеера: ${error.errorCodeName}, переподключение")
|
||||
scheduleReconnect()
|
||||
}
|
||||
|
||||
override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
|
||||
@@ -188,6 +206,24 @@ class PlayerController @Inject constructor(
|
||||
audioEffects.attach(sessionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Переподключение после ошибки потока с нарастающей задержкой (2с→15с, до 10
|
||||
* попыток ≈ пережить туннель). Счётчик сбрасывается, как только снова заиграло.
|
||||
*/
|
||||
private fun scheduleReconnect() {
|
||||
reconnectJob?.cancel()
|
||||
if (retryCount >= 10) return
|
||||
val delayMs = (2_000L * (retryCount + 1)).coerceAtMost(15_000L)
|
||||
retryCount++
|
||||
reconnectJob = timerScope.launch {
|
||||
delay(delayMs)
|
||||
runCatching {
|
||||
exoPlayer.prepare()
|
||||
exoPlayer.playWhenReady = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val player: Player = object : ForwardingPlayer(exoPlayer) {
|
||||
override fun getAvailableCommands(): Player.Commands {
|
||||
return super.getAvailableCommands()
|
||||
@@ -212,6 +248,9 @@ class PlayerController @Inject constructor(
|
||||
|
||||
fun play(url: String, stationPrefix: String, stationName: String, stationId: Int? = null) {
|
||||
Log.d("PlayerController", "play() called with url=$url prefix=$stationPrefix")
|
||||
// Новая станция — сбрасываем переподключение предыдущего потока.
|
||||
reconnectJob?.cancel()
|
||||
retryCount = 0
|
||||
_currentStationId.value = stationId
|
||||
_icyTitle.value = null
|
||||
val mediaItem = MediaItem.Builder()
|
||||
@@ -373,6 +412,9 @@ class PlayerController @Inject constructor(
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
// Пауза пользователем — отменяем отложенное переподключение, иначе оно
|
||||
// позже само возобновит воспроизведение.
|
||||
reconnectJob?.cancel()
|
||||
exoPlayer.pause()
|
||||
}
|
||||
|
||||
@@ -381,6 +423,7 @@ class PlayerController @Inject constructor(
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
reconnectJob?.cancel()
|
||||
exoPlayer.stop()
|
||||
_currentStationPrefix.value = null
|
||||
_currentStationId.value = null
|
||||
|
||||
Reference in New Issue
Block a user