Making a GStreamer Plugin with OpenCV (Part 1)
I decided to dust off the GStreamer toolbox and try doing something fancier than just passing buffers using OpenCV. To change it up, I also wanted to incorporate the code as a GStreamer plugin - that way it could be brought in to any pipeline with the gst factory method gst_element_factory_make("myfilter", "myfilter0"). I usually lean on the appsink/appsrc method and pass the buffers between the two blocks, but figured this would be a good excercise and would scale better.
The first thing I did was setup a docker image with the GStreamer and OpenCV development frameworks installed. I’ll make a post on how I built out the Dockerfiles another time.
So the sequence looked something like this:
- Setup a basic GStreamer pipeline using a GLib mainloop to generate a test video of a ball bouncing around and send it over UDP to my host computer.
- Setup a GStreamer plugin called
myfilterand add it to my pipeline inmain.cpp. To start it wouldn’t do anything, just pass the buffers from the sink pad to the src pad. - Add the OpenCV libraries to the plugin. This involves changing the Gstreamer boilerplate
meson.buildto compile withc++instead ofc. - In the plugin get the video frame out of the
GstBufferformat and intocv::Matso OpenCV can use it. Then I’ll have to extract the data from thecv::Matand put it back into theGstBufferso it can run through the remaining downstream GStreamer pipeline.
A Basic UDP Pipeline.
First thing is to get some code up to run a basic pipeline.
videotestsrc -> capsfilter -> myfilter -> videoconvert -> capsfilter -> x264enc -> rtph264pay -> udpsink
On my host OS I’ll have run pipeline to display the video.
udpsrc -> rtpjitterbuffer -> rtph264depay -> decodebin -> videoconvert -> glimagesink
Some notes:
- I threw in the capsfilters on either side of my plugin to enforce the video format. There’s probably a better way to do this in the plugin itself, but this was my first run at it.
- To make it easier on myself, I’ll set the caps on the first capsfilter to
BGRsince this plays really nice with open CV. The resolution is 320x240, so thecv::Matsize will be 320x240x3 to capture the three color channels for the video frames. - The videoconvert is necessary because the x264 encoder won’t work with
BGRand needs something likeI420. I’ll use the second capsfilter to enforce this and make sure the videoconvert block changes to the appropriate caps. - Originally the UDP video was really jittery and I found the rtpjitterbuffer really smoothed out the quality of the ball jumping around.
Let’s start with the meson.build file to compile a little main.cpp test harness.
Here’s the directory structure:
myapp
|- meson.build
|- src
|- main.cpp
project('main', 'c', 'cpp',
version : '0.0.1',
meson_version : '>= 0.54',
default_options : ['cpp_std=c++17', 'warning_level=1', 'buildtype=debugoptimized'])
gst_version = '1.20.3'
glib_dep = dependency('glib-2.0', version : '>= 2.56.0',
fallback: ['glib', 'libglib_dep'])
gst_dep = dependency('gstreamer-1.0', version : '>= 1.20',
required : true, fallback: ['gstreamer', 'gst_dep'])
gstbase_dep = dependency('gstreamer-base-1.0', version : '>= 1.20',
fallback: ['gstreamer', 'gst_base_dep'])
plugins_install_dir = join_paths(get_option('libdir'), 'gstreamer-1.0')
plugin_cpp_args = ['-DHAVE_CONFIG_H']
api_version = '1.0'
warning_flags = [
'-Wmissing-declarations',
'-Wmissing-prototypes',
'-Wredundant-decls',
'-Wundef',
'-Wwrite-strings',
'-Wformat',
'-Wformat-nonliteral',
'-Wformat-security',
'-Wold-style-definition',
'-Waggregate-return',
'-Winit-self',
'-Wmissing-include-dirs',
'-Waddress',
'-Wno-multichar',
'-Wdeclaration-after-statement',
'-Wvla',
'-Wpointer-arith',
]
cc = meson.get_compiler('cpp')
executable('main', 'src/main.cpp', dependencies : [glib_dep, gst_dep])That should be enough to make a little executable to build a test harness using src/main.cpp
#include "gst/gstelementfactory.h"
#include <glib.h>
#include <iostream>
#include <gst/gst.h>
int
main(int argc, char *argv[]) {
g_info("building gst pipeline");
static GMainLoop *gloop;
gloop = g_main_loop_new(NULL, FALSE);
gst_init(&argc, &argv);
gloop = g_main_loop_new(NULL, FALSE);
/* videotestsrc pattern=ball \
* ! x264enc bitrate=4000 \
* ! rtph264pay config-interval=1 \
* ! udpsink host=$1 port=$2
*/
// setup the GStreamer elements
GstElement* pipeline;
GstElement* src;
GstElement* capsfilter0;
GstElement* myfilter;
GstElement* videoconvert;
GstElement* capsfilter1;
GstElement* encoder;
GstElement* rtph;
GstElement* sink;
// run the factory make method
pipeline = gst_pipeline_new("pipeline0");
src = gst_element_factory_make("videotestsrc", "src0");
capsfilter0 = gst_element_factory_make("capsfilter", "capfilter0");
myfilter = gst_element_factory_make("identity", "myfilter0");
videoconvert = gst_element_factory_make("videoconvert", "videoconvert0");
capsfilter1 = gst_element_factory_make("capsfilter", "capfilter1");
encoder = gst_element_factory_make("x264enc", "encoder0");
rtph = gst_element_factory_make("rtph264pay", "rtph0");
sink = gst_element_factory_make("udpsink", "sink0");
// make sure the elements got created
if (!src) {
g_warning("error creating videotestsrc");
}
if (!capsfilter0) {
g_warning("error creating capsfilter");
}
if (!app) {
g_warning("error creating myfilter");
}
if (!videoconvert) {
g_warning("error creating videoconvert");
}
if (!capsfilter1) {
g_warning("error creating capsfilter1");
}
if (!encoder) {
g_warning("error creating x264enc");
}
if (!rtph) {
g_warning("error creating rtph264pay");
}
if (!sink) {
g_warning("error creating sink");
}
// set the caps for myfilter
g_object_set(G_OBJECT(capsfilter0), "caps",
gst_caps_new_simple("video/x-raw",
"format", G_TYPE_STRING, "BGR",
"width", G_TYPE_INT, 320,
"height", G_TYPE_INT, 240,
//"framerate", GST_TYPE_FRACTION, 15, 1,
NULL), NULL);
// set the caps for the x264enc for the videoconvert to convert to
g_object_set(G_OBJECT(capsfilter1), "caps",
gst_caps_new_simple("video/x-raw",
"format", G_TYPE_STRING, "I420",
"width", G_TYPE_INT, 320,
"height", G_TYPE_INT, 240,
//"framerate", GST_TYPE_FRACTION, 15, 1,
NULL), NULL);
// we'll add some basic settings for myfilter
//g_object_set(G_OBJECT(myfilter), "silent", TRUE, "enabled", TRUE, NULL);
g_object_set(G_OBJECT(src), "pattern", 18, NULL);
g_object_set(G_OBJECT(rtph), "config-interval", 1, NULL);
g_object_set(G_OBJECT(sink), "host", "192.168.65.2", "port", 14500, NULL);
// add all the elements to the pipeline and link them
gst_bin_add_many(GST_BIN(pipeline),
src,
capsfilter0,
app,
videoconvert,
capsfilter1,
encoder,
rtph,
sink,
NULL);
gst_element_link_many(
src,
capsfilter0,
app,
videoconvert,
capsfilter1,
encoder,
rtph,
sink,
NULL);
g_info("Setting pipeline to PLAYING");
gst_element_set_state(pipeline, GST_STATE_PLAYING);
g_info("Starting main loop");
g_main_loop_run(gloop);
gst_element_set_state(pipeline, GST_STATE_NULL);
gst_object_unref(pipeline);
g_main_loop_unref(gloop);
return 0;
}
Since we haven’t build the plugin myfilter yet, note that we are using "identity" in the gst_element_factory_make call. Later on, we’ll replace this with myfilter once we’ve built and installed the plugin.
Setup the build directory and compile the code with
meson setup build
ninja -C build
run the application with:
GST_DEBUG=2 G_MESSAGES_DEBUG=all ./build/main
To recieve the video, you need to run the following GStreamer pipeline on the host:
gst-launch-1.0 -e \
udpsrc port=$1 \
caps="application/x-rtp, media=(string)video, clock-rate=(int)90000, encoding-name=(string)H264, payload=(int)96" \
! rtpjitterbuffer \
! rtph264depay \
! decodebin \
! videoconvert \
! glimagesink
You should get a window with a ball bouncing around like so:
Adding a Custom Plugin
The next step is we’ll want a custom GStreamer Plugin. We’ll use this plugin to run any kind of OpenCV code on the video buffers, but for now let’s just get a basic passthrough into the pipeline. We’ll also need to modify the meson.build file.
Grab the boilerplate code from https://gitlab.freedesktop.org/gstreamer/gst-template
Follow the instructions at https://gstreamer.freedesktop.org/documentation/plugin-development/basics/boiler.html?gi-language=c
You’ll run the command ../tools/make_element myfilter and it will generate the gstmyfilter.cpp and gstmyfilter.h in the gst-template repo directory.
I’ll modify my folder structure a little with the following:
myapp
|- meson.build
|- src
|- main.cpp
plugin
|-gstmyfilter.cpp
|-gstmyfilter.hpp
I coped the gst-plugin.c and gst-plugin.h into the src/plugin folder and renamed them to cpp/hpp
Then add the following to the meson.build file:
plugins_install_dir = join_paths(get_option('libdir'), 'gstreamer-1.0')
plugin_cpp_args = ['-DHAVE_CONFIG_H']
api_version = '1.0'
cdata = configuration_data()
cdata.set_quoted('PACKAGE_VERSION', gst_version)
cdata.set_quoted('PACKAGE', 'gst-template-plugin')
cdata.set_quoted('GST_LICENSE', 'LGPL')
cdata.set_quoted('GST_API_VERSION', api_version)
cdata.set_quoted('GST_PACKAGE_NAME', 'GStreamer template Plug-ins')
cdata.set_quoted('GST_PACKAGE_ORIGIN', 'https://gstreamer.freedesktop.org')
configure_file(output : 'config.h', configuration : cdata)
cc = meson.get_compiler('c')
# The myfilter Plugin
gstmyfilter_sources = [
'src/plugin/gstmyfilter.cpp',
]
gstmyfilterplugin = library('gstmyfilter',
gstmyfilter_sources,
cpp_args: plugin_cpp_args,
dependencies : [gst_dep, gstbase_dep, opencv_dep],
install : true,
install_dir : plugins_install_dir,
)Build and Install the plugin.
If use the above main.cpp with the myfilter plugin you can build everything and install your myfilter plugin to the
GST_PLUGIN_PATH directory.
You’ll need to change the identify block to our myfilter
myfilter = gst_element_factory_make("identity", "myfilter0");
myfilter = gst_element_factory_make("myfilter", "myfilter0");
You may want to set that in your .zshrc or .bashrc
export GST_PLUGIN_PATH=/usr/local/lib/aarch64-linux-gnu/gstreamer-1.0
ninja -C build install
You can test if you plugin build by running
gst-inspect-1.0 myfilter
You should get the following output:
Factory Details:
Rank none (0)
Long-name MyFilter
Klass FIXME:Generic
Description FIXME:Generic Template Element
Author root <<user@hostname.org>>
Plugin Details:
Name myfilter
Description myfilter
Filename /usr/local/lib/aarch64-linux-gnu/gstreamer-1.0/libgstmyfilter.so
Version 1.20.3
License LGPL
Source module gst-template-plugin
Binary package GStreamer template Plug-ins
Origin URL https://gstreamer.freedesktop.org
GObject
+----GInitiallyUnowned
+----GstObject
+----GstElement
+----GstMyFilter
Pad Templates:
SINK template: 'sink'
Availability: Always
Capabilities:
ANY
SRC template: 'src'
Availability: Always
Capabilities:
any
Running the application now should result in the same ball video you saw before.