[IPTV, Видеоконференцсвязь, Разработка под Linux, Разработка под Android, Производство и разработка электроники] Как разработать аналог Zoom для ТВ-приставок на RDK и Linux. Разбираемся с фреймворком GStreamer

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
29-Сен-2020 17:35


Сценарии: как использовать приложение для видеоконференций на SmartTV и ТВ-приставкахПандемия COVID-19 стала катализатором для новых полезных сервисов. Например, Zoom стал настолько успешным, что по стоимости обогнал в этом месяце IBM. Нас вдохновил этот пример, и мы решили пойти еще дальше: а что если онлайн-конференции реализовать на приставках и Smart TV, чтобы общаться не только по работе, но устраивать удаленные посиделки на диване с друзьями? Но ведь тогда можно на футболе вместе покричать, и кино посмотреть или спортом заняться под контролем тренера. Почему-то у операторов цифрового ТВ такой услуги не оказалось, хотя с инженерной точки зрения все эти функции вполне можно реализовать на ТВ-приставках на базе Linux/Android и RDK. Мы это проверили на практике и вот теперь делимся с читателями Хабра своим рецептом создания «аналога Zoom» и видеоконференций через Smart TV. Разберем архитектуру решения и кодирование видеопотока с использованием GStreamer. Информацию для работы с этим фреймворком мы собирали по крупицам, но оно того стоило.Разбираемся с архитектуройПри разработке архитектуры приложений для ТВ-приставок важно учитывать потенциальные ограничения программной и аппаратной части этих устройств. Те, кто создает desktop-программы, как правило, не сталкиваются с этими сложностями, а для embedded-разработчиков это обычное дело.Итак, с чем мы столкнемся на ТВ-приставках:
  • Ограниченность ресурсов процессоров и самих приставок. Чаще всего в STB-устройствах используются  разнообразные ARM-процессоры, они несут в себя ряд ограничений и дополнительных задач, в частности необходимость использования аппаратного кодирования/декодирование видеопотока. Всегда нужно учитывать, что быстродействие — одно из узких мест.
  • Разность архитектур в приставках разных производителей. Некоторые базируются на Android, другие — на RDK, третьи — на дистрибутивах на базе Linux со своими ограничениями и нюансами. Поэтому на старте разработки лучше выбрать наиболее общие и кроссплатформенные решения в разных модулях программы. Уже не говоря о поддержке desktop-версии. А уже потом переходить к частным случаям.
  • Сетевые ограничения. Многие приставки работают как по Ethernet так и по wifi. Сжатие и передача видео/аудиопотока — еще одно узкое место в приложениях такого рода.
  • Безопасность передачи потока и др. вопросы безопасности данных.
  • Поддержка камер и микрофонов на встроенных платформах.
Теперь можно разложить по полочкам и саму архитектуру. Аналог Zoom для ТВ-приставок будет состоять из нескольких больших компонентов и модулей:
  • Захват видеопотока
  • Захват аудиопотока
  • Сетевой модуль
  • Модуль кодирования видео/аудиопотока
  • Модуль декодирования видео/аудиопотока
  • Вывод видеоконференции на экран
  • Вывод звука
  • Цветовые преобразования
  • Несколько других второстепенных компонентов
В упрощенном виде один из вариантов архитектуры можно изобразить так:
Архитектура приложения для видеоконференций через Smart TVВ рамках статьи мы сфокусируемся на кодировании декодирования видеопотока и на возможной реализации с использованием фреймворка GStreamer, т.к. это один из ключевых моментов в разработке приложений для видеоконференций.Кодирование/декодирование потока 1) Преимущества GStreamerКак мы уже отметили, одним из узких мест является передача видеопотока. Предположим у вас есть камера, отдающие кадры на скорости 30 кадров с секунду на небольшом разрешении 640x480. Итого, в RGB24 получается:640 х 480 х 3 х 30 = 27 648 000 байт в секунду, т.е. более 26 Мбайт в секунду, что по понятным причинам совершенно неприемлемо в рамках сетевой передачи.Один из вариантов решения — реализовать кодирование какой-нибудь библиотекой. И тут, как можно уже было догадаться из названия статьи, выбор пал на фреймворк GStreamer. Почему именно эта библиотека? Вот несколько ее преимуществ на фоне остальных решений:
  • Хорошая кроссплатформенность и отличная поддержка Linux и Android.
  • На RDK Gstreamer является стандартом кодирования/декодирования и включен в дистрибутив по-умолчанию.
  • Поддержках широкого набора модулей, фильтров и кодеков. Тот же FFmpeg, который можно использовать для тех же целей, де-факто является лишь одним из модулей GStreamer’а.
  • Простота построения конвейера (pipeline). Выстроить цепочку кодирования/декодирования не составляет большого труда, а сам подход конвейера позволяет легко заменять кодеки, фильтры и прочее без лишнего переписывания кода.
  • API для работы С/C++ идет комплекте.
  • Поддержка аппаратных кодеров/декодеров, в частности OpenMAX API — что крайне важно при работе с ТВ-приставками.
2) Знакомимся с GStreamer разбираемся с конвейерами Прежде чем приступать к рассмотрению кода, давайте разберемся с тем, что можно делать пока и без него.  В состав GStreamer входят полезные утилиты для работы, в частности:gst-inspect-1.0 позволит посмотреть список доступных кодеков и модулей, так что можно сразу увидеть, что будет работать, и выбрать набор фильтров и кодеков.gst-launch-1.0 позволяет запустить любой конвейер (pipeline).В GStreamer используется схема декодирования, по которой поток последовательно проходит через разные компоненты, начиная с source, заканчивая sink-выводом. При этом source может быть любым — файл, устройство,  для вывода (sink) также можно указать нужное — файл, экран, сетевые выводы и протоколы (наподобие RTP).Подробнее про использование утилит и принцип построения конвейера можно почитать в другой статье на Хабре. Классический пример проигрывания mp4-файла:gst-launch-1.0 filesrc location=file.mp4 ! qtdemux ! h264parse ! avdec_h264 ! videoconvert ! autovideosinkНа входе принимается mp4-файл, который проходит через демуксер mp4 — qtdemux, парсер h264, затем через декодер, конвертер и, наконец, идет вывод на экран.Можно заменить autovideosink на filesink с параметром файла и вывести декодированный поток прямиком в файл.3) Пишем приложение на GStreamer C/C++ API. Попробуем декодироватьТеперь, когда мы разобрались немного с использованием gst-launch-1.0, проделаем все тоже самое, только уже в рамках приложения. Принцип останется тем же: мы встраиваем декодирующий конвейер (pipeline), однако уже с использованием библиотеки GStreamer и glib-событий.О простейшем примере построения filesrc в filesink с кодом хорошо рассказано в другой хабрастатье — «GStreamer: элементы и контейнеры». Мы же рассмотрим чуть более реалистичный и сложный живой пример H264-декодирования.Инициализация GStreamer-приложения происходит один раз при помощиgstinit (NULL, NULL);Если вы хотите сразу видеть происходящее в деталях, можно выстроить уровень логирования перед инициализацией
gst_debug_set_active(TRUE);
gst_debug_set_default_threshold(GST_LEVEL_LOG);
Учтите: сколько бы у вас не было конвейеров в приложении, инициализировать gstinit достаточно один раз.Создадим новый event-loop, в котором будут обрабатываться события:
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) {
    // не инициализирован один элемент - остановка
    return;
}
Установим тот самый параметр location через gob_ject_set: gob_ject_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);
Стоит также заметить, что такой подход линковки элементов прекрасно работает для потоковых выходов, но в случае именно проигрывания (autovideosink) необходима дополнительная синхронизация и динамическая линковка демуксера и парсера:
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);И запускаем event-loop: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);
4) Выбираем энкодеры и декодеры. ФоллбэкиСтоит еще рассказать о полезных, но не особо упомянутых вещах в документации — о том, как можно легко организовать фоллбэк декодеров или энкодеров.Поможет нам в этом функция gst_element_factory_find, которая проверяет, есть ли у нас кодек в factory элементов:
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))
Можно проделать это таким незамысловатым способом и вернуть название инициализированного кодека.5) Цветовые модели видеоНе стоит оставлять без внимания и цветовые модели, раз уж мы заговорили о кодировании видео с камер. И тут чаще подразумевается YUV, нежели RGB.Камеры просто обожают цветовую модель YUYV. Но это то, с чем любит работать GStreamer, для него более привычна модель I420. На выходе, если речь не идет о выводе в gl-фрейм, у нас также будут выходить I420-кадры. Будьте готовы подставить необходимые фильтры и выполнить преобразования. Некоторые энкодеры умеют работать и с другими цветовыми моделями, но чаще это исключения из правил.Еще стоит заметить что у GStreamer’а есть свой модуль получения видеопотока с камеры, и его можно использовать для построения конвейера, но об этом расскажем как-нибудь в другой раз. Разбираемся с буферами и берем данные на лету 1) Буфер вводаПришло время  разобраться с потоками данных. До этого момента мы просто кодировали то, что есть в файле, через filesrc и выводили все в тот же filesink или на экран.Теперь будем работать с буферами и входами и выходами 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);
}
image — это, условно говоря, псевдокод буфера картинки в I420.Далее мы создаем через gst_buffer_new_allocate буфер необходимого размера, который будет соответствовать размеру буфера картинки.При помощи gst_buffer_map устанавливаем буфер в режим записи и, используя memcpy, копируем нашу картинку в созданный буфер. И, наконец, сигнализируем GStream’у о том, что буфер готов.Ремарка: очень важно после записи использовать gst_buffer_unmap, а также очищать буфер после использования при помощи gst_buffer_unref. Иначе будет утечка памяти. В скудном количестве доступных примеров никто особо насчет вопроса освобождения памяти не заморачивался, а он, понятное дело, важный.Теперь, когда мы закончили с обработчиком, нужно проделать еще одну вещь: настроить caps на поступление ожидаемого формата.Делается это перед установкой обработчика сигнала 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.В данном случае мы задали тип потока, и его caps — формат данных. Указав, что на выход appsrc будет поступать данные I420 c разрешением 640x480 и частотой 30 кадров в секунду.Частота в нашем случае, да в целом, не играет роли. На практике мы не замечали, чтобы GStreamer как-то ограничивал вызовы need-data по частоте.Готово, теперь наши кадры поступают в энкодер. 2) Буфер выводаА теперь разберемся, как получить кодированный поток на выходе.Подключаем обработчик к sink pad:
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);
Похожим образом мы подключились к другому событию sink pad — GST_PAD_PROBE_TYPE_BUFFER, — которое будет вызываться по мере поступления буфера данных в sink pad.
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.Фактически мы добились цели — получили буфер с кодированными данными и его размер.Итак, мы рассмотрели ключевые и самые интересные компоненты из приложения для Smart TV — аналога Zoom для ТВ-приставок: архитектуру, модули кодирования / декодирования через GStreamer, буферы ввода / вывода и используемые цветовые преобразования.Для операторов цифрового ТВ такая программная платформа может стать новым абонентским сервисом. Для нас — инженеров — новым интересным embedded-проектом для реализации на разных приставках на базе RDK, Linux и Android. А для всех остальных — возможностью приятно проводить время за совместным просмотром фильмов и спортивных матчей, занятий спортом и посиделок с близкими в период карантина или удаленной работы.Эту идею с сервисом видеоконференций по Smart TV можно развивать и дальше, как с точки зрения инженерных решений, так и по сценариям их использования. Так что делитесь мыслями в комментариях. 
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_iptv, #_videokonferentssvjaz (Видеоконференцсвязь), #_razrabotka_pod_linux (Разработка под Linux), #_razrabotka_pod_android (Разработка под Android), #_proizvodstvo_i_razrabotka_elektroniki (Производство и разработка электроники), #_tvpristavki (тв-приставки), #_tsifrovoe_tv (цифровое тв), #_konferentssvjaz (конференцсвязь), #_zoom, #_gstreamer, #_rdk, #_linux, #_promwad, #_android, #_cpp, #_iptv, #_videokonferentssvjaz (
Видеоконференцсвязь
)
, #_razrabotka_pod_linux (
Разработка под Linux
)
, #_razrabotka_pod_android (
Разработка под Android
)
, #_proizvodstvo_i_razrabotka_elektroniki (
Производство и разработка электроники
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 08:25
Часовой пояс: UTC + 5