184 lines
18 KiB
Markdown
184 lines
18 KiB
Markdown
# Вечерний детектив — приложение площадки
|
||
|
||
Статус: программная часть SCREEN-004 реализована; SCREEN-001 и SCREEN-004
|
||
остаются IN_PROGRESS до аппаратной приёмки.
|
||
SCREEN-002 и SCREEN-003 завершены: описание пакета через существующий API,
|
||
загрузка, проверка, восстановление и локальная выдача файлов.
|
||
Управление с телефона идёт через существующий сервер сайта; desktop получает
|
||
обновления с сервера. Прямое LAN-подключение не используется.
|
||
|
||
Цель: ведущий управляет показом на компьютере с macOS, Windows или Linux, а подключённый монитор или
|
||
проектор показывает материалы игрокам. Все материалы показа заранее находятся
|
||
на диске; локальная команда не ждёт сеть или ответ сервера.
|
||
|
||
Стек: Electron + Vue + TypeScript + Vite. Первая версия включает шесть сборок:
|
||
macOS Intel/Apple Silicon, Windows x64/ARM64 и Linux x64/ARM64.
|
||
Поддерживаемые версии ОС и условия проверки описаны в PLAN.md.
|
||
Использовать текущие паттерны фронтенда и переиспользовать подходящие компоненты,
|
||
не переносить весь сайт в приложение и не выделять общий пакет заранее.
|
||
|
||
План: [PLAN.md](PLAN.md). Очередь реализации: [TASKS.md](TASKS.md).
|
||
Эти документы находятся здесь по прямому указанию владельца — исключение из
|
||
общего правила хранения задач в `../docs/agent`. Не дублировать их там.
|
||
Идентификаторы `SCREEN-*` относятся только к этому приложению.
|
||
|
||
Текущий этап: SCREEN-004; программная часть готова, аппаратная проверка открыта.
|
||
Новые задачи: `Новая задача: <описание>`.
|
||
Commit и push выполняет только пользователь. Вложенные `.git` не изменять.
|
||
|
||
## Запуск проигрывателя
|
||
|
||
Нужны Node.js 24.15+ и ffmpeg с libx264 в PATH.
|
||
В Linux приложение использует X11; для сессии Wayland нужен XWayland, чтобы
|
||
размещать показ на выбранном мониторе.
|
||
|
||
```sh
|
||
npm ci
|
||
npx --no-install install-electron --no
|
||
npm run media:generate
|
||
npm run dev
|
||
```
|
||
|
||
До запуска подключите второй дисплей в режиме расширенного рабочего стола
|
||
и назначьте экран компьютера основным. В пульте выберите
|
||
его и откройте показ. Аудио и видео — синтетические локальные файлы длительностью
|
||
60 секунд; бинарники находятся в игнорируемом fixtures/ и не входят в Git.
|
||
Тестовый режим сохраняет отдельный выбор WAV/видео; игровые пакеты выбираются
|
||
в разделе «Материалы показа». Полные игровые
|
||
режимы, интерфейс загрузки игр и авторизация пока не реализованы.
|
||
|
||
Управление телефоном через сервер, подключение игры и авторизация относятся к
|
||
SCREEN-006 и в техническом стенде ещё не реализованы. Интернет нужен для получения
|
||
команд телефона; локальное воспроизведение и пульт компьютера работают без него.
|
||
|
||
Проверки: npm run type-check, npm run lint, npm run lint:css, npm test, npm run build.
|
||
[Сборки и аппаратная приёмка](docs/SCREEN-001_ACCEPTANCE.md).
|
||
|
||
## Описание пакета SCREEN-002
|
||
|
||
`electron/screen-package.ts` экспортирует `prepareScreenPackage({ server, gameId,
|
||
token, signal? })` для вызова из основного процесса. Метод делает три GET-запроса:
|
||
`/api/games/{ID}`, `/api/games/{ID}/screen-plan`, `/api/games/{ID}/music`.
|
||
Авторизация — существующий Bearer JWT; токен используется только в запросах API,
|
||
не входит в результат. Редиректы API запрещены, ожидание ограничено 30 секундами;
|
||
поддерживается отмена через AbortSignal. Ответы с ошибками не выдают частичный пакет.
|
||
Интерфейс входа и подключение к пульту остаются в SCREEN-006; сейчас это программный
|
||
контракт, новых кнопок в стенде нет. Бэкенд, frontend и protobuf не изменены.
|
||
|
||
Результат `ScreenPackage` содержит `formatVersion: 1`, `metadataVersion`, адрес
|
||
сервера, ID/название игры, ссылки на материалы, шаги плана и связанные плейлисты.
|
||
`assets` содержит уникальные URL и их идентификаторы (SHA-256 адреса, не файла).
|
||
В `media` и треках используются эти идентификаторы. Фон по умолчанию — обложка.
|
||
Плейлисты сценария, игры и текущий выбранный включаются со всеми треками; прочая
|
||
библиотека, данные участников и живые команды не сохраняются. Ссылки с секретами
|
||
в параметрах и адреса с логином/паролем отклоняются.
|
||
|
||
`metadataVersion` — SHA-256 нормализованных метаданных. Он не замечает замену
|
||
байтов по прежнему URL. Само описание не означает готовность к показу. В SCREEN-003 размер и SHA-256
|
||
вычисляются при скачивании, после чего формируется итоговая версия и пакет
|
||
активируется атомарно. Внешним медиа нельзя передавать JWT сервера.
|
||
Существующий API не предоставляет атомарного снимка или серверных хешей файлов;
|
||
права доступа к играм и публичная выдача файлов остаются прежними.
|
||
|
||
## Локальное хранилище SCREEN-003
|
||
|
||
Основной процесс открывает `PackageStore` из `electron/package-store.ts` в
|
||
`app.getPath('userData')/packages`. Приложение допускает только один экземпляр,
|
||
чтобы две копии не изменяли хранилище одновременно. При запуске проверяются
|
||
манифесты, размеры и SHA-256 всех файлов; повреждённые пакеты не становятся готовыми.
|
||
|
||
Программный интерфейс (для основного процесса; подключение к UI — SCREEN-006):
|
||
|
||
```ts
|
||
const store = new PackageStore(packagesDirectory)
|
||
await store.initialize()
|
||
const description = await prepareScreenPackage({ server, gameId, token })
|
||
const ready = await store.download(description, { signal, onProgress })
|
||
// Другие операции: list(), cancel(), verify(key), remove(key), localFile(key, assetId).
|
||
```
|
||
|
||
Скачивание последовательно и потоково. Файл синхронизируется с диском и повторно
|
||
читается для проверки хеша. При обрыве текущий файл повторяется целиком: максимум
|
||
три попытки с паузами 1 и 3 секунды; завершённые файлы текущей попытки не повторяются.
|
||
Таймаут бездействия — 30 секунд. Ошибки 408/429/5xx повторяются; ошибки доступа,
|
||
неизвестные форматы и отсутствующие файлы останавливают подготовку. Незнакомый
|
||
общий размер в прогрессе обозначается `totalBytes: null`; итоговый размер известен
|
||
после загрузки. Поддерживаются MIME WAV/OGG, используемые текущим сервером.
|
||
|
||
Медиа запрашиваются без JWT и cookies. Разрешено до пяти проверенных HTTP(S)
|
||
редиректов; запрещены учётные данные в URL, секретные параметры и понижение
|
||
HTTPS до HTTP. Контроль места использует statfs перед загрузкой и перед записью
|
||
очередного блока, оставляя резерв 64 МиБ; фактическая ошибка ENOSPC также обрабатывается.
|
||
|
||
Каталог игры определяется хешем адреса сервера и ID игры; пути ресурсов — хешами
|
||
URL, независимо от исходного имени файла. Локальный манифест содержит исходное
|
||
описание, MIME, размеры и SHA-256 байтов; итоговая версия зависит от метаданных
|
||
и проверенных файлов. Секреты в манифест не записываются.
|
||
|
||
Новая версия готовится в `staging-*`, затем помещается в `v-<version>`. Указатель
|
||
`current.json` заменяется через временный файл и rename только после проверки.
|
||
Отмена и ошибки до переключения не затрагивают прежний готовый пакет. После
|
||
успеха прежняя версия удаляется; заблокированные ОС остатки повторно очищаются
|
||
при следующем запуске. Сбой после переключения не отменяет новую готовую версию.
|
||
Повреждённую текущую копию можно восстановить повторной загрузкой или удалить.
|
||
Симлинки при чтении не обходятся; удаление ограничено каталогом выбранной игры.
|
||
|
||
`screen-app://app/packages/<key>/<assetId>` выдаёт только ресурсы готового пакета,
|
||
с правильным MIME, запретом кеширования и поддержкой Range. Произвольные пути
|
||
и сетевые URL в протокол не передаются. Воспроизведение игровых пакетов подключено
|
||
в SCREEN-004; fixture-файлы доступны отдельно в тестовом режиме.
|
||
|
||
`npm test` включает настоящий временный HTTP-сервер: после скачивания сервер
|
||
отключается, новое хранилище проверяет пакет, локальная выдача читает все ресурсы.
|
||
`npm run smoke -- --no-sandbox --mute-audio --disable-gpu` дополнительно готовит
|
||
пакет с WAV, H.264 и двумя музыкальными треками в отдельном временном userData,
|
||
открывает его повторно через основной процесс и проверяет пульт, Range, команды
|
||
и переходы плейлиста. Эти флаги используются только в тесте.
|
||
Серверных эталонных хешей нет: локальная проверка подтверждает целостность
|
||
скачанной копии, но не неизменность источников во время подготовки.
|
||
|
||
## Локальный проигрыватель SCREEN-004
|
||
|
||
На пульте выберите готовый пакет или «Тестовый режим SCREEN-001», внешний дисплей
|
||
и откройте показ. В основном канале доступны вступление и заключительное видео;
|
||
их выбор останавливает прежнее основное медиа. Музыка воспроизводится отдельным
|
||
каналом без автоматического приглушения. Звук выводится только окном показа.
|
||
Начальная громкость каждого канала — 35%; общий mute не меняет сохранённые уровни.
|
||
|
||
«Пуск с начала» и «Повторить с начала» запускают выбранный ресурс с нуля,
|
||
«Пауза» сохраняет фактическую позицию, «Продолжить» возобновляет её, «Стоп»
|
||
сбрасывает позицию. Перемотка сохраняет паузу/воспроизведение и ограничена реальной
|
||
длительностью файла. Громкость не вызывает перемотку или повторный пуск.
|
||
Следующий трек сохраняет состояние воспроизведения или паузы; по окончании трека
|
||
переход выполняется локально. Повтор относится ко всему плейлисту; без повтора
|
||
последний трек завершает воспроизведение. Смена плейлиста останавливает музыку.
|
||
|
||
Основной процесс хранит каталог разрешённых локальных ресурсов и состояния
|
||
`main`/`music`; renderer получает только адреса screen-app. Команды IPC содержат
|
||
сессию и возрастающий sequence; повторные/устаревшие номера игнорируются.
|
||
Подтверждения содержат сессию, канал и operation, чтобы запоздавшие события
|
||
старого ресурса не меняли новый. Позиция обновляется по событиям медиа; подготовка,
|
||
успешный play и ошибки подтверждаются проигрывателем. Подготовлены выбранное
|
||
основное медиа и один следующий трек, без чтения всех видео в JavaScript.
|
||
|
||
`screenBridge.packages()` и `selectPackage(key)` доступны только пульту;
|
||
`selectPackage(null)` включает тестовый режим. Выбор повторно проверяет пакет.
|
||
Закрытие или пересоздание окна и смена пакета создают новую сессию и останавливают
|
||
каналы; повторное открытие не возобновляет звук. После перезапуска всего приложения
|
||
выбор начинается с тестового режима; сохранение выбора относится к SCREEN-007.
|
||
|
||
Для ручной проверки с настоящим внешним дисплеем выполните `npm run acceptance`.
|
||
Команда использует синтетические fixtures и отдельный временный userData, создаёт
|
||
локальный пакет с двумя тональными музыкальными треками и оставляет пульт открытым
|
||
без автоматических команд. Подготовка не требует сети после установки зависимостей.
|
||
При отсутствии внешнего дисплея команда завершится с сообщением, синтетический
|
||
монитор в ручном режиме не подставляется. Встроенная аппаратная инструкция:
|
||
[SCREEN-004_ACCEPTANCE.md](docs/SCREEN-004_ACCEPTANCE.md).
|
||
|
||
Проверки test и smoke запускайте последовательно: обе команды пересобирают
|
||
`dist-electron`, из которого Electron загружает preload при открытии окна.
|
||
Программные замеры в пульте разделены по каналу и текущему виду медиа; они
|
||
не измеряют физический звук/первый кадр. Аппаратная приёмка, остальные ОС,
|
||
реальные игровые материалы и p95 на M1 пока не подтверждены. Полные визуальные
|
||
режимы остаются в SCREEN-005, авторизация и получение серверных команд — в SCREEN-006.
|