Привет! Меня зовут Макс, я web-инженер и предприниматель. В этой статье расскажу о кейсе, где мы с командой работали над непростой интеграцией Pixel Streaming - и как из эксперимента это почти стало продуктом.

С клиентом, который поставил такую задачу, я успел поработать в разных форматах - и в найме, и в статусе подрядчика. Проект, который начался как легаси, содержал множество камней под ногами: нестабильная инфраструктура, высокая стоимость масштабирования и довольно расплывчатая зона ответственности между командами. Тем не менее, нам удалось довести его до состояния, близкого к production-ready - хотя и не запустить в прод по итогу.

Описание технологии

Pixel Streaming - разработка Epic Games для стриминга в web приложений, разработанных на базе Unreal Engine. В самом первом приближении это выглядит вот так:

Работает всё на базе webRTC, в качестве стека - Node и несколько сервисов на нём. В качестве основных можно выделить:

  • Cirrus (Signaling server) - отвечает за трансляцию, содержит backend-часть и базовый web-интерфейс для работы.

  • Matchmaker - организатор очереди, следит за свободными слотами на сигналинг серверах и направляет пользователя на освободившийся

Приложения могут подключаться как к одному, так и нескольким streaming (signaling) серверам, они в свою очередь подключаются к матчмейкеру, который следит за свободными подключениями и перенаправляет туда пользователей
Приложения могут подключаться как к одному, так и нескольким streaming (signaling) серверам, они в свою очередь подключаются к матчмейкеру, который следит за свободными подключениями и перенаправляет туда пользователей

Само решение может пригодиться для запуска облачного гейминга, демонстрации визуализаций, построенных на базе UE, презентаций для клиентов или потенциальных покупателей - когда у вас нет желания разворачивать у себя большой и ресурсоёмкий продукт, а хочется просто открыть вкладку в браузере.

И хотя это отличная идея сама по себе, Pixel Streaming остаётся экспериментальным решением: нестабильным, плохо масштабируемым и не лучшим образом документированным. Он задуман скорее как витрина возможностей Unreal, чем как что-то, готовое к продакшену. Тем не менее, существуют сервисы, которые смогли его использовать и поднять до уровня продукта. Но все они находятся за рубежом, что в сегодняшних условиях является проблемой, и стоят весьма недёшево. В итоге заказчик по тем или иным соображениям отказался от них.

Начало работы

Когда я пришёл на этот проект, в моём распоряжении оказались: 

  • дистрибутив, неизвестно насколько отставший от официального репозитория и неизвестно как кастомизированный,

  • несколько адресов вида http://<ip>:<порт>, на которых были развёрнуты демки, которые показывались заказчикам.

Какая-то команда работала над проектом до меня, оставив после себя довольно тяжёлое наследие - и огромный объём нереализованных задач.

Клиент занимался архитектурными визуализациями, и мне и моим коллегам предстояло:

  • Запустить портфолио клиента через пиксель стриминг в web

  • Обеспечить возможность заказчиков его услуг - девелоперов - встраивать визуализации на собственные сайты

  • Сделать так, чтобы посетители - потенциальные клиенты - могли взаимодействовать с 3D-презентациями без установки приложений

И эти задачи оказались почти невыполнимыми.

Проблемы

Первые серьёзные сложности возникли из-за архитектурных ограничений, заложенных в саму технологию. В теории, стрим можно раздать на неограниченное количество пользователей — но на практике всё упирается в одну особенность: Pixel Streaming организует не только видеопоток, но и обратную связь.

То есть, каждый клиент, получающий видео, может отправлять в приложение сигналы с клавиатуры, мыши, тачпада и произвольные команды UI. Всё это возвращается в единственный инстанс Unreal-приложения.

Вот схемка, как это примерно организовано:

Возможности разделить управляющие сигналы нет - ни на уровне передачи данных, ни на уровне Unreal, который использует для этого единый контроллер.

Что означает, что одно приложение мы можем расшарить на одного пользователя. Два пользователя смогут смотреть трансляцию, но либо мы отнимаем у второго возможность взаимодействия с управлением, либо два потока управляющих команд сливаются в один, создавая ситуацию из басни “Лебедь, рак и щука”, которая убьёт пользовательский опыт.

Выход из этой коллизии - организация очереди и запуск нескольких экземпляров приложения. Тем более что сервис очереди включен в базовую поставку Pixel Streaming. Но Unreal-приложение съедает более или менее все ресурсы графического ядра. На картах уровня 4080/4090 запуск всего двух приложений ронял FPS до 15-25 кадров. Три приложения давали 5-15 кадров.

Итого на одной достаточно мощной машине мы могли обеспечить спорное по качеству обслуживание всего двух пользователей. Стоимость аренды такой машины в облаке была в районе $1000 - и, понятное дело, ни о каком серьёзном количестве пользователей тут речи идти не могло: стоимость эксплуатации такой системы улетала в небо уже на первом десятке человек.

На старте клиенту было предложено самое очевидное решение: отказ от стриминга и переход на полноценный WebGL — например, с использованием three.js. Это снимало бы почти все инфраструктурные ограничения на масштабирование.

Но клиент отказался рассматривать такой вариант по следующим причинам:

  • качество визуализации падало;

  • текущая 3D-команда работала только с Unreal Engine

  • полный переход означал бы серьёзную перестройку рабочих процессов компании.

Следующим логичным шагом выглядел переход к контейнеризации Unreal-приложения и запуск в Kubernetes-кластере. Но и тут нас ждал лес проблем:

  • В силу особенностей проектов билды Unreal делались только под Windows

  • Даже в кластере требовались GPU-ресурсы, которые нужно было как-то связывать с контейнерами

  • Возможности хостинга и масштабирования Windows-Kubernetes + GPU вызывали серьёзные вопросы и технического, и экономического характера

  • Для работы с контейнерами требовалась кастомная логика, которую нужно было ещё придумать, как реализовать

Запуск системы такого уровня явно превышал компетенции команды. Мы начали консультироваться с девопс-инженерами и запросили у клиента бюджет на найм соответствующего специалиста.

Но даже наши консультанты рисовали неясные перспективы. Проблемы были на каждом шагу: контейнеризация Windows-приложения, подключение ресурсов GPU, распределение и управление ресурсами. Проблема рисовала горизонт в несколько месяцев экспериментов без гарантии, что на выходе получится достойное решение.

Пока заказчик думал и искал ресурсы, мы занялись более насущными вопросами.

Оптимизация сборки

Мы начали наводить порядок в том, что есть. Вынужденно отказались от всего объёма правок предыдущей команды - в текущем виде не работало несколько важных функций, которые в репозитории были доступны из коробки - например, обнаружилось, что текущая сборка крайне плохо поддерживает запуск на мобилках.

Для начала, весь комплекс был разделён на три уровня:

  1. Unreal-приложение — всё, что касается запуска конкретной визуализации

  2. Контейнеризированная обвязка — стандартные сервисы Pixel Streaming в docker-окружении

  3. Web-слой — nginx, умеющий работать с доменами и сертификатами, и проксирующий пользователей туда, куда надо.

Получилась вот такая схема:

Unreal изолирован на Windows-машинах, сигналинг серверы крутятся в контейнерах, nginx разруливает обращения
Unreal изолирован на Windows-машинах, сигналинг серверы крутятся в контейнерах, nginx разруливает обращения

Уровень Unreal-приложения

Дорога шла по кочкам и канавам - даже базовый запуск Unreal представлял собой проблему. В режиме стриминга приложение запускалось offcanvas, не предоставляя никакого интерфейса взаимодействия. Заказчики жаловались, что после запуска сессии невозможно было ни остановить её, ни управлять процессом. Каждый сбой требовал ручного вмешательства администратора.

Дальше - хуже: в режиме стриминга Unreal мог уронить GPU, зависнуть, вылететь или просто отключить видеопоток.

Чтобы справиться с этим, мы создали внешний скрипт, который отслеживал процесс Unreal и при необходимости перезапускал его в нужной конфигурации.

Так появился Unreal Launcher - маленькая, но важная надстройка.

Вот что у нас получилось:

Так мы стандартизировали запуск, сделали его управляемым и надеялись повысить надёжность работы.

Контейнеризированная обвязка

Pixel Streaming из коробки давал конфигурацию для докера. Оставалось только доработать её до необходимого состояния и вынести в отдельный сервер, который действовал независимо от Windows-машины.

Пожалуй, это была самая простая часть всей системы - решение, которое просто работало. Единственное, что потребовало отдельного внимания - взаимодействие с Unreal, запущенным из-под NAT. Здесь пришлось немного повозиться с пробросами портов, сетевыми настройками и конфигурированием запуска Signaling-сервера.

На том же сервере, что и докер, размещался последний слой:

Nginx-сервер

Его задачей было проксирование обращений по нужным доменным именам внутрь Docker-контейнеров (HTTP и WebSockets). Ну и, конечно, обеспечение https с нормальными обновляемыми сертификатами.

Так мы ушли от ссылки вида http://<ip>:<port> к привычному https://<project>.domain.ru

Хоть и маленькая, но победа: плюс к пользовательскому доверию и облику проекта.

Проблема очереди

Следующей проблемой стала очередь. Формально она решала проблему ограничения на подключение к серверу: из всех подключенных стримов сервис очереди находил свободный, и перенаправлял пользователя на него.

Но остальным пользователям оставалось ждать.

Учитывая, что почти везде мы были ограничены одним сервером на проект, а значит - двумя пользователями максимум, очередь убила бы интерес пользователей при любой серьёзной посещаемости.

Здесь мы приняли решение смягчить опыт ожидания. Вместо базовой заглушки вида “ожидайте” мы собрали нечто куда более дружелюбное:

  • Вывели живую трансляцию (без возможности управления) из первого доступного сеанса, чтобы пользователь мог если не поучаствовать, то хотя бы посмотреть на визуализацию

  • Добавили возможность скачать себе приложение, чтобы посмотреть демо локально

  • Снабдили страничку возможностью забронировать демонстрацию или связаться с командой продаж

В итоге получилось так (было-стало):

Было: базовый экран с лого анрила и предложением подождать. Стало - просмотр трансляции, возможность установить приложение себе локально, запросить демо или связаться с компанией
Было: базовый экран с лого анрила и предложением подождать. Стало - просмотр трансляции, возможность установить приложение себе локально, запросить демо или связаться с компанией

Очередь всё ещё оставалась узким местом — но теперь, по крайней мере, выглядела прилично.

Дальше нужно было защитить её от обхода. Проблема была в том, что сервис очереди перенаправлял пользователя на свободный стрим по постоянному адресу. Запомнив его, любой мог заходить напрямую, минуя очередь, и мешать другим.

Чтобы предотвратить такую возможность, была добавлена система тикетов:

  • Заходя в очередь, пользователь получал уникальный тикет на посещение трансляции

  • На страничке трансляции тикет пробивался и более не был валидным

  • При посещении странички трансляции без валидного тикета пользователь перенаправлялся в очередь.

Схема “было-стало”:

Теперь кейс ожидания в очереди был полностью закрыт и максимально гуманизирован.

Всё ещё проблемы

Параллельно мы собирали обратную связь от заказчиков по тому, как Pixel Streaming работал на их стороне. И становилось очевидно, что Unreal Launcher в текущем виде решал не всё:

  • Выбивание GPU не завершало процесс, и следовательно, за ним не следовал перезапуск

  • Зависание приложения никак не отслеживалось

  • Завершение процесса из консоли не убивало визуализацию - как выяснилось, через исполнимый файл UE запускался загрузчик, а реальное приложение крутилось в дочерних процессах

В результате клиенты запускали демо и часто видели чёрный экран - особенно болезненно это было, когда менеджеры показывали визуализации своим потенциальным клиентам

Пришлось дорабатывать наш лаунчер:

  • Завершать все дочерние процессы Unreal, а не только родительский

  • Поднять вспомогательный сервис на стороне сигналинг-сервера, который проверял входящий видеопоток и, при его отсутствии, давал команду на перезапуск

  • Добавить перезапуск по расписанию (например, ночью) для освобождения ресурсов

Схема работы Unreal-части усложнилась:

Сигналинг-сервер обновлял статус трансляции для супервизора. Unreal Launcher проверял у супервизора состояние и выполнял перезапуск при соответствующем статусе
Сигналинг-сервер обновлял статус трансляции для супервизора. Unreal Launcher проверял у супервизора состояние и выполнял перезапуск при соответствующем статусе

В результате большинство аварийных ситуаций ушло: Unreal-сессии перестали умирать молча, и клиент наконец-то мог доверять системе

Обратно к масштабированию

Всё это время мы пробовали искать обходные пути для решения вопроса масштабирования.

Одним из найденных вариантов был запуск внутри Unreal-приложения нескольких камер, отправляющих поток в нашу стриминговую обвязку, и кастомизация контроллера. Опытным путём было установлено, что мы можем запустить одновременно до четырёх камер, что, конечно, не было реальным решением проблемы - но давало больше слотов для пользователей.

Вот так могло выглядеть такое решение:

Передаём из Unreal ID камеры (доступно из коробки), на стороне обработки фидбека добавляем в данные объект с ID камеры, кастомизируем контроллер внутри Unreal на распознавание ID и перенаправление данных на соответствующую камеру
Передаём из Unreal ID камеры (доступно из коробки), на стороне обработки фидбека добавляем в данные объект с ID камеры, кастомизируем контроллер внутри Unreal на распознавание ID и перенаправление данных на соответствующую камеру

На своей стороне мы нашли возможность отправки уточнённой информации и переписали отправку данных так, чтобы передавать ID камеры как ID клиента. Задача перешла к отделу Unreal - совместно мы смогли оттестировать запуск нескольких камер и кастомизировать контроллер на их стороне. Но работа мультикамеры, по словам Unreal-команды, не соответствовала их ожиданиям по качеству и стабильности — и направление было заморожено.

Впрочем, помимо того, что с мультикамерой мы не решали вопрос масштабирования принципиально, был и другой момент, который нельзя было игнорировать: несколько камер внутри одного приложения означали, что весь интерфейс управления окружением (например, выставление дня или ночи, или выбор времени года) требовал доработки: его нельзя было показывать сразу на всех камерах. Отдельный вызов — взаимодействие с объектами в сцене: пользователи не видели друг друга, но могли менять общее окружение, что приводило к непредсказуемым эффектам: предметы двигались, свет менялся — всё могло выглядеть как баг.

Также мы не могли снова не поднять вопрос о переходе к three.js . Клиент вёл переговоры с крупными сервисами о выводе на их стороне наших трансляций - что предполагало десятки, если не сотни, конкурентных посещений. Объективно система не была к этому готова, и даже в теории дать такую возможность было крайне сложно.

Простота запуска решения на WebGL, в сравнении со всем остальным адом, пленила:

Красота и изящество!
Красота и изящество!

Но, несмотря на нашу настойчивость, клиент снова дал отказ по этому направлению.

В рамках Kubernetes движений тоже не происходило. Раз за разом мы получали ответ, что ресурсов на найм толкового девопса не было, а наши эксперименты с контейнеризацией потерпели неудачу на этапе попытки запустить Unreal через wine или аналоги.

Параллельно росло давление на команду. Клиенты делали запросы, которые мы не могли обработать - например, на хостинг всего решения на нашей стороне, или на работу в режиме нагрузки десятков пользователей. Конкуренты запускали похожие, но лучшие чем у нас, продукты. Собственных мощностей не хватало даже на то, чтобы полноценно выложить в сеть портфолио компании: все пять доступных серверов были заняты до предела — на каждом крутился какой-нибудь экземпляр Unreal.

Так было принято самое нежеланное решение - делать свою оркестрацию.

Своё решение

Основной проблемой с запуском портфолио было то, что на него банально не хватало серверов. При этом большей частью серверы простаивали: посещаемость демок всё ещё была низкой. Так само собой просилось решение запускать нужную трансляцию на свободном сервере по запросу.

В нашем активе был сервис цифровой дистрибуции - мы разработали для клиента автоматизированное решение доставки и запуска для конечных пользователей билдов Unreal, чтобы не было нужды кидать каждый раз ссылки на яндекс.диск. Этот же сервис мы адаптировали для автоматической загрузки приложений на серверы.

Вот как модифицировался при этом Unreal Launcher:

Сервис дистрибуции обновляет registry и даёт скачивать обновлённые билды Unreal, супервизор меняет состояние на "запустить/погасить такой-то процесс", Unreal Launcher выполняет инструкцию, всё работает
Сервис дистрибуции обновляет registry и даёт скачивать обновлённые билды Unreal, супервизор меняет состояние на "запустить/погасить такой-то процесс", Unreal Launcher выполняет инструкцию, всё работает

Следующим компонентом стал менеджер запросов. Его задача — получить запрос на демо, передать задачу супервизору, запустить нужное приложение на свободном сервере и сгенерировать ссылку для пользователя.

Менеджер запросов даёт команду супервизору, супервизор выбирает свободный слот и ставит задачу на выполнение, генерируя ссылку на соответствующую трансляцию. Ссылка уходит в веб-клиент, пользователь получает её и перенаправляется на страничку с опытом
Менеджер запросов даёт команду супервизору, супервизор выбирает свободный слот и ставит задачу на выполнение, генерируя ссылку на соответствующую трансляцию. Ссылка уходит в веб-клиент, пользователь получает её и перенаправляется на страничку с опытом

В итоге мы получили полноценный продукт. Он оптимизировал использование серверов, обеспечивал гибкое управление загрузкой и позволял масштабировать сервис под клиентов. Продукт всё ещё был нишевым и более подходящим для индивидуальных запусков, но по крайней мере давал весь необходимый функционал, на который рассчитывали заказчики. Дальше уже можно было прогнозировать распределение нагрузки и работать с чуть модифицированной очередью, исследовать пользовательский опыт и наращивать технические ресурсы - и в итоге понемногу становиться полноценным стриминг-провайдером.

Как это ни странно, итоговое решение оказалось довольно просто реализовать. Предыдущий объём правок сработал сильно в плюс стабильности и готовности системы встать на рельсы оркестрации - для всех критических узлов кроме менеджера запросов уже была подготовлена своя инфраструктура, и оставалось только немного поменять её логику. Но, к сожалению, проект был приостановлен именно в тот момент, когда мы приблизились к его запуску как SaaS-решения - из-за нехватки ресурсов и переориентирования на другие задачи.

Комментарии (1)


  1. constXife
    15.10.2025 05:06

    О, я решал тоже похожую задачу для своего хобби проекта. Я хотел сделать аналог игры для программистов Colobot, чтобы можно было программировать "роботов" для колонизации других миров. Сделал некий proof of concept на Unreal по такой схеме: есть внешний сервер, чтобы можно было "стримить" в интернет, через VPN подключение к моему ноуту с Linux где через docker compose запускалась в контейнере Unreal и обрабатывала команды.

    Тоже да, прошел через весь головняк запуска Unreal в докере на Linux, переход к запуску по требованию и мониторингу процесса.

    Проблема для меня случилась когда я захотел сделать поддержку сети: я хотел, чтобы можно было еще подключаться через VR шлем в игровой уровень и наблюдать через шлем как работают роботы, может быть проводить диагностику какую-то, видеть какие команды они выполняют + чтобы второй игрок мог параллельно подключаться и в соседней комнате тоже работать. И я даже это сделал, но запал иссяк. Морока с синхронизацией сетевого кода для меня оказалась изнуряющей, в отличие от легкой реализации подключения.

    Вот демо программирования робота — https://www.youtube.com/watch?v=vBKpZP6w6ss.