File indexing completed on 2024-05-12 05:53:13

0001 /*
0002  * Copyright 2019 Eike Hein <hein@kde.org>
0003  *
0004  * This program is free software; you can redistribute it and/or
0005  * modify it under the terms of the GNU General Public License as
0006  * published by the Free Software Foundation; either version 2 of
0007  * the License or (at your option) version 3 or any later version
0008  * accepted by the membership of KDE e.V. (or its successor approved
0009  * by the membership of KDE e.V.), which shall act as a proxy
0010  * defined in Section 14 of version 3 of the license.
0011  *
0012  * This program is distributed in the hope that it will be useful,
0013  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0014  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0015  * GNU General Public License for more details.
0016  *
0017  * You should have received a copy of the GNU General Public License
0018  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0019  */
0020 
0021 #include "gstreamerintegration.h"
0022 #include "videosurface.h"
0023 
0024 #include <QDateTime>
0025 #include <QQuickItem>
0026 #include <QQuickWindow>
0027 #include <QRunnable>
0028 #include <QStandardPaths>
0029 
0030 #include <gst/gst.h>
0031 #include <gst/gstcaps.h>
0032 #include <gst/gstsample.h>
0033 
0034 Q_LOGGING_CATEGORY(videoLogging, "kirogi.videosuppoert.gstreamerintegration")
0035 
0036 namespace
0037 {
0038 int pipelineWatcher(GstBus *bus, GstMessage *message, gpointer user_data)
0039 {
0040     Q_UNUSED(bus)
0041     auto videoController = qobject_cast<GStreamerIntegration *>(static_cast<QObject *>(user_data));
0042     if (videoController) {
0043         videoController->processPipelineMessage(message);
0044     }
0045     return G_SOURCE_CONTINUE;
0046 }
0047 
0048 }
0049 
0050 GStreamerIntegration::GStreamerIntegration(QObject *parent)
0051     : QObject(parent)
0052     , m_playing(false)
0053     , m_gstPipeline(nullptr)
0054     , m_videoSink(nullptr)
0055     , m_fallback(true)
0056 {
0057     updateGstPipeline();
0058 }
0059 
0060 GStreamerIntegration::~GStreamerIntegration()
0061 {
0062     if (m_gstPipeline) {
0063         gst_element_set_state(m_gstPipeline, GST_STATE_NULL);
0064     }
0065 }
0066 
0067 bool GStreamerIntegration::playing() const
0068 {
0069     return m_playing;
0070 }
0071 
0072 void GStreamerIntegration::setPlaying(bool playing)
0073 {
0074     if (m_playing != playing) {
0075         m_playing = playing;
0076 
0077         if (!m_gstPipeline) {
0078             updateGstPipeline();
0079         } else if (m_gstPipeline->current_state == GST_STATE_PLAYING && !playing) {
0080             gst_element_set_state(m_gstPipeline, GST_STATE_PAUSED);
0081         } else if (m_gstPipeline->current_state != GST_STATE_PLAYING && playing) {
0082             updateGstPipeline();
0083         }
0084 
0085         Q_EMIT playingChanged();
0086     }
0087 }
0088 
0089 QString GStreamerIntegration::stringPipeline() const
0090 {
0091     return m_stringPipeline;
0092 }
0093 
0094 void GStreamerIntegration::setStringPipeline(const QString &pipeline)
0095 {
0096     if (m_stringPipeline != pipeline) {
0097         m_stringPipeline = pipeline;
0098 
0099         updateGstPipeline();
0100 
0101         Q_EMIT stringPipelineChanged();
0102     }
0103 }
0104 
0105 void GStreamerIntegration::updateGstPipeline()
0106 {
0107     QString pipeline = m_stringPipeline;
0108 
0109     if (pipeline.isEmpty()) {
0110         qCDebug(videoLogging) << "Empty pipeline, using snow pattern.";
0111         pipeline = QStringLiteral("videotestsrc pattern=snow ! video/x-raw,width=800,height=450 !");
0112     }
0113     pipeline += QStringLiteral("glupload ! glcolorconvert ! qmlglsink name=sink force-aspect-ratio=false");
0114 
0115     qCDebug(videoLogging) << "Using new pipeline:" << pipeline;
0116 
0117     if (m_gstPipeline) {
0118         gst_element_set_state(m_gstPipeline, GST_STATE_NULL);
0119         gst_object_unref(m_gstPipeline);
0120     }
0121 
0122     // If we fail to build the pipeline, we also fail to load the QML gst plugin
0123     // and the entire application will crash after that.
0124     GError *error = nullptr;
0125     m_gstPipeline = gst_parse_launch(pipeline.toLatin1().data(), &error);
0126     Q_ASSERT_X(m_gstPipeline, "gstreamer pipeline", QStringLiteral("%0 with pipeline: %1").arg(error->message).arg(pipeline).toStdString().c_str());
0127 
0128     m_videoSink = gst_bin_get_by_name(GST_BIN(m_gstPipeline), "sink");
0129     Q_ASSERT_X(m_videoSink, "gstreamer video sink", "Could not retrieve the video sink.");
0130 
0131     // Add watcher in pipeline to detect and notify if any problem is detected
0132     gst_bus_add_watch(gst_pipeline_get_bus(GST_PIPELINE(m_gstPipeline)), &pipelineWatcher, this);
0133 }
0134 
0135 void GStreamerIntegration::takeSnapshot()
0136 {
0137     if (!m_videoSink) {
0138         qCDebug(videoLogging) << "No video, exiting";
0139         return;
0140     }
0141 
0142     GstSample *videobuffer = nullptr;
0143     g_object_get(G_OBJECT(m_videoSink), "last-sample", &videobuffer, nullptr);
0144     if (!videobuffer) {
0145         qCDebug(videoLogging) << "No video buffer, exiting";
0146         return;
0147     }
0148 
0149     GstCaps *capsStructure = gst_sample_get_caps(videobuffer);
0150     if (!capsStructure) {
0151         qCDebug(videoLogging) << "Can't get caps from video buffer, exiting";
0152         return;
0153     }
0154 
0155     GstStructure *s = gst_caps_get_structure(capsStructure, 0);
0156     if (!s) {
0157         qCDebug(videoLogging) << "Could not get the structure from the caps for the video.";
0158         return;
0159     }
0160     gint width, height;
0161 
0162     gboolean res = gst_structure_get_int(s, "width", &width) && gst_structure_get_int(s, "height", &height);
0163     if (!res) {
0164         qCDebug(videoLogging) << "Can't get width or height from caps";
0165         return;
0166     }
0167 
0168     const gchar *format = gst_structure_get_string(s, "format");
0169     if (!format) {
0170         qCDebug(videoLogging) << "Could not get the video format.";
0171         return;
0172     }
0173 
0174     GstBuffer *snapbuffer = gst_sample_get_buffer(videobuffer);
0175     if (!snapbuffer) {
0176         qCDebug(videoLogging) << "Can't get the sample buffer from the video";
0177         return;
0178     }
0179 
0180     if (GST_BUFFER_FLAG_IS_SET(snapbuffer, GST_BUFFER_FLAG_CORRUPTED)) {
0181         qCDebug(videoLogging) << "Buffer is corrupted, ignoring screenshoot";
0182     }
0183 
0184     GstMapInfo map{};
0185     gst_buffer_map(snapbuffer, &map, GST_MAP_READ);
0186 
0187     uchar *bufferData = reinterpret_cast<uchar *>(map.data);
0188     QImage::Format imageFormat{};
0189     if (g_str_equal(format, "RGBA")) {
0190         imageFormat = QImage::Format_RGB32;
0191     } else {
0192         qCDebug(videoLogging) << "Video Format unrecognized, ignoring screenshoot";
0193         return;
0194     }
0195 
0196     QImage image(bufferData, width, height, imageFormat);
0197 
0198     QString savePath; // TODO: Add a setting on KConfigXT
0199     if (savePath.isEmpty()) {
0200         savePath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
0201     }
0202     savePath += "/" + QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd_hh.mm.ss")) + ".png";
0203 
0204     if (image.save(savePath)) {
0205         qCDebug(videoLogging) << "Image saved in" << savePath;
0206     } else {
0207         qCDebug(videoLogging) << "Error saving image in" << savePath << "please verify your access rights";
0208     }
0209 }
0210 
0211 void GStreamerIntegration::processPipelineMessage(GstMessage *message)
0212 {
0213     QString string;
0214     GError *gerror = nullptr;
0215 
0216     switch (message->type) {
0217     case GST_MESSAGE_ERROR:
0218         gst_message_parse_error(message, &gerror, nullptr);
0219         string = gerror->message;
0220         break;
0221 
0222     case GST_MESSAGE_WARNING:
0223         gst_message_parse_warning(message, &gerror, nullptr);
0224         string = gerror->message;
0225         break;
0226 
0227     case GST_MESSAGE_INFO:
0228         gst_message_parse_info(message, &gerror, nullptr);
0229         string = gerror->message;
0230         break;
0231 
0232     default:
0233         break;
0234     }
0235 
0236     if (!string.isEmpty()) {
0237         qCDebug(videoLogging) << "From:" << GST_MESSAGE_SRC_NAME(message) << "event type" << GST_MESSAGE_TYPE_NAME(message) << "happened with" << string;
0238     }
0239 
0240     if (gerror) {
0241         g_error_free(gerror);
0242     }
0243 }
0244 
0245 void GStreamerIntegration::init()
0246 {
0247     // GStreamer needs the sink to be created before any Qml elements
0248     // so that the Qml elements are registered in the system. so we create
0249     // and free it.
0250     GstElement *sink = gst_element_factory_make("qmlglsink", nullptr);
0251     Q_ASSERT_X(sink, "Video Initialization", "Could not find the Qml Gl Sink GStreamer Plugin, please check your installation.");
0252 
0253     gst_object_unref(sink);
0254 
0255     qmlRegisterType<GStreamerIntegration>("org.kde.kirogi.video", 0, 1, "VideoReceiver");
0256     qmlRegisterType<VideoSurface>("org.kde.kirogi.video", 0, 1, "VideoSurface");
0257 }