diff options
Diffstat (limited to 'src/plugins/multimedia/gstreamer/common/qgstreamermediaplayer.cpp')
-rw-r--r-- | src/plugins/multimedia/gstreamer/common/qgstreamermediaplayer.cpp | 783 |
1 files changed, 511 insertions, 272 deletions
diff --git a/src/plugins/multimedia/gstreamer/common/qgstreamermediaplayer.cpp b/src/plugins/multimedia/gstreamer/common/qgstreamermediaplayer.cpp index c8e730c3d..687bcaba6 100644 --- a/src/plugins/multimedia/gstreamer/common/qgstreamermediaplayer.cpp +++ b/src/plugins/multimedia/gstreamer/common/qgstreamermediaplayer.cpp @@ -1,73 +1,41 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#include <qgstreamermediaplayer_p.h> -#include <qgstpipeline_p.h> -#include <qgstreamermetadata_p.h> +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <common/qgstreamermediaplayer_p.h> + +#include <audio/qgstreameraudiodevice_p.h> +#include <common/qgst_debug_p.h> +#include <common/qgstappsource_p.h> +#include <common/qgstpipeline_p.h> +#include <common/qgstreameraudiooutput_p.h> +#include <common/qgstreamermessage_p.h> +#include <common/qgstreamermetadata_p.h> +#include <common/qgstreamervideooutput_p.h> +#include <common/qgstreamervideosink_p.h> #include <qgstreamerformatinfo_p.h> -#include <qgstreameraudiooutput_p.h> -#include <qgstreamervideooutput_p.h> -#include <qgstreamervideosink_p.h> -#include "qgstreamermessage_p.h" -#include <qgstreameraudiodevice_p.h> -#include <qgstappsrc_p.h> -#include <qaudiodevice.h> +#include <QtMultimedia/qaudiodevice.h> #include <QtCore/qdir.h> #include <QtCore/qsocketnotifier.h> #include <QtCore/qurl.h> #include <QtCore/qdebug.h> #include <QtCore/qloggingcategory.h> -#include <QtNetwork/qnetworkaccessmanager.h> -#include <QtNetwork/qnetworkreply.h> +#include <QtCore/private/quniquehandle_p.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> -Q_LOGGING_CATEGORY(qLcMediaPlayer, "qt.multimedia.player") +#if QT_CONFIG(gstreamer_gl) +# include <gst/gl/gl.h> +#endif + +static Q_LOGGING_CATEGORY(qLcMediaPlayer, "qt.multimedia.player") QT_BEGIN_NAMESPACE -QGstreamerMediaPlayer::TrackSelector::TrackSelector(TrackType type, const char *name) - : selector(QGstElement("input-selector", name)), - type(type) +QGstreamerMediaPlayer::TrackSelector::TrackSelector(TrackType type, QGstElement selector) + : selector(selector), type(type) { selector.set("sync-streams", true); selector.set("sync-mode", 1 /*clock*/); @@ -110,35 +78,67 @@ QGstreamerMediaPlayer::TrackSelector &QGstreamerMediaPlayer::trackSelector(Track return ts; } -QGstreamerMediaPlayer::QGstreamerMediaPlayer(QMediaPlayer *parent) +void QGstreamerMediaPlayer::disconnectDecoderHandlers() +{ + auto handlers = std::initializer_list<QGObjectHandlerScopedConnection *>{ + &padAdded, &padRemoved, &sourceSetup, &uridecodebinElementAdded, + &unknownType, &elementAdded, &elementRemoved, + }; + for (QGObjectHandlerScopedConnection *handler : handlers) + handler->disconnect(); + + decodeBinQueues = 0; +} + +QMaybe<QPlatformMediaPlayer *> QGstreamerMediaPlayer::create(QMediaPlayer *parent) +{ + auto videoOutput = QGstreamerVideoOutput::create(); + if (!videoOutput) + return videoOutput.error(); + + static const auto error = + qGstErrorMessageIfElementsNotAvailable("input-selector", "decodebin", "uridecodebin"); + if (error) + return *error; + + return new QGstreamerMediaPlayer(videoOutput.value(), parent); +} + +QGstreamerMediaPlayer::QGstreamerMediaPlayer(QGstreamerVideoOutput *videoOutput, + QMediaPlayer *parent) : QObject(parent), QPlatformMediaPlayer(parent), - trackSelectors{{{ VideoStream, "videoInputSelector" }, - { AudioStream, "audioInputSelector" }, - { SubtitleStream, "subTitleInputSelector" }}}, - playerPipeline("playerPipeline") + trackSelectors{ { + { VideoStream, + QGstElement::createFromFactory("input-selector", "videoInputSelector") }, + { AudioStream, + QGstElement::createFromFactory("input-selector", "audioInputSelector") }, + { SubtitleStream, + QGstElement::createFromFactory("input-selector", "subTitleInputSelector") }, + } }, + playerPipeline(QGstPipeline::create("playerPipeline")), + gstVideoOutput(videoOutput) { playerPipeline.setFlushOnConfigChanges(true); - gstVideoOutput = new QGstreamerVideoOutput(this); + gstVideoOutput->setParent(this); gstVideoOutput->setPipeline(playerPipeline); for (auto &ts : trackSelectors) playerPipeline.add(ts.selector); - playerPipeline.setState(GST_STATE_NULL); playerPipeline.installMessageFilter(static_cast<QGstreamerBusMessageFilter *>(this)); playerPipeline.installMessageFilter(static_cast<QGstreamerSyncMessageFilter *>(this)); - gst_pipeline_use_clock(playerPipeline.pipeline(), gst_system_clock_obtain()); + QGstClockHandle systemClock{ + gst_system_clock_obtain(), + }; + + gst_pipeline_use_clock(playerPipeline.pipeline(), systemClock.get()); - /* Taken from gstdicoverer.c: - * This is ugly. We get the GType of decodebin so we can quickly detect - * when a decodebin is added to uridecodebin so we can set the - * post-stream-topology setting to TRUE */ - auto decodebin = QGstElement("decodebin", nullptr); - decodebinType = G_OBJECT_TYPE(decodebin.element()); - connect(&positionUpdateTimer, &QTimer::timeout, this, &QGstreamerMediaPlayer::updatePosition); + connect(&positionUpdateTimer, &QTimer::timeout, this, [this] { + updatePositionFromPipeline(); + }); } QGstreamerMediaPlayer::~QGstreamerMediaPlayer() @@ -146,20 +146,27 @@ QGstreamerMediaPlayer::~QGstreamerMediaPlayer() playerPipeline.removeMessageFilter(static_cast<QGstreamerBusMessageFilter *>(this)); playerPipeline.removeMessageFilter(static_cast<QGstreamerSyncMessageFilter *>(this)); playerPipeline.setStateSync(GST_STATE_NULL); - topology.free(); } -qint64 QGstreamerMediaPlayer::position() const +std::chrono::nanoseconds QGstreamerMediaPlayer::pipelinePosition() const { - if (playerPipeline.isNull() || m_url.isEmpty()) - return 0; + if (m_url.isEmpty()) + return {}; - return playerPipeline.position()/1e6; + Q_ASSERT(playerPipeline); + return playerPipeline.position(); +} + +void QGstreamerMediaPlayer::updatePositionFromPipeline() +{ + using namespace std::chrono; + + positionChanged(round<milliseconds>(pipelinePosition())); } qint64 QGstreamerMediaPlayer::duration() const { - return m_duration; + return m_duration.count(); } float QGstreamerMediaPlayer::bufferProgress() const @@ -179,18 +186,28 @@ qreal QGstreamerMediaPlayer::playbackRate() const void QGstreamerMediaPlayer::setPlaybackRate(qreal rate) { - if (playerPipeline.setPlaybackRate(rate)) - playbackRateChanged(rate); + if (rate == m_rate) + return; + + m_rate = rate; + + playerPipeline.setPlaybackRate(rate); + playbackRateChanged(rate); } void QGstreamerMediaPlayer::setPosition(qint64 pos) { - qint64 currentPos = playerPipeline.position()/1e6; - if (pos == currentPos) + std::chrono::milliseconds posInMs{ pos }; + setPosition(posInMs); +} + +void QGstreamerMediaPlayer::setPosition(std::chrono::milliseconds pos) +{ + if (pos == playerPipeline.position()) return; playerPipeline.finishStateChange(); - playerPipeline.setPosition(pos*1e6); - qCDebug(qLcMediaPlayer) << Q_FUNC_INFO << pos << playerPipeline.position()/1e6; + playerPipeline.setPosition(pos); + qCDebug(qLcMediaPlayer) << Q_FUNC_INFO << pos << playerPipeline.positionInMs(); if (mediaStatus() == QMediaPlayer::EndOfMedia) mediaStatusChanged(QMediaPlayer::LoadedMedia); positionChanged(pos); @@ -200,12 +217,14 @@ void QGstreamerMediaPlayer::play() { if (state() == QMediaPlayer::PlayingState || m_url.isEmpty()) return; - resetCurrentLoop(); + + if (state() != QMediaPlayer::PausedState) + resetCurrentLoop(); playerPipeline.setInStoppedState(false); if (mediaStatus() == QMediaPlayer::EndOfMedia) { - playerPipeline.setPosition(0); - updatePosition(); + playerPipeline.setPosition({}); + positionChanged(0); } qCDebug(qLcMediaPlayer) << "play()."; @@ -215,18 +234,23 @@ void QGstreamerMediaPlayer::play() // immediately, when they happen while paused. playerPipeline.flush(); m_requiresSeekOnPlay = false; + } else { + // we get an assertion failure during instant playback rate changes + // https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/3545 + constexpr bool performInstantRateChange = false; + playerPipeline.applyPlaybackRate(/*instantRateChange=*/performInstantRateChange); } if (ret == GST_STATE_CHANGE_FAILURE) qCDebug(qLcMediaPlayer) << "Unable to set the pipeline to the playing state."; - if (mediaStatus() == QMediaPlayer::LoadedMedia) - mediaStatusChanged(QMediaPlayer::BufferedMedia); - emit stateChanged(QMediaPlayer::PlayingState); + positionUpdateTimer.start(100); + emit stateChanged(QMediaPlayer::PlayingState); } void QGstreamerMediaPlayer::pause() { - if (state() == QMediaPlayer::PausedState || m_url.isEmpty()) + if (state() == QMediaPlayer::PausedState || m_url.isEmpty() + || m_resourceErrorState != ResourceErrorState::NoError) return; positionUpdateTimer.stop(); @@ -234,80 +258,149 @@ void QGstreamerMediaPlayer::pause() playerPipeline.setInStoppedState(false); playerPipeline.flush(); } - int ret = playerPipeline.setState(GST_STATE_PAUSED); + int ret = playerPipeline.setStateSync(GST_STATE_PAUSED); if (ret == GST_STATE_CHANGE_FAILURE) qCDebug(qLcMediaPlayer) << "Unable to set the pipeline to the paused state."; if (mediaStatus() == QMediaPlayer::EndOfMedia) { - playerPipeline.setPosition(0); - mediaStatusChanged(QMediaPlayer::BufferedMedia); + playerPipeline.setPosition({}); + positionChanged(0); + } else { + updatePositionFromPipeline(); } - updatePosition(); emit stateChanged(QMediaPlayer::PausedState); + + if (m_bufferProgress > 0 || !canTrackProgress()) + mediaStatusChanged(QMediaPlayer::BufferedMedia); + else + mediaStatusChanged(QMediaPlayer::BufferingMedia); + + emit bufferProgressChanged(m_bufferProgress / 100.); } void QGstreamerMediaPlayer::stop() { - if (state() == QMediaPlayer::StoppedState) + using namespace std::chrono_literals; + if (state() == QMediaPlayer::StoppedState) { + if (position() != 0) { + playerPipeline.setPosition({}); + positionChanged(0ms); + mediaStatusChanged(QMediaPlayer::LoadedMedia); + } return; + } stopOrEOS(false); } +const QGstPipeline &QGstreamerMediaPlayer::pipeline() const +{ + return playerPipeline; +} + void QGstreamerMediaPlayer::stopOrEOS(bool eos) { + using namespace std::chrono_literals; + positionUpdateTimer.stop(); playerPipeline.setInStoppedState(true); bool ret = playerPipeline.setStateSync(GST_STATE_PAUSED); if (!ret) qCDebug(qLcMediaPlayer) << "Unable to set the pipeline to the stopped state."; - if (!eos) - playerPipeline.setPosition(0); - updatePosition(); + if (!eos) { + playerPipeline.setPosition(0ms); + positionChanged(0ms); + } emit stateChanged(QMediaPlayer::StoppedState); - mediaStatusChanged(eos ? QMediaPlayer::EndOfMedia : QMediaPlayer::LoadedMedia); + if (eos) + mediaStatusChanged(QMediaPlayer::EndOfMedia); + else + mediaStatusChanged(QMediaPlayer::LoadedMedia); + m_initialBufferProgressSent = false; } -bool QGstreamerMediaPlayer::processBusMessage(const QGstreamerMessage &message) +void QGstreamerMediaPlayer::detectPipelineIsSeekable() { - if (message.isNull()) - return false; + qCDebug(qLcMediaPlayer) << "detectPipelineIsSeekable"; + QGstQueryHandle query{ + gst_query_new_seeking(GST_FORMAT_TIME), + QGstQueryHandle::HasRef, + }; + gboolean canSeek = false; + if (gst_element_query(playerPipeline.element(), query.get())) { + gst_query_parse_seeking(query.get(), nullptr, &canSeek, nullptr, nullptr); + qCDebug(qLcMediaPlayer) << " pipeline is seekable:" << canSeek; + } else { + qCWarning(qLcMediaPlayer) << " query for seekable failed."; + } + seekableChanged(canSeek); +} -// qCDebug(qLcMediaPlayer) << "received bus message from" << message.source().name() << message.type() << (message.type() == GST_MESSAGE_TAG); +bool QGstreamerMediaPlayer::processBusMessage(const QGstreamerMessage &message) +{ + qCDebug(qLcMediaPlayer) << "received bus message:" << message; - GstMessage* gm = message.rawMessage(); + GstMessage* gm = message.message(); switch (message.type()) { case GST_MESSAGE_TAG: { // #### This isn't ideal. We shouldn't catch stream specific tags here, rather the global ones - GstTagList *tag_list; - gst_message_parse_tag(gm, &tag_list); - //qCDebug(qLcMediaPlayer) << "Got tags: " << message.source().name() << gst_tag_list_to_string(tag_list); - auto metaData = QGstreamerMetaData::fromGstTagList(tag_list); + QGstTagListHandle tagList; + gst_message_parse_tag(gm, &tagList); + + qCDebug(qLcMediaPlayer) << " Got tags: " << tagList.get(); + auto metaData = taglistToMetaData(tagList); + auto keys = metaData.keys(); for (auto k : metaData.keys()) m_metaData.insert(k, metaData.value(k)); + if (!keys.isEmpty()) + emit metaDataChanged(); + + if (gstVideoOutput) { + QVariant rotation = m_metaData.value(QMediaMetaData::Orientation); + gstVideoOutput->setRotation(rotation.value<QtVideo::Rotation>()); + } break; } case GST_MESSAGE_DURATION_CHANGED: { - qint64 d = playerPipeline.duration()/1e6; + std::chrono::milliseconds d = playerPipeline.durationInMs(); qCDebug(qLcMediaPlayer) << " duration changed message" << d; if (d != m_duration) { m_duration = d; - emit durationChanged(duration()); + emit durationChanged(m_duration); } return false; } - case GST_MESSAGE_EOS: + case GST_MESSAGE_EOS: { + positionChanged(playerPipeline.durationInMs()); if (doLoop()) { setPosition(0); break; } stopOrEOS(true); break; + } case GST_MESSAGE_BUFFERING: { - qCDebug(qLcMediaPlayer) << " buffering message"; int progress = 0; gst_message_parse_buffering(gm, &progress); + + qCDebug(qLcMediaPlayer) << " buffering message: " << progress; + + if (state() != QMediaPlayer::StoppedState && !prerolling) { + if (!m_initialBufferProgressSent) { + mediaStatusChanged(QMediaPlayer::BufferingMedia); + m_initialBufferProgressSent = true; + } + + if (m_bufferProgress > 0 && progress == 0) + mediaStatusChanged(QMediaPlayer::StalledMedia); + else if (progress >= 50) + // QTBUG-124517: rethink buffering + mediaStatusChanged(QMediaPlayer::BufferedMedia); + else + mediaStatusChanged(QMediaPlayer::BufferingMedia); + } + m_bufferProgress = progress; - mediaStatusChanged(m_bufferProgress == 100 ? QMediaPlayer::BufferedMedia : QMediaPlayer::BufferingMedia); - emit bufferProgressChanged(m_bufferProgress/100.); + + emit bufferProgressChanged(m_bufferProgress / 100.); break; } case GST_MESSAGE_STATE_CHANGED: { @@ -319,37 +412,26 @@ bool QGstreamerMediaPlayer::processBusMessage(const QGstreamerMessage &message) GstState pending; gst_message_parse_state_changed(gm, &oldState, &newState, &pending); - qCDebug(qLcMediaPlayer) << " state changed message" << oldState << newState << pending; - -#ifdef DEBUG_PLAYBIN - static QStringList states = { - QStringLiteral("GST_STATE_VOID_PENDING"), QStringLiteral("GST_STATE_NULL"), - QStringLiteral("GST_STATE_READY"), QStringLiteral("GST_STATE_PAUSED"), - QStringLiteral("GST_STATE_PLAYING") }; - - qCDebug(qLcMediaPlayer) << QStringLiteral("state changed: old: %1 new: %2 pending: %3") \ - .arg(states[oldState]) \ - .arg(states[newState]) \ - .arg(states[pending]); -#endif + qCDebug(qLcMediaPlayer) << " state changed message from" + << QCompactGstMessageAdaptor(message); switch (newState) { case GST_STATE_VOID_PENDING: case GST_STATE_NULL: case GST_STATE_READY: break; - case GST_STATE_PAUSED: - { + case GST_STATE_PAUSED: { if (prerolling) { qCDebug(qLcMediaPlayer) << "Preroll done, setting status to Loaded"; prerolling = false; - GST_DEBUG_BIN_TO_DOT_FILE(playerPipeline.bin(), GST_DEBUG_GRAPH_SHOW_ALL, "playerPipeline"); + GST_DEBUG_BIN_TO_DOT_FILE(playerPipeline.bin(), GST_DEBUG_GRAPH_SHOW_ALL, + "playerPipeline"); - qint64 d = playerPipeline.duration()/1e6; + std::chrono::milliseconds d = playerPipeline.durationInMs(); if (d != m_duration) { m_duration = d; qCDebug(qLcMediaPlayer) << " duration changed" << d; - emit durationChanged(duration()); + emit durationChanged(d); } parseStreamsAndMetadata(); @@ -357,78 +439,98 @@ bool QGstreamerMediaPlayer::processBusMessage(const QGstreamerMessage &message) emit tracksChanged(); mediaStatusChanged(QMediaPlayer::LoadedMedia); - GstQuery *query = gst_query_new_seeking(GST_FORMAT_TIME); - gboolean canSeek = false; - if (gst_element_query(playerPipeline.element(), query)) { - gst_query_parse_seeking(query, NULL, &canSeek, nullptr, nullptr); - qCDebug(qLcMediaPlayer) << " pipeline is seekable:" << canSeek; - } else { - qCDebug(qLcMediaPlayer) << " query for seekable failed."; + if (!playerPipeline.inStoppedState()) { + Q_ASSERT(!m_initialBufferProgressSent); + + bool immediatelySendBuffered = !canTrackProgress() || m_bufferProgress > 0; + mediaStatusChanged(QMediaPlayer::BufferingMedia); + m_initialBufferProgressSent = true; + if (immediatelySendBuffered) + mediaStatusChanged(QMediaPlayer::BufferedMedia); } - gst_query_unref(query); - seekableChanged(canSeek); } break; } - case GST_STATE_PLAYING: - mediaStatusChanged(QMediaPlayer::BufferedMedia); + case GST_STATE_PLAYING: { + if (!m_initialBufferProgressSent) { + bool immediatelySendBuffered = !canTrackProgress() || m_bufferProgress > 0; + mediaStatusChanged(QMediaPlayer::BufferingMedia); + m_initialBufferProgressSent = true; + if (immediatelySendBuffered) + mediaStatusChanged(QMediaPlayer::BufferedMedia); + } break; } + } break; } case GST_MESSAGE_ERROR: { - GError *err; - gchar *debug; + qCDebug(qLcMediaPlayer) << " error" << QCompactGstMessageAdaptor(message); + + QUniqueGErrorHandle err; + QUniqueGStringHandle debug; gst_message_parse_error(gm, &err, &debug); - if (err->domain == GST_STREAM_ERROR && err->code == GST_STREAM_ERROR_CODEC_NOT_FOUND) - emit error(QMediaPlayer::FormatError, tr("Cannot play stream of type: <unknown>")); - else - emit error(QMediaPlayer::ResourceError, QString::fromUtf8(err->message)); - playerPipeline.dumpGraph("error"); + GQuark errorDomain = err.get()->domain; + gint errorCode = err.get()->code; + + if (errorDomain == GST_STREAM_ERROR) { + if (errorCode == GST_STREAM_ERROR_CODEC_NOT_FOUND) + error(QMediaPlayer::FormatError, tr("Cannot play stream of type: <unknown>")); + else { + error(QMediaPlayer::FormatError, QString::fromUtf8(err.get()->message)); + } + } else if (errorDomain == GST_RESOURCE_ERROR) { + if (errorCode == GST_RESOURCE_ERROR_NOT_FOUND) { + if (m_resourceErrorState != ResourceErrorState::ErrorReported) { + // gstreamer seems to deliver multiple GST_RESOURCE_ERROR_NOT_FOUND events + error(QMediaPlayer::ResourceError, QString::fromUtf8(err.get()->message)); + m_resourceErrorState = ResourceErrorState::ErrorReported; + m_url.clear(); + } + } else { + error(QMediaPlayer::ResourceError, QString::fromUtf8(err.get()->message)); + } + } else { + playerPipeline.dumpGraph("error"); + } mediaStatusChanged(QMediaPlayer::InvalidMedia); - g_error_free(err); - g_free(debug); break; } - case GST_MESSAGE_WARNING: { - GError *err; - gchar *debug; - gst_message_parse_warning (gm, &err, &debug); - qCWarning(qLcMediaPlayer) << "Warning:" << QString::fromUtf8(err->message); + + case GST_MESSAGE_WARNING: + qCWarning(qLcMediaPlayer) << "Warning:" << QCompactGstMessageAdaptor(message); playerPipeline.dumpGraph("warning"); - g_error_free (err); - g_free (debug); break; - } - case GST_MESSAGE_INFO: { - if (qLcMediaPlayer().isDebugEnabled()) { - GError *err; - gchar *debug; - gst_message_parse_info (gm, &err, &debug); - qCDebug(qLcMediaPlayer) << "Info:" << QString::fromUtf8(err->message); - g_error_free (err); - g_free (debug); - } + + case GST_MESSAGE_INFO: + if (qLcMediaPlayer().isDebugEnabled()) + qCDebug(qLcMediaPlayer) << "Info:" << QCompactGstMessageAdaptor(message); break; - } + case GST_MESSAGE_SEGMENT_START: { qCDebug(qLcMediaPlayer) << " segment start message, updating position"; - QGstStructure structure(gst_message_get_structure(gm)); + QGstStructureView structure(gst_message_get_structure(gm)); auto p = structure["position"].toInt64(); if (p) { - qint64 position = (*p)/1000000; + std::chrono::milliseconds position{ + (*p) / 1000000, + }; emit positionChanged(position); } break; } case GST_MESSAGE_ELEMENT: { - QGstStructure structure(gst_message_get_structure(gm)); + QGstStructureView structure(gst_message_get_structure(gm)); auto type = structure.name(); - if (type == "stream-topology") { - topology.free(); - topology = structure.copy(); - } + if (type == "stream-topology") + topology = structure.clone(); + + break; + } + + case GST_MESSAGE_ASYNC_DONE: { + detectPipelineIsSeekable(); break; } @@ -447,7 +549,7 @@ bool QGstreamerMediaPlayer::processSyncMessage(const QGstreamerMessage &message) if (message.type() != GST_MESSAGE_NEED_CONTEXT) return false; const gchar *type = nullptr; - gst_message_parse_context_type (message.rawMessage(), &type); + gst_message_parse_context_type (message.message(), &type); if (strcmp(type, GST_GL_DISPLAY_CONTEXT_TYPE)) return false; if (!gstVideoOutput || !gstVideoOutput->gstreamerVideoSink()) @@ -455,7 +557,7 @@ bool QGstreamerMediaPlayer::processSyncMessage(const QGstreamerMessage &message) auto *context = gstVideoOutput->gstreamerVideoSink()->gstGlDisplayContext(); if (!context) return false; - gst_element_set_context(GST_ELEMENT(GST_MESSAGE_SRC(message.rawMessage())), context); + gst_element_set_context(GST_ELEMENT(GST_MESSAGE_SRC(message.message())), context); playerPipeline.dumpGraph("need_context"); return true; #else @@ -482,7 +584,7 @@ void QGstreamerMediaPlayer::decoderPadAdded(const QGstElement &src, const QGstPa auto caps = pad.currentCaps(); auto type = caps.at(0).name(); qCDebug(qLcMediaPlayer) << "Received new pad" << pad.name() << "from" << src.name() << "type" << type; - qCDebug(qLcMediaPlayer) << " " << caps.toString(); + qCDebug(qLcMediaPlayer) << " " << caps; TrackType streamType = NTrackTypes; if (type.startsWith("video/x-raw")) { @@ -520,7 +622,7 @@ void QGstreamerMediaPlayer::decoderPadAdded(const QGstElement &src, const QGstPa if (!prerolling) emit tracksChanged(); - decoderOutputMap.insert(pad.name(), sinkPad); + decoderOutputMap.emplace(pad, sinkPad); } void QGstreamerMediaPlayer::decoderPadRemoved(const QGstElement &src, const QGstPad &pad) @@ -529,9 +631,11 @@ void QGstreamerMediaPlayer::decoderPadRemoved(const QGstElement &src, const QGst return; qCDebug(qLcMediaPlayer) << "Removed pad" << pad.name() << "from" << src.name(); - auto track = decoderOutputMap.value(pad.name()); - if (track.isNull()) + + auto it = decoderOutputMap.find(pad); + if (it == decoderOutputMap.end()) return; + QGstPad track = it->second; auto ts = std::find_if(std::begin(trackSelectors), std::end(trackSelectors), [&](TrackSelector &ts){ return ts.selector == track.parent(); }); @@ -587,8 +691,8 @@ void QGstreamerMediaPlayer::connectOutput(TrackSelector &ts) if (!e.isNull()) { qCDebug(qLcMediaPlayer) << "connecting output for track type" << ts.type; playerPipeline.add(e); - ts.selector.link(e); - e.setState(GST_STATE_PAUSED); + qLinkGstElements(ts.selector, e); + e.syncStateWithParent(); } ts.isConnected = true; @@ -617,29 +721,129 @@ void QGstreamerMediaPlayer::removeOutput(TrackSelector &ts) if (!e.isNull()) { qCDebug(qLcMediaPlayer) << "removing output for track type" << ts.type; - playerPipeline.remove(e); - e.setStateSync(GST_STATE_NULL); + playerPipeline.stopAndRemoveElements(e); } ts.isConnected = false; } -void QGstreamerMediaPlayer::uridecodebinElementAddedCallback(GstElement */*uridecodebin*/, GstElement *child, QGstreamerMediaPlayer *that) +void QGstreamerMediaPlayer::removeDynamicPipelineElements() { - QGstElement c(child); + for (QGstElement *element : { &src, &decoder }) { + if (element->isNull()) + continue; + + element->setStateSync(GstState::GST_STATE_NULL); + playerPipeline.remove(*element); + *element = QGstElement{}; + } +} + +void QGstreamerMediaPlayer::uridecodebinElementAddedCallback(GstElement * /*uridecodebin*/, + GstElement *child, + QGstreamerMediaPlayer *) +{ + QGstElement c(child, QGstElement::NeedsRef); qCDebug(qLcMediaPlayer) << "New element added to uridecodebin:" << c.name(); - if (G_OBJECT_TYPE(child) == that->decodebinType) { + static const GType decodeBinType = [] { + QGstElementFactoryHandle factory = QGstElement::findFactory("decodebin"); + return gst_element_factory_get_element_type(factory.get()); + }(); + + if (c.type() == decodeBinType) { qCDebug(qLcMediaPlayer) << " -> setting post-stream-topology property"; c.set("post-stream-topology", true); } } +void QGstreamerMediaPlayer::sourceSetupCallback(GstElement *uridecodebin, GstElement *source, QGstreamerMediaPlayer *that) +{ + Q_UNUSED(uridecodebin) + Q_UNUSED(that) + + qCDebug(qLcMediaPlayer) << "Setting up source:" << g_type_name_from_instance((GTypeInstance*)source); + + if (std::string_view("GstRTSPSrc") == g_type_name_from_instance((GTypeInstance *)source)) { + QGstElement s(source, QGstElement::NeedsRef); + int latency{40}; + bool ok{false}; + int v = qEnvironmentVariableIntValue("QT_MEDIA_RTSP_LATENCY", &ok); + if (ok) + latency = v; + qCDebug(qLcMediaPlayer) << " -> setting source latency to:" << latency << "ms"; + s.set("latency", latency); + + bool drop{true}; + v = qEnvironmentVariableIntValue("QT_MEDIA_RTSP_DROP_ON_LATENCY", &ok); + if (ok && v == 0) + drop = false; + qCDebug(qLcMediaPlayer) << " -> setting drop-on-latency to:" << drop; + s.set("drop-on-latency", drop); + + bool retrans{false}; + v = qEnvironmentVariableIntValue("QT_MEDIA_RTSP_DO_RETRANSMISSION", &ok); + if (ok && v != 0) + retrans = true; + qCDebug(qLcMediaPlayer) << " -> setting do-retransmission to:" << retrans; + s.set("do-retransmission", retrans); + } +} + +void QGstreamerMediaPlayer::unknownTypeCallback(GstElement *decodebin, GstPad *pad, GstCaps *caps, + QGstreamerMediaPlayer *self) +{ + Q_UNUSED(decodebin) + Q_UNUSED(pad) + Q_UNUSED(self) + qCDebug(qLcMediaPlayer) << "Unknown type:" << caps; + + QMetaObject::invokeMethod(self, [self] { + self->stop(); + }); +} + +static bool isQueue(const QGstElement &element) +{ + static const GType queueType = [] { + QGstElementFactoryHandle factory = QGstElement::findFactory("queue"); + return gst_element_factory_get_element_type(factory.get()); + }(); + + static const GType multiQueueType = [] { + QGstElementFactoryHandle factory = QGstElement::findFactory("multiqueue"); + return gst_element_factory_get_element_type(factory.get()); + }(); + + return element.type() == queueType || element.type() == multiQueueType; +} + +void QGstreamerMediaPlayer::decodebinElementAddedCallback(GstBin * /*decodebin*/, + GstBin * /*sub_bin*/, GstElement *child, + QGstreamerMediaPlayer *self) +{ + QGstElement c(child, QGstElement::NeedsRef); + if (isQueue(c)) + self->decodeBinQueues += 1; +} + +void QGstreamerMediaPlayer::decodebinElementRemovedCallback(GstBin * /*decodebin*/, + GstBin * /*sub_bin*/, GstElement *child, + QGstreamerMediaPlayer *self) +{ + QGstElement c(child, QGstElement::NeedsRef); + if (isQueue(c)) + self->decodeBinQueues -= 1; +} + void QGstreamerMediaPlayer::setMedia(const QUrl &content, QIODevice *stream) { + using namespace std::chrono_literals; + qCDebug(qLcMediaPlayer) << Q_FUNC_INFO << "setting location to" << content; prerolling = true; + m_resourceErrorState = ResourceErrorState::NoError; bool ret = playerPipeline.setStateSync(GST_STATE_NULL); if (!ret) @@ -648,73 +852,109 @@ void QGstreamerMediaPlayer::setMedia(const QUrl &content, QIODevice *stream) m_url = content; m_stream = stream; - if (!src.isNull()) - playerPipeline.remove(src); - if (!decoder.isNull()) - playerPipeline.remove(decoder); - src = QGstElement(); - decoder = QGstElement(); + removeDynamicPipelineElements(); + disconnectDecoderHandlers(); removeAllOutputs(); seekableChanged(false); - playerPipeline.setInStoppedState(true); + Q_ASSERT(playerPipeline.inStoppedState()); - if (m_duration != 0) { - m_duration = 0; - durationChanged(0); + if (m_duration != 0ms) { + m_duration = 0ms; + durationChanged(0ms); } stateChanged(QMediaPlayer::StoppedState); if (position() != 0) - positionChanged(0); - mediaStatusChanged(QMediaPlayer::NoMedia); + positionChanged(0ms); if (!m_metaData.isEmpty()) { m_metaData.clear(); metaDataChanged(); } + if (content.isEmpty() && !stream) + mediaStatusChanged(QMediaPlayer::NoMedia); + if (content.isEmpty()) return; if (m_stream) { - if (!m_appSrc) - m_appSrc = new QGstAppSrc(this); + if (!m_appSrc) { + auto maybeAppSrc = QGstAppSource::create(this); + if (maybeAppSrc) { + m_appSrc = maybeAppSrc.value(); + } else { + error(QMediaPlayer::ResourceError, maybeAppSrc.error()); + return; + } + } src = m_appSrc->element(); - decoder = QGstElement("decodebin", "decoder"); + decoder = QGstElement::createFromFactory("decodebin", "decoder"); + if (!decoder) { + error(QMediaPlayer::ResourceError, qGstErrorMessageCannotFindElement("decodebin")); + return; + } decoder.set("post-stream-topology", true); + decoder.set("use-buffering", true); + unknownType = decoder.connect("unknown-type", GCallback(unknownTypeCallback), this); + elementAdded = decoder.connect("deep-element-added", + GCallback(decodebinElementAddedCallback), this); + elementRemoved = decoder.connect("deep-element-removed", + GCallback(decodebinElementAddedCallback), this); + playerPipeline.add(src, decoder); - src.link(decoder); + qLinkGstElements(src, decoder); m_appSrc->setup(m_stream); seekableChanged(!stream->isSequential()); } else { // use uridecodebin - decoder = QGstElement("uridecodebin", "uridecoder"); + decoder = QGstElement::createFromFactory("uridecodebin", "decoder"); + if (!decoder) { + error(QMediaPlayer::ResourceError, qGstErrorMessageCannotFindElement("uridecodebin")); + return; + } playerPipeline.add(decoder); - // can't set post-stream-topology to true, as uridecodebin doesn't have the property. Use a hack - decoder.connect("element-added", GCallback(QGstreamerMediaPlayer::uridecodebinElementAddedCallback), this); + + constexpr bool hasPostStreamTopology = GST_CHECK_VERSION(1, 22, 0); + if constexpr (hasPostStreamTopology) { + decoder.set("post-stream-topology", true); + } else { + // can't set post-stream-topology to true, as uridecodebin doesn't have the property. + // Use a hack + uridecodebinElementAdded = decoder.connect( + "element-added", GCallback(uridecodebinElementAddedCallback), this); + } + + sourceSetup = decoder.connect("source-setup", GCallback(sourceSetupCallback), this); + unknownType = decoder.connect("unknown-type", GCallback(unknownTypeCallback), this); decoder.set("uri", content.toEncoded().constData()); + decoder.set("use-buffering", true); + + constexpr int mb = 1024 * 1024; + decoder.set("ring-buffer-max-size", 2 * mb); + if (m_bufferProgress != 0) { m_bufferProgress = 0; emit bufferProgressChanged(0.); } + + elementAdded = decoder.connect("deep-element-added", + GCallback(decodebinElementAddedCallback), this); + elementRemoved = decoder.connect("deep-element-removed", + GCallback(decodebinElementAddedCallback), this); } - decoder.onPadAdded<&QGstreamerMediaPlayer::decoderPadAdded>(this); - decoder.onPadRemoved<&QGstreamerMediaPlayer::decoderPadRemoved>(this); + padAdded = decoder.onPadAdded<&QGstreamerMediaPlayer::decoderPadAdded>(this); + padRemoved = decoder.onPadRemoved<&QGstreamerMediaPlayer::decoderPadRemoved>(this); mediaStatusChanged(QMediaPlayer::LoadingMedia); - - if (state() == QMediaPlayer::PlayingState) { - int ret = playerPipeline.setState(GST_STATE_PLAYING); - if (ret == GST_STATE_CHANGE_FAILURE) - qCWarning(qLcMediaPlayer) << "Unable to set the pipeline to the playing state."; - } else { - int ret = playerPipeline.setState(GST_STATE_PAUSED); - if (!ret) - qCWarning(qLcMediaPlayer) << "Unable to set the pipeline to the paused state."; + if (!playerPipeline.setStateSync(GST_STATE_PAUSED)) { + qCWarning(qLcMediaPlayer) << "Unable to set the pipeline to the paused state."; + // Note: no further error handling: errors will be delivered via a GstMessage + return; } - playerPipeline.setPosition(0); - positionChanged(0); + playerPipeline.setPosition(0ms); + positionChanged(0ms); } void QGstreamerMediaPlayer::setAudioOutput(QPlatformAudioOutput *output) @@ -724,17 +964,14 @@ void QGstreamerMediaPlayer::setAudioOutput(QPlatformAudioOutput *output) auto &ts = trackSelector(AudioStream); - playerPipeline.beginConfig(); - if (gstAudioOutput) { - removeOutput(ts); - gstAudioOutput->setPipeline({}); - } - gstAudioOutput = static_cast<QGstreamerAudioOutput *>(output); - if (gstAudioOutput) { - gstAudioOutput->setPipeline(playerPipeline); - connectOutput(ts); - } - playerPipeline.endConfig(); + playerPipeline.modifyPipelineWhileNotRunning([&] { + if (gstAudioOutput) + removeOutput(ts); + + gstAudioOutput = static_cast<QGstreamerAudioOutput *>(output); + if (gstAudioOutput) + connectOutput(ts); + }); } QMediaMetaData QGstreamerMediaPlayer::metaData() const @@ -747,9 +984,9 @@ void QGstreamerMediaPlayer::setVideoSink(QVideoSink *sink) gstVideoOutput->setVideoSink(sink); } -static QGstStructure endOfChain(const QGstStructure &s) +static QGstStructureView endOfChain(const QGstStructureView &s) { - QGstStructure e = s; + QGstStructureView e = s; while (1) { auto next = e["next"].toStructure(); if (!next.isNull()) @@ -763,28 +1000,30 @@ static QGstStructure endOfChain(const QGstStructure &s) void QGstreamerMediaPlayer::parseStreamsAndMetadata() { qCDebug(qLcMediaPlayer) << "============== parse topology ============"; - if (topology.isNull()) { + + if (!topology) { qCDebug(qLcMediaPlayer) << " null topology"; return; } - auto caps = topology["caps"].toCaps(); - auto structure = caps.at(0); + + QGstStructureView topologyView{ topology }; + + QGstCaps caps = topologyView.caps(); + QGstStructureView structure = caps.at(0); auto fileFormat = QGstreamerFormatInfo::fileFormatForCaps(structure); - qCDebug(qLcMediaPlayer) << caps.toString() << fileFormat; + qCDebug(qLcMediaPlayer) << caps << fileFormat; m_metaData.insert(QMediaMetaData::FileFormat, QVariant::fromValue(fileFormat)); m_metaData.insert(QMediaMetaData::Duration, duration()); m_metaData.insert(QMediaMetaData::Url, m_url); - QGValue tags = topology["tags"]; - if (!tags.isNull()) { - GstTagList *tagList = nullptr; - gst_structure_get(topology.structure, "tags", GST_TYPE_TAG_LIST, &tagList, nullptr); - const auto metaData = QGstreamerMetaData::fromGstTagList(tagList); + QGstTagListHandle tagList = QGstStructureView{ topology }.tags(); + if (tagList) { + const auto metaData = taglistToMetaData(tagList); for (auto k : metaData.keys()) m_metaData.insert(k, metaData.value(k)); } - auto demux = endOfChain(topology); - auto next = demux["next"]; + QGstStructureView demux = endOfChain(topologyView); + QGValue next = demux["next"]; if (!next.isList()) { qCDebug(qLcMediaPlayer) << " no additional streams"; emit metaDataChanged(); @@ -795,36 +1034,38 @@ void QGstreamerMediaPlayer::parseStreamsAndMetadata() int size = next.listSize(); for (int i = 0; i < size; ++i) { auto val = next.at(i); - caps = val.toStructure()["caps"].toCaps(); + caps = val.toStructure().caps(); structure = caps.at(0); if (structure.name().startsWith("audio/")) { auto codec = QGstreamerFormatInfo::audioCodecForCaps(structure); m_metaData.insert(QMediaMetaData::AudioCodec, QVariant::fromValue(codec)); - qCDebug(qLcMediaPlayer) << " audio" << caps.toString() << (int)codec; + qCDebug(qLcMediaPlayer) << " audio" << caps << (int)codec; } else if (structure.name().startsWith("video/")) { auto codec = QGstreamerFormatInfo::videoCodecForCaps(structure); m_metaData.insert(QMediaMetaData::VideoCodec, QVariant::fromValue(codec)); - qCDebug(qLcMediaPlayer) << " video" << caps.toString() << (int)codec; + qCDebug(qLcMediaPlayer) << " video" << caps << (int)codec; auto framerate = structure["framerate"].getFraction(); if (framerate) m_metaData.insert(QMediaMetaData::VideoFrameRate, *framerate); - auto width = structure["width"].toInt(); - auto height = structure["height"].toInt(); - if (width && height) - m_metaData.insert(QMediaMetaData::Resolution, QSize(*width, *height)); + + QSize resolution = structure.resolution(); + if (resolution.isValid()) + m_metaData.insert(QMediaMetaData::Resolution, resolution); + + QSize nativeSize = structure.nativeSize(); + gstVideoOutput->setNativeSize(nativeSize); } } auto sinkPad = trackSelector(VideoStream).activeInputPad(); - if (!sinkPad.isNull()) { - bool hasTags = g_object_class_find_property (G_OBJECT_GET_CLASS (sinkPad.object()), "tags") != NULL; - - GstTagList *tl = nullptr; - g_object_get(sinkPad.object(), "tags", &tl, nullptr); - qCDebug(qLcMediaPlayer) << " tags=" << hasTags << (tl ? gst_tag_list_to_string(tl) : "(null)"); + if (sinkPad) { + QGstTagListHandle tagList = sinkPad.tags(); + if (tagList) + qCDebug(qLcMediaPlayer) << " tags=" << tagList.get(); + else + qCDebug(qLcMediaPlayer) << " tags=(null)"; } - qCDebug(qLcMediaPlayer) << "============== end parse topology ============"; emit metaDataChanged(); playerPipeline.dumpGraph("playback"); @@ -838,13 +1079,11 @@ int QGstreamerMediaPlayer::trackCount(QPlatformMediaPlayer::TrackType type) QMediaMetaData QGstreamerMediaPlayer::trackMetaData(QPlatformMediaPlayer::TrackType type, int index) { auto track = trackSelector(type).inputPad(index); - if (track.isNull()) + if (!track) return {}; - GstTagList *tagList = nullptr; - g_object_get(track.object(), "tags", &tagList, nullptr); - - return tagList ? QGstreamerMetaData::fromGstTagList(tagList) : QMediaMetaData{}; + QGstTagListHandle tagList = track.tags(); + return taglistToMetaData(tagList); } int QGstreamerMediaPlayer::activeTrack(TrackType type) @@ -866,14 +1105,14 @@ void QGstreamerMediaPlayer::setActiveTrack(TrackType type, int index) if (type == QPlatformMediaPlayer::SubtitleStream) gstVideoOutput->flushSubtitles(); - playerPipeline.beginConfig(); - if (track.isNull()) { - removeOutput(ts); - } else { - ts.setActiveInputPad(track); - connectOutput(ts); - } - playerPipeline.endConfig(); + playerPipeline.modifyPipelineWhileNotRunning([&] { + if (track.isNull()) { + removeOutput(ts); + } else { + ts.setActiveInputPad(track); + connectOutput(ts); + } + }); // seek to force an immediate change of the stream if (playerPipeline.state() == GST_STATE_PLAYING) |