qml6gloverlay: support directly passing a QQuickItem for QML the render tree

This is provided in addition to the text description in the qml-scene.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/9180>
This commit is contained in:
Matthew Waters 2025-05-08 21:19:38 +10:00
parent 97a193c3d8
commit 9f055e1eeb
11 changed files with 360 additions and 43 deletions

View File

@ -12439,7 +12439,7 @@
"writable": true
},
"root-item": {
"blurb": "The root QQuickItem from the qml-scene used to render",
"blurb": "On set, provides the QML scene to render. If not set (!= NULL), then the qml-scene property is used to construct the QML scene. In both cases, the returned value is the root QQuickItem used for rendering.",
"conditionally-available": false,
"construct": false,
"construct-only": false,
@ -12447,7 +12447,7 @@
"mutable": "null",
"readable": true,
"type": "gpointer",
"writable": false
"writable": true
},
"widget": {
"blurb": "The QQuickItem to place the input video in the object hierarchy",

View File

@ -203,8 +203,10 @@ gst_qml6_gl_overlay_class_init (GstQml6GLOverlayClass * klass)
g_object_class_install_property (gobject_class, PROP_ROOT_ITEM,
g_param_spec_pointer ("root-item", "QQuickItem",
"The root QQuickItem from the qml-scene used to render",
(GParamFlags) (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)));
"On set, provides the QML scene to render. If not set (!= NULL), "
"then the qml-scene property is used to construct the QML scene. "
"In both cases, the returned value is the root QQuickItem used for rendering.",
(GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
/**
* GstQmlGLOverlay::qml-scene-initialized
@ -246,6 +248,7 @@ gst_qml6_gl_overlay_init (GstQml6GLOverlay * qml6_gl_overlay)
{
qml6_gl_overlay->widget = QSharedPointer<Qt6GLVideoItemInterface>();
qml6_gl_overlay->qml_scene = NULL;
qml6_gl_overlay->renderer = new GstQt6QuickRenderer;
}
static void
@ -258,6 +261,9 @@ gst_qml6_gl_overlay_finalize (GObject * object)
qml6_gl_overlay->widget.clear();
delete qml6_gl_overlay->renderer;
qml6_gl_overlay->renderer = NULL;
G_OBJECT_CLASS (parent_class)->finalize (object);
}
@ -280,6 +286,12 @@ gst_qml6_gl_overlay_set_property (GObject * object, guint prop_id,
g_free (qml6_gl_overlay->qml_scene);
qml6_gl_overlay->qml_scene = g_value_dup_string (value);
break;
case PROP_ROOT_ITEM: {
GST_OBJECT_LOCK (qml6_gl_overlay);
qml6_gl_overlay->root_item = (QQuickItem *) g_value_get_pointer (value);
GST_OBJECT_UNLOCK (qml6_gl_overlay);
break;
}
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@ -305,19 +317,19 @@ gst_qml6_gl_overlay_get_property (GObject * object, guint prop_id,
case PROP_QML_SCENE:
g_value_set_string (value, qml6_gl_overlay->qml_scene);
break;
case PROP_ROOT_ITEM:
case PROP_ROOT_ITEM: {
QQuickItem *root;
GST_OBJECT_LOCK (qml6_gl_overlay);
if (qml6_gl_overlay->renderer) {
QQuickItem *root = qml6_gl_overlay->renderer->rootItem();
if (root)
g_value_set_pointer (value, root);
else
g_value_set_pointer (value, NULL);
} else {
root = qml6_gl_overlay->renderer->rootItem();
if (root)
g_value_set_pointer (value, root);
else if (qml6_gl_overlay->root_item)
g_value_set_pointer (value, qml6_gl_overlay->root_item);
else
g_value_set_pointer (value, NULL);
}
GST_OBJECT_UNLOCK (qml6_gl_overlay);
break;
}
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@ -330,11 +342,13 @@ gst_qml6_gl_overlay_gl_start (GstGLBaseFilter * bfilter)
GstQml6GLOverlay *qml6_gl_overlay = GST_QML6_GL_OVERLAY (bfilter);
QQuickItem *root;
GError *error = NULL;
gboolean have_scene = FALSE;
GST_TRACE_OBJECT (bfilter, "using scene:\n%s", qml6_gl_overlay->qml_scene);
if (!qml6_gl_overlay->qml_scene || g_strcmp0 (qml6_gl_overlay->qml_scene, "") == 0) {
GST_ELEMENT_ERROR (bfilter, RESOURCE, NOT_FOUND, ("qml-scene property not set"), (NULL));
have_scene = qml6_gl_overlay->qml_scene && g_strcmp0 (qml6_gl_overlay->qml_scene, "") != 0;
if (!have_scene && qml6_gl_overlay->root_item == NULL) {
GST_ELEMENT_ERROR (bfilter, RESOURCE, NOT_FOUND, ("root-item and qml-scene properties not set"), (NULL));
return FALSE;
}
@ -342,33 +356,44 @@ gst_qml6_gl_overlay_gl_start (GstGLBaseFilter * bfilter)
return FALSE;
GST_OBJECT_LOCK (bfilter);
qml6_gl_overlay->renderer = new GstQt6QuickRenderer;
if (!qml6_gl_overlay->renderer->init (bfilter->context, &error)) {
GST_ELEMENT_ERROR (GST_ELEMENT (bfilter), RESOURCE, NOT_FOUND,
("%s", error->message), (NULL));
delete qml6_gl_overlay->renderer;
qml6_gl_overlay->renderer = NULL;
qml6_gl_overlay->renderer = new GstQt6QuickRenderer;
GST_OBJECT_UNLOCK (bfilter);
return FALSE;
}
/* FIXME: Qml may do async loading and we need to propagate qml errors in that case as well */
if (!qml6_gl_overlay->renderer->setQmlScene (qml6_gl_overlay->qml_scene, &error)) {
GST_ELEMENT_ERROR (GST_ELEMENT (bfilter), RESOURCE, NOT_FOUND,
("%s", error->message), (NULL));
goto fail_renderer;
return FALSE;
if (have_scene) {
if (!qml6_gl_overlay->renderer->setQmlScene (qml6_gl_overlay->qml_scene, &error)) {
GST_ELEMENT_ERROR (GST_ELEMENT (bfilter), RESOURCE, NOT_FOUND,
("%s", error->message), (NULL));
goto fail_renderer;
return FALSE;
}
root = qml6_gl_overlay->renderer->rootItem();
if (!root) {
GST_ELEMENT_ERROR (GST_ELEMENT (bfilter), RESOURCE, NOT_FOUND,
("Qml scene does not have a root item"), (NULL));
goto fail_renderer;
}
GST_OBJECT_UNLOCK (bfilter);
g_object_notify (G_OBJECT (qml6_gl_overlay), "root-item");
} else {
root = qml6_gl_overlay->root_item;
if (!root) {
GST_ELEMENT_ERROR (GST_ELEMENT (bfilter), RESOURCE, NOT_FOUND,
("No root item provided"), (NULL));
goto fail_renderer;
}
qml6_gl_overlay->renderer->setRootItem(root);
GST_OBJECT_UNLOCK (bfilter);
}
root = qml6_gl_overlay->renderer->rootItem();
if (!root) {
GST_ELEMENT_ERROR (GST_ELEMENT (bfilter), RESOURCE, NOT_FOUND,
("Qml scene does not have a root item"), (NULL));
goto fail_renderer;
}
GST_OBJECT_UNLOCK (bfilter);
g_object_notify (G_OBJECT (qml6_gl_overlay), "root-item");
g_signal_emit (qml6_gl_overlay, gst_qml6_gl_overlay_signals[SIGNAL_QML_SCENE_INITIALIZED], 0);
GST_OBJECT_LOCK (bfilter);
@ -385,7 +410,7 @@ fail_renderer:
{
qml6_gl_overlay->renderer->cleanup();
delete qml6_gl_overlay->renderer;
qml6_gl_overlay->renderer = NULL;
qml6_gl_overlay->renderer = new GstQt6QuickRenderer;
GST_OBJECT_UNLOCK (bfilter);
return FALSE;
}
@ -399,9 +424,10 @@ gst_qml6_gl_overlay_gl_stop (GstGLBaseFilter * bfilter)
/* notify before actually destroying anything */
GST_OBJECT_LOCK (qml6_gl_overlay);
if (qml6_gl_overlay->renderer)
if (qml6_gl_overlay->renderer) {
renderer = qml6_gl_overlay->renderer;
qml6_gl_overlay->renderer = NULL;
qml6_gl_overlay->renderer = new GstQt6QuickRenderer;
}
GST_OBJECT_UNLOCK (qml6_gl_overlay);
g_signal_emit (qml6_gl_overlay, gst_qml6_gl_overlay_signals[SIGNAL_QML_SCENE_DESTROYED], 0);

View File

@ -54,6 +54,7 @@ struct _GstQml6GLOverlay
gchar *qml_scene;
GstQt6QuickRenderer *renderer;
QQuickItem *root_item;
QSharedPointer<Qt6GLVideoItemInterface> widget;
};

View File

@ -201,6 +201,7 @@ GstQt6QuickRenderer::GstQt6QuickRenderer()
m_qmlEngine(nullptr),
m_qmlComponent(nullptr),
m_rootItem(nullptr),
m_setRootItem(nullptr),
gl_allocator(NULL),
gl_params(NULL),
gl_mem(NULL),
@ -414,11 +415,6 @@ bool GstQt6QuickRenderer::init (GstGLContext * context, GError ** error)
m_renderControl->prepareThread (m_sharedRenderData->m_renderThread);
g_mutex_unlock (&m_sharedRenderData->lock);
/* Create a QML engine. */
m_qmlEngine = new QQmlEngine;
if (!m_qmlEngine->incubationController())
m_qmlEngine->setIncubationController(m_quickWindow->incubationController());
/* TODO: use buffer pool */
gl_context = static_cast<GstGLContext*>(gst_object_ref (context));
gl_allocator = (GstGLBaseMemoryAllocator *) gst_gl_memory_allocator_get_default (gl_context);
@ -468,7 +464,8 @@ bool GstQt6QuickRenderer::init (GstGLContext * context, GError ** error)
GstQt6QuickRenderer::~GstQt6QuickRenderer()
{
gst_gl_allocation_params_free (gl_params);
if (gl_params)
gst_gl_allocation_params_free (gl_params);
gst_clear_object (&gl_allocator);
}
@ -536,9 +533,10 @@ void GstQt6QuickRenderer::cleanup()
if (m_qmlEngine)
delete m_qmlEngine;
m_qmlEngine = nullptr;
if (m_rootItem)
if (m_rootItem && !m_setRootItem)
delete m_rootItem;
m_rootItem = nullptr;
m_setRootItem = nullptr;
if (gl_context)
gst_gl_context_thread_add (gl_context,
@ -583,7 +581,6 @@ GstQt6QuickRenderer::renderGstGL ()
gl_params = (GstGLAllocationParams *)
gst_gl_video_allocation_params_new (gl_context,
NULL, &this->v_info, 0, NULL, GST_GL_TEXTURE_TARGET_2D, GST_GL_RGBA8);
gl_mem = (GstGLMemory *) gst_gl_base_memory_alloc (gl_allocator, gl_params);
m_quickWindow->setRenderTarget(QQuickRenderTarget::fromOpenGLTexture(gst_gl_memory_get_texture_id (gl_mem), gl_params_get_QSize(gl_params)));
@ -697,6 +694,11 @@ void GstQt6QuickRenderer::initializeQml()
return;
}
initializeWinsys();
}
void GstQt6QuickRenderer::initializeWinsys()
{
/* The root item is ready. Associate it with the window. */
m_rootItem->setParentItem(m_quickWindow->contentItem());
@ -736,10 +738,18 @@ void GstQt6QuickRenderer::setSize(int w, int h)
bool GstQt6QuickRenderer::setQmlScene (const gchar * scene, GError ** error)
{
/* replacing the scene is not supported */
g_return_val_if_fail (m_qmlEngine == NULL, false);
g_return_val_if_fail (m_qmlComponent == NULL, false);
g_return_val_if_fail (m_setRootItem == NULL, false);
g_return_val_if_fail (m_rootItem == NULL, false);
m_errorString = "";
/* Create a QML engine. */
m_qmlEngine = new QQmlEngine;
if (!m_qmlEngine->incubationController())
m_qmlEngine->setIncubationController(m_quickWindow->incubationController());
m_qmlComponent = new QQmlComponent(m_qmlEngine);
/* XXX: do we need to provide a propper base name? */
m_qmlComponent->setData(QByteArray (scene), QUrl(""));
@ -760,6 +770,20 @@ bool GstQt6QuickRenderer::setQmlScene (const gchar * scene, GError ** error)
return TRUE;
}
bool GstQt6QuickRenderer::setRootItem(QQuickItem *root)
{
g_return_val_if_fail (m_qmlEngine == NULL, false);
g_return_val_if_fail (m_qmlComponent == NULL, false);
m_setRootItem = root;
m_rootItem = root;
if (m_rootItem)
initializeWinsys();
return true;
}
QQuickItem * GstQt6QuickRenderer::rootItem() const
{
return m_rootItem;

View File

@ -62,8 +62,9 @@ public:
void cleanup();
/* retrieve the rootItem from the qml scene. Only valid after
* setQmlScene() has been successfully called */
* setQmlScene() has been successfully called, or setRootItem() */
QQuickItem *rootItem() const;
bool setRootItem(QQuickItem *root);
private slots:
void initializeQml();
@ -77,6 +78,7 @@ private:
static void render_gst_gl_c (GstGLContext * context, GstQt6QuickRenderer * self) { self->renderGstGL (); }
void renderGstGL ();
void initializeWinsys();
static void initialize_gst_gl_c (GstGLContext * context, GstQt6QuickRenderer * self) { self->initializeGstGL (); }
void initializeGstGL ();
@ -97,6 +99,7 @@ private:
QQmlEngine *m_qmlEngine;
QQmlComponent *m_qmlComponent;
QQuickItem *m_rootItem;
QQuickItem *m_setRootItem;
GstGLBaseMemoryAllocator *gl_allocator;
GstGLAllocationParams *gl_params;

View File

@ -17,4 +17,5 @@ endif
subdir('qmlsink')
subdir('qmlsrc')
subdir('qmloverlay')
subdir('qmloverlayitem')
subdir('qmlmixer')

View File

@ -0,0 +1,125 @@
// SPDX-License-Identifier: BSD-2-Clause
//
// Copyright (C) 2025, Matthew Waters <matthew@centricular.com>
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// a) Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// b) Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in
// the documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
#include <QApplication>
#include <QQmlApplicationEngine>
#include <QQuickWindow>
#include <QQuickItem>
#include <QRunnable>
#include <QDirIterator>
#include <gst/gst.h>
class SetPlaying : public QRunnable
{
public:
SetPlaying(GstElement *);
~SetPlaying();
void run ();
private:
GstElement * pipeline_;
};
SetPlaying::SetPlaying (GstElement * pipeline)
{
this->pipeline_ = pipeline ? static_cast<GstElement *> (gst_object_ref (pipeline)) : NULL;
}
SetPlaying::~SetPlaying ()
{
if (this->pipeline_)
gst_object_unref (this->pipeline_);
}
void
SetPlaying::run ()
{
if (this->pipeline_)
gst_element_set_state (this->pipeline_, GST_STATE_PLAYING);
}
int main(int argc, char *argv[])
{
int ret;
gst_init (&argc, &argv);
{
QGuiApplication app(argc, argv);
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);
GstElement *pipeline = gst_pipeline_new (NULL);
GstElement *src = gst_element_factory_make ("videotestsrc", NULL);
GstElement *capsfilter = gst_element_factory_make ("capsfilter", NULL);
GstCaps *caps = gst_caps_from_string ("video/x-raw,format=RGBA");
g_object_set (capsfilter, "caps", caps, NULL);
gst_clear_caps (&caps);
GstElement *glupload = gst_element_factory_make ("glupload", NULL);
/* the plugin must be loaded before loading the qml file to register the
* GstGLVideoItem qml item */
GstElement *overlay = gst_element_factory_make ("qml6gloverlay", NULL);
GstElement *sink = gst_element_factory_make ("qml6glsink", NULL);
g_assert (src && glupload && overlay && sink);
gst_bin_add_many (GST_BIN (pipeline), src, capsfilter, glupload, overlay, sink, NULL);
gst_element_link_many (src, capsfilter, glupload, overlay, sink, NULL);
/* load qmlglsink output */
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
QQuickItem *videoItem;
QQuickWindow *rootObject;
/* find and set the videoItem on the sink */
rootObject = static_cast<QQuickWindow *> (engine.rootObjects().first());
videoItem = rootObject->findChild<QQuickItem *> ("videoItem");
g_assert (videoItem);
g_object_set(sink, "widget", videoItem, NULL);
/* load qmlgloverlay contents */
QQmlComponent overlay_component = QQmlComponent(&engine, QUrl(QStringLiteral("qrc:/overlay.qml")));
QQuickItem *overlay_item = qobject_cast<QQuickItem *>(overlay_component.create());
QQuickItem *inputVideoItem = overlay_item->findChild<QQuickItem *> ("inputVideoItem");
g_assert (inputVideoItem);
g_object_set (overlay, "root-item", overlay_item, "widget", inputVideoItem, NULL);
rootObject->scheduleRenderJob (new SetPlaying (pipeline),
QQuickWindow::BeforeSynchronizingStage);
ret = app.exec();
gst_element_set_state (pipeline, GST_STATE_NULL);
gst_object_unref (pipeline);
}
gst_deinit ();
return ret;
}

View File

@ -0,0 +1,64 @@
// SPDX-License-Identifier: BSD-2-Clause
//
// Copyright (C) 2023, Matthew Waters <matthew@centricular.com>
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// a) Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// b) Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in
// the documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
import QtQuick 6.0
import QtQuick.Controls 6.0
import QtQuick.Dialogs 6.0
import QtQuick.Window 6.0
import org.freedesktop.gstreamer.Qt6GLVideoItem 1.0
ApplicationWindow {
id: window
visible: true
width: 640
height: 480
x: 30
y: 30
color: "black"
Item {
anchors.fill: parent
GstGLQt6VideoItem {
id: video
objectName: "videoItem"
anchors.centerIn: parent
width: parent.width
height: parent.height
}
Text {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
text: "qmlglsink text"
font.pointSize: 20
color: "yellow"
style: Text.Outline
styleColor: "blue"
}
}
}

View File

@ -0,0 +1,11 @@
sources = [
'main.cpp',
]
qt_preprocessed = qt6_mod.preprocess(qresources : 'qmloverlayitem.qrc')
executable('qml6gloverlayitem', sources, qt_preprocessed,
dependencies : [gst_dep, qt6qml_example_deps],
override_options : ['cpp_std=c++17'],
c_args : gst_plugins_good_args,
include_directories : [configinc],
install: false)

View File

@ -0,0 +1,56 @@
import QtQuick 6.0
import org.freedesktop.gstreamer.Qt6GLVideoItem 1.0
Item {
objectName: "overlay"
/* render upside down for GStreamer */
transform: Scale { origin.x : 0; origin.y : height / 2.; yScale : -1 }
GstGLQt6VideoItem {
id: overlayVideo
objectName: "inputVideoItem"
anchors.fill: parent
}
Text {
id: rotatingText
anchors.centerIn: parent
text: "Qt Quick\nrendered to\na texture"
font.pointSize: 20
color: "black"
style: Text.Outline
styleColor: "white"
RotationAnimator {
target: rotatingText;
from: 0;
to: 360;
duration: 5000
running: true
loops: Animation.Infinite
}
}
Text {
property int elapsedTime: 0
id: time
anchors.top: rotatingText.bottom
anchors.horizontalCenter: rotatingText.horizontalCenter
font.pointSize: 12
style: Text.Outline
styleColor: "black"
color: "white"
Timer {
interval: 1000
running: true
repeat: true
onTriggered: {
parent.elapsedTime += interval / 1000
parent.text = "overlay: " + parent.elapsedTime.toString() + " seconds"
}
}
}
}

View File

@ -0,0 +1,6 @@
<RCC>
<qresource prefix="/">
<file>main.qml</file>
<file>overlay.qml</file>
</qresource>
</RCC>