From b11879520e9a396a52fe7b14036d62ee3c47cccf Mon Sep 17 00:00:00 2001 From: subochev Date: Sun, 30 Aug 2026 04:50:37 +0300 Subject: [PATCH] =?UTF-8?q?fix(phone):=20=D0=BF=D0=BE=D0=B4=D0=BD=D0=B8?= =?UTF-8?q?=D0=BC=D0=B0=D0=B5=D0=BC=20=D0=BF=D0=BE=D1=80=D0=BE=D0=B3=20?= =?UTF-8?q?=D0=B6=D1=91=D1=81=D1=82=D0=BA=D0=BE=D0=B3=D0=BE=20seek=20?= =?UTF-8?q?=D0=B0=D1=83=D0=B4=D0=B8=D0=BE=202000=E2=86=925000=20=D0=BC?= =?UTF-8?q?=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Жёсткий seekTo при |audio-video|>2000мс перебуферизировал ExoPlayer и давал слышимую тишину (0.5-1с) + прыжок позиции. На локальном файле обычная буферная пауза вывода пробивала порог и зацикливала seek→пауза. Порог 5000мс: умеренный рассинхрон закрывается плавной коррекцией скоростью (бесшумно). Плюс диагностика: лог типа источника (локальный файл/стриминг S3) и STATE_BUFFERING ExoPlayer, и пик diff в логе seek. --- .../binom/viewmate/phone/AudioSyncPlayer.kt | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/app-phone/src/main/kotlin/pw/binom/viewmate/phone/AudioSyncPlayer.kt b/app-phone/src/main/kotlin/pw/binom/viewmate/phone/AudioSyncPlayer.kt index 946fe1c..a143b65 100644 --- a/app-phone/src/main/kotlin/pw/binom/viewmate/phone/AudioSyncPlayer.kt +++ b/app-phone/src/main/kotlin/pw/binom/viewmate/phone/AudioSyncPlayer.kt @@ -6,6 +6,7 @@ import android.os.Handler import android.os.Looper import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackParameters +import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -40,8 +41,13 @@ internal const val DRIFT_GAIN = 0.0002f /** Потолок изменения темпа (±10%): дальше слышно, что «не в свой темп». */ 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): @@ -138,6 +144,16 @@ class AudioSyncPlayer(private val context: Context) { private var player: ExoPlayer? = 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. */ private var lastVideoPositionMs = 0L /** IsPlaying очков из последнего sync. */ @@ -162,12 +178,16 @@ class AudioSyncPlayer(private val context: Context) { } else { 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.prepare() p.seekTo(startPositionMs.coerceAtLeast(0L)) p.playWhenReady = true - log("audio", "играю $urlOrFile с ${startPositionMs} мс") + val isStream = urlOrFile.startsWith("http://") || urlOrFile.startsWith("https://") + log("audio", "играю ${if (isStream) "стриминг S3" else "локальный файл"}: $urlOrFile с ${startPositionMs} мс") startTicker() } } @@ -189,7 +209,7 @@ class AudioSyncPlayer(private val context: Context) { decision.seekToMs != null -> { p.seekTo(decision.seekToMs) p.setPlaybackParameters(PlaybackParameters(1f)) - log("audio sync:", "seek → ${decision.seekToMs}") + log("audio sync:", "seek → ${decision.seekToMs} (diff=${abs(audioPos - positionMs)})") } else -> { p.setPlaybackParameters(PlaybackParameters(decision.speed))