From b8cb9ae5260124c8e3b4fa4b37b53bcaf81da030 Mon Sep 17 00:00:00 2001 From: Dmitry Shusharin Date: Fri, 30 Jul 2021 16:32:13 +0700 Subject: [PATCH] gstqmlgl: add multisink test application Part-of: --- tests/examples/qt/meson.build | 1 + tests/examples/qt/qmlsink-multisink/main.cpp | 35 ++ tests/examples/qt/qmlsink-multisink/main.qml | 60 ++++ tests/examples/qt/qmlsink-multisink/meson.build | 13 + .../qt/qmlsink-multisink/qmlsink-multi.qrc | 6 + .../qt/qmlsink-multisink/videoitem/VideoItem.qml | 13 + .../qt/qmlsink-multisink/videoitem/videoitem.cpp | 365 +++++++++++++++++++++ .../qt/qmlsink-multisink/videoitem/videoitem.h | 67 ++++ 8 files changed, 560 insertions(+) create mode 100644 tests/examples/qt/qmlsink-multisink/main.cpp create mode 100644 tests/examples/qt/qmlsink-multisink/main.qml create mode 100644 tests/examples/qt/qmlsink-multisink/meson.build create mode 100644 tests/examples/qt/qmlsink-multisink/qmlsink-multi.qrc create mode 100644 tests/examples/qt/qmlsink-multisink/videoitem/VideoItem.qml create mode 100644 tests/examples/qt/qmlsink-multisink/videoitem/videoitem.cpp create mode 100644 tests/examples/qt/qmlsink-multisink/videoitem/videoitem.h diff --git a/tests/examples/qt/meson.build b/tests/examples/qt/meson.build index ede4b5a..08c477d 100644 --- a/tests/examples/qt/meson.build +++ b/tests/examples/qt/meson.build @@ -16,5 +16,6 @@ endif subdir('qmloverlay') subdir('qmlsink') +subdir('qmlsink-multisink') subdir('qmlsink-dynamically-added') subdir('qmlsrc') diff --git a/tests/examples/qt/qmlsink-multisink/main.cpp b/tests/examples/qt/qmlsink-multisink/main.cpp new file mode 100644 index 0000000..415b49b --- /dev/null +++ b/tests/examples/qt/qmlsink-multisink/main.cpp @@ -0,0 +1,35 @@ +#include +#include +#include + +#include + +int +main (int argc, char *argv[]) +{ + gst_init (&argc, &argv); + + QGuiApplication app (argc, argv); + QQmlApplicationEngine engine; + + /* make sure that plugin was loaded */ + GstElement *qmlglsink = gst_element_factory_make ("qmlglsink", NULL); + g_assert (qmlglsink); + + /* anything supported by videotestsrc */ + QStringList patterns ( + { + "smpte", "ball", "spokes", "gamut"}); + + engine.rootContext ()->setContextProperty ("patterns", + QVariant::fromValue (patterns)); + + QObject::connect (&engine, &QQmlEngine::quit, [&] { + gst_object_unref (qmlglsink); + qApp->quit (); + }); + + engine.load (QUrl (QStringLiteral ("qrc:///main.qml"))); + + return app.exec (); +} diff --git a/tests/examples/qt/qmlsink-multisink/main.qml b/tests/examples/qt/qmlsink-multisink/main.qml new file mode 100644 index 0000000..4d48f6f --- /dev/null +++ b/tests/examples/qt/qmlsink-multisink/main.qml @@ -0,0 +1,60 @@ +import QtQuick 2.4 +import QtQuick.Controls 1.1 + +import "videoitem" + +ApplicationWindow { + visible: true + + minimumWidth: videowall.cellWidth * Math.sqrt(videowall.model.length) + videowall.leftMargin + minimumHeight: videowall.cellHeight * Math.sqrt(videowall.model.length) + 32 + maximumWidth: minimumWidth + maximumHeight: minimumHeight + + GridView { + id: videowall + leftMargin: 10 + model: patterns + anchors.fill: parent + cellWidth: 500 + cellHeight: 500 + delegate: Rectangle { + border.color: "darkgray" + width: videowall.cellWidth - 10 + height: videowall.cellHeight - 10 + radius: 3 + Label { + anchors.centerIn: parent + text: "No signal" + } + Loader { + active: playing.checked + anchors.fill: parent + anchors.margins: 1 + sourceComponent: VideoItem { + id: player + source: playing.checked ? modelData : "" + } + } + Row { + anchors.margins: 1 + id: controls + height: 32 + spacing: 10 + Button { + id: playing + checkable: true + checked: true + width: height + height: controls.height + text: index + } + Label { + verticalAlignment: Qt.AlignVCenter + height: controls.height + text: modelData + } + } + } + } +} diff --git a/tests/examples/qt/qmlsink-multisink/meson.build b/tests/examples/qt/qmlsink-multisink/meson.build new file mode 100644 index 0000000..56bf157 --- /dev/null +++ b/tests/examples/qt/qmlsink-multisink/meson.build @@ -0,0 +1,13 @@ +sources = [ + 'videoitem/videoitem.cpp', + 'main.cpp' +] + +qt_preprocessed = qt5_mod.preprocess (qresources : 'qmlsink-multi.qrc', + moc_headers : 'videoitem/videoitem.h') +executable('qmlsink-multisink', sources, qt_preprocessed, + dependencies : [gst_dep, gstgl_dep, qt5qml_example_deps], + override_options : ['cpp_std=c++11'], + c_args : gst_plugins_good_args, + include_directories : [configinc], + install: false) diff --git a/tests/examples/qt/qmlsink-multisink/qmlsink-multi.qrc b/tests/examples/qt/qmlsink-multisink/qmlsink-multi.qrc new file mode 100644 index 0000000..9319251 --- /dev/null +++ b/tests/examples/qt/qmlsink-multisink/qmlsink-multi.qrc @@ -0,0 +1,6 @@ + + + main.qml + videoitem/VideoItem.qml + + diff --git a/tests/examples/qt/qmlsink-multisink/videoitem/VideoItem.qml b/tests/examples/qt/qmlsink-multisink/videoitem/VideoItem.qml new file mode 100644 index 0000000..7bc2995 --- /dev/null +++ b/tests/examples/qt/qmlsink-multisink/videoitem/VideoItem.qml @@ -0,0 +1,13 @@ +import QtQuick 2.0 +import ACME.VideoItem 1.0 +import org.freedesktop.gstreamer.GLVideoItem 1.0 + +VideoItem { + id: videoitem + + GstGLVideoItem { + id: video + objectName: "videoItem" + anchors.fill: parent + } +} diff --git a/tests/examples/qt/qmlsink-multisink/videoitem/videoitem.cpp b/tests/examples/qt/qmlsink-multisink/videoitem/videoitem.cpp new file mode 100644 index 0000000..711c65d --- /dev/null +++ b/tests/examples/qt/qmlsink-multisink/videoitem/videoitem.cpp @@ -0,0 +1,365 @@ +#include +#include +#include + +#include +#include + +#include "videoitem.h" + +static void registerMetatypes() +{ + qmlRegisterType("ACME.VideoItem", 1, 0, "VideoItem"); + qRegisterMetaType("VideoItem::State"); +} +Q_CONSTRUCTOR_FUNCTION(registerMetatypes) + +static const QStringList patterns = { + "smpte", + "snow", + "black", + "white", + "red", + "green", + "blue", + "checkers-1", + "checkers-2", + "checkers-4", + "checkers-8", + "circular", + "blink", + "smpte75", + "zone-plate", + "gamut", + "chroma-zone-plate", + "solid-color", + "ball", + "smpte100", + "bar", + "pinwheel", + "spokes", + "gradient", + "colors" +}; + +struct VideoItemPrivate { + explicit VideoItemPrivate(VideoItem *owner) : own(owner) { } + + VideoItem *own { nullptr }; + + GstElement *pipeline { nullptr }; + GstElement *src { nullptr }; + GstElement *sink { nullptr }; + + GstPad *renderPad { nullptr }; + GstBus *bus { nullptr }; + + VideoItem::State state { VideoItem::STATE_VOID_PENDING }; + + QString pattern {}; + QRect rect { 0, 0, 0, 0 }; + QSize resolution { 0, 0 }; + + /* TODO: make q-properties? */ + quint64 timeout { 3000 }; /* 3s timeout */ +}; + +struct RenderJob : public QRunnable { + using Callable = std::function; + + explicit RenderJob(Callable c) : _c(c) { } + + void run() { _c(); } + +private: + Callable _c; +}; + +namespace { + +GstBusSyncReply messageHandler(GstBus * /*bus*/, GstMessage *msg, gpointer userData) +{ + auto priv = static_cast(userData); + + switch (GST_MESSAGE_TYPE(msg)) { + case GST_MESSAGE_ERROR: { + GError *error { nullptr }; + QString str { "GStreamer error: " }; + + gst_message_parse_error(msg, &error, nullptr); + str.append(error->message); + g_error_free(error); + + emit priv->own->errorOccurred(str); + qWarning() << str; + } break; + + case GST_MESSAGE_STATE_CHANGED: { + if (GST_MESSAGE_SRC(msg) == GST_OBJECT(priv->pipeline)) { + GstState oldState { GST_STATE_NULL }, newState { GST_STATE_NULL }; + + gst_message_parse_state_changed(msg, &oldState, &newState, nullptr); + priv->own->setState(static_cast(newState)); + } + } break; + + case GST_MESSAGE_HAVE_CONTEXT: { + GstContext *context { nullptr }; + + gst_message_parse_have_context(msg, &context); + + if (gst_context_has_context_type(context, GST_GL_DISPLAY_CONTEXT_TYPE)) + gst_element_set_context(priv->pipeline, context); + + if (context) + gst_context_unref(context); + + gst_message_unref(msg); + + return GST_BUS_DROP; + } break; + + default: + break; + } + + return GST_BUS_PASS; +} + +} // end namespace + +VideoItem::VideoItem(QQuickItem *parent) + : QQuickItem(parent), _priv(new VideoItemPrivate(this)) +{ + connect(this, &VideoItem::rectChanged, this, &VideoItem::updateRect); + connect(this, &VideoItem::stateChanged, this, &VideoItem::updateRect); + + // gst init pipeline + _priv->pipeline = gst_pipeline_new(nullptr); + _priv->src = gst_element_factory_make("videotestsrc", nullptr); + _priv->sink = gst_element_factory_make("glsinkbin", nullptr); + + GstElement *fakesink = gst_element_factory_make("fakesink", nullptr); + + Q_ASSERT(_priv->pipeline && _priv->src && _priv->sink); + Q_ASSERT(fakesink); + + g_object_set(_priv->sink, "sink", fakesink, nullptr); + + gst_bin_add_many(GST_BIN(_priv->pipeline), _priv->src, _priv->sink, nullptr); + gst_element_link_many (_priv->src, _priv->sink, nullptr); + + // add watch + _priv->bus = gst_pipeline_get_bus(GST_PIPELINE(_priv->pipeline)); + gst_bus_set_sync_handler(_priv->bus, messageHandler, _priv.get(), nullptr); + + gst_element_set_state(_priv->pipeline, GST_STATE_READY); + gst_element_get_state(_priv->pipeline, nullptr, nullptr, _priv->timeout * GST_MSECOND); +} + +VideoItem::~VideoItem() +{ + gst_bus_set_sync_handler(_priv->bus, nullptr, nullptr, nullptr); // stop handling messages + + gst_element_set_state(_priv->pipeline, GST_STATE_NULL); + + gst_object_unref(_priv->pipeline); + gst_object_unref(_priv->bus); +} + +bool VideoItem::hasVideo() const +{ + return _priv->renderPad && (state() & STATE_PLAYING); +} + +QString VideoItem::source() const +{ + return _priv->pattern; +} + +void VideoItem::setSource(const QString &source) +{ + if (_priv->pattern == source) + return; + + _priv->pattern = source; + + stop(); + + if (!_priv->pattern.isEmpty()) { + + auto it = std::find (patterns.begin(), patterns.end(), source); + + if (it != patterns.end()) { + g_object_set(_priv->src, "pattern", std::distance(patterns.begin(), it), nullptr); + play(); + } + } + emit sourceChanged(_priv->pattern); +} + +void VideoItem::play() +{ + if (_priv->state > STATE_NULL) { + const auto status = gst_element_set_state(_priv->pipeline, GST_STATE_PLAYING); + + if (status == GST_STATE_CHANGE_FAILURE) + qWarning() << "GStreamer error: unable to start playback"; + } +} + +void VideoItem::stop() +{ + if (_priv->state > STATE_NULL) { + const auto status = gst_element_set_state(_priv->pipeline, GST_STATE_READY); + + if (status == GST_STATE_CHANGE_FAILURE) + qWarning() << "GStreamer error: unable to stop playback"; + } +} + +void VideoItem::componentComplete() +{ + QQuickItem::componentComplete(); + + QQuickItem *videoItem = findChild("videoItem"); + Q_ASSERT(videoItem); // should not fail: check VideoItem.qml + + // needed for proper OpenGL context setup for GStreamer elements (QtQuick renderer) + auto setRenderer = [=](QQuickWindow *window) { + if (window) { + GstElement *glsink = gst_element_factory_make("qmlglsink", nullptr); + Q_ASSERT(glsink); + + GstState current {GST_STATE_NULL}, pending {GST_STATE_NULL}, target {GST_STATE_NULL}; + auto status = gst_element_get_state(_priv->pipeline, ¤t, &pending, 0); + + switch (status) { + case GST_STATE_CHANGE_FAILURE: { + qWarning() << "GStreamer error: while setting renderer: pending state change failure"; + return; + } + case GST_STATE_CHANGE_SUCCESS: + Q_FALLTHROUGH(); + case GST_STATE_CHANGE_NO_PREROLL: { + target = current; + break; + } + case GST_STATE_CHANGE_ASYNC: { + target = pending; + break; + } + } + + gst_element_set_state(_priv->pipeline, GST_STATE_NULL); + + glsink = GST_ELEMENT(gst_object_ref(glsink)); + + window->scheduleRenderJob(new RenderJob([=] { + g_object_set(glsink, "widget", videoItem, nullptr); + _priv->renderPad = gst_element_get_static_pad(glsink, "sink"); + g_object_set(_priv->sink, "sink", glsink, nullptr); + gst_element_set_state(_priv->pipeline, target); + }), + QQuickWindow::BeforeSynchronizingStage); + } + }; + + setRenderer(window()); + connect(this, &QQuickItem::windowChanged, this, setRenderer); +} + +void VideoItem::releaseResources() +{ + GstElement *sink { nullptr }; + QQuickWindow *win { window() }; + + gst_element_set_state(_priv->pipeline, GST_STATE_NULL); + g_object_get(_priv->sink, "sink", &sink, nullptr); + + if (_priv->renderPad) { + g_object_set(sink, "widget", nullptr, nullptr); + _priv->renderPad = nullptr; + } + + connect(this, &VideoItem::destroyed, this, [sink, win] { + auto job = new RenderJob(std::bind(&gst_object_unref, sink)); + win->scheduleRenderJob(job, QQuickWindow::AfterSwapStage); + }); +} + +void VideoItem::updateRect() +{ + // WARNING: don't touch this + if (!_priv->renderPad || _priv->state < STATE_PLAYING) { + setRect(QRect(0, 0, 0, 0)); + setResolution(QSize(0, 0)); + return; + } + + // update size + GstCaps *caps = gst_pad_get_current_caps(_priv->renderPad); + GstStructure *caps_struct = gst_caps_get_structure(caps, 0); + + gint picWidth { 0 }, picHeight { 0 }; + gst_structure_get_int(caps_struct, "width", &picWidth); + gst_structure_get_int(caps_struct, "height", &picHeight); + + qreal winWidth { this->width() }, winHeight { this->height() }; + + float picScaleRatio = picWidth * 1.0f / picHeight; + float wndScaleRatio = winWidth / winHeight; + + if (picScaleRatio >= wndScaleRatio) { + float span = winHeight - winWidth * picHeight / picWidth; + setRect(QRect(0, span / 2, winWidth, winHeight - span)); + } else { + float span = winWidth - winHeight * picWidth / picHeight; + setRect(QRect(span / 2, 0, winWidth - span, winHeight)); + } + setResolution(QSize(picWidth, picHeight)); +} + +VideoItem::State VideoItem::state() const +{ + return _priv->state; +} + +void VideoItem::setState(VideoItem::State state) +{ + if (_priv->state == state) + return; + + _priv->state = state; + + emit hasVideoChanged(_priv->state & STATE_PLAYING); + emit stateChanged(_priv->state); +} + +QRect VideoItem::rect() const +{ + return _priv->rect; +} + +void VideoItem::setRect(const QRect &rect) +{ + if (_priv->rect == rect) + return; + + _priv->rect = rect; + emit rectChanged(_priv->rect); +} + +QSize VideoItem::resolution() const +{ + return _priv->resolution; +} + +void VideoItem::setResolution(const QSize &size) +{ + if (_priv->resolution == size) + return; + + _priv->resolution = size; + emit resolutionChanged(_priv->resolution); +} diff --git a/tests/examples/qt/qmlsink-multisink/videoitem/videoitem.h b/tests/examples/qt/qmlsink-multisink/videoitem/videoitem.h new file mode 100644 index 0000000..828dce2 --- /dev/null +++ b/tests/examples/qt/qmlsink-multisink/videoitem/videoitem.h @@ -0,0 +1,67 @@ +#ifndef VIDEOITEM_H +#define VIDEOITEM_H + +#include + +struct VideoItemPrivate; + +class VideoItem : public QQuickItem +{ + Q_OBJECT + Q_PROPERTY(bool hasVideo READ hasVideo NOTIFY hasVideoChanged) + Q_PROPERTY(State state READ state NOTIFY stateChanged) + Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged) + Q_PROPERTY(QRect rect READ rect NOTIFY rectChanged) + Q_PROPERTY(QSize resolution READ resolution NOTIFY resolutionChanged) + +public: + enum State { + STATE_VOID_PENDING = 0, + STATE_NULL = 1, + STATE_READY = 2, + STATE_PAUSED = 3, + STATE_PLAYING = 4 + }; + Q_ENUM(State); + + explicit VideoItem(QQuickItem *parent = nullptr); + ~VideoItem(); + + bool hasVideo() const; + + QString source() const; + void setSource(const QString &source); + + State state() const; + void setState(State state); + + QRect rect() const; + + QSize resolution() const; + + Q_INVOKABLE void play(); + Q_INVOKABLE void stop(); + +signals: + void hasVideoChanged(bool hasVideo); + void stateChanged(VideoItem::State state); + void sourceChanged(const QString &source); + void rectChanged(const QRect &rect); + void resolutionChanged(const QSize &resolution); + + void errorOccurred(const QString &error); + +protected: + void componentComplete() override; + void releaseResources() override; + +private: + void updateRect(); + void setRect(const QRect &rect); + void setResolution(const QSize &resolution); + +private: + QSharedPointer _priv; +}; + +#endif // VIDEOITEM_H -- 2.7.4