fix(phone): поднимаем порог жёсткого seek аудио 2000→5000 мс

Жёсткий seekTo при |audio-video|>2000мс перебуферизировал ExoPlayer и давал
слышимую тишину (0.5-1с) + прыжок позиции. На локальном файле обычная буферная
пауза вывода пробивала порог и зацикливала seek→пауза. Порог 5000мс: умеренный
рассинхрон закрывается плавной коррекцией скоростью (бесшумно). Плюс диагностика:
лог типа источника (локальный файл/стриминг S3) и STATE_BUFFERING ExoPlayer,
и пик diff в логе seek.
This commit is contained in:
2026-08-30 04:50:37 +03:00
parent 3af74851db
commit b11879520e
@@ -6,6 +6,7 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackParameters import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -40,8 +41,13 @@ internal const val DRIFT_GAIN = 0.0002f
/** Потолок изменения темпа (±10%): дальше слышно, что «не в свой темп». */ /** Потолок изменения темпа (±10%): дальше слышно, что «не в свой темп». */
internal const val MAX_DRIFT_SPEED = 0.10f internal const val MAX_DRIFT_SPEED = 0.10f
/** Экстремальный рассинхрон: |diff| > HARD_SEEK_THRESHOLD_MS — скорость не нагонит, жёсткий seekTo. */ /**
internal const val HARD_SEEK_THRESHOLD_MS = 2000L * Экстремальный рассинхрон: |diff| > HARD_SEEK_THRESHOLD_MS — скорость не нагонит, жёсткий seekTo.
* Поднят с 2000 до 5000: обычная буферная пауза вывода (0.5–1.5 с) при локальном файле не должна
* форсировать жёсткий seek (он перебуферизирует ExoPlayer и даёт слышимую тишину + прыжок позиции);
* умеренный рассинхрон закрывается плавной коррекцией скоростью, бесшовно.
*/
internal const val HARD_SEEK_THRESHOLD_MS = 5000L
/** /**
* Правило коррекции аудио по мастер-таймкоду очков (чистая функция, тестируемая без Android): * Правило коррекции аудио по мастер-таймкоду очков (чистая функция, тестируемая без Android):
@@ -138,6 +144,16 @@ class AudioSyncPlayer(private val context: Context) {
private var player: ExoPlayer? = null private var player: ExoPlayer? = null
private var ticker: Job? = null private var ticker: Job? = null
/** Слушатель буферизации ExoPlayer: логирует паузы вывода (причина «немого» звука). */
private val bufferingListener = object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
when (playbackState) {
Player.STATE_BUFFERING -> log("audio", "буферизация: вывод приостановлен (возможен пропуск звука 0.5–1с)")
Player.STATE_READY -> log("audio", "буфер готов, вывод возобновлён")
}
}
}
/** Позиция очков из последнего sync. */ /** Позиция очков из последнего sync. */
private var lastVideoPositionMs = 0L private var lastVideoPositionMs = 0L
/** IsPlaying очков из последнего sync. */ /** IsPlaying очков из последнего sync. */
@@ -162,12 +178,16 @@ class AudioSyncPlayer(private val context: Context) {
} else { } else {
Uri.fromFile(File(urlOrFile)) Uri.fromFile(File(urlOrFile))
} }
val p = player ?: ExoPlayer.Builder(context).build().also { player = it } val p = player ?: ExoPlayer.Builder(context).build().also {
it.addListener(bufferingListener)
player = it
}
p.setMediaItem(MediaItem.fromUri(uri)) p.setMediaItem(MediaItem.fromUri(uri))
p.prepare() p.prepare()
p.seekTo(startPositionMs.coerceAtLeast(0L)) p.seekTo(startPositionMs.coerceAtLeast(0L))
p.playWhenReady = true p.playWhenReady = true
log("audio", "играю $urlOrFile с ${startPositionMs} мс") val isStream = urlOrFile.startsWith("http://") || urlOrFile.startsWith("https://")
log("audio", "играю ${if (isStream) "стриминг S3" else "локальный файл"}: $urlOrFile с ${startPositionMs} мс")
startTicker() startTicker()
} }
} }
@@ -189,7 +209,7 @@ class AudioSyncPlayer(private val context: Context) {
decision.seekToMs != null -> { decision.seekToMs != null -> {
p.seekTo(decision.seekToMs) p.seekTo(decision.seekToMs)
p.setPlaybackParameters(PlaybackParameters(1f)) p.setPlaybackParameters(PlaybackParameters(1f))
log("audio sync:", "seek → ${decision.seekToMs}") log("audio sync:", "seek → ${decision.seekToMs} (diff=${abs(audioPos - positionMs)})")
} }
else -> { else -> {
p.setPlaybackParameters(PlaybackParameters(decision.speed)) p.setPlaybackParameters(PlaybackParameters(decision.speed))