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:
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user