Клиент-плеер для цифровых экранов (digital signage) на Flutter. Устройство один раз настраивается, отправляет заявку на регистрацию, после подтверждения в панели играет контент по расписанию с сервера.
Сопутствующие документы: TODO_Client.md — статус до production, ROADMAP.md — развитие, deploy/DEPLOY.md — установка на устройства.
- Kiosk/fullscreen: в release-сборке экран нельзя случайно свернуть; wakelock, immersive-режим на Android.
- Оффлайн-устойчивость: локальный кэш медиа (sha256-проверка), кэш манифеста, аварийный резервный плейлист.
- Хранилище на выбор: внутренняя память / флешка / диск; авто-откат на внутреннюю при извлечении носителя и авто-возврат, контроль медленного носителя.
- Сервисная зона (за жестом + опц. PIN): таймлайн «Что дальше» с превью, резервный режим, настройки, диагностика.
- Авто-подбор адреса сервера: оператор вводит только хост — протокол (https→http) и порт клиент определяет сам.
- Диагностика: экран состояния (сервер, heartbeat, ревизия, кэш, носитель, версии) + телеметрия в heartbeat.
Готовые сборки — в GitHub Releases (собираются CI по тегу vX.Y.Z, версия артефактов берётся из тега):
| Платформа | Артефакт | Установка |
|---|---|---|
| Windows | *-setup.exe (Inno Setup) |
Запустить установщик; VC++ Runtime подтянется автоматически. Автозапуск/watchdog: deploy/windows/install-watchdog.ps1 |
| Linux | *_linux_amd64.tar.gz |
Распаковать, sudo ./install.sh <user>; systemd-сервис: deploy/linux/install-service.sh (автозапуск + restart-after-crash) |
| Android / Android TV | *.apk |
Установить APK (минимум Android 6.0 / API 23) |
Подробности (автологин kiosk-пользователя, управление сервисом) — в deploy/DEPLOY.md.
- На экране настройки ввести адрес сервера — достаточно хоста (
panel.example.comили192.168.1.50): протокол и стандартный порт клиент подберёт сам (https → http, явный порт — как указан)./api/...дописывать не нужно. - «Проверить соединение» → «Отправить заявку».
- Подтвердить устройство в панели управления («Экраны → Заявки»). Device-token выдаётся один раз; если токен уже был выдан — просто отправить новую заявку.
- Плеер начнёт синхронизацию автоматически. Имя устройства задаётся при заявке и далее меняется только из панели.
Дополнительно на экране настройки: выбор места хранения контента (внутренняя память/носитель), целевой дисплей и поворот, сервисный PIN (пустой = вход в сервисную зону без PIN).
Вход из плеера:
- Desktop: 3×
Escподряд (илиF2); - Android/TV-пульт: 5×
Backподряд или кнопкаMenu; - Тач-экран: 5 быстрых тапов.
Если задан PIN — будет запрошен. Редактор и PIN-диалог сами закрываются через 30 с бездействия. F12 — debug-overlay поверх плеера.
Внутри: таймлайн «Что дальше», переключатель аварийного оффлайн-режима с локальным плейлистом, ⚙️ настройки (сервер/хранилище/дисплей/PIN), диагностика устройства.
Documents/efir/
├── config.json # server_url, media_root, дисплей, PIN
├── device.json # device_id + token (chmod 600 на Unix)
├── manifest.json # кэш манифеста для оффлайн-старта
├── log.txt # ротация: 5 МБ × 3 файла
└── media/ # кэш контента (если не выбран внешний носитель)
POST /device/register/request → approve в панели → GET /device/register/status (одноразовый token) → далее цикл: GET /device/manifest (полл с backoff), POST /device/heartbeat (25 с, телеметрия), GET /device/media/{id} (Bearer, sha256-валидация, докачка через .download). Напрямую в MinIO/S3 клиент не ходит. При 401/403/404 — сброс регистрации и возврат в настройку.
Требования: Flutter 3.44 (stable).
| Платформа | Что нужно дополнительно |
|---|---|
| Windows | Visual Studio с рабочей нагрузкой «Разработка классических приложений на C++». Сборка WebView-плагина тянет NuGet-пакеты (Microsoft.Web.WebView2, WIL), поэтому нужен доступ в интернет. |
| Linux | libmpv-dev, libwebkit2gtk-4.1-dev |
| Android | — |
HTML-страницам нужен WebView. На Windows это рантайм WebView2: в Windows 10/11
он обычно уже стоит, а установщик клиента доставит его сам, если нет. На Linux —
пакет libwebkit2gtk-4.1-0 (в Debian/Ubuntu ставится вместе с -dev).
Без них клиент работает, но HTML-слоты пропускаются с записью в лог: видео,
картинки и плейлисты не затрагиваются.
flutter pub get
flutter build windows --release # или: flutter build linux --release / flutter build apk --releaseРелиз: создать тег vX.Y.Z и запушить — GitHub Actions соберёт Windows-инсталлятор, Linux tar.gz и APK (для APK нужны секреты подписи, см. android/key.properties.example).
Локальная release-сборка APK требует android/key.properties с release-keystore —
без него сборка падает (fallback на debug-подпись убран сознательно: debug-подписанный
«релиз» нельзя распространять и обновлять поверх).
Панель раздаёт сборки экранам, но доверенной стороной не является: артефакт подписывается Ed25519-ключом, приватная половина которого живёт офлайн и в секретах CI, а публичная вшита в клиент. Компрометация сервера не даёт разлить на экраны посторонний бинарь — подпись не сойдётся, и клиент откажется ставить.
python tools/sign_release.py --generate-key # один раз- приватный ключ → секрет репозитория
UPDATE_SIGNING_KEY_BASE64и офлайн-бэкап; - публичный → переменная репозитория
EFIR_UPDATE_PUBLIC_KEY(вшивается в сборку через--dart-define) иCLIENT_UPDATE_PUBLIC_KEYв.envсервера.
Без EFIR_UPDATE_PUBLIC_KEY сборка выходит с отключёнными обновлениями
(fail-closed): лучше не обновляться, чем поставить непроверенный бинарь. Потеря
приватного ключа = невозможность выпускать обновления для уже установленных
клиентов: сменить вшитый ключ можно только сборкой, установленной вручную.
Релизный workflow кладёт в GitHub Release SIGNATURES.txt (файл sha256 подпись) —
подпись оттуда вставляется в панель при загрузке сборки.
| Платформа | Как ставится | Условие автоматической установки |
|---|---|---|
| Android / Android TV | PackageInstaller |
молча — только если приложение Device Owner; иначе система требует подтверждения, и панель показывает ready |
| Windows | тихий запуск Inno-инсталлятора | всегда: клиент ставится в профиль пользователя, поэтому UAC не всплывает |
| Linux | распаковка новой версии + перестановка симлинка current | всегда: каталог принадлежит пользователю сервиса, root не нужен |
Device Owner на Android включается только на свежесброшенном устройстве без добавленных аккаунтов и задним числом не назначается:
adb shell dpm set-device-owner com.efir.client/.EfirDeviceAdminReceiver
# Пайп в кавычках — чтобы grep выполнялся на устройстве, а не в хостовой
# оболочке (в PowerShell его просто нет).
adb shell "dumpsys device_policy | grep -i owner"Ни на одной платформе обновление не поднимает привилегии. Схема «root-хелпер, которому процесс пользователя говорит, что выполнить» отвергнута сознательно: это готовое локальное повышение привилегий, а закрыть его нечем без сертификата подписи кода. Вместо этого каталог установки принадлежит тому же пользователю, что и приложение.
Linux: /opt/efir-client/versions/<версия> + симлинк current, на который смотрит
ExecStart. Обновление распаковывает версию рядом и переставляет симлинк (атомарно,
через rename), после чего клиент завершается, а Restart=always поднимает уже новую
сборку. Предыдущая версия остаётся на диске — откат делается перестановкой симлинка.
Windows ставится в профиль пользователя, а не в Program Files — иначе каждое
обновление упиралось бы в UAC, а на экране, к которому никто не подходит, диалог UAC
означает, что обновления нет. Права администратора нужны один раз и только если в
системе отсутствует Visual C++ Runtime (мастер об этом скажет).
lib/controllers/playlist_controller.dart — состояние устройства: регистрация, манифест-полл, heartbeat, кэш, хранилище. lib/views/ — плеер (двойной видеоплеер media_kit/mpv для бесшовных переходов), экран настройки, сервисный редактор, диагностика. lib/services/ — API, кэш медиа, конфиг, хранилище, логи. Планы — ROADMAP.md.