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:
@@ -57,10 +57,20 @@ data class DownloadProgress(
|
||||
val phase: DownloadPhase,
|
||||
) : GlassesToHost
|
||||
|
||||
/** Позиция воспроизведения (очки — мастер таймкода, шлют регулярно). */
|
||||
/**
|
||||
* Позиция воспроизведения (очки — мастер таймкода, шлют регулярно).
|
||||
* [itemId]+[audioIndex] позволяют телефону автоподхватить звук после рестарта:
|
||||
* очки продолжают играть, а телефон создаёт аудио-плеер по этим полям.
|
||||
* Дефолты — обратная совместимость со старыми очками.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("playback_position")
|
||||
data class PlaybackPosition(val positionMs: Long, val playing: Boolean) : GlassesToHost
|
||||
data class PlaybackPosition(
|
||||
val positionMs: Long,
|
||||
val playing: Boolean,
|
||||
val itemId: String? = null,
|
||||
val audioIndex: Int = 0,
|
||||
) : GlassesToHost
|
||||
|
||||
/** Очки выключились / экран погас. */
|
||||
@Serializable
|
||||
|
||||
@@ -118,6 +118,37 @@ class GlassesToHostTest {
|
||||
assertTrue(decoded.playing)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun playbackPositionWithContentRoundTrip() {
|
||||
val msg: GlassesToHost = PlaybackPosition(
|
||||
positionMs = 120_500,
|
||||
playing = true,
|
||||
itemId = "movie-1",
|
||||
audioIndex = 2,
|
||||
)
|
||||
val text = json.encodeToString(GlassesToHost.serializer(), msg)
|
||||
assertTrue("\"itemId\":\"movie-1\"" in text)
|
||||
assertTrue("\"audioIndex\":2" in text)
|
||||
val decoded = json.decodeFromString(GlassesToHost.serializer(), text)
|
||||
assertIs<PlaybackPosition>(decoded)
|
||||
assertEquals(120_500, decoded.positionMs)
|
||||
assertTrue(decoded.playing)
|
||||
assertEquals("movie-1", decoded.itemId)
|
||||
assertEquals(2, decoded.audioIndex)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun playbackPositionOldFormatGetsDefaults() {
|
||||
// старое сообщение без itemId/audioIndex должно декодироваться с дефолтами
|
||||
val oldText = """{"type":"playback_position","positionMs":7000,"playing":false}"""
|
||||
val decoded = json.decodeFromString(GlassesToHost.serializer(), oldText)
|
||||
assertIs<PlaybackPosition>(decoded)
|
||||
assertEquals(7000, decoded.positionMs)
|
||||
assertTrue(!decoded.playing)
|
||||
assertNull(decoded.itemId)
|
||||
assertEquals(0, decoded.audioIndex)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun glassesOffRoundTripWithDefault() {
|
||||
val msg: GlassesToHost = GlassesOff()
|
||||
|
||||
Reference in New Issue
Block a user