mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-02 06:31:08 +00:00
388 lines
11 KiB
C++
388 lines
11 KiB
C++
// SPDX-License-Identifier: BSD-2-Clause
|
|
//
|
|
// Copyright (C) 2021, Dmitry Shusharin <pmdvsh@gmail.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 <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.data(), 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);
|
|
gst_object_ref_sink (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);
|
|
|
|
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);
|
|
gst_object_unref (glsink);
|
|
}),
|
|
QQuickWindow::BeforeSynchronizingStage);
|
|
}
|
|
};
|
|
|
|
setRenderer(window());
|
|
connect(this, &QQuickItem::windowChanged, this, setRenderer);
|
|
}
|
|
|
|
void VideoItem::releaseResources()
|
|
{
|
|
GstElement *sink { nullptr };
|
|
|
|
gst_element_set_state(_priv->pipeline, GST_STATE_NULL);
|
|
g_object_get(_priv->sink, "sink", &sink, nullptr);
|
|
|
|
if (sink && _priv->renderPad) {
|
|
g_object_set(sink, "widget", nullptr, nullptr);
|
|
}
|
|
|
|
gst_clear_object (&_priv->renderPad);
|
|
gst_clear_object (&sink);
|
|
}
|
|
|
|
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));
|
|
gst_clear_caps(&caps);
|
|
}
|
|
|
|
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);
|
|
}
|