Пандемия COVID-19 стала катализатором появления новых онлайн-сервисов. Например, Zoom стал настолько успешным, что в этом месяце обогнал IBM по капитализации. Программные инженеры Promwad были вдохновлены этим успехом и решили пойти еще дальше: как насчет реализации видеоконференцсвязи на Smart TV и STB? Тогда у пользователей такого приложения будет возможность не только общаться на работе, но и наслаждаться удаленными встречами с друзьями, болеть за футбольную команду, вместе смотреть фильм или заниматься спортом с тренером.
Почему-то у большинства операторов цифрового ТВ нет такой услуги, хотя с инженерной точки зрения все эти функции могут быть реализованы на приставках на базе Linux/Android и RDK.
Давайте проанализируем архитектуру Zoom-подобного приложения для видеоконференцсвязи для Smart TV и реализуем кодирование видеопотока с помощью мультимедийной платформы с открытым исходным кодом GStreamer. Мы собирали информацию для работы с этим фреймворком по частям, но оно того стоило.
Проблемы и архитектура программного обеспечения
При разработке архитектуры приложений для телевизионных приставок важно учитывать все возможные программные и аппаратные ограничения. Инженеры, создающие программы для настольных ПК, как правило, не сталкиваются с этими проблемами, в то время как для разработчиков встраиваемых систем это обычное дело.
Итак, что нас ждет на приставках:
- Ограниченные ресурсы процессора и самих устройств. В большинстве случаев устройства STB используют различные процессоры ARM, что подразумевает несколько ограничений и дополнительных задач, таких как необходимость использования аппаратного кодирования/декодирования для видеопотока. Итак, производительность — «узкое место».
- Разные архитектуры в приставках разных производителей. Некоторые из них основаны на Android; другие используют дистрибутивы на основе RDK или Linux с их ограничениями и нюансами. Поэтому на старте процесса разработки лучше выбрать наиболее распространенные и кроссплатформенные решения в разных программных модулях. Не говоря уже о поддержке десктоп-версии. А затем перейдем к частным случаям.
- Сетевые ограничения. Многие приставки работают как через Ethernet, так и через Wi-Fi. Сжатие и передача видео/аудиопотоков — еще одно «узкое место» в приложениях такого типа.
- Безопасность потоковой передачи и другие вопросы безопасности данных.
- Поддержка камер и микрофонов на встраиваемых платформах.
Теперь мы можем кластеризовать саму архитектуру. Наше приложение для видеоконференцсвязи для приставок будет состоять из нескольких крупных компонентов и модулей:
- Захват видеопотока
- Захват аудиопотока
- Сетевой модуль
- Модуль кодирования видео / аудиопотока
- Модуль декодирования видео / аудиопотока
- Отображение видеоконференции на экране
- Вывод звука
- Цветовые преобразования
- Несколько других второстепенных компонентов
В упрощенном виде архитектуру можно изобразить следующим образом:
В этом обзоре мы сосредоточимся на декодировании/кодировании видеопотока и возможной реализации с помощью фреймворка GStreamer, поскольку это один из критических моментов в разработке приложений для видеоконференцсвязи.
Кодирование и декодирование аудио/видеопотока
Преимущества GStreamer для видеоконференцсвязи
Как мы уже отмечали, потоковое видео — одно из «узких мест». Предположим, у вас есть камера, которая выводит кадры со скоростью 30 кадров в секунду при небольшом разрешении 640 × 480. В сумме получается в RGB24: 640 * 480 * 3 * 30 = 27 648 000 байт в секунду, то есть более 26 мегабайт в секунду, что не работает по очевидным причинам, в частности, по пропускной способности сети.
Одно из решений — реализовать кодирование видео с помощью какой-нибудь библиотеки. Как можно догадаться из названия обзора, наш выбор пал на фреймворк GStreamer. Почему именно эта библиотека? Вот некоторые из его преимуществ перед другими решениями:
- Хорошее кроссплатформенное решение с отличной поддержкой Linux и Android.
- В RDK Gstreamer — это стандарт кодирования/декодирования, включенный в дистрибутив по умолчанию.
- Он поддерживает широкий спектр модулей, фильтров и кодеков. Например, FFmpeg, который можно использовать для тех же целей, является одним из модулей GStreamer.
- Легко построить конвейер. Легко создать цепочку кодирования/декодирования, а конвейерный подход позволяет плавно заменять кодеки, фильтры и т. д. без необходимости переписывать код.
- C/C++ API включен.
- Поддержка аппаратных кодеров/декодеров, в частности OpenMAX API — важная вещь для работы с приставками.
Изучение GStreamer и конвейеров
Прежде чем переходить к обзору кода, давайте посмотрим, что мы можем без этого сделать. GStreamer включает полезные утилиты для работы, в частности:
- gst-inspect-1.0 позволит вам увидеть список доступных кодеков и модулей, чтобы вы могли сразу увидеть, что с ним делать, и выбрать набор фильтров и кодеков.
- gst-launch-1.0 позволяет запускать любой конвейер.
GStreamer использует схему декодирования, при которой поток последовательно проходит через различные компоненты, от источника к выходу приемника. Вы можете выбрать что угодно в качестве источника: файл, устройство, выход (приемник), также может быть файл, экран, сетевые выходы и протоколы (например, RTP).
Классический пример воспроизведения файла mp4:
gst-launch-1.0 filesrc location=file.mp4 ! qtdemux ! h264parse ! avdec_h264 ! videoconvert ! autovideosink
Вход принимает файл mp4, который проходит через демультиплексор mp4 — qtdemux, затем через синтаксический анализатор h264, затем через декодер, конвертер и, наконец, вывод.
Вы можете заменить autovideosink на filesink с параметром файла и вывести декодированный поток непосредственно в файл.
Программирование приложения с помощью GStreamer C/C ++ API. Попробуем расшифровать.
Теперь, когда мы знаем, как использовать gst-launch-1.0, мы делаем то же самое в нашем приложении. Принцип остается прежним: мы строим конвейер декодирования, но теперь мы используем библиотеку GStreamer и glib-events.
Мы рассмотрим живой пример декодирования H264.
Инициализация приложения GStreamer происходит один раз с помощью
gst_init (NULL, NULL);
Если вы хотите подробно узнать, что происходит, вы можете настроить уровень ведения журнала перед инициализацией.
gst_debug_set_active(TRUE);
gst_debug_set_default_threshold(GST_LEVEL_LOG);
Примечание: независимо от того, сколько конвейеров у вас в приложении, достаточно один раз инициализировать gst_init.
Давайте создадим новый цикл событий, в котором будут обрабатываться события:
GMainLoop *loop;
loop = g_main_loop_new (NULL, FALSE);
И теперь мы можем приступить к созданию нашего конвейера. Назовем необходимые элементы, в частности, сам конвейер GstElement:
GstElement *pipeline, *source, *demuxer, *parser, *decoder, *conv, *sink;
pipeline = gst_pipeline_new ("video-decoder");
source = gst_element_factory_make ("filesrc", "file-source");
demuxer = gst_element_factory_make ("qtdemux", "h264-demuxer");
parser = gst_element_factory_make ("h264parse", "h264-parser");
decoder = gst_element_factory_make ("avdec_h264", "h264-decoder");
conv = gst_element_factory_make ("videoconvert", "converter");
sink = gst_element_factory_make ("appsink", "video-output");
Каждый элемент конвейера создается через gst_element_factory_make, где первый параметр — это тип, а второй — его условное имя для GStreamer, на которое он позже будет полагаться (например, при выдаче ошибок).
Также было бы неплохо проверить, что все компоненты найдены, иначе gst_element_factory_make вернет NULL.
if (!pipeline || !source || !demuxer || !parser || !decoder || !conv || !sink) {
// one element is not initialized - stop
return;
}
Мы устанавливаем тот же параметр местоположения через g_object_set:
g_object_set (G_OBJECT (source), "location", argv[1], NULL);
Таким же образом можно установить другие параметры в других элементах.
Теперь нам нужен обработчик сообщений GStreamer, давайте создадим соответствующий bus_call:
GstBus *bus;
guint bus_watch_id;
bus = gst_pipeline_get_bus (GST_PIPELINE (pipeline));
bus_watch_id = gst_bus_add_watch (bus, bus_call, loop);
gst_object_unref (bus);
gst_object_unref и другие подобные вызовы необходимы для очистки выбранных объектов.
Затем назовем сам обработчик сообщений:
static gboolean
bus_call (GstBus *bus,
GstMessage *msg,
gpointer data)
{
GMainLoop *loop = (GMainLoop *) data;
switch (GST_MESSAGE_TYPE (msg)) {
case GST_MESSAGE_EOS:
LOGI ("End of stream\n");
g_main_loop_quit (loop);
break;
case GST_MESSAGE_ERROR: {
gchar *debug;
GError *error;
gst_message_parse_error (msg, &error, &debug);
g_free (debug);
LOGE ("Error: %s\n", error->message);
g_error_free (error);
g_main_loop_quit (loop);
break;
}
default:
break;
}
return TRUE;
}
А теперь самое главное: мы собираем и складываем все созданные элементы в единый конвейер, построенный через gst-launch. Конечно, важен порядок добавления:
gst_bin_add_many (GST_BIN (pipeline), source, demuxer, parser, decoder, conv, sink, NULL);
gst_element_link_many (source, demuxer, parser, decoder, conv, sink, NULL);
Также стоит отметить, что такое связывание элементов отлично работает для потокового вывода, но в случае воспроизведения (автосвязка) требует дополнительной синхронизации и динамического связывания демультиплексора и парсера:
gst_element_link (source, demuxer);
gst_element_link_many (parser, decoder, conv, sink, NULL);
g_signal_connect (demuxer, "pad-added", G_CALLBACK (on_pad_added), parser);
static void
on_pad_added (GstElement *element,
GstPad *pad,
gpointer data)
{
GstPad *sinkpad;
GstElement *decoder = (GstElement *) data;
/* We can now link this pad with the sink pad */
g_print ("Dynamic pad created, linking demuxer/decoder\n");
sinkpad = gst_element_get_static_pad (decoder, "sink");
gst_pad_link (pad, sinkpad);
gst_object_unref (sinkpad);
}
Динамическое соединение позволяет определять тип и количество потоков в отличие от статического и будет работать в некоторых случаях, когда это необходимо.
И, наконец, превратим статус конвейера в воспроизведение:
gst_element_set_state (pipeline, GST_STATE_PLAYING);
И давайте запустим цикл обработки событий:
g_main_loop_run (loop);
После этой процедуры нужно очистить все:
gst_element_set_state (pipeline, GST_STATE_NULL);
gst_object_unref (GST_OBJECT (pipeline));
g_source_remove (bus_watch_id);
g_main_loop_unref (loop);
Выбор кодеров и декодеров. Запасные варианты.
В документации можно подробнее рассказать о полезных, но редко упоминаемых вещах: как легко организовать резервный декодер или кодировщик.
В этом нам поможет функция gst_element_factory_find, проверив, есть ли у нас кодек в фабрике элементов:
if(gst_element_factory_find("omxh264dec"))
decoder = gst_element_factory_make ("omxh264dec", "h264-decoder");
else
decoder = gst_element_factory_make ("avdec_h264", "h264-decoder");
В этом примере мы отдали приоритет выбору аппаратного декодера OMX на платформе RDK, а в случае его отсутствия выберем программную реализацию.
Еще одна чрезвычайно полезная, но еще более редко используемая функция — это проверить, что мы фактически инициализировали в GstElement (какой из многих кодеков):
gst_plugin_feature_get_name(gst_element_get_factory(encoder))
Вы можете сделать это таким простым способом и вернуть имя инициализированного кодека.
Цветовые модели видео
Нельзя не упомянуть и цветовые модели, поскольку речь идет о кодировании видео с камер. И тогда на сцену выходит YUV (гораздо чаще, чем RGB).
Камерам просто нравится цветовая модель YUYV. Но GStreamer гораздо больше любит работать с обычной моделью I420. Если речь идет не о выводе в gl-кадре, у нас также будут кадры I420. Приготовьтесь настроить нужные фильтры и выполнить преобразования.
Некоторые кодировщики могут работать и с другими цветовыми моделями, но чаще всего это исключения из правил.
Также стоит отметить, что у GStreamer есть собственный модуль для приема видеопотоков с вашей камеры, и его можно использовать для построения конвейера, но мы поговорим об этом в другой раз.
Давайте разберемся с буферами и будем получать данные на лету
Входной буфер
Пришло время разобраться с потоками данных. До сих пор мы просто кодировали через filesrc то, что находится в файле, и отображали все в той же файловой ссылке или на экране.
Теперь мы будем работать с буферами и входами и выходами appsrc/appsink. Почему-то в официальной документации этот вопрос практически не рассматривался.
Так как же организовать постоянный поток данных в созданных конвейерах, а если точнее — в выходной буфер и получить закодированный или декодированный выходной буфер? Допустим, мы получили изображение с камеры и нам нужно его закодировать. Мы уже решили, что нам нужен кадр в формате I420. Допустим, у нас есть, что дальше? Как пропустить картинку через весь поток конвейера?
Во-первых, давайте настроим обработчик события need-data, который будет запускаться, когда необходимо передать данные в конвейер и начать загрузку входного буфера:
g_signal_connect (source, "need-data", G_CALLBACK (encoder_cb_need_data), NULL);
Сам обработчик имеет следующий вид:
encoder_cb_need_data (GstElement *appsrc,
guint unused_size,
gpointer user_data)
{
GstBuffer *buffer;
GstFlowReturn ret;
GstMapInfo map;
int size;
uint8_t* image;
// get image
buffer = gst_buffer_new_allocate (NULL, size, NULL);
gst_buffer_map (buffer, &map, GST_MAP_WRITE);
memcpy((guchar *)map.data, image, gst_buffer_get_size( buffer ) );
gst_buffer_unmap(buffer, &map);
g_signal_emit_by_name (appsrc, "push-buffer", buffer, &ret);
gst_buffer_unref(buffer);
}
Можно сказать, что «изображение» — это псевдокод нашего буфера изображений в I420.
Далее через gst_buffer_new_allocate создаем буфер необходимого размера, который будет соответствовать размеру буфера изображения.
С помощью gst_buffer_map мы устанавливаем буфер в режим записи и используем memcpy для копирования нашего изображения в созданный буфер.
И наконец, мы сигнализируем GStream, что буфер готов.
Примечание: важно использовать gst_buffer_unmap после записи и очищать буфер после использования gst_buffer_unref. Иначе будет утечка памяти. В небольшом количестве доступных примеров никто особо не беспокоился об использовании памяти, хотя это очень важно.
Теперь, когда мы закончили с обработчиком, еще одна вещь, которую нужно сделать, — это настроить ограничения при получении нашего ожидаемого формата.
Это делается перед установкой обработчика сигнала need-data:
g_object_set (G_OBJECT (source),
"stream-type", 0,
"format", GST_FORMAT_TIME, NULL);
g_object_set (G_OBJECT (source), "caps",
gst_caps_new_simple ("video/x-raw",
"format", G_TYPE_STRING, "I420",
"width", G_TYPE_INT, 640,
"height", G_TYPE_INT, 480,
"framerate", GST_TYPE_FRACTION, 30, 1,
NULL),
NULL);
Как и все параметры GstElement, параметры устанавливаются через g_object_set.
В данном случае мы определили тип потока — формат данных. Мы указываем, что вывод appsrc будет получать данные I420 с разрешением 640 × 480 и 30 кадрами в секунду.
Частота в нашем случае, да и вообще, никакой роли не играет. Во время работы мы не заметили, что GStreamer каким-то образом ограничивает вызовы необходимых данных по частоте.
Готово, теперь наши кадры загружены в кодировщик.
Выходной буфер
Теперь давайте узнаем, как получить закодированный выходной поток.
Подключаем обработчик к устройству вывода:
GstPad *pad = gst_element_get_static_pad (sink, "sink");
gst_pad_add_probe (pad, GST_PAD_PROBE_TYPE_BUFFER, encoder_cb_have_data, NULL, NULL);
gst_object_unref (pad);
Точно так же мы подключились к другому устройству вывода, GST_PAD_PROBE_TYPE_BUFFER, которое будет запускаться, когда буфер данных войдет в панель приемника.
static GstPadProbeReturn
encoder_cb_have_data (GstPad * pad,
GstPadProbeInfo * info,
gpointer user_data) {
GstBuffer *buf = gst_pad_probe_info_get_buffer (info);
GstMemory *bufMem = gst_buffer_get_memory(buf, 0);
GstMapInfo bufInfo;
gst_memory_map(bufMem, &bufInfo, GST_MAP_READ);
// bufInfo.data, bufInfo.size
gst_memory_unmap(bufMem, &bufInfo);
return GST_PAD_PROBE_OK;
}
Обратный вызов имеет аналогичную структуру. Теперь нам нужно добраться до буферной памяти. Сначала мы получаем GstBuffer, затем указатель его памяти с использованием gst_buffer_get_memory по индексу 0 (как правило, он единственный задействован). Наконец, используя gst_memory_map, мы получаем адрес буфера данных bufInfo.data и его размер bufInfo.size.
Фактически, мы достигли цели — получить буфер с закодированными данными и их размером. Итак, мы рассмотрели ключевые и наиболее интересные компоненты нашего Zoom-подобного приложения для видеоконференцсвязи для Smart TV и телевизионных приставок: архитектура, модули кодирования/декодирования с GStreamer, буферы ввода/вывода и используемые преобразования цвета.
Для операторов цифрового телевидения такая программная платформа может стать новой абонентской услугой. А для нас, инженеров, это новый интересный встраиваемый проект для реализации различных приставок на базе RDK, Linux и Android. Что касается всех остальных, это возможность вместе провести время, смотреть фильмы и спортивные матчи, заниматься спортом и встречаться с близкими во время карантина COVID-19 или удаленной работы.
Эта идея с услугой видеоконференцсвязи на Smart TV может получить дальнейшее развитие, как с точки зрения инженерных решений, так и с точки зрения бизнес-сценариев. Так что не стесняйтесь делиться своими мыслями в комментариях ниже.
Выражаем свою благодарность источнику из которого взята и переведена статья, сайту cnx-software.com.
Оригинал статьи вы можете прочитать здесь.
«…хотя с инженерной точки зрения все эти функции могут быть реализованы на приставках на базе Linux/Android и RDK.»
На Андроид приставку можно поставить Скайт и вести видеоконференцию.