plugin: Add clapper GStreamer plugin

Add new GStreamer plugin with custom video sink for Clapper video player.

The main difference is that unlike the old one, this does not operate using `GtkGLArea`
anymore, but processes and displays `GdkTextures` directly through `GtkPicture` widget.
Also this one is installed like any other GStreamer plugin, thus can be used via
`gst-launch-1.0` binary or even by other GTK4 apps if they wish to integrate it.

In order to not depend on GL stuff at build time, this plugin uses seperate GModules
called "importers" in order to import different kind of memories into GdkTexture. This
allows expanding its capabilities further then we were able to do before.
This commit is contained in:
Rafostar
2022-05-01 16:48:23 +02:00
committed by Rafał Dzięgiel
parent f771e0320c
commit 044710f97e
17 changed files with 2824 additions and 18 deletions

8
lib/gst/meson.build vendored
View File

@@ -1 +1,7 @@
subdir('clapper')
if get_option('lib')
subdir('clapper')
endif
if not get_option('gst-plugin').disabled()
subdir('plugin')
endif

439
lib/gst/plugin/gstclapperimporter.c vendored Normal file
View File

@@ -0,0 +1,439 @@
/*
* Copyright (C) 2022 Rafał Dzięgiel <rafostar.github@gmail.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 the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "gstclapperimporter.h"
#include "gstgtkutils.h"
#define GST_CAT_DEFAULT gst_clapper_importer_debug
GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
#define parent_class gst_clapper_importer_parent_class
G_DEFINE_TYPE (GstClapperImporter, gst_clapper_importer, GST_TYPE_OBJECT);
typedef struct
{
GdkTexture *texture;
GstVideoOverlayRectangle *rectangle;
gint x, y;
guint width, height;
gint index;
gatomicrefcount ref_count;
} GstClapperGdkOverlay;
static GstClapperGdkOverlay *
gst_clapper_gdk_overlay_new (GdkTexture *texture, GstVideoOverlayRectangle *rectangle,
gint x, gint y, guint width, guint height, guint index)
{
GstClapperGdkOverlay *overlay = g_slice_new (GstClapperGdkOverlay);
overlay->texture = g_object_ref (texture);
overlay->rectangle = gst_video_overlay_rectangle_ref (rectangle);
overlay->x = x;
overlay->y = y;
overlay->width = width;
overlay->height = height;
overlay->index = index;
g_atomic_ref_count_init (&overlay->ref_count);
return overlay;
}
static GstClapperGdkOverlay *
gst_clapper_gdk_overlay_ref (GstClapperGdkOverlay *overlay)
{
g_atomic_ref_count_inc (&overlay->ref_count);
return overlay;
}
static void
gst_clapper_gdk_overlay_unref (GstClapperGdkOverlay *overlay)
{
if (g_atomic_ref_count_dec (&overlay->ref_count)) {
GST_TRACE ("Freeing overlay: %" GST_PTR_FORMAT, overlay);
g_object_unref (overlay->texture);
gst_video_overlay_rectangle_unref (overlay->rectangle);
g_slice_free (GstClapperGdkOverlay, overlay);
}
}
static GstBufferPool *
_default_create_pool (GstClapperImporter *self, GstStructure **config)
{
GST_FIXME_OBJECT (self, "Need to create buffer pool");
return NULL;
}
static GdkTexture *
_default_generate_texture (GstClapperImporter *self,
GstBuffer *buffer, GstVideoInfo *v_info)
{
GST_FIXME_OBJECT (self, "GdkTexture generation not implemented");
return NULL;
}
static void
gst_clapper_importer_init (GstClapperImporter *self)
{
gst_video_info_init (&self->pending_v_info);
gst_video_info_init (&self->v_info);
self->pending_overlays = g_ptr_array_new_with_free_func (
(GDestroyNotify) gst_clapper_gdk_overlay_unref);
self->overlays = g_ptr_array_new_with_free_func (
(GDestroyNotify) gst_clapper_gdk_overlay_unref);
gdk_rgba_parse (&self->bg, "black");
}
static void
gst_clapper_importer_finalize (GObject *object)
{
GstClapperImporter *self = GST_CLAPPER_IMPORTER_CAST (object);
GST_TRACE ("Finalize");
gst_clear_buffer (&self->pending_buffer);
gst_clear_buffer (&self->buffer);
g_ptr_array_unref (self->pending_overlays);
g_ptr_array_unref (self->overlays);
g_clear_object (&self->texture);
GST_CALL_PARENT (G_OBJECT_CLASS, finalize, (object));
}
static void
gst_clapper_importer_class_init (GstClapperImporterClass *klass)
{
GObjectClass *gobject_class = (GObjectClass *) klass;
GstClapperImporterClass *importer_class = (GstClapperImporterClass *) klass;
GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperimporter", 0,
"Clapper Importer");
gobject_class->finalize = gst_clapper_importer_finalize;
importer_class->create_pool = _default_create_pool;
importer_class->generate_texture = _default_generate_texture;
}
static GstClapperGdkOverlay *
_get_cached_overlay (GPtrArray *overlays, GstVideoOverlayRectangle *rectangle)
{
guint i;
for (i = 0; i < overlays->len; i++) {
GstClapperGdkOverlay *overlay = g_ptr_array_index (overlays, i);
if (overlay->rectangle == rectangle)
return overlay;
}
return NULL;
}
static gint
_sort_overlays_cb (gconstpointer a, gconstpointer b)
{
GstClapperGdkOverlay *overlay_a, *overlay_b;
overlay_a = *((GstClapperGdkOverlay **) a);
overlay_b = *((GstClapperGdkOverlay **) b);
return (overlay_a->index - overlay_b->index);
}
/*
* Prepares overlays to show with the next rendered buffer.
*
* In order for overlays caching to work correctly, this should be called for
* every received buffer (even if its going to be disgarded), also must be
* called together with pending buffer replacement within a single importer
* locking, to make sure prepared overlays always match the pending buffer.
*/
static void
gst_clapper_importer_prepare_overlays_locked (GstClapperImporter *self)
{
GstVideoOverlayCompositionMeta *comp_meta;
guint num_overlays, i;
if (G_UNLIKELY (!self->pending_buffer)
|| !(comp_meta = gst_buffer_get_video_overlay_composition_meta (self->pending_buffer))) {
guint n_pending = self->pending_overlays->len;
/* Remove all cached overlays if new buffer does not have any */
if (n_pending > 0) {
GST_TRACE ("No overlays in buffer, removing all cached ones");
g_ptr_array_remove_range (self->pending_overlays, 0, n_pending);
}
return;
}
GST_LOG_OBJECT (self, "Preparing overlays...");
/* Mark all old overlays as unused by giving them negative index */
for (i = 0; i < self->pending_overlays->len; i++) {
GstClapperGdkOverlay *overlay = g_ptr_array_index (self->pending_overlays, i);
overlay->index = -1;
}
num_overlays = gst_video_overlay_composition_n_rectangles (comp_meta->overlay);
for (i = 0; i < num_overlays; i++) {
GdkTexture *texture;
GstBuffer *comp_buffer;
GstVideoFrame comp_frame;
GstVideoMeta *v_meta;
GstVideoInfo v_info;
GstVideoOverlayRectangle *rectangle;
GstClapperGdkOverlay *overlay;
GstVideoOverlayFormatFlags flags, alpha_flags = 0;
gint comp_x, comp_y;
guint comp_width, comp_height;
rectangle = gst_video_overlay_composition_get_rectangle (comp_meta->overlay, i);
if ((overlay = _get_cached_overlay (self->pending_overlays, rectangle))) {
overlay->index = i;
GST_TRACE ("Reusing cached overlay: %" GST_PTR_FORMAT, overlay);
continue;
}
if (G_UNLIKELY (!gst_video_overlay_rectangle_get_render_rectangle (rectangle,
&comp_x, &comp_y, &comp_width, &comp_height))) {
GST_WARNING ("Invalid overlay rectangle dimensions: %" GST_PTR_FORMAT, rectangle);
continue;
}
flags = gst_video_overlay_rectangle_get_flags (rectangle);
if (flags & GST_VIDEO_OVERLAY_FORMAT_FLAG_PREMULTIPLIED_ALPHA)
alpha_flags |= GST_VIDEO_OVERLAY_FORMAT_FLAG_PREMULTIPLIED_ALPHA;
comp_buffer = gst_video_overlay_rectangle_get_pixels_unscaled_argb (rectangle, alpha_flags);
/* Update overlay video info from video meta */
if ((v_meta = gst_buffer_get_video_meta (comp_buffer))) {
gst_video_info_set_format (&v_info, v_meta->format, v_meta->width, v_meta->height);
v_info.stride[0] = v_meta->stride[0];
}
if (G_UNLIKELY (!gst_video_frame_map (&comp_frame, &v_info, comp_buffer, GST_MAP_READ)))
return;
if ((texture = gst_video_frame_into_gdk_texture (&comp_frame))) {
overlay = gst_clapper_gdk_overlay_new (texture, rectangle, comp_x, comp_y,
comp_width, comp_height, i);
g_object_unref (texture);
GST_TRACE_OBJECT (self, "Created overlay: %"
GST_PTR_FORMAT ", x: %i, y: %i, width: %u, height: %u",
overlay, overlay->x, overlay->y, overlay->width, overlay->height);
g_ptr_array_insert (self->pending_overlays, i, overlay);
}
gst_video_frame_unmap (&comp_frame);
}
/* Remove all overlays that are not going to be used */
for (i = self->pending_overlays->len; i > 0; i--) {
GstClapperGdkOverlay *overlay = g_ptr_array_index (self->pending_overlays, i - 1);
if (overlay->index < 0) {
GST_TRACE ("Removing unused cached overlay: %" GST_PTR_FORMAT, overlay);
g_ptr_array_remove (self->pending_overlays, overlay);
}
}
/* Sort remaining overlays */
if (self->pending_overlays->len > 1) {
GST_LOG_OBJECT (self, "Sorting overlays");
g_ptr_array_sort (self->pending_overlays, (GCompareFunc) _sort_overlays_cb);
}
if (G_UNLIKELY (num_overlays != self->pending_overlays->len)) {
GST_WARNING_OBJECT (self, "Some overlays could not be prepared, %u != %u",
num_overlays, self->pending_overlays->len);
}
GST_LOG_OBJECT (self, "Prepared overlays: %u", self->pending_overlays->len);
}
gboolean
gst_clapper_importer_prepare (GstClapperImporter *self)
{
GstClapperImporterClass *importer_class = GST_CLAPPER_IMPORTER_GET_CLASS (self);
if (importer_class->prepare) {
if (!importer_class->prepare (self))
return FALSE;
}
GST_DEBUG_OBJECT (self, "Importer prepared");
return TRUE;
}
void
gst_clapper_importer_share_data (GstClapperImporter *self, GstClapperImporter *dest)
{
GstClapperImporterClass *importer_class = GST_CLAPPER_IMPORTER_GET_CLASS (self);
if (importer_class->share_data)
importer_class->share_data (self, dest);
}
void
gst_clapper_importer_set_caps (GstClapperImporter *self, GstCaps *caps)
{
GstClapperImporterClass *importer_class = GST_CLAPPER_IMPORTER_GET_CLASS (self);
GST_OBJECT_LOCK (self);
self->has_pending_v_info = gst_video_info_from_caps (&self->pending_v_info, caps);
GST_OBJECT_UNLOCK (self);
if (importer_class->set_caps)
importer_class->set_caps (self, caps);
}
void
gst_clapper_importer_set_buffer (GstClapperImporter *self, GstBuffer *buffer)
{
/* Both overlays and pending buffer must be
* set within a single importer locking */
GST_OBJECT_LOCK (self);
gst_buffer_replace (&self->pending_buffer, buffer);
gst_clapper_importer_prepare_overlays_locked (self);
GST_OBJECT_UNLOCK (self);
}
GstBufferPool *
gst_clapper_importer_create_pool (GstClapperImporter *self, GstStructure **config)
{
GstClapperImporterClass *importer_class = GST_CLAPPER_IMPORTER_GET_CLASS (self);
return importer_class->create_pool (self, config);
}
void
gst_clapper_importer_add_allocation_metas (GstClapperImporter *self, GstQuery *query)
{
GstClapperImporterClass *importer_class = GST_CLAPPER_IMPORTER_GET_CLASS (self);
if (importer_class->add_allocation_metas)
importer_class->add_allocation_metas (self, query);
}
gboolean
gst_clapper_importer_handle_context_query (GstClapperImporter *self,
GstBaseSink *bsink, GstQuery *query)
{
GstClapperImporterClass *importer_class = GST_CLAPPER_IMPORTER_GET_CLASS (self);
if (!importer_class->handle_context_query)
return FALSE;
return importer_class->handle_context_query (self, bsink, query);
}
void
gst_clapper_importer_snapshot (GstClapperImporter *self, GdkSnapshot *snapshot,
gdouble width, gdouble height, gfloat scale_x, gfloat scale_y)
{
guint i;
gboolean buffer_changed;
/* Collect all data that we need to snapshot pending buffer,
* lock ourselves to make sure everything matches */
GST_OBJECT_LOCK (self);
buffer_changed = gst_buffer_replace (&self->buffer, self->pending_buffer);
/* Only replace v_info when buffer changed, this way
* we still use old (correct) v_info when resizing */
if (buffer_changed && self->has_pending_v_info) {
self->v_info = self->pending_v_info;
self->has_pending_v_info = FALSE;
}
/* Ref overlays associated with current buffer */
for (i = 0; i < self->pending_overlays->len; i++) {
GstClapperGdkOverlay *overlay = g_ptr_array_index (self->pending_overlays, i);
g_ptr_array_insert (self->overlays, i, gst_clapper_gdk_overlay_ref (overlay));
}
GST_OBJECT_UNLOCK (self);
/* Draw black BG when no buffer or imported format has alpha */
if (!self->buffer || GST_VIDEO_INFO_HAS_ALPHA (&self->v_info))
gtk_snapshot_append_color (snapshot, &self->bg, &GRAPHENE_RECT_INIT (0, 0, width, height));
if (self->buffer) {
if (buffer_changed || !self->texture) {
GstClapperImporterClass *importer_class = GST_CLAPPER_IMPORTER_GET_CLASS (self);
GST_TRACE_OBJECT (self, "Importing %" GST_PTR_FORMAT, self->buffer);
g_clear_object (&self->texture);
self->texture = importer_class->generate_texture (self, self->buffer, &self->v_info);
} else {
GST_TRACE_OBJECT (self, "Reusing texture from %" GST_PTR_FORMAT, self->buffer);
}
if (G_LIKELY (self->texture)) {
gtk_snapshot_append_texture (snapshot, self->texture, &GRAPHENE_RECT_INIT (0, 0, width, height));
for (i = 0; i < self->overlays->len; i++) {
GstClapperGdkOverlay *overlay = g_ptr_array_index (self->overlays, i);
gtk_snapshot_append_texture (snapshot, overlay->texture,
&GRAPHENE_RECT_INIT (overlay->x * scale_x, overlay->y * scale_y,
overlay->width * scale_x, overlay->height * scale_y));
}
} else {
GST_ERROR_OBJECT (self, "Failed import of %" GST_PTR_FORMAT, self->buffer);
/* Draw black instead of texture on failure if not drawn already */
if (!GST_VIDEO_INFO_HAS_ALPHA (&self->v_info))
gtk_snapshot_append_color (snapshot, &self->bg, &GRAPHENE_RECT_INIT (0, 0, width, height));
}
}
/* Unref all used overlays */
if (self->overlays->len > 0)
g_ptr_array_remove_range (self->overlays, 0, self->overlays->len);
}

102
lib/gst/plugin/gstclapperimporter.h vendored Normal file
View File

@@ -0,0 +1,102 @@
/*
* Copyright (C) 2022 Rafał Dzięgiel <rafostar.github@gmail.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 the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#pragma once
#include <gtk/gtk.h>
#include <gst/gst.h>
#include <gst/video/video.h>
G_BEGIN_DECLS
#define GST_TYPE_CLAPPER_IMPORTER (gst_clapper_importer_get_type())
#define GST_IS_CLAPPER_IMPORTER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_CLAPPER_IMPORTER))
#define GST_IS_CLAPPER_IMPORTER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_CLAPPER_IMPORTER))
#define GST_CLAPPER_IMPORTER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_CLAPPER_IMPORTER, GstClapperImporterClass))
#define GST_CLAPPER_IMPORTER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_CLAPPER_IMPORTER, GstClapperImporter))
#define GST_CLAPPER_IMPORTER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_CLAPPER_IMPORTER, GstClapperImporterClass))
#define GST_CLAPPER_IMPORTER_CAST(obj) ((GstClapperImporter *)(obj))
#define GST_CLAPPER_IMPORTER_DEFINE(camel,lower,type) \
G_DEFINE_TYPE (camel, lower, type) \
G_MODULE_EXPORT GstClapperImporter *make_importer (void); \
G_MODULE_EXPORT GstCaps *make_caps (GstRank *rank, GStrv *context_types);
typedef struct _GstClapperImporter GstClapperImporter;
typedef struct _GstClapperImporterClass GstClapperImporterClass;
#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC
G_DEFINE_AUTOPTR_CLEANUP_FUNC (GstClapperImporter, gst_object_unref)
#endif
struct _GstClapperImporter
{
GstObject parent;
GstBuffer *pending_buffer, *buffer;
GPtrArray *pending_overlays, *overlays;
GstVideoInfo pending_v_info, v_info;
gboolean has_pending_v_info;
GdkTexture *texture;
GdkRGBA bg;
};
struct _GstClapperImporterClass
{
GstObjectClass parent_class;
gboolean (* prepare) (GstClapperImporter *importer);
void (* share_data) (GstClapperImporter *src,
GstClapperImporter *dest);
void (* set_caps) (GstClapperImporter *importer,
GstCaps *caps);
gboolean (* handle_context_query) (GstClapperImporter *importer,
GstBaseSink *bsink,
GstQuery *query);
GstBufferPool * (* create_pool) (GstClapperImporter *importer,
GstStructure **config);
void (* add_allocation_metas) (GstClapperImporter *importer,
GstQuery *query);
GdkTexture * (* generate_texture) (GstClapperImporter *importer,
GstBuffer *buffer,
GstVideoInfo *v_info);
};
GType gst_clapper_importer_get_type (void);
gboolean gst_clapper_importer_prepare (GstClapperImporter *importer);
void gst_clapper_importer_share_data (GstClapperImporter *importer, GstClapperImporter *dest);
gboolean gst_clapper_importer_handle_context_query (GstClapperImporter *importer, GstBaseSink *bsink, GstQuery *query);
GstBufferPool * gst_clapper_importer_create_pool (GstClapperImporter *importer, GstStructure **config);
void gst_clapper_importer_add_allocation_metas (GstClapperImporter *importer, GstQuery *query);
void gst_clapper_importer_set_caps (GstClapperImporter *importer, GstCaps *caps);
void gst_clapper_importer_set_buffer (GstClapperImporter *importer, GstBuffer *buffer);
void gst_clapper_importer_snapshot (GstClapperImporter *importer, GdkSnapshot *snapshot, gdouble width, gdouble height, gfloat scale_x, gfloat scale_y);
G_END_DECLS

View File

@@ -0,0 +1,416 @@
/*
* Copyright (C) 2022 Rafał Dzięgiel <rafostar.github@gmail.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 the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <gmodule.h>
#include "gstclapperimporterloader.h"
#include "gstclapperimporter.h"
#define GST_CAT_DEFAULT gst_clapper_importer_loader_debug
GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
typedef GstClapperImporter* (* MakeImporter) (void);
typedef GstCaps* (* MakeCaps) (GstRank *rank, GStrv *context_types);
typedef struct
{
gchar *module_path;
GModule *open_module;
GstCaps *caps;
GstRank rank;
GStrv context_types;
} GstClapperImporterData;
static void
gst_clapper_importer_data_free (GstClapperImporterData *data)
{
g_free (data->module_path);
if (data->open_module)
g_module_close (data->open_module);
gst_clear_caps (&data->caps);
g_strfreev (data->context_types);
g_free (data);
}
static gboolean
_open_importer (GstClapperImporterData *data)
{
g_return_val_if_fail (data && data->module_path, FALSE);
/* Already open */
if (data->open_module)
return TRUE;
GST_DEBUG ("Opening module: %s", data->module_path);
data->open_module = g_module_open (data->module_path, G_MODULE_BIND_LAZY);
if (!data->open_module) {
GST_WARNING ("Could not load importer: %s, reason: %s",
data->module_path, g_module_error ());
return FALSE;
}
GST_DEBUG ("Opened importer module");
/* Make sure module stays loaded. Seems to be needed for
* reusing exported symbols from the same module again */
g_module_make_resident (data->open_module);
return TRUE;
}
static void
_close_importer (GstClapperImporterData *data)
{
if (!data || !data->open_module)
return;
if (G_LIKELY (g_module_close (data->open_module)))
GST_DEBUG ("Closed module: %s", data->module_path);
else
GST_WARNING ("Could not close importer module");
data->open_module = NULL;
}
static GstClapperImporter *
_obtain_importer_internal (GstClapperImporterData *data)
{
MakeImporter make_importer;
GstClapperImporter *importer = NULL;
if (!_open_importer (data))
goto finish;
if (!g_module_symbol (data->open_module, "make_importer", (gpointer *) &make_importer)
|| make_importer == NULL) {
GST_WARNING ("Make function missing in importer");
goto fail;
}
/* Do not close the module, we are gonna continue using it */
if ((importer = make_importer ()))
goto finish;
fail:
_close_importer (data);
finish:
return importer;
}
static GstClapperImporterData *
_fill_importer_data (const gchar *module_path)
{
MakeCaps make_caps;
GstClapperImporterData *data;
data = g_new0 (GstClapperImporterData, 1);
data->module_path = g_strdup (module_path);
data->open_module = g_module_open (data->module_path, G_MODULE_BIND_LAZY);
if (!data->open_module)
goto fail;
if (!g_module_symbol (data->open_module, "make_caps", (gpointer *) &make_caps)
|| make_caps == NULL) {
GST_WARNING ("Make caps function missing in importer");
goto fail;
}
data->caps = make_caps (&data->rank, &data->context_types);
GST_DEBUG ("Caps reading %ssuccessful", data->caps ? "" : "un");
if (!data->caps)
goto fail;
/* Once we obtain importer data, close module afterwards */
_close_importer (data);
return data;
fail:
gst_clapper_importer_data_free (data);
return NULL;
}
static gint
_sort_importers_cb (gconstpointer a, gconstpointer b)
{
GstClapperImporterData *data_a, *data_b;
data_a = *((GstClapperImporterData **) a);
data_b = *((GstClapperImporterData **) b);
return (data_b->rank - data_a->rank);
}
static gpointer
_obtain_available_importers (G_GNUC_UNUSED gpointer data)
{
GPtrArray *importers;
GFile *dir;
GFileEnumerator *dir_enum;
GError *error = NULL;
importers = g_ptr_array_new_with_free_func (
(GDestroyNotify) gst_clapper_importer_data_free);
GST_INFO ("Checking available clapper sink importers");
dir = g_file_new_for_path (CLAPPER_SINK_IMPORTER_PATH);
if ((dir_enum = g_file_enumerate_children (dir,
G_FILE_ATTRIBUTE_STANDARD_NAME,
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, &error))) {
while (TRUE) {
GFileInfo *info = NULL;
GstClapperImporterData *data;
gchar *module_path;
const gchar *module_name;
if (!g_file_enumerator_iterate (dir_enum, &info,
NULL, NULL, &error) || !info)
break;
module_name = g_file_info_get_name (info);
if (!g_str_has_suffix (module_name, G_MODULE_SUFFIX))
continue;
module_path = g_module_build_path (CLAPPER_SINK_IMPORTER_PATH, module_name);
data = _fill_importer_data (module_path);
g_free (module_path);
if (!data) {
GST_WARNING ("Could not read importer data: %s", module_name);
continue;
}
GST_INFO ("Found importer: %s, caps: %" GST_PTR_FORMAT, module_name, data->caps);
g_ptr_array_add (importers, data);
}
}
g_object_unref (dir);
g_object_unref (dir_enum);
if (error) {
GST_ERROR ("Could not load importer, reason: %s",
(error->message) ? error->message : "unknown");
g_error_free (error);
}
g_ptr_array_sort (importers, (GCompareFunc) _sort_importers_cb);
return importers;
}
static const GPtrArray *
gst_clapper_importer_loader_get_available_importers (void)
{
static GOnce once = G_ONCE_INIT;
g_once (&once, _obtain_available_importers, NULL);
return (const GPtrArray *) once.retval;
}
static GstClapperImporterData *
_find_open_importer_data (const GPtrArray *importers)
{
guint i;
for (i = 0; i < importers->len; i++) {
GstClapperImporterData *data = g_ptr_array_index (importers, i);
if (data->open_module)
return data;
}
return NULL;
}
static GstClapperImporterData *
_get_importer_data_for_caps (const GPtrArray *importers, const GstCaps *caps)
{
guint i;
for (i = 0; i < importers->len; i++) {
GstClapperImporterData *data = g_ptr_array_index (importers, i);
if (!gst_caps_is_always_compatible (caps, data->caps))
continue;
return data;
}
return NULL;
}
static GstClapperImporterData *
_get_importer_data_for_context_type (const GPtrArray *importers, const gchar *context_type)
{
guint i;
for (i = 0; i < importers->len; i++) {
GstClapperImporterData *data = g_ptr_array_index (importers, i);
guint j;
if (!data->context_types)
continue;
for (j = 0; data->context_types[j]; j++) {
if (strcmp (context_type, data->context_types[j]))
continue;
return data;
}
}
return NULL;
}
void
gst_clapper_importer_loader_unload_all (void)
{
const GPtrArray *importers;
guint i;
importers = gst_clapper_importer_loader_get_available_importers ();
GST_TRACE ("Unloading all open modules");
for (i = 0; i < importers->len; i++) {
GstClapperImporterData *data = g_ptr_array_index (importers, i);
_close_importer (data);
}
}
GstPadTemplate *
gst_clapper_importer_loader_make_sink_pad_template (void)
{
const GPtrArray *importers;
GstCaps *sink_caps;
GstPadTemplate *templ;
guint i;
/* This is only called once from sink class init function */
GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperimporterloader", 0,
"Clapper Importer Loader");
importers = gst_clapper_importer_loader_get_available_importers ();
sink_caps = gst_caps_new_empty ();
for (i = 0; i < importers->len; i++) {
GstClapperImporterData *data = g_ptr_array_index (importers, i);
GstCaps *copied_caps;
copied_caps = gst_caps_copy (data->caps);
gst_caps_append (sink_caps, copied_caps);
}
if (G_UNLIKELY (gst_caps_is_empty (sink_caps)))
gst_caps_append (sink_caps, gst_caps_new_any ());
templ = gst_pad_template_new ("sink", GST_PAD_SINK, GST_PAD_ALWAYS, sink_caps);
gst_caps_unref (sink_caps);
return templ;
}
static gboolean
_find_importer_internal (GstCaps *caps, GstQuery *query, GstClapperImporter **importer)
{
const GPtrArray *importers;
GstClapperImporterData *old_data = NULL, *new_data = NULL;
GstClapperImporter *found_importer = NULL;
importers = gst_clapper_importer_loader_get_available_importers ();
old_data = _find_open_importer_data (importers);
if (caps) {
GST_DEBUG ("Requested importer for caps: %" GST_PTR_FORMAT, caps);
new_data = _get_importer_data_for_caps (importers, caps);
} else if (query) {
const gchar *context_type;
gst_query_parse_context_type (query, &context_type);
GST_DEBUG ("Requested importer for context: %s", context_type);
new_data = _get_importer_data_for_context_type (importers, context_type);
/* In case missing importer for context query, leave the old one.
* We should allow some queries to go through unresponded */
if (!new_data)
new_data = old_data;
}
GST_LOG ("Old importer path: %s, new path: %s",
(old_data != NULL) ? old_data->module_path : NULL,
(new_data != NULL) ? new_data->module_path : NULL);
if (old_data == new_data) {
GST_DEBUG ("No importer change");
if (*importer && caps)
gst_clapper_importer_set_caps (*importer, caps);
return (*importer != NULL);
}
if (new_data) {
found_importer = _obtain_importer_internal (new_data);
if (*importer && found_importer)
gst_clapper_importer_share_data (*importer, found_importer);
}
gst_clear_object (importer);
_close_importer (old_data);
if (found_importer && gst_clapper_importer_prepare (found_importer)) {
if (caps)
gst_clapper_importer_set_caps (found_importer, caps);
*importer = found_importer;
return TRUE;
}
gst_clear_object (&found_importer);
_close_importer (new_data);
return FALSE;
}
gboolean
gst_clapper_importer_loader_find_importer_for_caps (GstCaps *caps, GstClapperImporter **importer)
{
return _find_importer_internal (caps, NULL, importer);
}
gboolean
gst_clapper_importer_loader_find_importer_for_context_query (GstQuery *query, GstClapperImporter **importer)
{
return _find_importer_internal (NULL, query, importer);
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2022 Rafał Dzięgiel <rafostar.github@gmail.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 the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#pragma once
#include <gst/gst.h>
#include "gstclapperimporter.h"
G_BEGIN_DECLS
GstPadTemplate * gst_clapper_importer_loader_make_sink_pad_template (void);
gboolean gst_clapper_importer_loader_find_importer_for_caps (GstCaps *caps, GstClapperImporter **importer);
gboolean gst_clapper_importer_loader_find_importer_for_context_query (GstQuery *query, GstClapperImporter **importer);
void gst_clapper_importer_loader_unload_all (void);
G_END_DECLS

428
lib/gst/plugin/gstclapperpaintable.c vendored Normal file
View File

@@ -0,0 +1,428 @@
/*
* Copyright (C) 2022 Rafał Dzięgiel <rafostar.github@gmail.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 the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "gstclapperpaintable.h"
#define DEFAULT_PAR_N 1
#define DEFAULT_PAR_D 1
#define GST_CAT_DEFAULT gst_clapper_paintable_debug
GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
static void gst_clapper_paintable_iface_init (GdkPaintableInterface *iface);
static void gst_clapper_paintable_dispose (GObject *object);
static void gst_clapper_paintable_finalize (GObject *object);
#define parent_class gst_clapper_paintable_parent_class
G_DEFINE_TYPE_WITH_CODE (GstClapperPaintable, gst_clapper_paintable, G_TYPE_OBJECT,
G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE,
gst_clapper_paintable_iface_init));
static void
gst_clapper_paintable_class_init (GstClapperPaintableClass *klass)
{
GObjectClass *gobject_class = (GObjectClass *) klass;
GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperpaintable", 0,
"Clapper Paintable");
gobject_class->dispose = gst_clapper_paintable_dispose;
gobject_class->finalize = gst_clapper_paintable_finalize;
}
static void
gst_clapper_paintable_init (GstClapperPaintable *self)
{
self->par_n = DEFAULT_PAR_N;
self->par_d = DEFAULT_PAR_D;
self->pixel_aspect = ((gdouble) self->par_d / self->par_n);
g_mutex_init (&self->lock);
gst_video_info_init (&self->v_info);
g_weak_ref_init (&self->widget, NULL);
g_weak_ref_init (&self->importer, NULL);
gdk_rgba_parse (&self->bg, "black");
}
static void
gst_clapper_paintable_dispose (GObject *object)
{
GstClapperPaintable *self = GST_CLAPPER_PAINTABLE (object);
GST_CLAPPER_PAINTABLE_LOCK (self);
if (self->draw_id > 0) {
g_source_remove (self->draw_id);
self->draw_id = 0;
}
GST_CLAPPER_PAINTABLE_UNLOCK (self);
GST_CALL_PARENT (G_OBJECT_CLASS, dispose, (object));
}
static void
gst_clapper_paintable_finalize (GObject *object)
{
GstClapperPaintable *self = GST_CLAPPER_PAINTABLE (object);
GST_TRACE ("Finalize");
g_weak_ref_clear (&self->widget);
g_weak_ref_clear (&self->importer);
g_mutex_clear (&self->lock);
GST_CALL_PARENT (G_OBJECT_CLASS, finalize, (object));
}
static gboolean
calculate_display_par (GstClapperPaintable *self, const GstVideoInfo *info)
{
gint width, height, par_n, par_d, req_par_n, req_par_d;
gboolean success;
width = GST_VIDEO_INFO_WIDTH (info);
height = GST_VIDEO_INFO_HEIGHT (info);
/* Cannot apply aspect ratio if there is no video */
if (width == 0 || height == 0)
return FALSE;
par_n = GST_VIDEO_INFO_PAR_N (info);
par_d = GST_VIDEO_INFO_PAR_D (info);
req_par_n = self->par_n;
req_par_d = self->par_d;
if (par_n == 0)
par_n = 1;
/* Use defaults if user set zero */
if (req_par_n == 0 || req_par_d == 0) {
req_par_n = DEFAULT_PAR_N;
req_par_d = DEFAULT_PAR_D;
}
GST_LOG_OBJECT (self, "PAR: %u/%u, DAR: %u/%u", par_n, par_d, req_par_n, req_par_d);
if (!(success = gst_video_calculate_display_ratio (&self->display_ratio_num,
&self->display_ratio_den, width, height, par_n, par_d,
req_par_n, req_par_d))) {
GST_ERROR_OBJECT (self, "Could not calculate display ratio values");
}
return success;
}
static void
invalidate_paintable_size_internal (GstClapperPaintable *self)
{
gint video_width, video_height;
guint display_ratio_num, display_ratio_den;
GST_CLAPPER_PAINTABLE_LOCK (self);
video_width = GST_VIDEO_INFO_WIDTH (&self->v_info);
video_height = GST_VIDEO_INFO_HEIGHT (&self->v_info);
display_ratio_num = self->display_ratio_num;
display_ratio_den = self->display_ratio_den;
self->pixel_aspect = ((gdouble) self->par_d / self->par_n);
GST_CLAPPER_PAINTABLE_UNLOCK (self);
if (video_height % display_ratio_den == 0) {
GST_LOG ("Keeping video height");
self->display_width = (guint)
gst_util_uint64_scale_int (video_height, display_ratio_num, display_ratio_den);
self->display_height = video_height;
} else if (video_width % display_ratio_num == 0) {
GST_LOG ("Keeping video width");
self->display_width = video_width;
self->display_height = (guint)
gst_util_uint64_scale_int (video_width, display_ratio_den, display_ratio_num);
} else {
GST_LOG ("Approximating while keeping video height");
self->display_width = (guint)
gst_util_uint64_scale_int (video_height, display_ratio_num, display_ratio_den);
self->display_height = video_height;
}
self->display_aspect_ratio = ((gdouble) self->display_width
/ (gdouble) self->display_height);
GST_DEBUG_OBJECT (self, "Invalidate paintable size, display: %dx%d",
self->display_width, self->display_height);
gdk_paintable_invalidate_size ((GdkPaintable *) self);
}
static gboolean
invalidate_paintable_size_on_main_cb (GstClapperPaintable *self)
{
GST_CLAPPER_PAINTABLE_LOCK (self);
self->draw_id = 0;
GST_CLAPPER_PAINTABLE_UNLOCK (self);
invalidate_paintable_size_internal (self);
return G_SOURCE_REMOVE;
}
static gboolean
update_paintable_on_main_cb (GstClapperPaintable *self)
{
gboolean size_changed;
GST_CLAPPER_PAINTABLE_LOCK (self);
/* Check if we will need to invalidate size */
if ((size_changed = self->pending_resize))
self->pending_resize = FALSE;
self->draw_id = 0;
GST_CLAPPER_PAINTABLE_UNLOCK (self);
if (size_changed)
invalidate_paintable_size_internal (self);
GST_LOG_OBJECT (self, "Invalidate paintable contents");
gdk_paintable_invalidate_contents ((GdkPaintable *) self);
return G_SOURCE_REMOVE;
}
GstClapperPaintable *
gst_clapper_paintable_new (void)
{
return g_object_new (GST_TYPE_CLAPPER_PAINTABLE, NULL);
}
void
gst_clapper_paintable_set_widget (GstClapperPaintable *self, GtkWidget *widget)
{
g_weak_ref_set (&self->widget, widget);
}
void
gst_clapper_paintable_set_importer (GstClapperPaintable *self, GstClapperImporter *importer)
{
g_weak_ref_set (&self->importer, importer);
}
void
gst_clapper_paintable_queue_draw (GstClapperPaintable *self)
{
GST_CLAPPER_PAINTABLE_LOCK (self);
if (self->draw_id > 0) {
GST_CLAPPER_PAINTABLE_UNLOCK (self);
GST_TRACE ("Already have pending draw");
return;
}
self->draw_id = g_idle_add_full (G_PRIORITY_DEFAULT,
(GSourceFunc) update_paintable_on_main_cb, self, NULL);
GST_CLAPPER_PAINTABLE_UNLOCK (self);
}
gboolean
gst_clapper_paintable_set_video_info (GstClapperPaintable *self, const GstVideoInfo *v_info)
{
GST_CLAPPER_PAINTABLE_LOCK (self);
if (gst_video_info_is_equal (&self->v_info, v_info)) {
GST_CLAPPER_PAINTABLE_UNLOCK (self);
return TRUE;
}
/* Reject info if values would cause integer overflow */
if (G_UNLIKELY (!calculate_display_par (self, v_info))) {
GST_CLAPPER_PAINTABLE_UNLOCK (self);
return FALSE;
}
self->pending_resize = TRUE;
self->v_info = *v_info;
GST_CLAPPER_PAINTABLE_UNLOCK (self);
return TRUE;
}
void
gst_clapper_paintable_set_pixel_aspect_ratio (GstClapperPaintable *self,
gint par_n, gint par_d)
{
gboolean success;
GST_CLAPPER_PAINTABLE_LOCK (self);
/* No change */
if (self->par_n == par_n && self->par_d == par_d) {
GST_CLAPPER_PAINTABLE_UNLOCK (self);
return;
}
self->par_n = par_n;
self->par_d = par_d;
/* Check if we can accept new values. This will update
* display `ratio_num` and `ratio_den` only when successful */
success = calculate_display_par (self, &self->v_info);
/* If paintable update is queued, wait for it, otherwise invalidate
* size only for change to be applied even when paused */
if (!success || self->draw_id > 0) {
self->pending_resize = success;
GST_CLAPPER_PAINTABLE_UNLOCK (self);
return;
}
self->draw_id = g_idle_add_full (G_PRIORITY_DEFAULT,
(GSourceFunc) invalidate_paintable_size_on_main_cb, self, NULL);
GST_CLAPPER_PAINTABLE_UNLOCK (self);
}
/*
* GdkPaintableInterface
*/
static void
gst_clapper_paintable_snapshot_internal (GstClapperPaintable *self,
GdkSnapshot *snapshot, gdouble width, gdouble height,
gint widget_width, gint widget_height)
{
GstClapperImporter *importer;
gfloat scale_x, scale_y;
GST_LOG_OBJECT (self, "Snapshot");
scale_x = (gfloat) width / self->display_width;
scale_y = (gfloat) height / self->display_height;
/* Apply black borders when keeping aspect ratio */
if (scale_x == scale_y || abs (scale_x - scale_y) <= FLT_EPSILON) {
if (widget_height - height > 0) {
/* XXX: Top uses integer to work with GTK rounding (not going offscreen) */
gint top_bar_height = (widget_height - height) / 2;
gdouble bottom_bar_height = (widget_height - top_bar_height - height);
gtk_snapshot_append_color (snapshot, &self->bg, &GRAPHENE_RECT_INIT (0, 0, width, -top_bar_height));
gtk_snapshot_append_color (snapshot, &self->bg, &GRAPHENE_RECT_INIT (0, height, width, bottom_bar_height));
} else if (widget_width - width > 0) {
gint left_bar_width = (widget_width - width) / 2;
gdouble right_bar_width = (widget_width - left_bar_width - width);
gtk_snapshot_append_color (snapshot, &self->bg, &GRAPHENE_RECT_INIT (0, 0, -left_bar_width, height));
gtk_snapshot_append_color (snapshot, &self->bg, &GRAPHENE_RECT_INIT (width, 0, right_bar_width, height));
}
}
if ((importer = g_weak_ref_get (&self->importer))) {
gst_clapper_importer_snapshot (importer, snapshot, width, height,
scale_x * self->pixel_aspect, scale_y);
g_object_unref (importer);
} else {
GST_LOG_OBJECT (self, "No texture importer, drawing black");
gtk_snapshot_append_color (snapshot, &self->bg, &GRAPHENE_RECT_INIT (0, 0, width, height));
}
}
static void
gst_clapper_paintable_snapshot (GdkPaintable *paintable,
GdkSnapshot *snapshot, gdouble width, gdouble height)
{
GstClapperPaintable *self = GST_CLAPPER_PAINTABLE_CAST (paintable);
GtkWidget *widget;
gint widget_width = 0, widget_height = 0;
if ((widget = g_weak_ref_get (&self->widget))) {
gint scale_factor;
scale_factor = gtk_widget_get_scale_factor (widget);
widget_width = gtk_widget_get_width (widget) * scale_factor;
widget_height = gtk_widget_get_height (widget) * scale_factor;
g_object_unref (widget);
}
gst_clapper_paintable_snapshot_internal (self, snapshot,
width, height, widget_width, widget_height);
}
static GdkPaintable *
gst_clapper_paintable_get_current_image (GdkPaintable *paintable)
{
GstClapperPaintable *self = GST_CLAPPER_PAINTABLE_CAST (paintable);
GtkSnapshot *snapshot = gtk_snapshot_new ();
/* Snapshot without widget size in order to get
* paintable without black borders */
gst_clapper_paintable_snapshot_internal (self, snapshot,
self->display_width, self->display_height, 0, 0);
return gtk_snapshot_free_to_paintable (snapshot, NULL);
}
static gint
gst_clapper_paintable_get_intrinsic_width (GdkPaintable *paintable)
{
GstClapperPaintable *self = GST_CLAPPER_PAINTABLE_CAST (paintable);
return self->display_width;
}
static gint
gst_clapper_paintable_get_intrinsic_height (GdkPaintable *paintable)
{
GstClapperPaintable *self = GST_CLAPPER_PAINTABLE_CAST (paintable);
return self->display_height;
}
static gdouble
gst_clapper_paintable_get_intrinsic_aspect_ratio (GdkPaintable *paintable)
{
GstClapperPaintable *self = GST_CLAPPER_PAINTABLE_CAST (paintable);
return self->display_aspect_ratio;
}
static void
gst_clapper_paintable_iface_init (GdkPaintableInterface *iface)
{
iface->snapshot = gst_clapper_paintable_snapshot;
iface->get_current_image = gst_clapper_paintable_get_current_image;
iface->get_intrinsic_width = gst_clapper_paintable_get_intrinsic_width;
iface->get_intrinsic_height = gst_clapper_paintable_get_intrinsic_height;
iface->get_intrinsic_aspect_ratio = gst_clapper_paintable_get_intrinsic_aspect_ratio;
}

78
lib/gst/plugin/gstclapperpaintable.h vendored Normal file
View File

@@ -0,0 +1,78 @@
/*
* Copyright (C) 2022 Rafał Dzięgiel <rafostar.github@gmail.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 the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#pragma once
#include <gtk/gtk.h>
#include <gst/gst.h>
#include <gst/video/video.h>
#include "gstclapperimporter.h"
G_BEGIN_DECLS
#define GST_TYPE_CLAPPER_PAINTABLE (gst_clapper_paintable_get_type())
G_DECLARE_FINAL_TYPE (GstClapperPaintable, gst_clapper_paintable, GST, CLAPPER_PAINTABLE, GObject)
#define GST_CLAPPER_PAINTABLE_CAST(obj) ((GstClapperPaintable *)(obj))
#define GST_CLAPPER_PAINTABLE_GET_LOCK(obj) (&GST_CLAPPER_PAINTABLE_CAST(obj)->lock)
#define GST_CLAPPER_PAINTABLE_LOCK(obj) g_mutex_lock (GST_CLAPPER_PAINTABLE_GET_LOCK(obj))
#define GST_CLAPPER_PAINTABLE_UNLOCK(obj) g_mutex_unlock (GST_CLAPPER_PAINTABLE_GET_LOCK(obj))
struct _GstClapperPaintable
{
GObject parent;
GMutex lock;
GstVideoInfo v_info;
GdkRGBA bg;
GWeakRef widget, importer;
/* Sink properties */
gint par_n, par_d;
/* For drawing overlays */
gdouble pixel_aspect;
/* Resize */
gboolean pending_resize;
guint display_ratio_num;
guint display_ratio_den;
/* GdkPaintableInterface */
gint display_width;
gint display_height;
gdouble display_aspect_ratio;
/* Pending draw signal id */
guint draw_id;
};
GstClapperPaintable * gst_clapper_paintable_new (void);
void gst_clapper_paintable_queue_draw (GstClapperPaintable *paintable);
void gst_clapper_paintable_set_widget (GstClapperPaintable *paintable, GtkWidget *widget);
void gst_clapper_paintable_set_importer (GstClapperPaintable *paintable, GstClapperImporter *importer);
gboolean gst_clapper_paintable_set_video_info (GstClapperPaintable *paintable, const GstVideoInfo *v_info);
void gst_clapper_paintable_set_pixel_aspect_ratio (GstClapperPaintable *paintable, gint par_n, gint par_d);
G_END_DECLS

910
lib/gst/plugin/gstclappersink.c vendored Normal file
View File

@@ -0,0 +1,910 @@
/*
* Copyright (C) 2022 Rafał Dzięgiel <rafostar.github@gmail.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 the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "gstclappersink.h"
#include "gstclapperimporterloader.h"
#include "gstgtkutils.h"
#define DEFAULT_FORCE_ASPECT_RATIO TRUE
#define DEFAULT_PAR_N 1
#define DEFAULT_PAR_D 1
#define DEFAULT_KEEP_LAST_FRAME FALSE
#define WINDOW_CSS_CLASS_NAME "clappersinkwindow"
enum
{
PROP_0,
PROP_WIDGET,
PROP_FORCE_ASPECT_RATIO,
PROP_PIXEL_ASPECT_RATIO,
PROP_KEEP_LAST_FRAME,
PROP_LAST
};
#define GST_CAT_DEFAULT gst_clapper_sink_debug
GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
static void gst_clapper_sink_navigation_interface_init (
GstNavigationInterface *iface);
#define parent_class gst_clapper_sink_parent_class
G_DEFINE_TYPE_WITH_CODE (GstClapperSink, gst_clapper_sink, GST_TYPE_VIDEO_SINK,
G_IMPLEMENT_INTERFACE (GST_TYPE_NAVIGATION,
gst_clapper_sink_navigation_interface_init));
GST_ELEMENT_REGISTER_DEFINE (clappersink, "clappersink", GST_RANK_NONE,
GST_TYPE_CLAPPER_SINK);
static void
window_clear_no_lock (GstClapperSink *self)
{
if (!self->window)
return;
GST_TRACE_OBJECT (self, "Window clear");
if (self->window_destroy_id) {
g_signal_handler_disconnect (self->window, self->window_destroy_id);
self->window_destroy_id = 0;
}
self->window = NULL;
self->presented_window = FALSE;
}
static void
widget_clear_no_lock (GstClapperSink *self)
{
if (!self->widget)
return;
GST_TRACE_OBJECT (self, "Widget clear");
if (self->widget_destroy_id) {
g_signal_handler_disconnect (self->widget, self->widget_destroy_id);
self->widget_destroy_id = 0;
}
g_clear_object (&self->widget);
}
static void
widget_destroy_cb (GtkWidget *widget, GstClapperSink *self)
{
GST_CLAPPER_SINK_LOCK (self);
widget_clear_no_lock (self);
GST_CLAPPER_SINK_UNLOCK (self);
}
static void
window_destroy_cb (GtkWidget *window, GstClapperSink *self)
{
GST_DEBUG_OBJECT (self, "Window destroy");
GST_CLAPPER_SINK_LOCK (self);
widget_clear_no_lock (self);
window_clear_no_lock (self);
GST_CLAPPER_SINK_UNLOCK (self);
}
static void
calculate_stream_coords (GstClapperSink *self, GtkWidget *widget,
gdouble x, gdouble y, gdouble *stream_x, gdouble *stream_y)
{
GstVideoRectangle result;
gint scaled_width, scaled_height, scale_factor;
gint video_width, video_height;
gboolean force_aspect_ratio;
GST_CLAPPER_SINK_LOCK (self);
video_width = GST_VIDEO_INFO_WIDTH (&self->v_info);
video_height = GST_VIDEO_INFO_HEIGHT (&self->v_info);
force_aspect_ratio = self->force_aspect_ratio;
GST_CLAPPER_SINK_UNLOCK (self);
scale_factor = gtk_widget_get_scale_factor (widget);
scaled_width = gtk_widget_get_width (widget) * scale_factor;
scaled_height = gtk_widget_get_height (widget) * scale_factor;
if (force_aspect_ratio) {
GstVideoRectangle src, dst;
src.x = 0;
src.y = 0;
src.w = gdk_paintable_get_intrinsic_width ((GdkPaintable *) self->paintable);
src.h = gdk_paintable_get_intrinsic_height ((GdkPaintable *) self->paintable);
dst.x = 0;
dst.y = 0;
dst.w = scaled_width;
dst.h = scaled_height;
gst_video_center_rect (&src, &dst, &result, TRUE);
} else {
result.x = 0;
result.y = 0;
result.w = scaled_width;
result.h = scaled_height;
}
/* Display coordinates to stream coordinates */
*stream_x = (result.w > 0)
? (x - result.x) / result.w * video_width
: 0;
*stream_y = (result.h > 0)
? (y - result.y) / result.h * video_height
: 0;
/* Clip to stream size */
*stream_x = CLAMP (*stream_x, 0, video_width);
*stream_y = CLAMP (*stream_y, 0, video_height);
GST_LOG ("Transform coords %fx%f => %fx%f", x, y, *stream_x, *stream_y);
}
static void
gst_clapper_sink_widget_motion_event (GtkEventControllerMotion *motion,
gdouble x, gdouble y, GstClapperSink *self)
{
GtkWidget *widget;
gdouble stream_x, stream_y;
gboolean is_inactive;
if (x == self->last_pos_x && y == self->last_pos_y)
return;
GST_OBJECT_LOCK (self);
is_inactive = (GST_STATE (self) < GST_STATE_PLAYING);
GST_OBJECT_UNLOCK (self);
if (is_inactive)
return;
self->last_pos_x = x;
self->last_pos_y = y;
widget = gtk_event_controller_get_widget ((GtkEventController *) motion);
calculate_stream_coords (self, widget, x, y, &stream_x, &stream_y);
GST_LOG ("Event \"mouse-move\", x: %f, y: %f", stream_x, stream_y);
gst_navigation_send_mouse_event ((GstNavigation *) self, "mouse-move",
0, stream_x, stream_y);
}
static void
gst_clapper_sink_widget_button_event (GtkGestureClick *click,
gint n_press, gdouble x, gdouble y, GstClapperSink *self)
{
GtkWidget *widget;
GdkEvent *event;
GdkEventType event_type;
const gchar *event_name;
gdouble stream_x, stream_y;
gboolean is_inactive;
GST_OBJECT_LOCK (self);
is_inactive = (GST_STATE (self) < GST_STATE_PLAYING);
GST_OBJECT_UNLOCK (self);
if (is_inactive)
return;
event = gtk_event_controller_get_current_event ((GtkEventController *) click);
event_type = gdk_event_get_event_type (event);
/* FIXME: Touchscreen handling should probably use new touch events from GStreamer 1.22 */
event_name = (event_type == GDK_BUTTON_PRESS || event_type == GDK_TOUCH_BEGIN)
? "mouse-button-press"
: (event_type == GDK_BUTTON_RELEASE || event_type == GDK_TOUCH_END)
? "mouse-button-release"
: NULL;
/* Can be NULL on touch */
if (!event_name)
return;
widget = gtk_event_controller_get_widget ((GtkEventController *) click);
calculate_stream_coords (self, widget, x, y, &stream_x, &stream_y);
GST_LOG ("Event \"%s\", x: %f, y: %f", event_name, stream_x, stream_y);
/* Gesture is set to handle only primary button, so we do not have to check */
gst_navigation_send_mouse_event ((GstNavigation *) self, event_name,
1, stream_x, stream_y);
}
/* Must call from main thread only with a lock */
static GtkWidget *
gst_clapper_sink_get_widget (GstClapperSink *self)
{
if (G_UNLIKELY (!self->widget)) {
GtkEventController *controller;
GtkGesture *gesture;
/* Make sure GTK is initialized */
if (!gtk_init_check ()) {
GST_ERROR_OBJECT (self, "Could not ensure GTK initialization");
return NULL;
}
self->widget = gtk_picture_new ();
/* Otherwise widget in grid will appear as a 1x1px
* video which might be misleading for users */
gtk_widget_set_hexpand (self->widget, TRUE);
gtk_widget_set_vexpand (self->widget, TRUE);
gtk_widget_set_focusable (self->widget, TRUE);
gtk_widget_set_can_focus (self->widget, TRUE);
controller = gtk_event_controller_motion_new ();
g_signal_connect (controller, "motion",
G_CALLBACK (gst_clapper_sink_widget_motion_event), self);
gtk_widget_add_controller (self->widget, controller);
gesture = gtk_gesture_click_new ();
gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (gesture), 1);
g_signal_connect (gesture, "pressed",
G_CALLBACK (gst_clapper_sink_widget_button_event), self);
g_signal_connect (gesture, "released",
G_CALLBACK (gst_clapper_sink_widget_button_event), self);
gtk_widget_add_controller (self->widget, GTK_EVENT_CONTROLLER (gesture));
/* TODO: Implement touch events once we depend on GStreamer 1.22 */
/* Take floating ref */
g_object_ref_sink (self->widget);
/* Set widget back pointer */
gst_clapper_paintable_set_widget (self->paintable, self->widget);
/* Set earlier remembered property */
gtk_picture_set_keep_aspect_ratio (GTK_PICTURE (self->widget),
self->force_aspect_ratio);
gtk_picture_set_paintable (GTK_PICTURE (self->widget), GDK_PAINTABLE (self->paintable));
self->widget_destroy_id = g_signal_connect (self->widget,
"destroy", G_CALLBACK (widget_destroy_cb), self);
}
return self->widget;
}
static GtkWidget *
gst_clapper_sink_obtain_widget (GstClapperSink *self)
{
GtkWidget *widget;
GST_CLAPPER_SINK_LOCK (self);
widget = gst_clapper_sink_get_widget (self);
if (widget)
g_object_ref (widget);
GST_CLAPPER_SINK_UNLOCK (self);
return widget;
}
static void
gst_clapper_sink_get_property (GObject *object, guint prop_id,
GValue *value, GParamSpec *pspec)
{
GstClapperSink *self = GST_CLAPPER_SINK_CAST (object);
GST_CLAPPER_SINK_LOCK (self);
switch (prop_id) {
case PROP_WIDGET:
if (self->widget) {
g_value_set_object (value, self->widget);
} else {
GtkWidget *widget;
GST_CLAPPER_SINK_UNLOCK (self);
widget = gst_gtk_invoke_on_main ((GThreadFunc) gst_clapper_sink_obtain_widget, self);
GST_CLAPPER_SINK_LOCK (self);
g_value_set_object (value, widget);
g_object_unref (widget);
}
break;
case PROP_FORCE_ASPECT_RATIO:
g_value_set_boolean (value, self->force_aspect_ratio);
break;
case PROP_PIXEL_ASPECT_RATIO:
gst_value_set_fraction (value, self->par_n, self->par_d);
break;
case PROP_KEEP_LAST_FRAME:
g_value_set_boolean (value, self->keep_last_frame);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
GST_CLAPPER_SINK_UNLOCK (self);
}
static void
gst_clapper_sink_set_property (GObject *object, guint prop_id,
const GValue *value, GParamSpec *pspec)
{
GstClapperSink *self = GST_CLAPPER_SINK_CAST (object);
GST_CLAPPER_SINK_LOCK (self);
switch (prop_id) {
case PROP_FORCE_ASPECT_RATIO:
self->force_aspect_ratio = g_value_get_boolean (value);
if (self->widget) {
gtk_picture_set_keep_aspect_ratio (GTK_PICTURE (self->widget),
self->force_aspect_ratio);
}
break;
case PROP_PIXEL_ASPECT_RATIO:
self->par_n = gst_value_get_fraction_numerator (value);
self->par_d = gst_value_get_fraction_denominator (value);
gst_clapper_paintable_set_pixel_aspect_ratio (self->paintable,
self->par_n, self->par_d);
break;
case PROP_KEEP_LAST_FRAME:
self->keep_last_frame = g_value_get_boolean (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
GST_CLAPPER_SINK_UNLOCK (self);
}
static void
gst_clapper_sink_navigation_send_event (GstNavigation *navigation,
GstStructure *structure)
{
GstClapperSink *sink = GST_CLAPPER_SINK_CAST (navigation);
GstEvent *event;
GST_TRACE_OBJECT (sink, "Navigation event: %" GST_PTR_FORMAT, structure);
event = gst_event_new_navigation (structure);
if (G_LIKELY (event)) {
GstPad *pad;
pad = gst_pad_get_peer (GST_VIDEO_SINK_PAD (sink));
if (G_LIKELY (pad)) {
if (!gst_pad_send_event (pad, gst_event_ref (event))) {
/* If upstream didn't handle the event we'll post a message with it
* for the application in case it wants to do something with it */
gst_element_post_message (GST_ELEMENT_CAST (sink),
gst_navigation_message_new_event (GST_OBJECT_CAST (sink), event));
}
gst_object_unref (pad);
}
gst_event_unref (event);
}
}
static gboolean
gst_clapper_sink_propose_allocation (GstBaseSink *bsink, GstQuery *query)
{
GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink);
GstBufferPool *pool = NULL;
GstCaps *caps;
GstVideoInfo info;
guint size, min_buffers;
gboolean need_pool;
gst_query_parse_allocation (query, &caps, &need_pool);
if (!caps) {
GST_DEBUG_OBJECT (self, "No caps specified");
return FALSE;
}
if (!gst_video_info_from_caps (&info, caps)) {
GST_DEBUG_OBJECT (self, "Invalid caps specified");
return FALSE;
}
/* Normal size of a frame */
size = GST_VIDEO_INFO_SIZE (&info);
/* We keep around current buffer and a pending one */
min_buffers = 3;
if (need_pool) {
GstStructure *config = NULL;
GST_DEBUG_OBJECT (self, "Need to create buffer pool");
GST_CLAPPER_SINK_LOCK (self);
pool = gst_clapper_importer_create_pool (self->importer, &config);
GST_CLAPPER_SINK_UNLOCK (self);
if (pool) {
/* If we did not get config, use default one */
if (!config)
config = gst_buffer_pool_get_config (pool);
gst_buffer_pool_config_set_params (config, caps, size, min_buffers, 0);
if (!gst_buffer_pool_set_config (pool, config)) {
gst_object_unref (pool);
GST_ERROR_OBJECT (self, "Failed to set config");
return FALSE;
}
} else if (config) {
GST_WARNING_OBJECT (self, "Got config without a pool to apply it");
gst_structure_free (config);
}
}
gst_query_add_allocation_pool (query, pool, size, min_buffers, 0);
if (pool)
gst_object_unref (pool);
GST_CLAPPER_SINK_LOCK (self);
gst_clapper_importer_add_allocation_metas (self->importer, query);
GST_CLAPPER_SINK_UNLOCK (self);
return TRUE;
}
static gboolean
gst_clapper_sink_query (GstBaseSink *bsink, GstQuery *query)
{
GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink);
gboolean res = FALSE;
GST_CLAPPER_SINK_LOCK (self);
if (GST_QUERY_TYPE (query) == GST_QUERY_CONTEXT) {
gboolean is_inactive;
GST_OBJECT_LOCK (self);
is_inactive = (GST_STATE (self) < GST_STATE_PAUSED);
GST_OBJECT_UNLOCK (self);
/* Some random context query in the middle of playback
* should not trigger importer replacement */
if (is_inactive)
gst_clapper_importer_loader_find_importer_for_context_query (query, &self->importer);
if (self->importer)
res = gst_clapper_importer_handle_context_query (self->importer, bsink, query);
}
GST_CLAPPER_SINK_UNLOCK (self);
if (res)
return TRUE;
return GST_BASE_SINK_CLASS (parent_class)->query (bsink, query);
}
static gboolean
gst_clapper_sink_start_on_main (GstClapperSink *self)
{
GtkWidget *widget;
GST_CLAPPER_SINK_LOCK (self);
/* Make sure widget is created */
if (!(widget = gst_clapper_sink_get_widget (self))) {
GST_CLAPPER_SINK_UNLOCK (self);
return FALSE;
}
/* When no toplevel window, make our own */
if (G_UNLIKELY (!gtk_widget_get_root (widget) && !self->window)) {
GtkWidget *toplevel, *parent;
GtkCssProvider *provider;
gchar *win_title;
if ((parent = gtk_widget_get_parent (widget))) {
GtkWidget *temp_parent;
while ((temp_parent = gtk_widget_get_parent (parent)))
parent = temp_parent;
}
toplevel = (parent) ? parent : widget;
self->window = (GtkWindow *) gtk_window_new ();
gtk_widget_add_css_class (GTK_WIDGET (self->window), WINDOW_CSS_CLASS_NAME);
provider = gtk_css_provider_new ();
gtk_css_provider_load_from_data (provider,
"." WINDOW_CSS_CLASS_NAME " { background: none; }", -1);
gtk_style_context_add_provider_for_display (
gdk_display_get_default (), GTK_STYLE_PROVIDER (provider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
g_object_unref (provider);
win_title = g_strdup_printf ("Clapper Sink - GTK %u.%u.%u Window",
gtk_get_major_version (),
gtk_get_minor_version (),
gtk_get_micro_version ());
/* Set some common default size, adding stock headerbar height
* to it in order to display 4:3 aspect video widget */
gtk_window_set_default_size (self->window, 640, 480 + 37);
gtk_window_set_title (self->window, win_title);
gtk_window_set_child (self->window, toplevel);
g_free (win_title);
self->window_destroy_id = g_signal_connect (self->window,
"destroy", G_CALLBACK (window_destroy_cb), self);
}
GST_CLAPPER_SINK_UNLOCK (self);
return TRUE;
}
static gboolean
window_present_on_main_idle (GtkWindow *window)
{
GST_INFO ("Presenting window");
gtk_window_present (window);
return G_SOURCE_REMOVE;
}
static gboolean
gst_clapper_sink_start (GstBaseSink *bsink)
{
GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink);
GST_INFO_OBJECT (self, "Start");
if (G_UNLIKELY (!(! !gst_gtk_invoke_on_main ((GThreadFunc) (GCallback)
gst_clapper_sink_start_on_main, self)))) {
GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND,
("GtkWidget could not be created"), (NULL));
return FALSE;
}
return TRUE;
}
static gboolean
gst_clapper_sink_stop_on_main (GstClapperSink *self)
{
GtkWindow *window = NULL;
GST_CLAPPER_SINK_LOCK (self);
if (self->window)
window = g_object_ref (self->window);
GST_CLAPPER_SINK_UNLOCK (self);
if (window) {
gtk_window_destroy (window);
g_object_unref (window);
}
return TRUE;
}
static gboolean
gst_clapper_sink_stop (GstBaseSink *bsink)
{
GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink);
gboolean has_window;
GST_INFO_OBJECT (self, "Stop");
GST_CLAPPER_SINK_LOCK (self);
has_window = (self->window != NULL);
GST_CLAPPER_SINK_UNLOCK (self);
if (G_UNLIKELY (has_window)) {
return (! !gst_gtk_invoke_on_main ((GThreadFunc) (GCallback)
gst_clapper_sink_stop_on_main, self));
}
return TRUE;
}
static GstStateChangeReturn
gst_clapper_sink_change_state (GstElement *element, GstStateChange transition)
{
GstClapperSink *self = GST_CLAPPER_SINK_CAST (element);
GST_DEBUG_OBJECT (self, "Changing state: %s => %s",
gst_element_state_get_name (GST_STATE_TRANSITION_CURRENT (transition)),
gst_element_state_get_name (GST_STATE_TRANSITION_NEXT (transition)));
switch (transition) {
case GST_STATE_CHANGE_PAUSED_TO_READY:
GST_CLAPPER_SINK_LOCK (self);
if (!self->keep_last_frame && self->importer) {
gst_clapper_importer_set_buffer (self->importer, NULL);
gst_clapper_paintable_queue_draw (self->paintable);
}
GST_CLAPPER_SINK_UNLOCK (self);
break;
case GST_STATE_CHANGE_PAUSED_TO_PLAYING:
GST_CLAPPER_SINK_LOCK (self);
if (G_UNLIKELY (self->window && !self->presented_window)) {
g_idle_add_full (G_PRIORITY_DEFAULT,
(GSourceFunc) window_present_on_main_idle,
g_object_ref (self->window), (GDestroyNotify) g_object_unref);
self->presented_window = TRUE;
}
GST_CLAPPER_SINK_UNLOCK (self);
break;
default:
break;
}
return GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
}
static void
gst_clapper_sink_get_times (GstBaseSink *bsink, GstBuffer *buffer,
GstClockTime *start, GstClockTime *end)
{
if (!GST_BUFFER_TIMESTAMP_IS_VALID (buffer))
return;
*start = GST_BUFFER_TIMESTAMP (buffer);
if (GST_BUFFER_DURATION_IS_VALID (buffer)) {
*end = *start + GST_BUFFER_DURATION (buffer);
} else {
GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink);
gint fps_n, fps_d;
GST_CLAPPER_SINK_LOCK (self);
fps_n = GST_VIDEO_INFO_FPS_N (&self->v_info);
fps_d = GST_VIDEO_INFO_FPS_D (&self->v_info);
GST_CLAPPER_SINK_UNLOCK (self);
if (fps_n > 0)
*end = *start + gst_util_uint64_scale_int (GST_SECOND, fps_d, fps_n);
}
}
static GstCaps *
gst_clapper_sink_get_caps (GstBaseSink *bsink, GstCaps *filter)
{
GstCaps *result, *tmp;
tmp = gst_pad_get_pad_template_caps (GST_BASE_SINK_PAD (bsink));
if (filter) {
GST_DEBUG ("Intersecting with filter caps: %" GST_PTR_FORMAT, filter);
result = gst_caps_intersect_full (filter, tmp, GST_CAPS_INTERSECT_FIRST);
gst_caps_unref (tmp);
} else {
result = tmp;
}
GST_DEBUG ("Returning caps: %" GST_PTR_FORMAT, result);
return result;
}
static gboolean
gst_clapper_sink_set_caps (GstBaseSink *bsink, GstCaps *caps)
{
GstClapperSink *self = GST_CLAPPER_SINK_CAST (bsink);
GST_INFO_OBJECT (self, "Set caps: %" GST_PTR_FORMAT, caps);
GST_CLAPPER_SINK_LOCK (self);
if (G_UNLIKELY (!self->widget)) {
GST_CLAPPER_SINK_UNLOCK (self);
GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND,
("Output widget was destroyed"), (NULL));
return FALSE;
}
if (!gst_clapper_importer_loader_find_importer_for_caps (caps, &self->importer)) {
GST_CLAPPER_SINK_UNLOCK (self);
GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND,
("No importer for given caps found"), (NULL));
return FALSE;
}
gst_clapper_paintable_set_importer (self->paintable, self->importer);
GST_CLAPPER_SINK_UNLOCK (self);
return GST_BASE_SINK_CLASS (parent_class)->set_caps (bsink, caps);
}
static gboolean
gst_clapper_sink_set_info (GstVideoSink *vsink, GstCaps *caps, const GstVideoInfo *info)
{
GstClapperSink *self = GST_CLAPPER_SINK_CAST (vsink);
gboolean res;
GST_CLAPPER_SINK_LOCK (self);
self->v_info = *info;
GST_DEBUG_OBJECT (self, "Video info changed");
res = gst_clapper_paintable_set_video_info (self->paintable, info);
GST_CLAPPER_SINK_UNLOCK (self);
return res;
}
static GstFlowReturn
gst_clapper_sink_show_frame (GstVideoSink *vsink, GstBuffer *buffer)
{
GstClapperSink *self = GST_CLAPPER_SINK_CAST (vsink);
GST_TRACE ("Got %" GST_PTR_FORMAT, buffer);
GST_CLAPPER_SINK_LOCK (self);
if (G_UNLIKELY (!self->widget)) {
GST_CLAPPER_SINK_UNLOCK (self);
GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND,
("Output widget was destroyed"), (NULL));
return GST_FLOW_ERROR;
}
gst_clapper_importer_set_buffer (self->importer, buffer);
gst_clapper_paintable_queue_draw (self->paintable);
GST_CLAPPER_SINK_UNLOCK (self);
return GST_FLOW_OK;
}
static void
gst_clapper_sink_init (GstClapperSink *self)
{
GObjectClass *gobject_class;
gobject_class = (GObjectClass *) GST_CLAPPER_SINK_GET_CLASS (self);
/* HACK: install here instead of class init to avoid GStreamer
* plugin scanner GObject type conflicts with older GTK versions */
if (!g_object_class_find_property (gobject_class, "widget")) {
g_object_class_install_property (gobject_class, PROP_WIDGET,
g_param_spec_object ("widget", "GTK Widget",
"The GtkWidget to place in the widget hierarchy",
GTK_TYPE_WIDGET, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
}
self->force_aspect_ratio = DEFAULT_FORCE_ASPECT_RATIO;
self->par_n = DEFAULT_PAR_N;
self->par_d = DEFAULT_PAR_D;
self->keep_last_frame = DEFAULT_KEEP_LAST_FRAME;
g_mutex_init (&self->lock);
gst_video_info_init (&self->v_info);
self->paintable = gst_clapper_paintable_new ();
}
static void
gst_clapper_sink_dispose (GObject *object)
{
GstClapperSink *self = GST_CLAPPER_SINK_CAST (object);
GST_CLAPPER_SINK_LOCK (self);
window_clear_no_lock (self);
widget_clear_no_lock (self);
g_clear_object (&self->paintable);
gst_clear_object (&self->importer);
GST_CLAPPER_SINK_UNLOCK (self);
GST_CALL_PARENT (G_OBJECT_CLASS, dispose, (object));
}
static void
gst_clapper_sink_finalize (GObject *object)
{
GstClapperSink *self = GST_CLAPPER_SINK_CAST (object);
GST_TRACE ("Finalize");
gst_clapper_importer_loader_unload_all ();
g_mutex_clear (&self->lock);
GST_CALL_PARENT (G_OBJECT_CLASS, finalize, (object));
}
static void
gst_clapper_sink_class_init (GstClapperSinkClass *klass)
{
GstPadTemplate *sink_pad_templ;
GObjectClass *gobject_class = (GObjectClass *) klass;
GstElementClass *gstelement_class = (GstElementClass *) klass;
GstBaseSinkClass *gstbasesink_class = (GstBaseSinkClass *) klass;
GstVideoSinkClass *gstvideosink_class = (GstVideoSinkClass *) klass;
GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clappersink", 0,
"Clapper Sink");
gobject_class->get_property = gst_clapper_sink_get_property;
gobject_class->set_property = gst_clapper_sink_set_property;
gobject_class->dispose = gst_clapper_sink_dispose;
gobject_class->finalize = gst_clapper_sink_finalize;
g_object_class_install_property (gobject_class, PROP_FORCE_ASPECT_RATIO,
g_param_spec_boolean ("force-aspect-ratio", "Force aspect ratio",
"When enabled, scaling will respect original aspect ratio",
DEFAULT_FORCE_ASPECT_RATIO,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (gobject_class, PROP_PIXEL_ASPECT_RATIO,
gst_param_spec_fraction ("pixel-aspect-ratio", "Pixel Aspect Ratio",
"The pixel aspect ratio of the device",
DEFAULT_PAR_N, DEFAULT_PAR_D,
G_MAXINT, 1, 1, 1, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (gobject_class, PROP_KEEP_LAST_FRAME,
g_param_spec_boolean ("keep-last-frame", "Keep last frame",
"Keep showing last video frame after playback instead of black screen",
DEFAULT_KEEP_LAST_FRAME,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
gstelement_class->change_state = gst_clapper_sink_change_state;
gstbasesink_class->get_caps = gst_clapper_sink_get_caps;
gstbasesink_class->set_caps = gst_clapper_sink_set_caps;
gstbasesink_class->get_times = gst_clapper_sink_get_times;
gstbasesink_class->propose_allocation = gst_clapper_sink_propose_allocation;
gstbasesink_class->query = gst_clapper_sink_query;
gstbasesink_class->start = gst_clapper_sink_start;
gstbasesink_class->stop = gst_clapper_sink_stop;
gstvideosink_class->set_info = gst_clapper_sink_set_info;
gstvideosink_class->show_frame = gst_clapper_sink_show_frame;
gst_element_class_set_metadata (gstelement_class,
"Clapper video sink",
"Sink/Video", "A GTK4 video sink used by Clapper media player",
"Rafał Dzięgiel <rafostar.github@gmail.com>");
sink_pad_templ = gst_clapper_importer_loader_make_sink_pad_template ();
gst_element_class_add_pad_template (gstelement_class, sink_pad_templ);
}
/*
* GstNavigationInterface
*/
static void
gst_clapper_sink_navigation_interface_init (GstNavigationInterface *iface)
{
/* TODO: Port to "send_event_simple" once we depend on GStreamer 1.22 */
iface->send_event = gst_clapper_sink_navigation_send_event;
}

72
lib/gst/plugin/gstclappersink.h vendored Normal file
View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2022 Rafał Dzięgiel <rafostar.github@gmail.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 the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#pragma once
#include <gtk/gtk.h>
#include <gst/gst.h>
#include <gst/video/gstvideosink.h>
#include <gst/video/video.h>
#include "gstclapperpaintable.h"
#include "gstclapperimporter.h"
G_BEGIN_DECLS
#define GST_TYPE_CLAPPER_SINK (gst_clapper_sink_get_type())
G_DECLARE_FINAL_TYPE (GstClapperSink, gst_clapper_sink, GST, CLAPPER_SINK, GstVideoSink)
#define GST_CLAPPER_SINK_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_CLAPPER_SINK, GstClapperSinkClass))
#define GST_CLAPPER_SINK_CAST(obj) ((GstClapperSink *)(obj))
#define GST_CLAPPER_SINK_GET_LOCK(obj) (&GST_CLAPPER_SINK_CAST(obj)->lock)
#define GST_CLAPPER_SINK_LOCK(obj) g_mutex_lock (GST_CLAPPER_SINK_GET_LOCK(obj))
#define GST_CLAPPER_SINK_UNLOCK(obj) g_mutex_unlock (GST_CLAPPER_SINK_GET_LOCK(obj))
struct _GstClapperSink
{
GstVideoSink parent;
GMutex lock;
GstClapperPaintable *paintable;
GstClapperImporter *importer;
GstVideoInfo v_info;
GtkWidget *widget;
GtkWindow *window;
gboolean presented_window;
/* Properties */
gboolean force_aspect_ratio;
gint par_n, par_d;
gboolean keep_last_frame;
/* Position coords */
gdouble last_pos_x;
gdouble last_pos_y;
gulong widget_destroy_id;
gulong window_destroy_id;
};
GST_ELEMENT_REGISTER_DECLARE (clappersink);
G_END_DECLS

37
lib/gst/plugin/gstgdkformats.h vendored Normal file
View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) 2022 Rafał Dzięgiel <rafostar.github@gmail.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 the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include <glib.h>
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
#define GST_GDK_MEMORY_ENDIAN_FORMATS "RGBA64_LE"
#define GST_GDK_GL_TEXTURE_ENDIAN_FORMATS "RGBA64_LE"
#elif G_BYTE_ORDER == G_BIG_ENDIAN
#define GST_GDK_MEMORY_ENDIAN_FORMATS "RGBA64_BE"
#define GST_GDK_GL_TEXTURE_ENDIAN_FORMATS "RGBA64_BE"
#endif
#define GST_GDK_MEMORY_FORMATS \
GST_GDK_MEMORY_ENDIAN_FORMATS ", " \
"ABGR, BGRA, ARGB, RGBA, BGRx, RGBx, BGR, RGB"
/* Formats that `GdkGLTexture` supports */
#define GST_GDK_GL_TEXTURE_FORMATS \
GST_GDK_GL_TEXTURE_ENDIAN_FORMATS ", " \
"RGBA, RGBx, RGB"

132
lib/gst/plugin/gstgtkutils.c vendored Normal file
View File

@@ -0,0 +1,132 @@
/*
* GStreamer
* Copyright (C) 2015 Matthew Waters <matthew@centricular.com>
* Copyright (C) 2015 Thibault Saunier <tsaunier@gnome.org>
* Copyright (C) 2022 Rafał Dzięgiel <rafostar.github@gmail.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 the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "gstgtkutils.h"
struct invoke_context
{
GThreadFunc func;
gpointer data;
GMutex lock;
GCond cond;
gboolean fired;
gpointer res;
};
static gboolean
gst_gtk_invoke_func (struct invoke_context *info)
{
g_mutex_lock (&info->lock);
info->res = info->func (info->data);
info->fired = TRUE;
g_cond_signal (&info->cond);
g_mutex_unlock (&info->lock);
return G_SOURCE_REMOVE;
}
gpointer
gst_gtk_invoke_on_main (GThreadFunc func, gpointer data)
{
GMainContext *main_context = g_main_context_default ();
struct invoke_context info;
g_mutex_init (&info.lock);
g_cond_init (&info.cond);
info.fired = FALSE;
info.func = func;
info.data = data;
g_main_context_invoke (main_context, (GSourceFunc) gst_gtk_invoke_func,
&info);
g_mutex_lock (&info.lock);
while (!info.fired)
g_cond_wait (&info.cond, &info.lock);
g_mutex_unlock (&info.lock);
g_mutex_clear (&info.lock);
g_cond_clear (&info.cond);
return info.res;
}
/* For use with `GdkMemoryTexture` only! */
GdkMemoryFormat
gst_video_format_to_gdk_memory_format (GstVideoFormat format)
{
switch (format) {
case GST_VIDEO_FORMAT_RGBA64_LE:
case GST_VIDEO_FORMAT_RGBA64_BE:
return GDK_MEMORY_R16G16B16A16_PREMULTIPLIED;
case GST_VIDEO_FORMAT_RGBA:
return GDK_MEMORY_R8G8B8A8;
case GST_VIDEO_FORMAT_BGRA:
return GDK_MEMORY_B8G8R8A8;
case GST_VIDEO_FORMAT_ARGB:
return GDK_MEMORY_A8R8G8B8;
case GST_VIDEO_FORMAT_ABGR:
return GDK_MEMORY_A8B8G8R8;
case GST_VIDEO_FORMAT_RGBx:
return GDK_MEMORY_R8G8B8A8_PREMULTIPLIED;
case GST_VIDEO_FORMAT_BGRx:
return GDK_MEMORY_B8G8R8A8_PREMULTIPLIED;
case GST_VIDEO_FORMAT_RGB:
return GDK_MEMORY_R8G8B8;
case GST_VIDEO_FORMAT_BGR:
return GDK_MEMORY_B8G8R8;
default:
break;
}
/* This should never happen as long as above switch statement
* is updated when new formats are added to caps */
g_assert_not_reached ();
/* Fallback format */
return GDK_MEMORY_R8G8B8A8_PREMULTIPLIED;
}
GdkTexture *
gst_video_frame_into_gdk_texture (GstVideoFrame *frame)
{
GdkTexture *texture;
GBytes *bytes;
bytes = g_bytes_new_with_free_func (
GST_VIDEO_FRAME_PLANE_DATA (frame, 0),
GST_VIDEO_FRAME_HEIGHT (frame) * GST_VIDEO_FRAME_PLANE_STRIDE (frame, 0),
(GDestroyNotify) gst_buffer_unref,
gst_buffer_ref (frame->buffer));
texture = gdk_memory_texture_new (
GST_VIDEO_FRAME_WIDTH (frame),
GST_VIDEO_FRAME_HEIGHT (frame),
gst_video_format_to_gdk_memory_format (GST_VIDEO_FRAME_FORMAT (frame)),
bytes,
GST_VIDEO_FRAME_PLANE_STRIDE (frame, 0));
g_bytes_unref (bytes);
return texture;
}

37
lib/gst/plugin/gstgtkutils.h vendored Normal file
View File

@@ -0,0 +1,37 @@
/*
* GStreamer
* Copyright (C) 2015 Matthew Waters <matthew@centricular.com>
* Copyright (C) 2015 Thibault Saunier <tsaunier@gnome.org>
* Copyright (C) 2022 Rafał Dzięgiel <rafostar.github@gmail.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 the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#pragma once
#include <glib.h>
#include <gtk/gtk.h>
#include <gst/video/video.h>
G_BEGIN_DECLS
gpointer gst_gtk_invoke_on_main (GThreadFunc func, gpointer data);
GdkMemoryFormat gst_video_format_to_gdk_memory_format (GstVideoFormat format);
GdkTexture * gst_video_frame_into_gdk_texture (GstVideoFrame *frame);
G_END_DECLS

43
lib/gst/plugin/gstplugin.c vendored Normal file
View File

@@ -0,0 +1,43 @@
/*
* Copyright (C) 2022 Rafał Dzięgiel <rafostar.github@gmail.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 the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <gmodule.h>
#include "gstclappersink.h"
static gboolean
plugin_init (GstPlugin *plugin)
{
if (!g_module_supported ())
return FALSE;
gst_plugin_add_dependency_simple (plugin,
NULL, CLAPPER_SINK_IMPORTER_PATH, NULL,
GST_PLUGIN_DEPENDENCY_FLAG_NONE);
return GST_ELEMENT_REGISTER (clappersink, plugin);
}
GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, GST_VERSION_MINOR,
clapper, "Clapper elements", plugin_init, VERSION, "LGPL",
GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)

70
lib/gst/plugin/meson.build vendored Normal file
View File

@@ -0,0 +1,70 @@
gst_plugins_libdir = join_paths(prefix, libdir, 'gstreamer-1.0')
gst_clapper_plugin_args = [
'-DHAVE_CONFIG_H',
'-DGST_USE_UNSTABLE_API',
]
gtk4_dep = dependency('gtk4', version: '>=4.6.0', required: false)
gmodule_dep = dependency('gmodule-2.0',
version: glib_req,
required: false,
fallback: ['glib', 'libgmodule_dep'],
)
gst_clapper_plugin_deps = [
gtk4_dep,
gst_dep,
gstbase_dep,
gstvideo_dep,
gmodule_dep,
]
foreach dep : gst_clapper_plugin_deps
if not dep.found()
if gst_clapper_plugin_option.enabled()
error('GStreamer plugin was enabled, but required dependencies were not found')
endif
subdir_done()
endif
endforeach
if get_option('default_library') == 'static'
gst_clapper_plugin_args += ['-DGST_STATIC_COMPILATION']
endif
gst_clapper_plugin_option = get_option('gst-plugin')
if gst_clapper_plugin_option.disabled()
subdir_done()
endif
foreach dep : gst_clapper_plugin_deps
if not dep.found()
if gst_clapper_plugin_option.enabled()
error('GStreamer plugin was enabled, but required dependencies were not found')
endif
subdir_done()
endif
endforeach
gst_clapper_plugin_sources = [
'gstclappersink.c',
'gstclapperpaintable.c',
'gstgtkutils.c',
'gstplugin.c',
'gstclapperimporter.c',
'gstclapperimporterloader.c',
]
gst_clapper_sink_dep = declare_dependency(
link_with: library('gstclapper',
gst_clapper_plugin_sources,
c_args: gst_clapper_plugin_args,
include_directories: configinc,
dependencies: gst_clapper_plugin_deps,
install: true,
install_dir: gst_plugins_libdir,
),
include_directories: configinc,
dependencies: gst_clapper_plugin_deps,
)

24
lib/meson.build vendored
View File

@@ -1,5 +1,5 @@
glib_req = '>= 2.56.0'
gst_req = '>= 1.18.0'
glib_req = '>= 2.68.0'
gst_req = '>= 1.20.0'
api_version = '1.0'
libversion = meson.project_version()
@@ -42,10 +42,6 @@ endif
# Symbol visibility
if cc.get_id() == 'msvc'
export_define = '__declspec(dllexport) extern'
elif cc.has_argument('-fvisibility=hidden')
add_project_arguments('-fvisibility=hidden', language: 'c')
add_project_arguments('-fvisibility=hidden', language: 'cpp')
export_define = 'extern __attribute__ ((visibility ("default")))'
else
export_define = 'extern'
endif
@@ -132,7 +128,7 @@ cdata.set('SIZEOF_SHORT', cc.sizeof('short'))
cdata.set('SIZEOF_VOIDP', cc.sizeof('void*'))
cdata.set_quoted('VERSION', libversion)
cdata.set_quoted('PACKAGE', 'gst-plugins-clapper')
cdata.set_quoted('PACKAGE', 'clapper')
cdata.set_quoted('PACKAGE_VERSION', libversion)
cdata.set_quoted('PACKAGE_BUGREPORT', 'https://github.com/Rafostar/clapper/issues/new')
cdata.set_quoted('PACKAGE_NAME', 'GStreamer Clapper Libs')
@@ -184,7 +180,7 @@ foreach extra_arg : warning_flags
endif
endforeach
cdata.set_quoted('GST_PACKAGE_NAME', 'GStreamer Plugins Clapper')
cdata.set_quoted('GST_PACKAGE_NAME', 'gst-plugin-clapper')
cdata.set_quoted('GST_PACKAGE_ORIGIN', 'https://github.com/Rafostar/clapper')
# Mandatory GST deps
@@ -251,21 +247,21 @@ giounix_dep = dependency('gio-unix-2.0', version: glib_req, fallback: ['glib', '
cdata.set('DISABLE_ORC', 1)
cdata.set('GST_ENABLE_EXTRA_CHECKS', get_option('devel-checks'))
cdata.set_quoted('GST_PACKAGE_RELEASE_DATETIME', 'Unknown')
configinc = include_directories('.')
libsinc = include_directories('gst')
gir = find_program('g-ir-scanner', required: true)
if not gir.found()
error('Clapper requires GI bindings to be compiled')
endif
gir = find_program('g-ir-scanner')
gir_init_section = ['--add-init-section=extern void gst_init(gint*,gchar**);' + \
'g_setenv("GST_REGISTRY_1.0", "@0@", TRUE);'.format(meson.current_build_dir() + '/gir_empty_registry.reg') + \
'g_setenv("GST_PLUGIN_PATH_1_0", "", TRUE);' + \
'g_setenv("GST_PLUGIN_SYSTEM_PATH_1_0", "", TRUE);' + \
'gst_init(NULL,NULL);', '--quiet'
]
gst_clapper_plugin_libdir = join_paths(get_option('prefix'), libdir, 'clapper-0.0', 'gst', 'plugin')
gst_clapper_importers_libdir = join_paths(gst_clapper_plugin_libdir, 'importers')
cdata.set_quoted('CLAPPER_SINK_IMPORTER_PATH', gst_clapper_importers_libdir)
subdir('gst')
configure_file(output: 'config.h', configuration: cdata)

View File

@@ -19,9 +19,7 @@ datadir = join_paths(get_option('prefix'), get_option('datadir'))
pkglibdir = join_paths(libdir, meson.project_name())
pkgdatadir = join_paths(datadir, meson.project_name())
if get_option('lib')
subdir('lib')
endif
subdir('lib')
if get_option('player')
subdir('bin')

View File

@@ -8,6 +8,12 @@ option('lib',
value: true,
description: 'Build GstClapper lib'
)
option('gst-plugin',
type: 'feature',
value: 'enabled',
description: 'Build GStreamer plugin (includes GTK video sink element)'
)
option('devel-checks',
type: 'boolean',
value: false,