Добавляем индикатор рассинхронизации звука и видео в статус-панели очков
This commit is contained in:
@@ -149,12 +149,20 @@ class AudioSyncPlayer(private val context: Context) {
|
||||
private var player: ExoPlayer? = null
|
||||
private var ticker: Job? = null
|
||||
|
||||
/** Слушатель буферизации ExoPlayer: логирует паузы вывода (причина «немого» звука). */
|
||||
/** Слушатель буферизации ExoPlayer: логирует паузы вывода с длительностью (причина «немого» звука:
|
||||
* длительная буферизация = локальная/S3-дорога, короткая/абсолютное отсутствие = BT-стек). */
|
||||
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", "буфер готов, вывод возобновлён")
|
||||
Player.STATE_BUFFERING -> {
|
||||
bufferingStartMs = System.currentTimeMillis()
|
||||
log("audio", "буферизация: вывод приостановлен (возможен пропуск звука 0.5–1с)")
|
||||
}
|
||||
Player.STATE_READY -> {
|
||||
val bufferedMs = if (bufferingStartMs > 0L) System.currentTimeMillis() - bufferingStartMs else 0L
|
||||
bufferingStartMs = 0L
|
||||
log("audio", "буфер готов, вывод возобновлён${if (bufferedMs > 0L) " (буферизация ${bufferedMs} мс)" else ""}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,10 +175,25 @@ class AudioSyncPlayer(private val context: Context) {
|
||||
private var linkLatencyMs = 0.0
|
||||
private var lastLatencyLoggedAt = 0L
|
||||
|
||||
/** Текущий источник для логов: «стриминг S3» или «локальный файл» — виден в каждой периодической
|
||||
* строке «audio sync:», чтобы любой фрагмент logcat отвечал на вопрос «стримлю или скачанное?». */
|
||||
private var sourceLabel = "нет"
|
||||
|
||||
/** Момент входа в STATE_BUFFERING (для лога длительности; 0 — не в буферизации). */
|
||||
private var bufferingStartMs = 0L
|
||||
|
||||
private val _positionMs = MutableStateFlow(0L)
|
||||
/** Текущая позиция аудио (для индикатора «🔊 звук: N мс»). Обновляется каждые 500 мс. */
|
||||
val positionMs: StateFlow<Long> = _positionMs.asStateFlow()
|
||||
|
||||
private val _diffMs = MutableStateFlow(0L)
|
||||
/** |звук − видео| из последнего sync-цикла (мс), для индикатора статус-панели очков. 0 — синхронизации не было. */
|
||||
val diffMs: StateFlow<Long> = _diffMs.asStateFlow()
|
||||
|
||||
/** Играет ли видео на очках (по последнему PlaybackPosition). Только с главного потока. */
|
||||
val videoPlaying: Boolean
|
||||
get() = lastVideoPlaying
|
||||
|
||||
/** Есть ли активный аудио-плеер (просмотр на очках). */
|
||||
val isActive: Boolean
|
||||
get() = player != null
|
||||
@@ -195,7 +218,8 @@ class AudioSyncPlayer(private val context: Context) {
|
||||
p.seekTo(startPositionMs.coerceAtLeast(0L))
|
||||
p.playWhenReady = true
|
||||
val isStream = urlOrFile.startsWith("http://") || urlOrFile.startsWith("https://")
|
||||
log("audio", "играю ${if (isStream) "стриминг S3" else "локальный файл"}: $urlOrFile с ${startPositionMs} мс")
|
||||
sourceLabel = if (isStream) "стриминг S3" else "локальный файл"
|
||||
log("audio", "играю $sourceLabel: $urlOrFile с ${startPositionMs} мс")
|
||||
startTicker()
|
||||
}
|
||||
}
|
||||
@@ -227,16 +251,17 @@ class AudioSyncPlayer(private val context: Context) {
|
||||
// Истинная позиция мастера = позиция в сообщении + латентность (очки ушли вперёд, пока пакет летел).
|
||||
val videoPos = positionMs + linkLatencyMs.toLong()
|
||||
val audioPos = p.currentPosition
|
||||
_diffMs.value = abs(audioPos - videoPos)
|
||||
val decision = decideSync(audioPos, videoPos, p.isPlaying, isPlaying)
|
||||
when {
|
||||
decision.seekToMs != null -> {
|
||||
p.seekTo(decision.seekToMs)
|
||||
p.setPlaybackParameters(PlaybackParameters(1f))
|
||||
log("audio sync:", "seek → ${decision.seekToMs} (diff=${abs(audioPos - videoPos)})")
|
||||
log("audio sync:", "seek → ${decision.seekToMs} (diff=${abs(audioPos - videoPos)} src=$sourceLabel)")
|
||||
}
|
||||
else -> {
|
||||
p.setPlaybackParameters(PlaybackParameters(decision.speed))
|
||||
log("audio sync:", "video=$videoPos audio=$audioPos diff=${abs(audioPos - videoPos)} speed=${decision.speed}")
|
||||
log("audio sync:", "video=$videoPos audio=$audioPos diff=${abs(audioPos - videoPos)} speed=${decision.speed} src=$sourceLabel")
|
||||
}
|
||||
}
|
||||
decision.playing?.let { if (it) p.play() else p.pause() }
|
||||
@@ -262,7 +287,10 @@ class AudioSyncPlayer(private val context: Context) {
|
||||
ticker = null
|
||||
val p = player
|
||||
player = null
|
||||
lastVideoPlaying = false
|
||||
sourceLabel = "нет"
|
||||
_positionMs.value = 0L
|
||||
_diffMs.value = 0L
|
||||
if (p != null) {
|
||||
p.playWhenReady = false
|
||||
p.stop()
|
||||
|
||||
@@ -28,6 +28,7 @@ import pw.binom.viewmate.core.phone.PhoneActions
|
||||
import pw.binom.viewmate.core.phone.PhoneState
|
||||
import pw.binom.viewmate.core.protocol.AssistantState
|
||||
import pw.binom.viewmate.core.protocol.AssistantStateMsg
|
||||
import pw.binom.viewmate.core.protocol.AudioSyncInfo
|
||||
import pw.binom.viewmate.core.protocol.ChatHistoryItem
|
||||
import pw.binom.viewmate.core.protocol.ChatHistoryMsg
|
||||
import pw.binom.viewmate.core.protocol.ShowText
|
||||
@@ -507,6 +508,16 @@ class PhoneApp : Application() {
|
||||
scope.launch { autostartAudio(itemId, audioIndex, positionMs, playing) }
|
||||
}
|
||||
|
||||
// Индикатор аудио-синхронизации в статус-панели очков: |звук − видео| в мс
|
||||
// на каждый sync-цикл (~5с); active=false (видео на очках не играет) — панель скрывает.
|
||||
scope.launch {
|
||||
audioSync.diffMs.collect { diff ->
|
||||
server.hub.broadcast(
|
||||
AudioSyncInfo(diffMs = diff.toInt(), active = audioSync.isActive && audioSync.videoPlaying),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// watchdog: очки молчат (экран погас/радио спит) — пауза звука по таймауту
|
||||
scope.launch {
|
||||
while (true) {
|
||||
|
||||
Reference in New Issue
Block a user