feat: бинокулярный видеорендер в app-glasses + иммерсивный режим + тест-план
- render/: Grafika-связка (EglCore/WindowSurface/FullFrameRect/Texture2dProgram/GlUtil/Drawable2d/EglSurfaceBase) — перенос из pair-view - ui/BinocularVideoPlayer: один SimpleExoPlayer → SurfaceTexture → EGL-блит в оба глаза (EglVideoRenderer шарится между LEFT/RIGHT) - ui/ViewUtils: Binocular/BinocularScreenSide/BinocularScreenSize (Compose, из pair-view) - ui/GlassesMovieScreen: «нос к носу», calculateVideoSizeFull, FLAG_KEEP_SCREEN_ON - MovieController: PlayVideo/MediaCommand (PLAY/PAUSE/TOGGLE/SEEK/CLOSE), доступ к ExoPlayer только с main-потока - HostConnection: PlayVideo→плеер, реальная позиция в PlaybackPosition (withContext Main) - MainActivity: иммерсивный режим (setDecorFitsSystemWindows(false) + hide systemBars) - TESTING.md: 10 тест-кейсов human-style; QA-прогон (opencode, 88.19) — 10/10 ПРОЙДЕН
This commit is contained in:
@@ -7,6 +7,7 @@ import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -26,10 +27,11 @@ import pw.binom.viewmate.core.protocol.Welcome
|
||||
/**
|
||||
* Обвязка над [GlassesWsClient] для UI очков:
|
||||
* реконнект, Hello при подключении, периодический статус/позиция,
|
||||
* обработка входящих HostToGlasses в StateFlow (пока — лог).
|
||||
* обработка входящих HostToGlasses (PlayVideo/MediaCommand выполняются плеером).
|
||||
*/
|
||||
class HostConnection(
|
||||
private val url: String,
|
||||
private val movieController: MovieController,
|
||||
) {
|
||||
private val client = GlassesWsClient(url)
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
@@ -62,9 +64,10 @@ class HostConnection(
|
||||
scope.launch {
|
||||
while (isActive) {
|
||||
delay(5_000)
|
||||
// статус/позиция — заглушки (реальные батарея/память/таймкод — позже)
|
||||
// статус/позиция — заглушки (реальные батарея/память — позже)
|
||||
client.send(GlassesStatus(batteryPercent = 80, storageUsedGb = 10.5, storageTotalGb = 32.0))
|
||||
client.send(PlaybackPosition(positionMs = 0, playing = false))
|
||||
val (positionMs, playing) = withContext(Dispatchers.Main) { movieController.playbackPosition() }
|
||||
client.send(PlaybackPosition(positionMs = positionMs, playing = playing))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,9 +81,15 @@ class HostConnection(
|
||||
|
||||
is SetMode -> addMessage("SetMode: ${msg.mode}")
|
||||
|
||||
is PlayVideo -> addMessage("получено видео: «${msg.title}» — ${msg.videoUrl}")
|
||||
is PlayVideo -> {
|
||||
addMessage("получено видео: «${msg.title}» — ${msg.videoUrl}")
|
||||
movieController.playVideo(msg)
|
||||
}
|
||||
|
||||
is MediaCommandMsg -> addMessage("MediaCommand: ${msg.command} (seekMs=${msg.seekMs})")
|
||||
is MediaCommandMsg -> {
|
||||
addMessage("MediaCommand: ${msg.command} (seekMs=${msg.seekMs})")
|
||||
movieController.applyMediaCommand(msg.command, msg.seekMs)
|
||||
}
|
||||
|
||||
is AssistantStateMsg -> addMessage("AssistantState: ${msg.state} (${msg.recordingSeconds}с)")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user