GUI по-русски, или ВКС терминал своим руками
Опыт разработки GUI на С++ для российской системы видеоконференцсвязи (ВКС). Синтез современных технологий и требований сертификации. Главные «грабли» разработки и пути их обхода. Что общего у GUI и русского балета.
Первое, что видит пользователь ВКС-системы — это интерфейс. И в большинстве случаев именно по его внешнему виду и функционалу судят о системе. Неудобный или расползающийся интерфейс не позволит оценить ни высокую производительность системы, ни широкий функционал. Технически «красивая» система должна быть обернута в привлекательную и стабильно работающую оболочку. Поэтому при старте разработки отечественной ВКС системы этот момент был сразу учтен.
Кто будет пользователем российской ВКС?
С весны 2020 года ответ на вопрос о целесообразности разработки полноценной ВКС-системы стал очевиден. Для государственных ведомств, и коммерческих компаний, и больниц, и школ нужны современные средства связи с определенным уровнем производительности и защищенности. Пообщаться можно и в Zoom, но стоит ли его использовать для серьезных коммерческих переговоров или оперативного совещания?
Для задач государственной важности стало необходимо создание отечественной ВКС-системы. Причем системы, состоящей не только из программной составляющей, но и полноценной аппаратной части. Среди известных мировых вендоров как минимум 5 компаний предлагают многофункциональные системамы ВКС. Но в России концепция импортозамещения постепенно начинает работать. Плюс вопросы безопасности для многих стали важнее страны происхождения продукта, да и цена цена при нынешних курсах валют не на последнем месте. А «красоту» интерфейса оказалось вполне реально разработать с нуля.
GUI на старте
Главными требованиями к современным интерфейсам являются быстрота реализации, актуальный внешний вид и полноценное юзабилити. Таком образом первой задачей разработчиков графического пользовательского интерфейса (GUI) стало четкое определение функционала ПО для ВКС.
С точки зрения GUI требования были сформулированы следующие:
• Осуществление исходящих видео/аудио вызовов;
• Прием/отклонение входящих вызовов;
• Автоответ на входящий вызов через настраиваемый интервал времени;
• Переключение между двумя аудиоустройствами (гарнитура/ громкая связь) как во время, так и вне вызова;
• Включение/выключение микрофона и камеры как во время, так и вне вызова;
• DTMF-набор во время вызова;
• Сбор конференции на терминале;
• Управление PTZ-камерами, сохранение PTZ-пресетов и применение их;
• Возможность вывода видео в несколько различных окон;
• Управление мышью, клавиатурой, пультом дистанционного управления;
• Возможность удалённого управления терминалом из Web-интерфейса.
Такой список функций позволяет решать задачу разработки интерфейса различными способами. Причем на выбор конкретного типа реализации повлияли ограничения типа языков программирования (например, Java категорически не подходил по соображениям сертификации, CSS/HTML — по функционалу), специализация разработчиков и сроки. По совокупности выбор был сделан в пользу C++ и использования фрэймворка Qt5, так как, например, более современная технологя QML не позволяет рендерить видео с использованием произвольного OpenGL-контекста, а это было необходимо по ТЗ для терминалов ВКС.
Быстро и качественно
Первый прототип GUI создавался для софтфона на Qt и использовал множество opensource-библиотек. Например, для протокола SIP использовались библиотеки eXosip/oSIP, для кодирования/декодирования видео и аудио — ffmpeg, для работы с аудиоустройствами — PortAudio. Данный софтфон работал под Linux, Windows, MacOS и являлся демонстратором технологий, а не реальным устройством.
Позже абстрактный софтфон трансформировался в реальный проект видеотелефона, причем первая версия ПО для него должна была быть создана уже через 2 месяца после старта. Для решения этой задачи в столь сжатые сроки ПО телефона было разделено на модули и распределено между несколькими группами разработчиков в соответствии с компетенциями. Такая организация процесса помогла быстро и качественно развивать проект видеотелефона.
Core and Front
Для унификации и возможности использования уже имеющихся GUI-разработок в других устройствах из действующего проекта общую кодовую базу в отдельный модуль — бэкэнд GUI, или модуль GUI core. А непосредственно представления, которые различны у разных устройств реализовывать в отдельных модулях GUI front.
Такая архитектура GUI-модулей оказалась выигрышной и привела к нужному результату: разработка интерфейсов для новых компонентов собственно системы ВКС стала относительно быстрой и качественной. Ведь теперь интерфейсы для терминалов ВКС не нужно было переписывать с нуля.
Муки и победы
На пути создания любого ПО, естественно, встречаются свои сложности и проблемы. Создание GUI для ВКС не стало исключением. Вне зависимости от конкретного назначения системы они могут повторяться в любой команде. Трудности и победы на пути разработки интересны коллегам, и возможно, подскажут эффективные пути решения без наших «граблей».
Консистентность forever
Исторически самой первой интересной проблемой, которая возникла ещё при разработке GUI для различных типов терминалов ВКС, стала проблема консистентности, то есть согласованного состояния всех модулей. В процессе функционирования GUI взаимодействует с несколькими другими модулями: модулем взаимодействия с аппаратными средствами, подсистемой управления вызовами, модулем об работки медиа (MCU) и подсистемой взаимодействия с пользователем.
Изначально GUI работало со всеми этими модулями как с независимыми, то есть могло одновременно отправить запросы в 2 разных модуля. Это оказалось неправильным и иногда приводило к проблемам, так как сами эти модули не являлись независимыми и активно взаимодействовали между собой. Решением проблемы оказалось создание специальной схемы работы, при которой обеспечивалось строго последовательное выполнение запросов в рамках всех модулей.
Сложностей добавляло 2 момента: во-первых, некоторые (но не все) запросы требуют ответа, в ожидании которого терминал, по сути, находится в неконсистентном состоянии, поэтому другие запросы выполнять нельзя. Однако блокировать на время ожидания ответов пользовательский интерфейс тоже нежелательно. Во-вторых, ответы на запросы GUI от модулей, а также запросы со стороны модулей к GUI приходят в своих собственных потоках, отличных от GUI, но GUI должно все изменения своего состояния осуществлять в своем потоке (для некоторых действий этого требует Qt, а в некоторых случаях это позволяет избежать лишних сложностей при обеспечении синхронизации потоков).
Решение было найдено и состояло из двух частей. Сначала все запросы/ответы от других модулей перенаправлялись в GUI-поток с использованием механизма сигналов-слотов Qt в совокупности с QueuedConnection, то есть с использованием глобального цикла событий Qapplication. Затем для обеспечения последовательной обработки всех запросов была разработана система «переходов» (Transitions) с собственной очередью и циклом обработки (TransitionLoop).
Таким образом, когда пользователь нажимает какую-нибудь кнопку действия в GUI (например, кнопку вызова), создается соответствующий Transition, который помещается в очередь переходов. После этого генерируется сигнал для цикла обработки переходов. TransitionLoop, который, получив сигнал, смотрит, есть ли сейчас какой-то переход в состоянии выполнения. Если есть, то продолжается ожидание завершения текущего перехода; если нет, то из очереди переходов извлекается следующий Transition и запускается на выполнение. При получении ответа от другого модуля TransitionLoop с помощью такого же сигнала извещается о завершении текущего перехода и TransitionLoop может запустить следующий переход из очереди.
Важным здесь является то, что вся обработка переходов осуществляется в GUI-потоке. Это обеспечивается применением механизма сигналов-слотов Qt в варианте QueuedConnection, при котором для каждого сигнала создаётся событие, помещающееся в главный EventLoop приложения.
OpenGL-рендеринг на маломощном железе
Еще одно трудностью, с которой пришлось столкнуться, стала проблема рендеринга видео. Qt предоставляет для OpenGL-рендеринга специальный класс QOpenGLWidget и соответствующие вспомогательные классы, который изначально и был использован для рендеринга видео. Сами данные для рендеринга (декодированные кадры видео) предоставляет модуль обработки медиа (MCU), который, в том числе реализует аппаратное декодирование видеопотока (на GPU). На маломощных процессорах было обнаружено «подтормаживание» рендеринга FullHD-видео. Прямым решением была замена процессора, но это потребовало бы серьезной переработки уже готовых компонентов системы ВКС и повысило бы стоимость самих устройств. Поэтому был весь процесс рендеринга был тщательно проанализирован для поиска более красивых путей решения проблемы.
При стандартном OpenGL-рендеринге и аппаратном декодировании происходит следующее: из сети приходят данные с закодированным видео, они сохраняются в оперативной памяти, затем эти данные из оперативной памяти переносятся в видеопамять на GPU, там они декодируются. Затем декодированные данные, имеющие существенно больший объём, чем закодированные, переносятся опять в оперативную память. Далее в работу вступает код рендеринга, который эти данные переносит из оперативной памяти обратно в GPU непосредственно для рендеринга. Таким образом по шине памяти туда-сюда перекачиваются довольно большие объемы данных, и просто-напросто шина с этим не справляется.
В современных версиях OpenGL есть специальные расширения, позволяющие указать для рендеринга данные, уже находящиеся в памяти GPU, а не данные в основной оперативной памяти, как обычно. Этот механизм исключал перемещение данных аппаратно декодированных кадров из GPU в оперативную память, а потом обратно. Таким образом проблема рендеринга на маломощных процессорах была почти решена.
Другой серьезной проблемой стали OpenGL-контексты, поддерживаемые в Qt. Они не позволяют использовать необходимое расширение OpenGL, то есть использовать QOpenGLWidget при таком варианте нельзя. Решением стало применение обычного Qwidget с выключенным из конвейера рендерином Qt. Такая возможность в Qt существует. Однако и здесь возник вопрос, ведь в таком варианте за всю отрисовку в области данного виджета полностью отвечает GUI, Qt нам ничем не помогает. Для отображения видео это нормально, но для отображения виджетов поверх видео штатные средства Qt использовать невозможно, так как поверх видео необходимо отображать, например, дополнительное всплывающее меню.
Эта проблема была решена так: для получения из существующего виджета его изображения (для этого у QWidget есть метод grab ()), само изображение можно переводить в текстуру OpenGL и рендерить полученную текстуру поверх видео средствами OpenGL. Добавив соответствующее окружение, был реализован универсальный механизм, который можно использовать для отображения поверх видео любых стандартных виджетов таким нестандартным способом.
«Киоски» и виджеты
Непростой была и задача по менеджменту дисплеев и распределению по ним фрагментов пользовательского интерфейса в режиме «киоска». ВКС терминал может работать в 2-х режимах — оконном, то есть как любое другое оконное приложение в среде рабочего стола операционной системы и «режиме киоска» (то есть в операционной системе запущено только одно приложение с графическим интерфейсом — ВКСТ — и отсутствует среда рабочего стола).
В оконном режиме все относительно просто: окнами управляет оконный менеджер среды рабочего стола, приложение при необходимости создает второе окно, а пользователь разносит окна по дисплеям так, как ему нужно. А вот в режиме «киоска» все куда сложнее, так как в системе нет менеджера окон и окно может быть только одно, и пользователь не имеет возможности его перемещать. Поэтому появилась задача автоматического детектирования события, например, подключение/отключение дисплея. При наступлении этого события необходимо было автоматически конфигурировать дисплеи и правильно разместить на них фрагменты пользовательского интерфейса.
Ответ пришел со стороны системной библиотеки ОС LINUX Xrandr, отвечающщей за работу с дисплеями. Документации по ней в Интернете крайне мало, поэтому реализация шла с использованиеми примеров из сети Интернет, в том числе с Хабра. Кроме того, необходимо было придумать алгоритм распределения фрагментов интерфейса по дисплеям, а также встроить два различных окна в одно единственное. Последнее было реализовано следующим образом: то, что является окнами в оконном режиме, в режиме «киоска» является виджетами внутри одного большого окна, которое растягивается на 2 дисплея (если их 2). При этом необходимо сконфигурировать позиции дисплеев так, чтобы создавалось непрерывное виртуальное пространство (это осуществляется с помощью библиотеки XRandr), а потом задать геометрию внутренних виджетов внутри единственного глобального окна так, чтобы каждый попал на свой дисплей.
Создаем российское
Весь путь создания российской ВКС системы состоял и состоит из множества этапов, и GUI — это только верхушка айсберга. Самая заметная и не самая сложная. Однако комплексность решения, сочетание программных и программно-аппаратных компонентов, да и желание сделать технически и эстетически «красивую» систему создало немало трудностей на пути. Новые задачи породили нестандартные пути решения и помогли создать продукт, который не стыдно показать не только в России, но и за рубежом.
Российские разработки уже давно доказали свою работоспособность, а в красивой оболочке и конкурентоспособность. Наши лайфхаки будут полезны каждому, кто серьезно занимается разработкой GUI, и надеемся, помогут другим разработчикам ускорить и упростить процесс создания современных оболочек для новых российских софтверных продуктов. Мы верим, что российские решения будут ценится в мире не меньше русского балета или черной икры.