Три аудитории, один продукт, ноль общих допущений.
У OOH-рекламной сети три разных пользователя, у которых нет ничего общего. Владельцы площадок хотят знать, что и когда идёт на их экранах. Рекламодатели хотят покупать слоты, таргетировать по местоположению и видеть число показов. Сами экраны вообще не пользователи — это устройства, которым нужно надёжно получать расписания и проигрывать их без участия человека.
У клиента были рабочая концепция и сжатое окно запуска. Им нужен был маркетплейс-бэкенд для согласований рекламодателей, биллинга и планирования кампаний. Консоль, в которой операторы и владельцы площадок перетаскивают слоты в календарь. И плеер-рантайм, способный работать на Android-планшетах за стойкой отеля, Linux-киосках на транспортном узле и Windows-приставках в розничной сети — без развёртывания отдельной кодовой базы для каждого.
Удобным дефолтом для плеера был Electron. Electron упаковывает экземпляр Chromium и рантайм Node и запускается везде. Но он же весит сотни мегабайт, обновляется непредсказуемо и добавляет сложности на целый браузер к тому, что должно быть медиаплеером. На ограниченном железе киосков размер имел значение. История с обновлениями — ещё большее.
Маркетплейс-бэкенд, консоль с drag-and-drop и нативный плеер, работающий на четырёх операционных системах.
Бэкенд — это многоарендное приложение на Laravel 12, реализующее всё, что нужно маркетплейсу: подключение и согласование рекламодателей, планирование кампаний с разбивкой по времени суток и гео-таргетингом, сверку биллинга Stripe, загрузку и транскодирование медиа в очереди через слой фоновых задач Laravel и модель агрегации показов, питающую отчётность. Sanctum отвечает за аутентификацию API. Ассеты хранятся на S3.
Консоль оператора — это админ-поверхность на Next.js 16 с планировщиком drag-and-drop на @dnd-kit. Владельцы площадок видят свои экраны и свои слоты. Рекламодатели видят свои кампании и свой охват. Роль администратора видит всё. Все трое — одно и то же приложение, разграниченное по роли.
Плеер — это Qt 6 и C++20. Он работает на macOS, Linux, Windows и Android из единой CMake-кодовой базы с пресетами под каждую платформу. При запуске он подключается к Supabase по WebSockets и подписывается на свой канал расписания. Когда оператор перемещает слот в консоли, изменение приходит на экран в реальном времени — без опроса, без цикла обновления. Если сеть пропадает, плеер откатывается к локально кэшированным расписанию и медиа. Экран продолжает проигрывать.
- F · 01Многоарендная консоль
- Владельцы площадок, рекламодатели и администратор в одной поверхности, разграниченной по роли. Никаких отдельных порталов, никаких дублирующихся кодовых баз.
- F · 02Планировщик drag-and-drop
- Слоты, гео-зоны и интервалы суток, расставленные в календаре на @dnd-kit. Операторы видят конфликты до сохранения.
- F · 03Синхронизация плеера в реальном времени
- Изменения расписания доходят до парка плееров через Supabase WebSockets в момент сохранения оператором. Без опроса, без перезапуска.
- F · 04Плеер с поддержкой офлайн
- Qt Sql с SQLite кэширует текущее расписание и медиа локально. Пропадание Wi-Fi не гасит экран.
- F · 05Кросс-платформенный нативный бинарник
- Одна CMake-кодовая база, четыре целевые платформы: macOS, Linux, Windows, Android. Каждая сборка — самодостаточный нативный бинарник — без Chromium, без V8.
- F · 06Биллинг маркетплейса
- Сверка биллинга Stripe и агрегация показов в Laravel-бэкенде. Рекламодатели платят за подтверждённые показы.
Выбор Qt и чего он стоил против дефолта Electron.
Electron был очевидным выбором. Вся команда знала JavaScript. Админ-консоль уже была на TypeScript. Плеер на Electron разделял бы треть кодовой базы с веб-поверхностью. Мы отказались от него в первую же неделю.
Решающим фактором стала реальность развёртывания. Целевое железо варьировалось от Android-планшетов на 4G до Windows-мини-ПК на корпоративном Wi-Fi и Linux-киосков с заблокированными образами ОС. Размер Chromium в Electron — обычно 150–300 МБ в установленном виде — был жёстким «нет» для ограниченных устройств. Поведение автообновления Chromium, конфликтующее с заблокированными политиками ОС киосков, было ещё более жёстким «нет» для корпоративных развёртываний.
Qt 6 с C++20 даёт нативный бинарник под каждую платформу, скомпилированный примерно в 15 МБ на Linux и менее 30 МБ на Windows и Android. Путь обновления — это замена файла, оркеструемая самим плеером. Нет движка браузера, нет V8, нет брокера протоколов Electron. WebSocket-соединение с Supabase идёт через Qt Network; воспроизведение медиа — через Qt Multimedia. Офлайн-кэш использует Qt Sql с SQLite. Каждая зависимость поставляется в бинарнике.
Плеер, подходящий железу, лучше плеера, подходящего команде.
Цена была в переключении контекста. Команда вела три рантайма — PHP, TypeScript, C++ — параллельно. Кросс-рантайм-интеграционные тесты требовали больше обвязки, чем потребовал бы одноязычный стек. CMake-матрица сборки под четыре целевые платформы добавляла сложности в CI. GitHub Actions запускает четыре задачи сборки плеера на каждый push; сборка под Linux на базе Docker оказалась самой надёжной базой для воспроизводимости.
Трёхуровневая OOH-платформа, выпущенная меньше чем за месяц.
Первая версия всех трёх поверхностей была готова к продакшену за 27 дней: 223 коммита в трёх репозиториях, мультиплатформенный CI на каждом push, бинарники плеера подтверждены на всех четырёх целевых операционных системах до финального ревью. Маркетплейс-бэкенд берёт на себя сверку биллинга рекламодателей и агрегацию показов. Консоль берёт на себя планирование без таблиц. Плеер справляется с пропаданием сети, не гася экран.
Платформа работает. Консоль оператора — система учёта расписаний экранов по всей сети. Парк плееров получает обновления в реальном времени через Supabase WebSockets — изменения расписания, сделанные в консоли, доходят до физических экранов без перезапуска или ручной синхронизации.
Мораль выбора стека проста. Electron — верный ответ, когда удобство команды и скорость поставки являются связывающими ограничениями. Здесь он не был верным ответом. Выбирайте плеер-рантайм, подходящий вашему железу, а не тот, что подходит зоне комфорта вашей команды.
Мы сказали им, что плеер должен работать на всём, что мы уже закупили. Они вернулись с бинарником, который это делал.
