Commit Graph

71 Commits

Author SHA1 Message Date
Hermes Agent a4ade1ffcf fix(app-phone): автоподхват восстанавливает контекст просмотра (слайдер/длительность)
После рестарта телефона автоподхват создавал звук, но state.currentItem
оставался null — пульт не знал длительность: слайдер скрыт, «сколько
всего» не показывалось. Теперь при успешном подхвате восстанавливаются
currentItem (jellyfin.item по itemId), currentAudioIndex, mode=MOVIE,
playing. Железо: «Акира · 05:21 / 2:04:28» + слайдер на месте.
2026-08-19 21:50:10 +03:00
Hermes Agent e29bc5032d 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), подтверждено пользователем
2026-08-19 21:45:23 +03:00
Hermes Agent 8072d0c95a feat(app-phone): трекбар-таймлайн в пульте (seek на любую позицию)
- MediaCommand.SEEK (абсолютный seek на seekMs, поле в протоколе уже было)
- MovieController (очки): SEEK → player.seekTo(seekMs.clamp(0,duration)); чистая seekTargetMs
- GlassesScreen (пульт): material3 Slider под статус-карточкой (виден при durationMs>0),
  локальный стейт драга — время под слайдером следует за пальцем (sliderValue ?: positionMs),
  после отпускания цель держится до подтверждения позиции от очков (±2.5с, таймаут 10с),
  звук: SEEK → audio.seekTo(seekMs)
- Тесты 70/0 phone, 37/0 glasses (MovieControllerSeekTest, сериализация SEEK)
2026-08-19 21:35:20 +03:00
Hermes Agent 64f0efde37 fix: детект обрыва связи ≤5с (ping 15с→5с) — пауза при обрыве обязательна
Требование пользователя (проверено на железе): при потере связи видео
обязательно ставится на паузу и быстро. Было: ping 15с + таймаут 15с на
обоих концах — обрыв замечался до ~35с (видео убегало, звук вставал —
рассинхрон). Стало: pingIntervalMillis 5с (lib-core Ktor), pingPeriod/
timeoutMillis 5с (app-phone OkHttp) — детект ≤10с.
Железо: обрыв → «связь потеряна — пауза» → «связь восстановлена —
продолжаю» → diff 139. Визуально подтверждено пользователем.
2026-08-19 21:21:43 +03:00
Hermes Agent 16d207a117 fix(app-phone): watch при перезапуске очков — guard по реальному состоянию (shouldSkipReWatch)
Штатный сценарий (подтверждён на железе): очки перезапустились при живом телефоне — audioSync.isActive остаётся true от прошлого просмотра и блокировал watch («пропущено: аудио-плеер уже активен») до перезапуска телефона.
- shouldSkipReWatch(playing, positionMs) = playing && positionMs > 0: повторный тап пропускается только если очки реально играют; иначе audioSync.stop() и новый watch
- Обрыв-сценарий не затронут (позиция>0 → guard пропускает, resume)
- Тесты 70/0 (3 новых); железо: перезапуск очков → тап «Смотреть» → просмотр с 0 + звук синхронен (diff 145)
2026-08-19 21:14:24 +03:00
Hermes Agent 5efe14aed1 chore: удалён TASK.md 2026-08-19 21:04:00 +03:00
Hermes Agent b4420f446c fix(app-glasses): би-нокулярный рендер RayNeo — letterbox + scale=false (полный кадр в каждый глаз)
Живой тест на RayNeo X2: каждый глаз = 640x480 (пол-экрана 1280x480), видео 896x480 (16:9) растягивалось на весь глаз → «квадратное» (сплющено по горизонтали).
- letterboxRect(videoW,videoH,viewW,viewH): FIT-вьюпорт с полями (страховка для произвольных поверхностей)
- Binocular(scale=true→false): два полных кадра 640x343 в двух половинах экрана (вместо scaleX 0.5 половинки кадра)
- onVideoSizeChanged → videoWidth/Height для letterbox
- Тесты 32/0 (LetterboxRectTest 5)
- Пользователь: «на глаз размер поменялся, будто бы норм»
2026-08-19 21:03:55 +03:00
Hermes Agent 25b85ea191 docs(README): авто-обнаружение — mDNS + скан /24 (RayNeo) реализовано 2026-08-19 20:06:41 +03:00
Hermes Agent 698b6cf6dd feat(app-glasses): авто-обнаружение телефона — mDNS-клиент + скан /24 (RayNeo mDNS мёртв)
- MdnsClient: свой mDNS (UDP 5353, PTR _viewmate._tcp, парсер со сжатием 0xC0), join на всех Wi-Fi-интерфейсах, MulticastLock
- PhoneScan: план Б — TCP-скан своей /24 :8080 батчами, детект по WS-handshake (101), таймаут 5с
- HostConnection: цепочка mDNS → scan /24 (пропуск на эмуляторе) → fallback 10.0.2.2
- app-phone: NsdPublisher (_viewmate._tcp, порт 8080) + NEARBY_WIFI_DEVICES
- Тесты 27/0 glasses (MdnsClientTest, PhoneScanTest), 67/0 phone
- Железо (RayNeo X2 + HONOR): mDNS multicast режется WiFi-стеком очков («ни одного пакета за 15с»), скан /24 нашёл телефон за 1.2с, ws://192.168.76.121:8080, hello→welcome, статусы идут
2026-08-19 20:06:27 +03:00
Hermes Agent 35f462b34a feat: кнопка «⏹ Закрыть» в пульте + автоматика обрыва связи (README «Автоматика»)
- GlassesScreen: кнопка ⏹ Закрыть → sendCommand(CLOSE) (обработчики были: телефон audio.stop, очки MovieController.close)
- app-glasses: MovieController.pauseIfPlaying()/resume(); HostConnection — при обрыве WS пауза + флаг, при восстановлении — continue (лог «связь потеряна — пауза» / «связь восстановлена — продолжаю»); флаг сбрасывается на PlayVideo/CLOSE
- app-phone: ConnectionAutomatics (connectionEvent, авто-resume по флагу «играл ли») + коллектор на hub.connected
- Тесты: app-phone 67/0, app-glasses 15/0
- OFF7 3/3: A5 стоп + обрыв-пауза-продолжение подтверждены живым прогоном
2026-08-19 19:20:04 +03:00
Hermes Agent 535f4e2050 docs(TESTING.md): OFF5 5/5 — дефекты A и B подтверждены исправленными (звук diff 21–316мс, удаление без воскрешения) 2026-08-19 17:48:52 +03:00
Hermes Agent 49dac8b33f fix(app-phone): звук при watch (диагностика+подстраховка mirrorByItem) + качка не воскресает после удаления
- onWatch: явные логи в каждой точке отказа (watch result / audio source / уже играет / не скачано / mediaDir пуст), свежий mirrorByItem если files не догрузился, звук стартует с позиции очков при уже идущем видео
- resolveAudioSource → resolveAudioSourceDetailed (AudioSourceResult с причиной null)
- DownloadManager: поколения generations — delete() инвалидирует старую корутину (не воскрешает качку), ensureActive перед .part/rename, jobs.remove(itemId, job)
- Тесты 62/0 (в т.ч. поколения — поймали null != 0L)

Инфраструктурный блокер (не код): SeaweedFS на Server3 76.66 в IO-шторме (load ~184, wa 20%, чужой yt-dlp на хосте) — большие GET с s3.binom.pw висят/рвутся с ~14:00; качка на очки FAILED по этой причине.
2026-08-19 17:19:53 +03:00
Hermes Agent fafdddc121 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)
2026-08-19 14:41:40 +03:00
Hermes Agent 724f90a385 fix(app-phone): персистентный статус скачивания + докачка с места обрыва + StatusBlock
- DownloadManager: .part-файлы, resume через Range (206/200), status.json (фаза/процент/total), скан диска при старте, guard от повторной качки
- DetailsScreen: кнопка «✓ скачано на телефон»/«Продолжить (N%)», чипы из диска
- Components: StatusBlock Box→Column (строки статуса перекрывались кнопкой «Удалить»); приоритет бейджа: «✓» done выше «↓ часть»
- 8 новых тестов (Range, скан диска, status.json)
2026-08-19 13:46:03 +03:00
Hermes Agent 6069400b88 feat(app-phone): полноценный UI телефона — каталог Jellyfin, детали, пульт очков, реальное скачивание с S3
- 4 экрана по макету sketches/phone-ui: Каталог (табы Фильмы/Сериалы/Папки, поиск, бейджи зеркала), Детали (озвучки, статус конвертации, чипы скачивания), Очки (пульт, статус), Чат-заглушка
- S3 SigV4 presign (S3Presigner): mirror-client read-only креды; watch() и DownloadManager подписывают URL (бакет приватный)
- DownloadManager: реальное скачивание video+audio с прогрессом, readTimeout=0 (длинные файлы)
- PhoneActions.watch: параметр presign (app-host работает без изменений)
- TESTING.md: блок телефонных кейсов TP-1..6
2026-08-19 09:13:56 +03:00
Hermes Agent 4458678698 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 ПРОЙДЕН
2026-08-19 04:15:55 +03:00
Hermes Agent 298d8e0b69 feat: сетевая часть для app-phone (WS-сервер) и app-glasses (WS-клиент), PhoneState/PhoneActions → lib-core
lib-core:
- core/phone/: PhoneState, PhoneActions, GlassesSender (интерфейс) — перенесены из app-host
- core/net/: GlassesWsClient — KMP WS-клиент очков (реконнект, Hello, send GlassesToHost)
- GlassesWsClientTest + PhoneActionsTest (перенесён)

app-phone (Android):
- GlassesServer: Ktor WS-сервер (CIO) 0.0.0.0:8080/ws/glasses, принимает очки, Welcome из стейта
- PhoneApp (Application): клиенты, стейт, действия, сервер
- PhoneConfig: креды Jellyfin/Mirror
- MainActivity: статус сервера (порт, счётчик подключений)
- GlassesServerTest: hello→welcome, gesture

app-glasses (Android):
- GlassesApp (Application), HostConnection: WS-клиент к телефону, Hello, реконнект, обработка HostToGlasses (PlayVideo/SetMode и т.д.), StateFlow для UI
- MainActivity: статус подключения

app-host: PhoneActions/PhoneState удалены (теперь lib-core), адаптирован под GlassesSender

171 тест, 0 провалов; app-host жив (catalog/watch/status работают через lib-core)
2026-08-11 15:06:07 +03:00
Hermes Agent f0f1f71575 feat: app-host → телефон-эмулятор (Jellyfin + mirror + стейт + команды)
- Config.kt: env > application.properties > дефолты (креды не в коде)
- PhoneState.kt: единый стейт — mode, currentItem, audioIndex, mirrorStatus, activeSessionId, сессии (эмуляция), downloaded
- PhoneActions.kt: catalog (с бейджами зеркал), open, watch (PlayVideo на очки), makeMirror, mirrorStatus, download/delete, sessions/selectSession, status
- Main.kt: новые консольные команды (catalog/open/watch/mirror/dl/rm/sessions/session/status)
- GlassesHub: Welcome берёт mode/sessionId из стейта, glasses_off → пауза
- JellyfinClient.item() — элемент по id (для open/watch)
- 58 тестов, 130 всего, все зелёные; живые проверки: catalog (реальный Jellyfin), watch (PlayVideo с s3-url), status (стейт обновляется)
2026-08-11 14:37:43 +03:00
Hermes Agent d95d289d29 feat: HTTP-клиенты Jellyfin + media-mirror в lib-core (KMP, Ktor)
media/JellyfinClient: users, rootItems, items, search, allItems, posterUrl (X-Emby-Token)
media/MirrorClient: createMirror, mirrorByItem (404→null), list, delete, scan (X-API-Key)
media/HttpClient: defaultHttpClient (ContentNegotiation+json, таймауты, ApiClientException)
Ktor 3.3.0 (совместим с Kotlin 2.2; 3.5.2 требует Kotlin 2.3 — не берём)

17 тестов (MockEngine): пути, методы, заголовки, JSON, 201/404, round-trip DTO.
Проверено на живых серверах: Jellyfin (6 users, корень, поиск 'дюна'), mirror (by-item done + url, список done)
2026-08-11 12:59:44 +03:00
Hermes Agent 9271517b3c feat: WS-протокол очки↔телефон + app-host (JVM Ktor-сервер)
lib-core (commonMain):
- Messages.kt: GlassesToHost (hello, gesture, glasses_status, playback_position, glasses_off)
- HostToGlasses.kt: Welcome, SetMode, MediaCommandMsg, PlayVideo, AssistantStateMsg, ShowText, ChatSessionMsg
- AssistantState enum (IDLE/LISTENING/THINKING/SPEAKING)
- round-trip тесты сериализации обоих направлений

app-host (переведён с Android на JVM):
- Ktor WS-сервер 0.0.0.0:8080/ws/glasses — очки подключаются как WS-клиенты
- GlassesHub: broadcast всем очкам, счётчик подключений (StateFlow)
- консольный ввод: play/mode/state/text/session/cmd/exit — команды от «телефона»
- логирование всех сообщений обоих направлений
- HostServerTest: hello→welcome, счётчик подключений

Проверено: сборка зелёная, 21 тест 0 провалов, живой WS-цикл (hello→welcome, gesture, статус, позиция, glasses_off)
2026-08-11 12:35:30 +03:00
Hermes Agent 5268a12d20 view-mate: болванка + концепт + эскизы + сценарии (единый коммит, история очищена от hprof/build) 2026-08-11 11:34:50 +03:00