gstreamer/subprojects/gst-libav/ext/libav/gstavvidcmp.c
U. Artie Eoff 649d59b88b gst-libav: add avvideocompare element
The avvideocompare element compares two incoming video buffers using
the specified comparison method (e.g. ssim or psnr).  The first
video buffer is passthrough, unchanged.

The comparison is calculated by using libav's ssim or psnr filters.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/3366>
2023-05-04 19:34:06 +00:00

728 lines
20 KiB
C

/* GStreamer
* Copyright (C) 2022 Intel Corporation
* Author: U. Artie Eoff <ullysses.a.eoff@intel.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the0
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
/**
* SECTION:element-avvideocompare
* @title: avvideocompare
* @short_description: A libav based video compare element
*
* avvideocompare accepts two input video streams with the same width, height,
* framerate and format. The two incoming buffers are compared to each other
* via the chosen compare method (e.g. ssim or psnr).
*
* If the stats-file property is specified, then the computed result for each
* frame comparison will be written to the file, or stdout if stats-file is '-'.
*
* The first incoming buffer is passed through, unchanged, to the srcpad.
*
* ## Sample pipelines
* ```
* gst-launch-1.0 videotestsrc num-buffers=100 \
* ! video/x-raw,format=NV12 \
* ! videobalance brightness=0.005 hue=0.005 \
* ! avvideocompare method=psnr stats-file=- name=cmp \
* ! fakesink videotestsrc ! video/x-raw,format=NV12 \
* ! cmp.
* ```
* ```
* gst-launch-1.0 videotestsrc num-buffers=100 \
* ! tee name=orig ! queue ! avenc_mjpeg \
* ! jpegparse ! avdec_mjpeg \
* ! avvideocompare method=ssim stats-file=stats.log name=cmp \
* ! fakesink orig. ! queue ! cmp.
* ```
*
* Since: 1.24
*
*/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
#include <libavfilter/avfilter.h>
#include <libavfilter/buffersrc.h>
#include <libavfilter/buffersink.h>
#include <glib/gprintf.h>
#include <gst/gst.h>
#include <gst/base/gstcollectpads.h>
#include <gst/video/video.h>
#include "gstav.h"
#include "gstavcodecmap.h"
#define GST_FFMPEGVIDCMP_FORMATS "{ " \
"ARGB, BGRA, ABGR, RGBA, xRGB, BGRx, xBGR, RGBx, RGB16, " \
"GRAY8, NV12, NV21, YUY2, UYVY, I420, Y42B, Y444, VUYA, " \
"P010_10LE, Y410, P012_LE, Y212_LE, Y412_LE" \
" }"
/* *INDENT-OFF* */
static GstStaticPadTemplate gst_ffmpegvidcmp_src_tmpl =
GST_STATIC_PAD_TEMPLATE ("src",
GST_PAD_SRC,
GST_PAD_ALWAYS,
GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE (GST_FFMPEGVIDCMP_FORMATS)));
/* *INDENT-ON* */
/* *INDENT-OFF* */
static GstStaticPadTemplate gst_ffmpegvidcmp_sink1_tmpl =
GST_STATIC_PAD_TEMPLATE ("sink_1",
GST_PAD_SINK,
GST_PAD_ALWAYS,
GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE (GST_FFMPEGVIDCMP_FORMATS)));
/* *INDENT-ON* */
/* *INDENT-OFF* */
static GstStaticPadTemplate gst_ffmpegvidcmp_sink2_tmpl =
GST_STATIC_PAD_TEMPLATE ("sink_2",
GST_PAD_SINK,
GST_PAD_ALWAYS,
GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE (GST_FFMPEGVIDCMP_FORMATS)));
/* *INDENT-ON* */
typedef enum
{
GST_FFMPEGVIDCMP_METHOD_SSIM,
GST_FFMPEGVIDCMP_METHOD_PSNR,
} GstFFMpegVidCmpMethod;
#define GST_FFMPEGVIDCMP_METHOD_TYPE (gst_ffmpegvidcmp_method_get_type())
/**
* GstFFMpegVidCmpMethod:
*
* Since: 1.24
*/
static GType
gst_ffmpegvidcmp_method_get_type (void)
{
static gsize g_type = 0;
static const GEnumValue enum_values[] = {
{GST_FFMPEGVIDCMP_METHOD_SSIM, "SSIM", "ssim"},
{GST_FFMPEGVIDCMP_METHOD_PSNR, "PSNR", "psnr"},
{0, NULL, NULL},
};
if (g_once_init_enter (&g_type)) {
const GType type =
g_enum_register_static ("GstFFMpegVidCmpMethod", enum_values);
g_once_init_leave (&g_type, type);
}
return g_type;
}
enum
{
PROP_0,
PROP_STATS_FILE,
PROP_METHOD,
};
#define DEFAULT_STATS_FILE NULL
#define DEFAULT_METHOD GST_FFMPEGVIDCMP_METHOD_SSIM
#define GST_TYPE_FFMPEGVIDCMP (gst_ffmpegvidcmp_get_type())
G_DECLARE_FINAL_TYPE (GstFFMpegVidCmp, gst_ffmpegvidcmp,
GST, FFMPEGVIDCMP, GstElement);
struct _GstFFMpegVidCmp
{
GstElement element;
/* pads */
GstPad *srcpad;
GstPad *sinkpad1;
GstPad *sinkpad2;
GstCollectPads *collect;
GstCollectData *collect_data1;
GstCollectData *collect_data2;
/* negotiated format */
gint width;
gint height;
gint fps_num;
gint fps_denom;
GstVideoInfo vinfo1;
GstVideoInfo vinfo2;
AVFilterGraph *filter_graph;
AVFilterContext *in1_ctx;
AVFilterContext *in2_ctx;
AVFilterContext *out_ctx;
enum AVPixelFormat pixfmt;
gchar *stats_file;
GstFFMpegVidCmpMethod method;
};
G_DEFINE_TYPE (GstFFMpegVidCmp, gst_ffmpegvidcmp, GST_TYPE_ELEMENT);
static void gst_ffmpegvidcmp_finalize (GObject * object);
static GstFlowReturn gst_ffmpegvidcmp_collected (GstCollectPads * pads,
GstFFMpegVidCmp * self);
static void gst_ffmpegvidcmp_set_property (GObject * object, guint prop_id,
const GValue * value, GParamSpec * pspec);
static void gst_ffmpegvidcmp_get_property (GObject * object, guint prop_id,
GValue * value, GParamSpec * pspec);
static GstStateChangeReturn gst_ffmpegvidcmp_change_state (GstElement * element,
GstStateChange transition);
static gboolean gst_ffmpegvidcmp_sink_event (GstCollectPads * pads,
GstCollectData * data, GstEvent * event, gpointer user_data);
static gboolean gst_ffmpegvidcmp_sink_query (GstCollectPads * pads,
GstCollectData * data, GstQuery * query, gpointer user_data);
static void
gst_ffmpegvidcmp_class_init (GstFFMpegVidCmpClass * klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
GstElementClass *gstelement_class = GST_ELEMENT_CLASS (klass);
gobject_class->set_property = gst_ffmpegvidcmp_set_property;
gobject_class->get_property = gst_ffmpegvidcmp_get_property;
gobject_class->finalize = (GObjectFinalizeFunc) gst_ffmpegvidcmp_finalize;
g_object_class_install_property (gobject_class, PROP_STATS_FILE,
g_param_spec_string ("stats-file", "Stats File Location",
"Set file where to store per-frame difference information"
", '-' for stdout", DEFAULT_STATS_FILE, G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS));
g_object_class_install_property (gobject_class, PROP_METHOD,
g_param_spec_enum ("method", "Method", "Method to compare video frames",
GST_FFMPEGVIDCMP_METHOD_TYPE, DEFAULT_METHOD,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
gst_type_mark_as_plugin_api (GST_FFMPEGVIDCMP_METHOD_TYPE, 0);
gstelement_class->change_state =
GST_DEBUG_FUNCPTR (gst_ffmpegvidcmp_change_state);
gst_element_class_add_static_pad_template (gstelement_class,
&gst_ffmpegvidcmp_sink1_tmpl);
gst_element_class_add_static_pad_template (gstelement_class,
&gst_ffmpegvidcmp_sink2_tmpl);
gst_element_class_add_static_pad_template (gstelement_class,
&gst_ffmpegvidcmp_src_tmpl);
gst_element_class_set_static_metadata (gstelement_class,
"A libav video compare element", "Filter/Compare/Video",
"Compare Video", "U. Artie Eoff <ullysses.a.eoff@intel.com");
}
static void
gst_ffmpegvidcmp_reset (GstFFMpegVidCmp * self)
{
GST_OBJECT_LOCK (self);
self->width = -1;
self->height = -1;
self->fps_num = 0;
self->fps_denom = 1;
self->pixfmt = AV_PIX_FMT_NONE;
self->in1_ctx = NULL;
self->in2_ctx = NULL;
self->out_ctx = NULL;
if (self->filter_graph)
avfilter_graph_free (&self->filter_graph);
GST_OBJECT_UNLOCK (self);
}
static void
gst_ffmpegvidcmp_init (GstFFMpegVidCmp * self)
{
gst_ffmpegvidcmp_reset (self);
self->stats_file = g_strdup (DEFAULT_STATS_FILE);
self->method = DEFAULT_METHOD;
self->sinkpad1 =
gst_pad_new_from_static_template (&gst_ffmpegvidcmp_sink1_tmpl, "sink_1");
gst_element_add_pad (GST_ELEMENT (self), self->sinkpad1);
self->sinkpad2 =
gst_pad_new_from_static_template (&gst_ffmpegvidcmp_sink2_tmpl, "sink_2");
gst_element_add_pad (GST_ELEMENT (self), self->sinkpad2);
self->srcpad =
gst_pad_new_from_static_template (&gst_ffmpegvidcmp_src_tmpl, "src");
gst_element_add_pad (GST_ELEMENT (self), self->srcpad);
self->collect = gst_collect_pads_new ();
gst_collect_pads_set_function (self->collect,
(GstCollectPadsFunction) GST_DEBUG_FUNCPTR (gst_ffmpegvidcmp_collected),
self);
gst_collect_pads_set_event_function (self->collect,
GST_DEBUG_FUNCPTR (gst_ffmpegvidcmp_sink_event), self);
gst_collect_pads_set_query_function (self->collect,
GST_DEBUG_FUNCPTR (gst_ffmpegvidcmp_sink_query), self);
self->collect_data1 = gst_collect_pads_add_pad (self->collect, self->sinkpad1,
sizeof (GstCollectData), NULL, TRUE);
self->collect_data2 = gst_collect_pads_add_pad (self->collect, self->sinkpad2,
sizeof (GstCollectData), NULL, TRUE);
}
static void
gst_ffmpegvidcmp_finalize (GObject * object)
{
GstFFMpegVidCmp *self = GST_FFMPEGVIDCMP (object);
g_free (self->stats_file);
gst_ffmpegvidcmp_reset (self);
if (self->collect)
gst_object_unref (self->collect);
G_OBJECT_CLASS (gst_ffmpegvidcmp_parent_class)->finalize (object);
}
static void
gst_ffmpegvidcmp_set_property (GObject * object, guint prop_id,
const GValue * value, GParamSpec * pspec)
{
GstFFMpegVidCmp *self = GST_FFMPEGVIDCMP (object);
GST_OBJECT_LOCK (self);
switch (prop_id) {
case PROP_STATS_FILE:
{
if (self->filter_graph) {
GST_WARNING_OBJECT (self, "changing the stats file after the filter "
"graph is initialized is not supported");
break;
}
g_free (self->stats_file);
self->stats_file = g_value_dup_string (value);
break;
}
case PROP_METHOD:
{
if (self->filter_graph) {
GST_WARNING_OBJECT (self, "changing the method after the filter "
"graph is initialized is not supported");
break;
}
self->method = g_value_get_enum (value);
break;
}
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
GST_OBJECT_UNLOCK (self);
}
static void
gst_ffmpegvidcmp_get_property (GObject * object, guint prop_id, GValue * value,
GParamSpec * pspec)
{
GstFFMpegVidCmp *self = GST_FFMPEGVIDCMP (object);
GST_OBJECT_LOCK (self);
switch (prop_id) {
case PROP_STATS_FILE:
g_value_set_string (value, self->stats_file);
break;
case PROP_METHOD:
g_value_set_enum (value, self->method);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
GST_OBJECT_UNLOCK (self);
}
static gboolean
gst_ffmpegvidcmp_setcaps (GstFFMpegVidCmp * self, GstPad * pad, GstCaps * caps)
{
GstVideoInfo vinfo;
g_return_val_if_fail (GST_IS_FFMPEGVIDCMP (self), FALSE);
gst_video_info_init (&vinfo);
if (!gst_video_info_from_caps (&vinfo, caps))
return FALSE;
GST_OBJECT_LOCK (self);
self->width = GST_VIDEO_INFO_WIDTH (&vinfo);
self->height = GST_VIDEO_INFO_HEIGHT (&vinfo);
self->fps_num = GST_VIDEO_INFO_FPS_N (&vinfo);
self->fps_denom = GST_VIDEO_INFO_FPS_D (&vinfo);
if (pad == self->sinkpad1)
self->vinfo1 = vinfo;
else
self->vinfo2 = vinfo;
self->pixfmt =
gst_ffmpeg_videoformat_to_pixfmt (GST_VIDEO_INFO_FORMAT (&vinfo));
if (self->pixfmt == AV_PIX_FMT_NONE) {
GST_OBJECT_UNLOCK (self);
GST_ERROR_OBJECT (self, "failed to find suitable ffmpeg pixfmt");
return FALSE;
}
GST_OBJECT_UNLOCK (self);
return TRUE;
}
static gboolean
gst_ffmpegvidcmp_sink_event (GstCollectPads * pads, GstCollectData * data,
GstEvent * event, gpointer user_data)
{
GstFFMpegVidCmp *self = GST_FFMPEGVIDCMP (user_data);
GstPad *pad = data->pad;
gboolean ret = FALSE;
switch (GST_EVENT_TYPE (event)) {
case GST_EVENT_CAPS:
{
GstCaps *caps;
gst_event_parse_caps (event, &caps);
ret = gst_ffmpegvidcmp_setcaps (self, pad, caps);
/* forward sinkpad1 caps to downstream */
if (ret && pad == self->sinkpad1) {
ret = gst_pad_push_event (self->srcpad, event);
event = NULL;
break;
}
gst_event_unref (event);
event = NULL;
break;
}
case GST_EVENT_STREAM_START:
case GST_EVENT_SEGMENT:
{
/* forward the sinkpad1 event to downstream */
if (pad == self->sinkpad1) {
ret = gst_pad_push_event (self->srcpad, event);
event = NULL;
}
break;
}
default:
break;
}
if (event != NULL)
return gst_collect_pads_event_default (pads, data, event, FALSE);
return ret;
}
static gboolean
gst_ffmpegvidcmp_sink_query (GstCollectPads * pads, GstCollectData * data,
GstQuery * query, gpointer user_data)
{
GstFFMpegVidCmp *self = GST_FFMPEGVIDCMP (user_data);
GstPad *pad = data->pad;
switch (GST_QUERY_TYPE (query)) {
case GST_QUERY_ALLOCATION:
if (pad == self->sinkpad1)
return gst_pad_peer_query (self->srcpad, query);
break;
default:
break;
}
return gst_collect_pads_query_default (pads, data, query, FALSE);
}
static GstStateChangeReturn
gst_ffmpegvidcmp_change_state (GstElement * element, GstStateChange transition)
{
GstFFMpegVidCmp *self = GST_FFMPEGVIDCMP (element);
GstStateChangeReturn ret;
switch (transition) {
case GST_STATE_CHANGE_READY_TO_PAUSED:
gst_ffmpegvidcmp_reset (self);
gst_collect_pads_start (self->collect);
break;
case GST_STATE_CHANGE_PAUSED_TO_READY:
gst_collect_pads_stop (self->collect);
break;
default:
break;
}
ret =
GST_ELEMENT_CLASS (gst_ffmpegvidcmp_parent_class)->change_state (element,
transition);
switch (transition) {
case GST_STATE_CHANGE_PAUSED_TO_READY:
gst_ffmpegvidcmp_reset (self);
break;
default:
break;
}
return ret;
}
static gint
init_filter_graph (GstFFMpegVidCmp * self)
{
AVFilterInOut *inputs = NULL;
AVFilterInOut *outputs = NULL;
GEnumClass *enum_class;
GEnumValue *method;
gchar *args = NULL;
gchar *f = NULL;
gint res = -1;
enum_class = g_type_class_ref (GST_FFMPEGVIDCMP_METHOD_TYPE);
method = g_enum_get_value (enum_class, self->method);
g_type_class_unref (enum_class);
if (!method) {
GST_ERROR_OBJECT (self, "unknown compare method");
return -1;
}
GST_INFO_OBJECT (self, " method : %s", method->value_nick);
GST_INFO_OBJECT (self, "stats-file : %s", self->stats_file);
if (self->stats_file)
f = g_strdup_printf ("=f=\\'%s\\'", self->stats_file);
else
f = g_strdup ("");
args =
g_strdup_printf
("buffer=video_size=%dx%d:pix_fmt=%d:time_base=1/1:pixel_aspect=0/1[in1];"
"buffer=video_size=%dx%d:pix_fmt=%d:time_base=1/1:pixel_aspect=0/1[in2];"
"[in1][in2]%s%s[out];[out]buffersink", self->width, self->height,
self->pixfmt, self->width, self->height, self->pixfmt, method->value_nick,
f);
g_free (f);
self->filter_graph = avfilter_graph_alloc ();
if (!self->filter_graph) {
GST_ERROR_OBJECT (self, "failed to allocate filter graph");
g_free (args);
return -1;
}
res = avfilter_graph_parse2 (self->filter_graph, args, &inputs, &outputs);
g_free (args);
if (res < 0) {
GST_ERROR_OBJECT (self, "failed to parse filter graph");
return res;
}
if (inputs || outputs) {
GST_ERROR_OBJECT (self, "unlinked inputs/outputs in filter graph");
return -1;
}
res = avfilter_graph_config (self->filter_graph, NULL);
if (res < 0) {
GST_ERROR_OBJECT (self, "failed to configure filter graph");
return res;
}
self->in1_ctx =
avfilter_graph_get_filter (self->filter_graph, "Parsed_buffer_0");
self->in2_ctx =
avfilter_graph_get_filter (self->filter_graph, "Parsed_buffer_1");
self->out_ctx =
avfilter_graph_get_filter (self->filter_graph, "Parsed_buffersink_3");
if (!self->in1_ctx || !self->in2_ctx || !self->out_ctx) {
GST_ERROR_OBJECT (self, "failed to get filter contexts");
return -1;
}
return res;
}
static gint
process_filter_graph (GstFFMpegVidCmp * self, AVFrame * in1, AVFrame * in2)
{
AVFrame *out;
gint res;
if (!self->filter_graph) {
res = init_filter_graph (self);
if (res < 0)
return res;
}
res = av_buffersrc_add_frame (self->in1_ctx, in1);
if (res < 0)
return res;
res = av_buffersrc_add_frame (self->in2_ctx, in2);
if (res < 0)
return res;
out = av_frame_alloc ();
out->width = self->width;
out->height = self->height;
out->format = self->pixfmt;
res = av_buffersink_get_frame (self->out_ctx, out);
av_frame_unref (out);
av_frame_free (&out);
return res;
}
static void
_fill_avpicture (GstFFMpegVidCmp * self, AVFrame * picture,
GstVideoFrame * vframe)
{
gint i;
for (i = 0; i < GST_VIDEO_FRAME_N_PLANES (vframe); ++i) {
picture->data[i] = GST_VIDEO_FRAME_PLANE_DATA (vframe, i);
picture->linesize[i] = GST_VIDEO_FRAME_PLANE_STRIDE (vframe, i);
}
picture->width = GST_VIDEO_FRAME_WIDTH (vframe);
picture->height = GST_VIDEO_FRAME_HEIGHT (vframe);
picture->format = self->pixfmt;
}
static GstFlowReturn
gst_ffmpegvidcmp_collected (GstCollectPads * pads, GstFFMpegVidCmp * self)
{
GstBuffer *buf1 = NULL, *buf2 = NULL;
GST_OBJECT_LOCK (self);
if (G_UNLIKELY (self->fps_num == 0))
goto not_negotiated;
if (!gst_pad_has_current_caps (self->sinkpad1) ||
!gst_pad_has_current_caps (self->sinkpad2))
goto not_negotiated;
if (GST_VIDEO_INFO_WIDTH (&self->vinfo1) !=
GST_VIDEO_INFO_WIDTH (&self->vinfo2) ||
GST_VIDEO_INFO_HEIGHT (&self->vinfo1) !=
GST_VIDEO_INFO_HEIGHT (&self->vinfo2) ||
GST_VIDEO_INFO_FORMAT (&self->vinfo1) !=
GST_VIDEO_INFO_FORMAT (&self->vinfo2) ||
GST_VIDEO_INFO_FPS_D (&self->vinfo1) !=
GST_VIDEO_INFO_FPS_D (&self->vinfo2) ||
GST_VIDEO_INFO_FPS_N (&self->vinfo1) !=
GST_VIDEO_INFO_FPS_N (&self->vinfo2))
goto input_formats_do_not_match;
buf1 = gst_collect_pads_pop (pads, self->collect_data1);
buf2 = gst_collect_pads_pop (pads, self->collect_data2);
/* compare */
if (buf1 && buf2) {
/* *INDENT-OFF* */
AVFrame in1 = { {0,} };
AVFrame in2 = { {0,} };
/* *INDENT-ON* */
GstVideoFrame frame1, frame2;
if (!gst_video_frame_map (&frame1, &self->vinfo1, buf1, GST_MAP_READ))
goto map_failed;
if (!gst_video_frame_map (&frame2, &self->vinfo2, buf2, GST_MAP_READ)) {
gst_video_frame_unmap (&frame1);
goto map_failed;
}
_fill_avpicture (self, &in1, &frame1);
_fill_avpicture (self, &in2, &frame2);
if (process_filter_graph (self, &in1, &in2) < 0)
GST_WARNING_OBJECT (self, "Could not process filter graph");
gst_video_frame_unmap (&frame1);
gst_video_frame_unmap (&frame2);
}
GST_OBJECT_UNLOCK (self);
if (buf2)
gst_buffer_unref (buf2);
if (!buf1) {
gst_pad_push_event (self->srcpad, gst_event_new_eos ());
return GST_FLOW_EOS;
}
return gst_pad_push (self->srcpad, buf1);
/* ERRORS */
not_negotiated:
{
GST_OBJECT_UNLOCK (self);
GST_ELEMENT_ERROR (self, CORE, NEGOTIATION, (NULL),
("No input format negotiated"));
return GST_FLOW_NOT_NEGOTIATED;
}
input_formats_do_not_match:
{
GstCaps *caps1, *caps2;
GST_OBJECT_UNLOCK (self);
caps1 = gst_pad_get_current_caps (self->sinkpad1);
caps2 = gst_pad_get_current_caps (self->sinkpad2);
GST_ELEMENT_ERROR (self, CORE, NEGOTIATION, (NULL),
("input formats don't match: %" GST_PTR_FORMAT " vs. %" GST_PTR_FORMAT,
caps1, caps2));
gst_caps_unref (caps1);
gst_caps_unref (caps2);
return GST_FLOW_ERROR;
}
map_failed:
{
GST_OBJECT_UNLOCK (self);
GST_DEBUG_OBJECT (self, "Failed to map frame");
gst_buffer_unref (buf2);
gst_buffer_unref (buf1);
return GST_FLOW_ERROR;
}
}
gboolean
gst_ffmpegvidcmp_register (GstPlugin * plugin)
{
return gst_element_register (plugin, "avvideocompare", GST_RANK_NONE,
GST_TYPE_FFMPEGVIDCMP);
}