Как стать автором
Обновить

Пишем плагин для GStreamer на MS Visual Studio

Время на прочтение 9 мин
Количество просмотров 15K
Меня всегда интересовали прикладные задачи обработки видеоданных в реальном времени. На Хабре я прочитал серию статей о мультимедиа фреймвоке GStreamer:

Очень захотелось что-нибудь сделать с его использованием. Но, как обычно бывает, текущие задачи полностью исчерпывали ресурс свободного времени.

И вот однажды, в процессе работы над проектом, мне понадобилось организовать на объекте систему видеонаблюдения и интегрировать ее в систему учета. Эта задача была успешно решена специалистам нашей компанией и не достойна внимания широкой общественности. Система учета работает на Microsoft .NET, все камеры выдают H264 RTSP поток. Для захвата видео используется коммерческая библиотека MediaSuite от Streamcoders.

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

Итак, дано:
  • RTSP H264 источник
  • Система Windows 7 32-bit
  • MS Visual Studio 2010
  • Язык С++
  • GStreamer 1.0

Необходимо получить:
  • поток видео, содержащий результаты обработки

Способ решения:
  • Разработка плагина для GStreamer 1.0

GStreamer предоставляет разработчику свою иерархию классов. Для наших целей мы можем унаследовать свой плагин из класса GstElement. GstElement является базовым абстрактным классом, необходимым для разработки элемента, который может быть использован в обработке потоков GStreamer.

Установка GStreamer

Загружаем и устанавливаем GStreamer gstreamer-1.0-ххх-1.2.4.msi. Его можно взять здесь.
Для разработки, нам также понадобится дистрибутив gstreamer-1.0-devel-xxx.msi например, gstreamer-1.0-devel-x86-1.2.4.msi, В процессе установки выбираем необходимые опции:



Забегая вперед, скажу, лучше будет установить Windows Device Driver Kit 7.1.0. По-умолчанию он ставится в C:\WinDDK\7600.16385.1 и именно там его будет искать Visual Studio при построении проекта. Если у вас уже установлен DDK по другому пути, это можно будет поправить потом, непосредственно в настройках проекта.

На сайте проекта GStreamer среди прочей документации есть руководство для разработчиков плагинов. Можно прочитав руководство написать весь код самому, но есть возможность скачать шаблон проекта плагина, из репозитория.

Шаблон содержит исходные файлы с++ и скрипт для генерации кода плагина. Как гласит инструкция, после развертывания шаблона из репозитория, необходимо перейти в директорий gst-template/gst-plugin/src и запустить утилиту ../tools/make_element. Утилита make_element имеет два параметра: имя плагина (dummy), имя исходного файла, который будет использован (gstplugin по-умолчанию).

В результате выполнения мы получим два файла: gstdummy.c и gstdummy.h. Внутри будет скелет плагина dummy, который еще глупый и нечего не делает, но уже может быть встроен в систему плагинов фреймвока.

Небольшая ремарка: все, что сказано выше, справедливо для Linux, Unix машин, а как быть скорбным обладателям Windows? Cmd.exe не станет выполнять make_element. Если заглянуть внутрь make_element станет ясно, что ничего сложного он не делает, а с помощью потокового редактора sed производит генерацию целевых исходников на основании данных ему параметров. Это можно сделать и самому. На всякий случай, я создал репозиторий, куда по ходу развития буду помещать свой тестовый проект: github.com/nostrum-service/gst.

После того, как мы проделали предварительную работу, настал черед формирования проекта непосредственно в MS Visual Studio 2010. К счастью, разработчики GStreamer позаботились о пользователях Visual Studio и поместили в дистрибутив все необходимое для создания проекта. Надо только правильно разместить файлы в каталогах Visual Studio.
Все необходимое лежит в директории gstreamer\1.0\x86\share\vs\2010.
Выполняем:
xcopy c:\gstreamer\1.0\x86\share\vs\2010\gst-template\*.* "C:\Program Files\Microsoft Visual Studio 10.0\VC\VCWizards\gst-template\*.*" /s /e /c
xcopy C:\gstreamer\1.0\x86\share\vs\2010\wizard\*.* "C:\Program Files\Microsoft Visual Studio 10.0\VC\vcprojects\"

Запускаем Visual Studio (или перезапускаем, чтобы она увидела новые настройки), создаем новый проект, и выбираем из установленных шаблонов Visual C++\gst-dk-template.



Если все прошло нормально, создастся пустой проект с необходимыми настройками. Поскольку, мы хотим создать плагин, идем в настройки проекта и меняем в Project Details Configuration Type с Application (.exe) на Dynamic Library (.dll).



В окне Property Manager наблюдаем следующую картину (включить Property Manager можно View->Other Windows->Property Manager):



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



Компилируем, если все правильно, получаем готовый DLL, который надо скопировать в каталог плагинов GStreamer (у меня — C:\gstreamer\1.0\x86\lib\gstreamer-1.0\).
На всякий случай проверим: gst-inspect-1.0 dummy. Здесь мы увидим, что узнал GStreamer о нашем плагине.



Минимальный набор функций плагина

Для того чтобы встроить плагин в цепочку, нам необходимо реализовать следующий жизненный цикл:
  • сообщить фреймвоку данные о себе (инициализировать класс)
  • произвести инициализацию экземпляра плагина
  • обработать процесс подключения потока
  • сделать что-то с входными данными и передать их на выход
  • по завершению очистить занятые ресурсы

Метаданные

За предоставление метаданных о плагине у нас отвечает функция
static void gst_dummy_class_init (GstdummyClass * klass)

В нашем примере элемент dummy имеет свойство Silent типа Boolean, отвечающее за вывод текста при обработке потока.

  gobject_class->set_property = gst_dummy_set_property;
  gobject_class->get_property = gst_dummy_get_property;

  g_object_class_install_property (gobject_class, PROP_SILENT,
      g_param_spec_boolean ("silent", "Silent", "Produce verbose output ?",
          FALSE, (GParamFlags)G_PARAM_READWRITE));

Этим кодом мы сообщаем среде GStreamer о том, что у плагина есть свойство Silent, оно имеет тип Boolean, за его установку отвечает делегат gst_dummy_set_property, чтение — gst_dummy_get_property, оно доступно по чтению и записи, значение по-умолчанию – FALSE. Далее мы регистрируем точки подключения к плагину – pads.

  gst_element_class_add_pad_template (gstelement_class,
      gst_static_pad_template_get (&src_factory));
  gst_element_class_add_pad_template (gstelement_class,
      gst_static_pad_template_get (&sink_factory));

Определяем входной pad sink, который имеется всегда в наличие — GST_PAD_ALWAYS и принимает любой формат GST_STATIC_CAPS («ANY»).
static GstStaticPadTemplate sink_factory = GST_STATIC_PAD_TEMPLATE ("sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("ANY")
    );

Определяем выходной pad src, который имеется всегда в наличие — GST_PAD_ALWAYS и выдает любой формат GST_STATIC_CAPS («ANY»).
static GstStaticPadTemplate src_factory = GST_STATIC_PAD_TEMPLATE ("src",
    GST_PAD_SRC,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("ANY")
    );

Инициализация экземпляра

В процессе построения pipeline, среда GStreamer вызывает функцию инициализации экземпляра плагина:
static void
gst_dummy_init (Gstdummy * filter)

на вход которой подается структура, которую необходимо заполнить:
struct _Gstdummy
{
  GstElement element;
  GstPad *sinkpad, *srcpad;
  gboolean silent;
};

Для того, чтобы обрабатывать события, происходящие на входе плагина, необходимо указать соответствующий делегат:
gst_pad_set_event_function (filter->sinkpad, GST_DEBUG_FUNCPTR(gst_dummy_sink_event));

Подключение к потоку

При возникновении событий на входе плагина, в нашем случае будет вызвана функция:
static gboolean
gst_dummy_sink_event (GstPad * pad, GstObject * parent, GstEvent * event)

которая пока ничего не делает.

Пробный запуск

Теперь можно попробовать запустить — gst-launch-1.0 videotestsrc! dummy! autovideosink –v. Данная команда передает тестовый поток видео сгенерированный videotestsrc на наш плагин, который передает его дальше без изменений на видео проигрыватель autovideosink. Ключ v позволяет увидеть, как происходит обработка всей цепочки.



Если мы увидели тестовую картинку – значит наш плагин успешно передал данные со своего входа на выход.
Для нашего случая, вывод обработки будет содержать следующее:
/GstPipeline:pipeline0/GstVideoTestSrc:videotestsrc0.GstPad:src: caps = video/x-raw, format=(string)I420, width=(int)320, height=(int)240, framerate=(fraction)30/1, pixel-aspect-ratio=(fraction)1/1, interlace-mode=(string)progressive

Из этого следует, что экземпляр класса GstVideoTestSrc с именем videotestsrc0 предоставил pad src, выдающий поток video/x-raw в формате I420 размерами кадра 320 на 240 и т.д.
Поскольку наш плагин может принять любой формат, его вход связался с выходом videotestsrc0:
/GstPipeline:pipeline0/Gstdummy:dummy0.GstPad:sink: caps = video/x-raw, format=(string)I420, width=(int)320, height=(int)240, framerate=(fraction)30/1, pixel-aspect-ratio=(fraction)1/1, interlace-mode=(string)progressive

Дальнейшее погружение

Выяснив, что механика работает, попробуем сделать несколько шагов к нашей цели. Чтобы проиллюстрировать эволюцию решения, я добавил в проект еще один плагин painter, который будет рисовать поверх изображения прямоугольник.
Прямоугольник можно рисовать без привлечения каких-либо библиотек, но мне захотелось продолжить эксперименты с использованием OpenCV.

При добавлении нового плагина возникает проблема его регистрации, т.к. регистратор должен быть один на проект. В связи с этим, был введен файл GstTestLib.cpp, в котором размещен код регистрации обоих плагинов.
struct _elements_entry
{
  const gchar *name;
    GType (*type) (void);
};

static const struct _elements_entry _elements[] = {
  {"dummy", gst_dummy_get_type},
  {"painter", gst_painter_get_type},
  {NULL, 0},
};

static gboolean
plugin_init (GstPlugin * plugin)
{
  gint i = 0;

  while (_elements[i].name) {
    if (!gst_element_register (plugin, _elements[i].name,
            GST_RANK_NONE, (_elements[i].type) ()))
      return FALSE;
    i++;
  }

  return TRUE;
}

Для painter я создал более жесткие ограничения на входной формат потока. Теперь это выглядит следующим образом:
static GstStaticPadTemplate sink_factory = GST_STATIC_PAD_TEMPLATE ("sink",
	GST_PAD_SINK,
	GST_PAD_ALWAYS,
	GST_STATIC_CAPS ( GST_VIDEO_CAPS_MAKE ("{ BGRx }") )
	);

Это значит, что на вход может подаваться потоковое видео в формате BGRx с любым разрешением и частотой кадров. OpenCV по-умолчанию использует схему BGR. Про цветовые схемы и преобразования можно почитать здесь.
Поскольку, интересующая меня камера выдает RTSP H264 поток, нам необходимо его раскодировать и подать на вход преобразователя videoconvert. Наш тестовый пример будет выглядеть следующим образом:
gst-launch-1.0 -v rtspsrc location=rtsp://10.10.0.15 ! rtph264depay ! avdec_h264 ! videoconvert ! painter ! videoconvert ! autovideosink

(10.10.0.15 – мой внутренний адрес, взят для примера).
Теперь на входе буфер у нас будет в BGRx формате. Если вы не знаете, как построить pipeline, можно воспользоваться универсальным контейнером decodebin. Он попытается сам подобрать и связать подходящие плагины:
gst-launch-1.0 -v rtspsrc location=rtsp://10.10.0.15 ! decodebin ! autovideosink

ключ –v необходим для диагностического вывода, там и будет видно как сформирован pipeline.

Вернемся к нашему плагину. Вся наша обработка будет заключаться в формировании нового изображения на основе входного и отрисовке поверх него прямоугольника. Рабочая функция имеет вид:
static GstBuffer *
	gst_painter_process_data (Gstpainter * filter, GstBuffer * buf)
{
	// объединяем все части буфера в непрерывную область для чтения
	GstMapInfo srcmapinfo;
	gst_buffer_map (buf, &srcmapinfo, GST_MAP_READ);

	// формируем новый буфер
	GstBuffer * outbuf = gst_buffer_new ();
	
	// создаем заголовок на исходное изображение
	IplImage * dst = cvCreateImageHeader (cvSize (filter->width, filter->height), IPL_DEPTH_8U, 4);

	// выделяем память для нового изображения
	GstMemory * memory = gst_allocator_alloc (NULL, dst->imageSize, NULL);
	GstMapInfo dstmapinfo;
	if (gst_memory_map(memory, &dstmapinfo, GST_MAP_WRITE)) {

		// копируем исходное изображение в новую область памяти
		memcpy (dstmapinfo.data, srcmapinfo.data, srcmapinfo.size);
		dst->imageData = (char*)dstmapinfo.data;

		// рисуем прямоугольник
		cvRectangle (dst, cvPoint(10,10), cvPoint(100, 100), CV_RGB(0, 255, 0), 1, 0);

		// добавляем память с модифицированным изображением в выходной буфер
		gst_buffer_insert_memory (outbuf, -1, memory);

		gst_memory_unmap(memory, &dstmapinfo);
	}

	cvReleaseImageHeader(&dst);
	
	gst_buffer_unmap(buf, &srcmapinfo);

	return outbuf;
}


В обработкчике событий от sink pad мы можем узнать информацию о входном потоке
static gboolean
	gst_painter_sink_event (GstPad * pad, GstObject * parent, GstEvent * event)
{
	gboolean ret;
	Gstpainter *filter;

	filter = GST_PAINTER (parent);

	switch (GST_EVENT_TYPE (event)) {
		case GST_EVENT_CAPS:
			{
				GstCaps * caps;

				gst_event_parse_caps (event, &caps);

				//получаем структуру
				GstStructure *structure = gst_caps_get_structure (caps, 0);

				//читаем параметы
				gst_structure_get_int (structure, "width", &filter->width);
				gst_structure_get_int (structure, "height", &filter->height);

				filter->format = gst_structure_get_string (structure, "format");

				ret = gst_pad_event_default (pad, parent, event);
				break;
			}
	default:
		ret = gst_pad_event_default (pad, parent, event);
		break;
	}
	return ret;
}

Задача решена в лоб. Эксперимент удался. По мере осовения GStreamer, открываются новые возможности, о которых по мере моего продвижения и наличия интереса к теме я буду рассказывать дальше.

Источники

GStreamer
иерархия объектов GStreamer
руководство по разработке плагина GStreamer
OpenCV
репозиторий шаблона плагина
мои исходники
Теги:
Хабы:
+19
Комментарии 0
Комментарии Комментировать

Публикации

Истории

Работа

Программист C++
121 вакансия
QT разработчик
13 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн