Как разработать приложения видеоконференцсвязи на основе Gstreamer для приставок RDK и Linux


Пандемия COVID-19 стала катализатором появления новых онлайн-сервисов. Например, Zoom стал настолько успешным, что в этом месяце обогнал IBM по капитализации. Программные инженеры Promwad были вдохновлены этим успехом и решили пойти еще дальше: как насчет реализации видеоконференцсвязи на Smart TV и STB? Тогда у пользователей такого приложения будет возможность не только общаться на работе, но и наслаждаться удаленными встречами с друзьями, болеть за футбольную команду, вместе смотреть фильм или заниматься спортом с тренером.

Почему-то у большинства операторов цифрового ТВ нет такой услуги, хотя с инженерной точки зрения все эти функции могут быть реализованы на приставках на базе Linux/Android и RDK.

Давайте проанализируем архитектуру Zoom-подобного приложения для видеоконференцсвязи для Smart TV и реализуем кодирование видеопотока с помощью мультимедийной платформы с открытым исходным кодом  GStreamer. Мы собирали информацию для работы с этим фреймворком по частям, но оно того стоило.

Проблемы и архитектура программного обеспечения

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

Итак, что нас ждет на приставках:

  1. Ограниченные ресурсы процессора и самих устройств. В большинстве случаев устройства STB используют различные процессоры ARM, что подразумевает несколько ограничений и дополнительных задач, таких как необходимость использования аппаратного кодирования/декодирования для видеопотока. Итак, производительность — «узкое место».
  2. Разные архитектуры в приставках разных производителей. Некоторые из них основаны на Android; другие используют дистрибутивы на основе RDK или Linux с их ограничениями и нюансами. Поэтому на старте процесса разработки лучше выбрать наиболее распространенные и кроссплатформенные решения в разных программных модулях. Не говоря уже о поддержке десктоп-версии. А затем перейдем к частным случаям.
  3. Сетевые ограничения. Многие приставки работают как через Ethernet, так и через Wi-Fi. Сжатие и передача видео/аудиопотоков — еще одно «узкое место» в приложениях такого типа.
  4. Безопасность потоковой передачи и другие вопросы безопасности данных.
  5. Поддержка камер и микрофонов на встраиваемых платформах.

Теперь мы можем кластеризовать саму архитектуру. Наше приложение для видеоконференцсвязи для приставок будет состоять из нескольких крупных компонентов и модулей:

  • Захват видеопотока
  • Захват аудиопотока
  • Сетевой модуль
  • Модуль кодирования видео / аудиопотока
  • Модуль декодирования видео / аудиопотока
  • Отображение видеоконференции на экране
  • Вывод звука
  • Цветовые преобразования
  • Несколько других второстепенных компонентов

В упрощенном виде архитектуру можно изобразить следующим образом:

Архитектура приложения видеоконференцсвязи для Smart TV

В этом обзоре мы сосредоточимся на декодировании/кодировании видеопотока и возможной реализации с помощью фреймворка GStreamer, поскольку это один из критических моментов в разработке приложений для видеоконференцсвязи.

Кодирование и декодирование аудио/видеопотока

Преимущества GStreamer для видеоконференцсвязи


Как мы уже отмечали, потоковое видео — одно из «узких мест». Предположим, у вас есть камера, которая выводит кадры со скоростью 30 кадров в секунду при небольшом разрешении 640 × 480. В сумме получается в RGB24: 640 * 480 * 3 * 30 = 27 648 000 байт в секунду, то есть более 26 мегабайт в секунду, что не работает по очевидным причинам, в частности, по пропускной способности сети.

Одно из решений — реализовать кодирование видео с помощью какой-нибудь библиотеки. Как можно догадаться из названия обзора, наш выбор пал на фреймворк GStreamer. Почему именно эта библиотека? Вот некоторые из его преимуществ перед другими решениями:

  1. Хорошее кроссплатформенное решение с отличной поддержкой Linux и Android.
  2. В RDK Gstreamer — это стандарт кодирования/декодирования, включенный в дистрибутив по умолчанию.
  3. Он поддерживает широкий спектр модулей, фильтров и кодеков. Например, FFmpeg, который можно использовать для тех же целей, является одним из модулей GStreamer.
  4. Легко построить конвейер. Легко создать цепочку кодирования/декодирования, а конвейерный подход позволяет плавно заменять кодеки, фильтры и т. д. без необходимости переписывать код.
  5. C/C++ API включен.
  6. Поддержка аппаратных кодеров/декодеров, в частности 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.

Оригинал статьи вы можете прочитать здесь.

5 2 votes
Article Rating
Подписаться
Уведомление о
guest

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.

1 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
radiolis
28 дней назад

«…хотя с инженерной точки зрения все эти функции могут быть реализованы на приставках на базе Linux/Android и RDK.»
На Андроид приставку можно поставить Скайт и вести видеоконференцию.