feat(app-phone): синхронизированный звук на телефоне при просмотре на очках + README-статус

- AudioSyncPlayer (ExoPlayer media3): играет дорожку (локальный файл или presigned S3-URL), коррекция по PlaybackPosition очков (каждые 5с, порог 1000мс), мгновенная реакция на пульт, индикатор «🔊 звук: N мс»
- Вещдок-лог: audio sync: video=N audio=M diff=K (на стенде diff ~150мс)
- Смена озвучки во время просмотра — перезапуск с позиции очков
- README: секция «Статус реализации»
- Тесты 24/0 (8 новых AudioSyncPlayerTest)
This commit is contained in:
Hermes Agent
2026-08-19 14:41:40 +03:00
parent 724f90a385
commit fafdddc121
9 changed files with 307 additions and 1 deletions
@@ -61,6 +61,9 @@ class GlassesHub(
private val _positionMs = MutableStateFlow<Long?>(null)
val positionMs: StateFlow<Long?> = _positionMs.asStateFlow()
/** Хук на позицию очков: вызывается на каждый PlaybackPosition (и GlassesOff с playing=false). */
var onPlaybackPosition: ((positionMs: Long, playing: Boolean) -> Unit)? = null
fun add(session: DefaultWebSocketServerSession) {
sessions[session] = Unit
_connected.value = sessions.size
@@ -111,11 +114,13 @@ class GlassesHub(
this.state.playing = msg.playing
val state = if (msg.playing) "играет" else "пауза"
log("glasses", "позиция: ${msg.positionMs} мс, $state")
onPlaybackPosition?.invoke(msg.positionMs, msg.playing)
}
is GlassesOff -> {
log("glasses", "очки выключились → пауза (${msg.reason})")
this.state.playing = false
onPlaybackPosition?.invoke(_positionMs.value ?: 0L, false)
}
}
}