feat: автоподхват звука после рестарта телефона (очки играют — звук молчит)

Сценарий (железо): телефон перезапущен, очки продолжают играть — звук
не создавался (плеер живёт только по watch). Решение:
- PlaybackPosition теперь несёт itemId+audioIndex (дефолты — совместимость)
- очки: MovieController хранит itemId/audioIndex (playVideo/close), шлёт в позиции
- телефон: onPlaybackPosition → autostartAudio: если itemId есть, аудио не
  активно и не было неудачи — mirrorByItem → resolveAudioSource → play(source,
  positionMs); lastFailed защита от спама каждые 5с
- Тесты: 13 lib-core (сериализация + старый формат), 4 glasses
  (contentId/close), 70/0 phone; железо: рестарт телефона → «audio sync»
  через ~5с (diff 483), подтверждено пользователем
This commit is contained in:
Hermes Agent
2026-08-19 21:45:23 +03:00
parent 8072d0c95a
commit e29bc5032d
7 changed files with 178 additions and 14 deletions
@@ -86,8 +86,12 @@ 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
/**
* Хук на позицию очков: вызывается на каждый PlaybackPosition (и GlassesOff
* с playing=false). [itemId]+[audioIndex] — для автоподхвата звука после
* рестарта телефона (см. PhoneApp.autostartAudio).
*/
var onPlaybackPosition: ((positionMs: Long, playing: Boolean, itemId: String?, audioIndex: Int) -> Unit)? = null
fun add(session: DefaultWebSocketServerSession) {
sessions[session] = Unit
@@ -155,14 +159,14 @@ class GlassesHub(
_positionMs.value = msg.positionMs
this.state.playing = msg.playing
val state = if (msg.playing) "играет" else "пауза"
log("glasses", "позиция: ${msg.positionMs} мс, $state")
onPlaybackPosition?.invoke(msg.positionMs, msg.playing)
log("glasses", "позиция: ${msg.positionMs} мс, $state, item=${msg.itemId} idx=${msg.audioIndex}")
onPlaybackPosition?.invoke(msg.positionMs, msg.playing, msg.itemId, msg.audioIndex)
}
is GlassesOff -> {
log("glasses", "очки выключились → пауза (${msg.reason})")
this.state.playing = false
onPlaybackPosition?.invoke(_positionMs.value ?: 0L, false)
onPlaybackPosition?.invoke(_positionMs.value ?: 0L, false, null, 0)
}
}
}