mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-13 12:51:16 +00:00
gstqmlgl: add multisink test application
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-good/-/merge_requests/1032>
This commit is contained in:
parent
0bb37c5135
commit
b8cb9ae526
8 changed files with 560 additions and 0 deletions
|
@ -16,5 +16,6 @@ endif
|
|||
|
||||
subdir('qmloverlay')
|
||||
subdir('qmlsink')
|
||||
subdir('qmlsink-multisink')
|
||||
subdir('qmlsink-dynamically-added')
|
||||
subdir('qmlsrc')
|
||||
|
|
35
tests/examples/qt/qmlsink-multisink/main.cpp
Normal file
35
tests/examples/qt/qmlsink-multisink/main.cpp
Normal file
|
@ -0,0 +1,35 @@
|
|||
#include <QApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
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 ();
|
||||
}
|
60
tests/examples/qt/qmlsink-multisink/main.qml
Normal file
60
tests/examples/qt/qmlsink-multisink/main.qml
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
13
tests/examples/qt/qmlsink-multisink/meson.build
Normal file
13
tests/examples/qt/qmlsink-multisink/meson.build
Normal file
|
@ -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)
|
6
tests/examples/qt/qmlsink-multisink/qmlsink-multi.qrc
Normal file
6
tests/examples/qt/qmlsink-multisink/qmlsink-multi.qrc
Normal file
|
@ -0,0 +1,6 @@
|
|||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>main.qml</file>
|
||||
<file>videoitem/VideoItem.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
13
tests/examples/qt/qmlsink-multisink/videoitem/VideoItem.qml
Normal file
13
tests/examples/qt/qmlsink-multisink/videoitem/VideoItem.qml
Normal file
|
@ -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
|
||||
}
|
||||
}
|
365
tests/examples/qt/qmlsink-multisink/videoitem/videoitem.cpp
Normal file
365
tests/examples/qt/qmlsink-multisink/videoitem/videoitem.cpp
Normal file
|
@ -0,0 +1,365 @@
|
|||
#include <QQuickWindow>
|
||||
#include <QQmlEngine>
|
||||
#include <QRunnable>
|
||||
|
||||
#include <gst/gst.h>
|
||||
#include <gst/gl/gl.h>
|
||||
|
||||
#include "videoitem.h"
|
||||
|
||||
static void registerMetatypes()
|
||||
{
|
||||
qmlRegisterType<VideoItem>("ACME.VideoItem", 1, 0, "VideoItem");
|
||||
qRegisterMetaType<VideoItem::State>("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<void()>;
|
||||
|
||||
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<VideoItemPrivate *>(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<VideoItem::State>(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<QQuickItem *>("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);
|
||||
}
|
67
tests/examples/qt/qmlsink-multisink/videoitem/videoitem.h
Normal file
67
tests/examples/qt/qmlsink-multisink/videoitem/videoitem.h
Normal file
|
@ -0,0 +1,67 @@
|
|||
#ifndef VIDEOITEM_H
|
||||
#define VIDEOITEM_H
|
||||
|
||||
#include <QQuickItem>
|
||||
|
||||
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<VideoItemPrivate> _priv;
|
||||
};
|
||||
|
||||
#endif // VIDEOITEM_H
|
Loading…
Reference in a new issue