diff options
Diffstat (limited to 'src/plugins/multimedia/gstreamer/common/qgstreamermediaplayer.cpp')
-rw-r--r-- | src/plugins/multimedia/gstreamer/common/qgstreamermediaplayer.cpp | 1124 |
1 files changed, 1124 insertions, 0 deletions
diff --git a/src/plugins/multimedia/gstreamer/common/qgstreamermediaplayer.cpp b/src/plugins/multimedia/gstreamer/common/qgstreamermediaplayer.cpp new file mode 100644 index 000000000..687bcaba6 --- /dev/null +++ b/src/plugins/multimedia/gstreamer/common/qgstreamermediaplayer.cpp @@ -0,0 +1,1124 @@ +// 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 <QtMultimedia/qaudiodevice.h> +#include <QtCore/qdir.h> +#include <QtCore/qsocketnotifier.h> +#include <QtCore/qurl.h> +#include <QtCore/qdebug.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/private/quniquehandle_p.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> + +#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, QGstElement selector) + : selector(selector), type(type) +{ + selector.set("sync-streams", true); + selector.set("sync-mode", 1 /*clock*/); + + if (type == SubtitleStream) + selector.set("cache-buffers", true); +} + +QGstPad QGstreamerMediaPlayer::TrackSelector::createInputPad() +{ + auto pad = selector.getRequestPad("sink_%u"); + tracks.append(pad); + return pad; +} + +void QGstreamerMediaPlayer::TrackSelector::removeAllInputPads() +{ + for (auto &pad : tracks) + selector.releaseRequestPad(pad); + tracks.clear(); +} + +void QGstreamerMediaPlayer::TrackSelector::removeInputPad(QGstPad pad) +{ + selector.releaseRequestPad(pad); + tracks.removeOne(pad); +} + +QGstPad QGstreamerMediaPlayer::TrackSelector::inputPad(int index) +{ + if (index >= 0 && index < tracks.count()) + return tracks[index]; + return {}; +} + +QGstreamerMediaPlayer::TrackSelector &QGstreamerMediaPlayer::trackSelector(TrackType type) +{ + auto &ts = trackSelectors[type]; + Q_ASSERT(ts.type == type); + return ts; +} + +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, + 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->setParent(this); + gstVideoOutput->setPipeline(playerPipeline); + + for (auto &ts : trackSelectors) + playerPipeline.add(ts.selector); + + playerPipeline.installMessageFilter(static_cast<QGstreamerBusMessageFilter *>(this)); + playerPipeline.installMessageFilter(static_cast<QGstreamerSyncMessageFilter *>(this)); + + QGstClockHandle systemClock{ + gst_system_clock_obtain(), + }; + + gst_pipeline_use_clock(playerPipeline.pipeline(), systemClock.get()); + + connect(&positionUpdateTimer, &QTimer::timeout, this, [this] { + updatePositionFromPipeline(); + }); +} + +QGstreamerMediaPlayer::~QGstreamerMediaPlayer() +{ + playerPipeline.removeMessageFilter(static_cast<QGstreamerBusMessageFilter *>(this)); + playerPipeline.removeMessageFilter(static_cast<QGstreamerSyncMessageFilter *>(this)); + playerPipeline.setStateSync(GST_STATE_NULL); +} + +std::chrono::nanoseconds QGstreamerMediaPlayer::pipelinePosition() const +{ + if (m_url.isEmpty()) + return {}; + + Q_ASSERT(playerPipeline); + return playerPipeline.position(); +} + +void QGstreamerMediaPlayer::updatePositionFromPipeline() +{ + using namespace std::chrono; + + positionChanged(round<milliseconds>(pipelinePosition())); +} + +qint64 QGstreamerMediaPlayer::duration() const +{ + return m_duration.count(); +} + +float QGstreamerMediaPlayer::bufferProgress() const +{ + return m_bufferProgress/100.; +} + +QMediaTimeRange QGstreamerMediaPlayer::availablePlaybackRanges() const +{ + return QMediaTimeRange(); +} + +qreal QGstreamerMediaPlayer::playbackRate() const +{ + return playerPipeline.playbackRate(); +} + +void QGstreamerMediaPlayer::setPlaybackRate(qreal rate) +{ + if (rate == m_rate) + return; + + m_rate = rate; + + playerPipeline.setPlaybackRate(rate); + playbackRateChanged(rate); +} + +void QGstreamerMediaPlayer::setPosition(qint64 pos) +{ + std::chrono::milliseconds posInMs{ pos }; + setPosition(posInMs); +} + +void QGstreamerMediaPlayer::setPosition(std::chrono::milliseconds pos) +{ + if (pos == playerPipeline.position()) + return; + playerPipeline.finishStateChange(); + playerPipeline.setPosition(pos); + qCDebug(qLcMediaPlayer) << Q_FUNC_INFO << pos << playerPipeline.positionInMs(); + if (mediaStatus() == QMediaPlayer::EndOfMedia) + mediaStatusChanged(QMediaPlayer::LoadedMedia); + positionChanged(pos); +} + +void QGstreamerMediaPlayer::play() +{ + if (state() == QMediaPlayer::PlayingState || m_url.isEmpty()) + return; + + if (state() != QMediaPlayer::PausedState) + resetCurrentLoop(); + + playerPipeline.setInStoppedState(false); + if (mediaStatus() == QMediaPlayer::EndOfMedia) { + playerPipeline.setPosition({}); + positionChanged(0); + } + + qCDebug(qLcMediaPlayer) << "play()."; + int ret = playerPipeline.setState(GST_STATE_PLAYING); + if (m_requiresSeekOnPlay) { + // Flushing the pipeline is required to get track changes + // 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."; + + positionUpdateTimer.start(100); + emit stateChanged(QMediaPlayer::PlayingState); +} + +void QGstreamerMediaPlayer::pause() +{ + if (state() == QMediaPlayer::PausedState || m_url.isEmpty() + || m_resourceErrorState != ResourceErrorState::NoError) + return; + + positionUpdateTimer.stop(); + if (playerPipeline.inStoppedState()) { + playerPipeline.setInStoppedState(false); + playerPipeline.flush(); + } + 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({}); + positionChanged(0); + } else { + updatePositionFromPipeline(); + } + emit stateChanged(QMediaPlayer::PausedState); + + if (m_bufferProgress > 0 || !canTrackProgress()) + mediaStatusChanged(QMediaPlayer::BufferedMedia); + else + mediaStatusChanged(QMediaPlayer::BufferingMedia); + + emit bufferProgressChanged(m_bufferProgress / 100.); +} + +void QGstreamerMediaPlayer::stop() +{ + 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(0ms); + positionChanged(0ms); + } + emit stateChanged(QMediaPlayer::StoppedState); + if (eos) + mediaStatusChanged(QMediaPlayer::EndOfMedia); + else + mediaStatusChanged(QMediaPlayer::LoadedMedia); + m_initialBufferProgressSent = false; +} + +void QGstreamerMediaPlayer::detectPipelineIsSeekable() +{ + 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); +} + +bool QGstreamerMediaPlayer::processBusMessage(const QGstreamerMessage &message) +{ + qCDebug(qLcMediaPlayer) << "received bus message:" << message; + + 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 + 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: { + std::chrono::milliseconds d = playerPipeline.durationInMs(); + qCDebug(qLcMediaPlayer) << " duration changed message" << d; + if (d != m_duration) { + m_duration = d; + emit durationChanged(m_duration); + } + return false; + } + case GST_MESSAGE_EOS: { + positionChanged(playerPipeline.durationInMs()); + if (doLoop()) { + setPosition(0); + break; + } + stopOrEOS(true); + break; + } + case GST_MESSAGE_BUFFERING: { + 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; + + emit bufferProgressChanged(m_bufferProgress / 100.); + break; + } + case GST_MESSAGE_STATE_CHANGED: { + if (message.source() != playerPipeline) + return false; + + GstState oldState; + GstState newState; + GstState pending; + + gst_message_parse_state_changed(gm, &oldState, &newState, &pending); + 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: { + 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"); + + std::chrono::milliseconds d = playerPipeline.durationInMs(); + if (d != m_duration) { + m_duration = d; + qCDebug(qLcMediaPlayer) << " duration changed" << d; + emit durationChanged(d); + } + + parseStreamsAndMetadata(); + + emit tracksChanged(); + mediaStatusChanged(QMediaPlayer::LoadedMedia); + + if (!playerPipeline.inStoppedState()) { + Q_ASSERT(!m_initialBufferProgressSent); + + bool immediatelySendBuffered = !canTrackProgress() || m_bufferProgress > 0; + mediaStatusChanged(QMediaPlayer::BufferingMedia); + m_initialBufferProgressSent = true; + if (immediatelySendBuffered) + mediaStatusChanged(QMediaPlayer::BufferedMedia); + } + } + + break; + } + 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: { + qCDebug(qLcMediaPlayer) << " error" << QCompactGstMessageAdaptor(message); + + QUniqueGErrorHandle err; + QUniqueGStringHandle debug; + gst_message_parse_error(gm, &err, &debug); + 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); + break; + } + + case GST_MESSAGE_WARNING: + qCWarning(qLcMediaPlayer) << "Warning:" << QCompactGstMessageAdaptor(message); + playerPipeline.dumpGraph("warning"); + break; + + 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"; + QGstStructureView structure(gst_message_get_structure(gm)); + auto p = structure["position"].toInt64(); + if (p) { + std::chrono::milliseconds position{ + (*p) / 1000000, + }; + emit positionChanged(position); + } + break; + } + case GST_MESSAGE_ELEMENT: { + QGstStructureView structure(gst_message_get_structure(gm)); + auto type = structure.name(); + if (type == "stream-topology") + topology = structure.clone(); + + break; + } + + case GST_MESSAGE_ASYNC_DONE: { + detectPipelineIsSeekable(); + break; + } + + default: +// qCDebug(qLcMediaPlayer) << " default message handler, doing nothing"; + + break; + } + + return false; +} + +bool QGstreamerMediaPlayer::processSyncMessage(const QGstreamerMessage &message) +{ +#if QT_CONFIG(gstreamer_gl) + if (message.type() != GST_MESSAGE_NEED_CONTEXT) + return false; + const gchar *type = nullptr; + gst_message_parse_context_type (message.message(), &type); + if (strcmp(type, GST_GL_DISPLAY_CONTEXT_TYPE)) + return false; + if (!gstVideoOutput || !gstVideoOutput->gstreamerVideoSink()) + return false; + auto *context = gstVideoOutput->gstreamerVideoSink()->gstGlDisplayContext(); + if (!context) + return false; + gst_element_set_context(GST_ELEMENT(GST_MESSAGE_SRC(message.message())), context); + playerPipeline.dumpGraph("need_context"); + return true; +#else + Q_UNUSED(message); + return false; +#endif +} + +QUrl QGstreamerMediaPlayer::media() const +{ + return m_url; +} + +const QIODevice *QGstreamerMediaPlayer::mediaStream() const +{ + return m_stream; +} + +void QGstreamerMediaPlayer::decoderPadAdded(const QGstElement &src, const QGstPad &pad) +{ + if (src != decoder) + return; + + 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; + + TrackType streamType = NTrackTypes; + if (type.startsWith("video/x-raw")) { + streamType = VideoStream; + } else if (type.startsWith("audio/x-raw")) { + streamType = AudioStream; + } else if (type.startsWith("text/")) { + streamType = SubtitleStream; + } else { + qCWarning(qLcMediaPlayer) << "Ignoring unknown media stream:" << pad.name() << type; + return; + } + + auto &ts = trackSelector(streamType); + QGstPad sinkPad = ts.createInputPad(); + if (!pad.link(sinkPad)) { + qCWarning(qLcMediaPlayer) << "Failed to add track, cannot link pads"; + return; + } + qCDebug(qLcMediaPlayer) << "Adding track"; + + if (ts.trackCount() == 1) { + if (streamType == VideoStream) { + connectOutput(ts); + ts.setActiveInputPad(sinkPad); + emit videoAvailableChanged(true); + } + else if (streamType == AudioStream) { + connectOutput(ts); + ts.setActiveInputPad(sinkPad); + emit audioAvailableChanged(true); + } + } + + if (!prerolling) + emit tracksChanged(); + + decoderOutputMap.emplace(pad, sinkPad); +} + +void QGstreamerMediaPlayer::decoderPadRemoved(const QGstElement &src, const QGstPad &pad) +{ + if (src != decoder) + return; + + qCDebug(qLcMediaPlayer) << "Removed pad" << pad.name() << "from" << src.name(); + + 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(); }); + if (ts == std::end(trackSelectors)) + return; + + qCDebug(qLcMediaPlayer) << " was linked to pad" << track.name() << "from" << ts->selector.name(); + ts->removeInputPad(track); + + if (ts->trackCount() == 0) { + removeOutput(*ts); + if (ts->type == AudioStream) + audioAvailableChanged(false); + else if (ts->type == VideoStream) + videoAvailableChanged(false); + } + + if (!prerolling) + tracksChanged(); +} + +void QGstreamerMediaPlayer::removeAllOutputs() +{ + for (auto &ts : trackSelectors) { + removeOutput(ts); + ts.removeAllInputPads(); + } + audioAvailableChanged(false); + videoAvailableChanged(false); +} + +void QGstreamerMediaPlayer::connectOutput(TrackSelector &ts) +{ + if (ts.isConnected) + return; + + QGstElement e; + switch (ts.type) { + case AudioStream: + e = gstAudioOutput ? gstAudioOutput->gstElement() : QGstElement{}; + break; + case VideoStream: + e = gstVideoOutput ? gstVideoOutput->gstElement() : QGstElement{}; + break; + case SubtitleStream: + if (gstVideoOutput) + gstVideoOutput->linkSubtitleStream(ts.selector); + break; + default: + return; + } + + if (!e.isNull()) { + qCDebug(qLcMediaPlayer) << "connecting output for track type" << ts.type; + playerPipeline.add(e); + qLinkGstElements(ts.selector, e); + e.syncStateWithParent(); + } + + ts.isConnected = true; +} + +void QGstreamerMediaPlayer::removeOutput(TrackSelector &ts) +{ + if (!ts.isConnected) + return; + + QGstElement e; + switch (ts.type) { + case AudioStream: + e = gstAudioOutput ? gstAudioOutput->gstElement() : QGstElement{}; + break; + case VideoStream: + e = gstVideoOutput ? gstVideoOutput->gstElement() : QGstElement{}; + break; + case SubtitleStream: + if (gstVideoOutput) + gstVideoOutput->unlinkSubtitleStream(); + break; + default: + break; + } + + if (!e.isNull()) { + qCDebug(qLcMediaPlayer) << "removing output for track type" << ts.type; + playerPipeline.stopAndRemoveElements(e); + } + + ts.isConnected = false; +} + +void QGstreamerMediaPlayer::removeDynamicPipelineElements() +{ + 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(); + + 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) + qCDebug(qLcMediaPlayer) << "Unable to set the pipeline to the stopped state."; + + m_url = content; + m_stream = stream; + + removeDynamicPipelineElements(); + disconnectDecoderHandlers(); + removeAllOutputs(); + seekableChanged(false); + Q_ASSERT(playerPipeline.inStoppedState()); + + if (m_duration != 0ms) { + m_duration = 0ms; + durationChanged(0ms); + } + stateChanged(QMediaPlayer::StoppedState); + if (position() != 0) + 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) { + auto maybeAppSrc = QGstAppSource::create(this); + if (maybeAppSrc) { + m_appSrc = maybeAppSrc.value(); + } else { + error(QMediaPlayer::ResourceError, maybeAppSrc.error()); + return; + } + } + src = m_appSrc->element(); + 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); + qLinkGstElements(src, decoder); + + m_appSrc->setup(m_stream); + seekableChanged(!stream->isSequential()); + } else { + // use uridecodebin + decoder = QGstElement::createFromFactory("uridecodebin", "decoder"); + if (!decoder) { + error(QMediaPlayer::ResourceError, qGstErrorMessageCannotFindElement("uridecodebin")); + return; + } + playerPipeline.add(decoder); + + 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); + } + padAdded = decoder.onPadAdded<&QGstreamerMediaPlayer::decoderPadAdded>(this); + padRemoved = decoder.onPadRemoved<&QGstreamerMediaPlayer::decoderPadRemoved>(this); + + mediaStatusChanged(QMediaPlayer::LoadingMedia); + 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(0ms); + positionChanged(0ms); +} + +void QGstreamerMediaPlayer::setAudioOutput(QPlatformAudioOutput *output) +{ + if (gstAudioOutput == output) + return; + + auto &ts = trackSelector(AudioStream); + + playerPipeline.modifyPipelineWhileNotRunning([&] { + if (gstAudioOutput) + removeOutput(ts); + + gstAudioOutput = static_cast<QGstreamerAudioOutput *>(output); + if (gstAudioOutput) + connectOutput(ts); + }); +} + +QMediaMetaData QGstreamerMediaPlayer::metaData() const +{ + return m_metaData; +} + +void QGstreamerMediaPlayer::setVideoSink(QVideoSink *sink) +{ + gstVideoOutput->setVideoSink(sink); +} + +static QGstStructureView endOfChain(const QGstStructureView &s) +{ + QGstStructureView e = s; + while (1) { + auto next = e["next"].toStructure(); + if (!next.isNull()) + e = next; + else + break; + } + return e; +} + +void QGstreamerMediaPlayer::parseStreamsAndMetadata() +{ + qCDebug(qLcMediaPlayer) << "============== parse topology ============"; + + if (!topology) { + qCDebug(qLcMediaPlayer) << " null topology"; + return; + } + + QGstStructureView topologyView{ topology }; + + QGstCaps caps = topologyView.caps(); + QGstStructureView structure = caps.at(0); + auto fileFormat = QGstreamerFormatInfo::fileFormatForCaps(structure); + 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); + QGstTagListHandle tagList = QGstStructureView{ topology }.tags(); + if (tagList) { + const auto metaData = taglistToMetaData(tagList); + for (auto k : metaData.keys()) + m_metaData.insert(k, metaData.value(k)); + } + + QGstStructureView demux = endOfChain(topologyView); + QGValue next = demux["next"]; + if (!next.isList()) { + qCDebug(qLcMediaPlayer) << " no additional streams"; + emit metaDataChanged(); + return; + } + + // collect stream info + int size = next.listSize(); + for (int i = 0; i < size; ++i) { + auto val = next.at(i); + 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 << (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 << (int)codec; + auto framerate = structure["framerate"].getFraction(); + if (framerate) + m_metaData.insert(QMediaMetaData::VideoFrameRate, *framerate); + + 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) { + 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"); +} + +int QGstreamerMediaPlayer::trackCount(QPlatformMediaPlayer::TrackType type) +{ + return trackSelector(type).trackCount(); +} + +QMediaMetaData QGstreamerMediaPlayer::trackMetaData(QPlatformMediaPlayer::TrackType type, int index) +{ + auto track = trackSelector(type).inputPad(index); + if (!track) + return {}; + + QGstTagListHandle tagList = track.tags(); + return taglistToMetaData(tagList); +} + +int QGstreamerMediaPlayer::activeTrack(TrackType type) +{ + return trackSelector(type).activeInputIndex(); +} + +void QGstreamerMediaPlayer::setActiveTrack(TrackType type, int index) +{ + auto &ts = trackSelector(type); + auto track = ts.inputPad(index); + if (track.isNull() && index != -1) { + qCWarning(qLcMediaPlayer) << "Attempt to set an incorrect index" << index + << "for the track type" << type; + return; + } + + qCDebug(qLcMediaPlayer) << "Setting the index" << index << "for the track type" << type; + if (type == QPlatformMediaPlayer::SubtitleStream) + gstVideoOutput->flushSubtitles(); + + 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) + playerPipeline.flush(); + else + m_requiresSeekOnPlay = true; +} + +QT_END_NAMESPACE |