Уже скоро два года как короновирус выгнал всю нашу кампанию на удалёнку. То, что у нас не практиковалось и не приветствовалось, буквально за месяц стало "нормой". Не стало крутого и уютного офиса с круассанами, коллеги из тёплых и мягких превратились в плоские аватарки в чатах и на видео звонках (где почти никто камеру и не включает). На долгих и не всегда интересных митингах появилась мысль "вот бы добавить уюта в происходящее, например, дорисовать каминчик или сделать митинг на берегу озера, кто-то костерок ворошит, кто-то - рыбачит, шашлычки жарятся" (прямо как в игре Русская Рыбалка 4). Постаравшись, мне удалось настроить "удалённый офис" по своему вкусу:
DevTools в Хроме — сильная вещь, с её помощью я изучал как работает web-клиент системы видеоконференций, принятой в нашей кампании. Оказалось, что используется "стандартный" стек для таких продуктов: REST API + WebSockets + WebRTC. И поддержка всего этого есть в Unreal Engine 4! Ну а дальше - дело техники =)
К слову о "технике" - в данном проекте Blueprints использовались по самому минимуму - только для работы с 3D объектами (визуальным представлением), остальное было сделано на С++.
Сразу хочу сказать, что я не являюсь "настоящим" разработчиком UE4, программистом на С++ или чтецом документаций. Мне просто захотелось "побыстрому сколотить" такую вот поделку на базе гугла и минимального опыта с UE4 и C.
Графическая часть
Сам офис и набор сидячих поз - готовые ассеты из Маркетплейса. Стол, кресло и манекен - стандартный пак из UE4. Круглый стол был взят чтобы упростить "рассадку" неизвестного числа участников митинга - достаточно просто с каждым новым пришедшим немного увеличивать радиус стола (масштабируя его по X и Y, конкретные значение подбирались опытным путём) и сдвигать всех по равным секторам.
Больше ничего особенного по визульному представлению не потребовалось реализовывать.
REST API
С помощью стандартного модуля HTTP из UE4, на базе первого нагугленного примера его использования, были реализованы:
авторизация (получение токена)
последовательный вызов REST API сервера (для получения токена, адреса подключения по WebSockets для активного митинга, адреса для PubNub подписок и т.п.)
общение с PubNub (для получения событий во время митинга: активный спикер, присоединение/уход участников, чат, "поднятие руки")
Некоторые моменты, на которые стоит обратить внимание:
Авторизация. Сначала я использовал "гостевой" вход, однако многие митинги оказались закрыты для неавторизованных пользователей и пришлось решать эту задачу. Осложнялось всё тем, что у нас используется SIngle Sign-On, т.е. просто захардкодить логин/пароль или сделать минимальную формочку не получалось. Тут тоже повезло - функциональности стандартного (надо включить в Plugins) UE4 виджета "WebBrowser" хватило для отображения SSO формы нашего провайдера и получения кодовой строки - а там уже она превращается в токен достаточно просто из C++ кода.
PubNub. Это был мой первый опыт работы с этим сервисом и я до сих пор не могу определиться является их способ доставки серверных сообщений на клиент отвратительным или гениальным. Если в двух словах, то вы должны сделать HTTP GET на PubNub адрес (для подписки на какой-то канал), дальше вам придёт ответ 200 через 30+ секунд со специальным флагом (ping) и таймстампом или сервер ответит быстрее (если произошло событие) в ответе вы получите это событие и таймстамп. Дальше вы опять делаете GET с указаным таймстампом и снова ожидание ответа сервера (ping или новое событие, произошедшее с того таймстампа). И так по кругу. Поначалу я это пытался сделать "в лоб" (создавался запрос, в OnResponseReceived которого снова создавался запрос, в OnResponseReceived которого снова создавался запрос... и т.д.), однако эта рекурсия сбоила и пришлось реализовать переподключение через флаг и Tick функцию основного актора (который ловит команду на подключение к митингу и всю логику содержит). Т.е. каждый Tick проверяется "не надо ли подключиться снова к PubNub", флаг "надо" выставляется как раз в обработчике OnResponseReceived. Это позволяет каждому запросу выполняться и спокойно умирать, ничего из OnResponseReceived напрямую не порождая.
WebSockets
Общение с сервером по WebSockets реализовано стандартным UE4-модулем на базе первого нагугленного примера его использования.
Добавить тут больше нечего - особенностей и подводных камней я не встретил, а сообщения, бегущие внутри WSS, не имеют общепринятого стандарта, т.е. полностью определяются разработчиками сервиса и надо просто восстановить их формат и последовательность на базе того, что вам покажет DevTools Хрома при работе "официального" web-клиента.
WebRTC
Тут официальная документация и примеры из интернета заканчиваются. Любой общий запрос ведёт на официальное демо стриминг возможностей UE4 - Pixel Streaming.
К сожалению, "из коробки" он делает то, что нужно, но в другую сторону, а разобраться в нём и "перевернуть" мне не позволяет квалификация. Но что-то из него можно подчерпнуть. Оказалось, что UE4 содержит в себе WebRTC Native API, можно подключить его и использовать в соответствии со стандартной документацией.
Подключается он добавлением "WebRTC" в PublicDependencyModuleNames вашего проекта. В коде же надо взять WebRTCIncludes.h файл из исходников PixelStreaming и инклудить его.
Документацией к использованию WebRTC NativeAPI как клиента, по сути, является следующая картинка:
с помощью её, а так же кода нативного клиента из примеров от Гугла, необходимо создать свой класс, имплементирующий функции от webrtc::PeerConnectionObserver и webrtc::CreateSessionDescriptionObserver. Т.е. наша задача инициировать WebRTC стек (создав PeerConnection), а дальше осуществить обмен SDP и Ice кандидатами между ним и сервером по WebSockets.
По идее это же делается в PixelStreaming UE4 примере, но мне его разобрать не удалось, клиентское С приложение от Гугла на порядок проще.
У меня получился полный хаос, который я не смогу описать, но могу попробовать описать какие-то моменты подробнее, если кому интересно.
Media
Эта часть является самой сложной, потому что тут уже не обойти UE4 (надо использовать его аудио подсистему, нативный пример от гугла, очевидно, звук выводит через чистый win32 или linux). На текущий момент мне удалось достичь проигрывания звука из конференции (с качеством "не очень"), микрофон поддерживать особо не собирался, а вот скриншаринг и видео от участников надо будет постараться и доделать.
Звук со стороны UE4 был сделан на базе компонента USynthComponent, который создан для программной генерации звука. Скорее всего есть путь "правильнее", но я его пока не нашёл. По итогу создан класс от USynthComponent, который реализует две вызываемые со стороны UE4 функции - Init и OnGenerateAudio. В принципе, гуглится несколько примеров использования этой схемы для генерации "писка", от себя отмечу только то, что у меня WebRTC выдавал аудиоданные пачками по 960 самплов, а UE4 вызывал OnGenerateAudio запрашивая 1024. Оказалось, что USynthComponent наследует переменную PreferredBufferLength, выставив которую в 960 я добился, чтобы и UE4 просил по 960 самплов. Качество звука лучше не стало по сравнению с тем, когда я ему на запрос 1024 только 960 отдавал. Ещё один момент - OnGenerateAudio ожидает, что каждый сампл будет float значением от -1.0 до 1.0. WebRTC же отдаёт самплы типа int16, так что надо поделить на 32к, чтобы они друг друга поняли.
WebRTC со своей стороны ожидает получить реализованный класс webrtc::AudioDeviceModule. Тут уже пришлось "расковыривать" исходники Pixel Streaming. По итогу оттуда я взял некий "legacy" класс AudioCapturer к которому прикрутил сбоку FAudioPlayoutRequester часть из PixelStreamingAudioDeviceModule. Получился некий франкенштейн, суть которого на текущий момент проста - после усшешного обмена Ice кандидатами и установки медийной сессии (кодек, к слову, OPUS - тоже стандартный для видеоконференций), опрашивать раз в 10мс WebRTC стек (вызовом this → AudioCallback → NeedMorePlayData() ) и полученные данные передавать в USynthComponent, чтобы он потом их в UE4 отдал. Решение далеко не идеально, но, по крайней мере, появился разборчивый звук и UE4 перестал крашиться по обращениям по недопустимому адресу и т.п.
Заключение
В итоге получился некий "обрезанный" клиент для системы конференций, позволяющий немного оживить ежедневные митинги. Всё это случилось исключительно благодаря использованию "стандартных" технологий на сервере и готовой поддержкой этих технологий в UE4.
Конечно, это просто игрушка и ни в коем случае не может быть продуктом ибо даже митинги в Хроме (другие браузеры обычно не поддерживаются) работают с проблемами, съедают батарейку и требуют технической поддержки, что уж говорить о клиенте в виде UE4 приложения - он вообще врятли запустится не у того, кто его собирал =)
Так же "интерактивность" должна быть одинаковой в любом клиенте, по этому пока что она ограничена "подниманием руки" (над манекеном всплывает восклицательный знак, делать анимации было лень). Самый простой способ обойти это ограничение (сделать интерактив без поддержки на сервере) - передавать команды через чат. Как демо реализована возможность менять позу манекена.
Реализуя этот проект я лучше познакомился с работой видеоконференций вообще и WebRTC стеком в частности, что было мне необходимо по работе. Так что время потрачено не зря.
Спасибо.