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