/**************************************************************************** ** ** 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 #include #include #include #include #if !GST_CHECK_VERSION(1,0,0) #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include //#define DEBUG_PLAYBIN QT_BEGIN_NAMESPACE static bool usePlaybinVolume() { static enum { Yes, No, Unknown } status = Unknown; if (status == Unknown) { QByteArray v = qgetenv("QT_GSTREAMER_USE_PLAYBIN_VOLUME"); bool value = !v.isEmpty() && v != "0" && v != "false"; if (value) status = Yes; else status = No; } return status == Yes; } typedef enum { GST_PLAY_FLAG_VIDEO = 0x00000001, GST_PLAY_FLAG_AUDIO = 0x00000002, GST_PLAY_FLAG_TEXT = 0x00000004, GST_PLAY_FLAG_VIS = 0x00000008, GST_PLAY_FLAG_SOFT_VOLUME = 0x00000010, GST_PLAY_FLAG_NATIVE_AUDIO = 0x00000020, GST_PLAY_FLAG_NATIVE_VIDEO = 0x00000040, GST_PLAY_FLAG_DOWNLOAD = 0x00000080, GST_PLAY_FLAG_BUFFERING = 0x000000100 } GstPlayFlags; #if !GST_CHECK_VERSION(1,0,0) #define DEFAULT_RAW_CAPS \ "video/x-raw-yuv; " \ "video/x-raw-rgb; " \ "video/x-raw-gray; " \ "video/x-surface; " \ "video/x-android-buffer; " \ "audio/x-raw-int; " \ "audio/x-raw-float; " \ "text/plain; " \ "text/x-pango-markup; " \ "video/x-dvd-subpicture; " \ "subpicture/x-pgs" static GstStaticCaps static_RawCaps = GST_STATIC_CAPS(DEFAULT_RAW_CAPS); #endif QGstreamerPlayerSession::QGstreamerPlayerSession(QObject *parent) : QObject(parent) { initPlaybin(); } void QGstreamerPlayerSession::initPlaybin() { m_playbin = gst_element_factory_make(QT_GSTREAMER_PLAYBIN_ELEMENT_NAME, nullptr); if (m_playbin) { //GST_PLAY_FLAG_NATIVE_VIDEO omits configuration of ffmpegcolorspace and videoscale, //since those elements are included in the video output bin when necessary. int flags = GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_AUDIO; QByteArray envFlags = qgetenv("QT_GSTREAMER_PLAYBIN_FLAGS"); if (!envFlags.isEmpty()) { flags |= envFlags.toInt(); #if !GST_CHECK_VERSION(1,0,0) } else { flags |= GST_PLAY_FLAG_NATIVE_VIDEO; #endif } g_object_set(G_OBJECT(m_playbin), "flags", flags, nullptr); const QByteArray envAudioSink = qgetenv("QT_GSTREAMER_PLAYBIN_AUDIOSINK"); GstElement *audioSink = gst_element_factory_make(envAudioSink.isEmpty() ? "autoaudiosink" : envAudioSink, "audiosink"); if (audioSink) { if (usePlaybinVolume()) { m_audioSink = audioSink; m_volumeElement = m_playbin; } else { m_volumeElement = gst_element_factory_make("volume", "volumeelement"); if (m_volumeElement) { m_audioSink = gst_bin_new("audio-output-bin"); gst_bin_add_many(GST_BIN(m_audioSink), m_volumeElement, audioSink, nullptr); gst_element_link(m_volumeElement, audioSink); GstPad *pad = gst_element_get_static_pad(m_volumeElement, "sink"); gst_element_add_pad(GST_ELEMENT(m_audioSink), gst_ghost_pad_new("sink", pad)); gst_object_unref(GST_OBJECT(pad)); } else { m_audioSink = audioSink; m_volumeElement = m_playbin; } } g_object_set(G_OBJECT(m_playbin), "audio-sink", m_audioSink, nullptr); addAudioBufferProbe(); } } #if GST_CHECK_VERSION(1,0,0) static const auto convDesc = qEnvironmentVariable("QT_GSTREAMER_PLAYBIN_CONVERT"); GError *err = nullptr; auto convPipeline = !convDesc.isEmpty() ? convDesc.toLatin1().constData() : "identity"; auto convElement = gst_parse_launch(convPipeline, &err); if (err) { qWarning() << "Error:" << convDesc << ":" << QLatin1String(err->message); g_clear_error(&err); } m_videoIdentity = convElement; #else m_videoIdentity = GST_ELEMENT(g_object_new(gst_video_connector_get_type(), 0)); // floating ref g_signal_connect(G_OBJECT(m_videoIdentity), "connection-failed", G_CALLBACK(insertColorSpaceElement), (gpointer)this); m_colorSpace = gst_element_factory_make(QT_GSTREAMER_COLORCONVERSION_ELEMENT_NAME, "ffmpegcolorspace-vo"); // might not get a parent, take ownership to avoid leak qt_gst_object_ref_sink(GST_OBJECT(m_colorSpace)); #endif m_nullVideoSink = gst_element_factory_make("fakesink", nullptr); g_object_set(G_OBJECT(m_nullVideoSink), "sync", true, nullptr); gst_object_ref(GST_OBJECT(m_nullVideoSink)); m_videoOutputBin = gst_bin_new("video-output-bin"); // might not get a parent, take ownership to avoid leak qt_gst_object_ref_sink(GST_OBJECT(m_videoOutputBin)); GstElement *videoOutputSink = m_videoIdentity; #if QT_CONFIG(gstreamer_gl) if (QGstUtils::useOpenGL()) { videoOutputSink = gst_element_factory_make("glupload", nullptr); GstElement *colorConvert = gst_element_factory_make("glcolorconvert", nullptr); gst_bin_add_many(GST_BIN(m_videoOutputBin), videoOutputSink, colorConvert, m_videoIdentity, m_nullVideoSink, nullptr); gst_element_link_many(videoOutputSink, colorConvert, m_videoIdentity, nullptr); } else { gst_bin_add_many(GST_BIN(m_videoOutputBin), m_videoIdentity, m_nullVideoSink, nullptr); } #else gst_bin_add_many(GST_BIN(m_videoOutputBin), m_videoIdentity, m_nullVideoSink, nullptr); #endif gst_element_link(m_videoIdentity, m_nullVideoSink); m_videoSink = m_nullVideoSink; // add ghostpads GstPad *pad = gst_element_get_static_pad(videoOutputSink, "sink"); gst_element_add_pad(GST_ELEMENT(m_videoOutputBin), gst_ghost_pad_new("sink", pad)); gst_object_unref(GST_OBJECT(pad)); if (m_playbin != 0) { // Sort out messages setBus(gst_element_get_bus(m_playbin)); g_object_set(G_OBJECT(m_playbin), "video-sink", m_videoOutputBin, nullptr); g_signal_connect(G_OBJECT(m_playbin), "notify::source", G_CALLBACK(playbinNotifySource), this); g_signal_connect(G_OBJECT(m_playbin), "element-added", G_CALLBACK(handleElementAdded), this); if (usePlaybinVolume()) { updateVolume(); updateMuted(); g_signal_connect(G_OBJECT(m_playbin), "notify::volume", G_CALLBACK(handleVolumeChange), this); g_signal_connect(G_OBJECT(m_playbin), "notify::mute", G_CALLBACK(handleMutedChange), this); } g_signal_connect(G_OBJECT(m_playbin), "video-changed", G_CALLBACK(handleStreamsChange), this); g_signal_connect(G_OBJECT(m_playbin), "audio-changed", G_CALLBACK(handleStreamsChange), this); g_signal_connect(G_OBJECT(m_playbin), "text-changed", G_CALLBACK(handleStreamsChange), this); #if QT_CONFIG(gstreamer_app) g_signal_connect(G_OBJECT(m_playbin), "deep-notify::source", G_CALLBACK(configureAppSrcElement), this); #endif m_pipeline = m_playbin; gst_object_ref(GST_OBJECT(m_pipeline)); } } QGstreamerPlayerSession::~QGstreamerPlayerSession() { if (m_pipeline) { stop(); removeVideoBufferProbe(); removeAudioBufferProbe(); delete m_busHelper; m_busHelper = nullptr; resetElements(); } } template static inline void resetGstObject(T *&obj, T *v = nullptr) { if (obj) gst_object_unref(GST_OBJECT(obj)); obj = v; } void QGstreamerPlayerSession::resetElements() { setBus(nullptr); resetGstObject(m_playbin); resetGstObject(m_pipeline); #if !GST_CHECK_VERSION(1,0,0) resetGstObject(m_colorSpace); #endif resetGstObject(m_nullVideoSink); resetGstObject(m_videoOutputBin); m_audioSink = nullptr; m_volumeElement = nullptr; m_videoIdentity = nullptr; m_pendingVideoSink = nullptr; m_videoSink = nullptr; } GstElement *QGstreamerPlayerSession::playbin() const { return m_playbin; } #if QT_CONFIG(gstreamer_app) void QGstreamerPlayerSession::configureAppSrcElement(GObject* object, GObject *orig, GParamSpec *pspec, QGstreamerPlayerSession* self) { Q_UNUSED(object); Q_UNUSED(pspec); if (!self->appsrc()) return; GstElement *appsrc; g_object_get(orig, "source", &appsrc, nullptr); if (!self->appsrc()->setup(appsrc)) qWarning()<<"Could not setup appsrc element"; g_object_unref(G_OBJECT(appsrc)); } #endif void QGstreamerPlayerSession::loadFromStream(const QNetworkRequest &request, QIODevice *appSrcStream) { #if QT_CONFIG(gstreamer_app) #ifdef DEBUG_PLAYBIN qDebug() << Q_FUNC_INFO; #endif m_request = request; m_duration = 0; m_lastPosition = 0; if (!m_appSrc) m_appSrc = new QGstAppSrc(this); m_appSrc->setStream(appSrcStream); if (!parsePipeline() && m_playbin) { m_tags.clear(); emit tagsChanged(); g_object_set(G_OBJECT(m_playbin), "uri", "appsrc://", nullptr); if (!m_streamTypes.isEmpty()) { m_streamProperties.clear(); m_streamTypes.clear(); emit streamsChanged(); } } #endif } void QGstreamerPlayerSession::loadFromUri(const QNetworkRequest &request) { #ifdef DEBUG_PLAYBIN qDebug() << Q_FUNC_INFO << request.url(); #endif m_request = request; m_duration = 0; m_lastPosition = 0; #if QT_CONFIG(gstreamer_app) if (m_appSrc) { m_appSrc->deleteLater(); m_appSrc = 0; } #endif if (!parsePipeline() && m_playbin) { m_tags.clear(); emit tagsChanged(); g_object_set(G_OBJECT(m_playbin), "uri", m_request.url().toEncoded().constData(), nullptr); if (!m_streamTypes.isEmpty()) { m_streamProperties.clear(); m_streamTypes.clear(); emit streamsChanged(); } } } bool QGstreamerPlayerSession::parsePipeline() { if (m_request.url().scheme() != QLatin1String("gst-pipeline")) { if (!m_playbin) { resetElements(); initPlaybin(); updateVideoRenderer(); } return false; } // Set current surface to video sink before creating a pipeline. auto renderer = qobject_cast(m_videoOutput); if (renderer) QVideoSurfaceGstSink::setSurface(renderer->surface()); QString url = m_request.url().toString(QUrl::RemoveScheme); QString desc = QUrl::fromPercentEncoding(url.toLatin1().constData()); GError *err = nullptr; GstElement *pipeline = gst_parse_launch(desc.toLatin1().constData(), &err); if (err) { auto errstr = QLatin1String(err->message); qWarning() << "Error:" << desc << ":" << errstr; emit error(QMediaPlayer::FormatError, errstr); g_clear_error(&err); } return setPipeline(pipeline); } static void gst_foreach(GstIterator *it, const std::function &cmp) { #if GST_CHECK_VERSION(1,0,0) GValue value = G_VALUE_INIT; while (gst_iterator_next (it, &value) == GST_ITERATOR_OK) { auto child = static_cast(g_value_get_object(&value)); #else GstElement *child = nullptr; while (gst_iterator_next(it, reinterpret_cast(&child)) == GST_ITERATOR_OK) { #endif if (cmp(child)) break; } gst_iterator_free(it); #if GST_CHECK_VERSION(1,0,0) g_value_unset(&value); #endif } bool QGstreamerPlayerSession::setPipeline(GstElement *pipeline) { GstBus *bus = pipeline ? gst_element_get_bus(pipeline) : nullptr; if (!bus) return false; if (m_playbin) gst_element_set_state(m_playbin, GST_STATE_NULL); resetElements(); setBus(bus); m_pipeline = pipeline; if (m_renderer) { gst_foreach(gst_bin_iterate_sinks(GST_BIN(pipeline)), [this](GstElement *child) { if (qstrcmp(GST_OBJECT_NAME(child), "qtvideosink") == 0) { m_renderer->setVideoSink(child); return true; } return false; }); } #if QT_CONFIG(gstreamer_app) if (m_appSrc) { gst_foreach(gst_bin_iterate_sources(GST_BIN(pipeline)), [this](GstElement *child) { if (qstrcmp(qt_gst_element_get_factory_name(child), "appsrc") == 0) { m_appSrc->setup(child); return true; } return false; }); } #endif emit pipelineChanged(); return true; } void QGstreamerPlayerSession::setBus(GstBus *bus) { resetGstObject(m_bus, bus); // It might still accept gst messages. if (m_busHelper) m_busHelper->deleteLater(); m_busHelper = nullptr; if (!m_bus) return; m_busHelper = new QGstreamerBusHelper(m_bus, this); m_busHelper->installMessageFilter(this); if (m_videoOutput) m_busHelper->installMessageFilter(m_videoOutput); } qint64 QGstreamerPlayerSession::duration() const { return m_duration; } qint64 QGstreamerPlayerSession::position() const { gint64 position = 0; if (m_pipeline && qt_gst_element_query_position(m_pipeline, GST_FORMAT_TIME, &position)) m_lastPosition = position / 1000000; return m_lastPosition; } qreal QGstreamerPlayerSession::playbackRate() const { return m_playbackRate; } void QGstreamerPlayerSession::setPlaybackRate(qreal rate) { #ifdef DEBUG_PLAYBIN qDebug() << Q_FUNC_INFO << rate; #endif if (!qFuzzyCompare(m_playbackRate, rate)) { m_playbackRate = rate; if (m_pipeline && m_seekable) { qint64 from = rate > 0 ? position() : 0; qint64 to = rate > 0 ? duration() : position(); gst_element_seek(m_pipeline, rate, GST_FORMAT_TIME, GstSeekFlags(GST_SEEK_FLAG_FLUSH), GST_SEEK_TYPE_SET, from * 1000000, GST_SEEK_TYPE_SET, to * 1000000); } emit playbackRateChanged(m_playbackRate); } } QMediaTimeRange QGstreamerPlayerSession::availablePlaybackRanges() const { QMediaTimeRange ranges; if (duration() <= 0) return ranges; #if GST_CHECK_VERSION(0, 10, 31) //GST_FORMAT_TIME would be more appropriate, but unfortunately it's not supported. //with GST_FORMAT_PERCENT media is treated as encoded with constant bitrate. GstQuery* query = gst_query_new_buffering(GST_FORMAT_PERCENT); if (!gst_element_query(m_pipeline, query)) { gst_query_unref(query); return ranges; } gint64 rangeStart = 0; gint64 rangeStop = 0; for (guint index = 0; index < gst_query_get_n_buffering_ranges(query); index++) { if (gst_query_parse_nth_buffering_range(query, index, &rangeStart, &rangeStop)) ranges.addInterval(rangeStart * duration() / 100, rangeStop * duration() / 100); } gst_query_unref(query); #endif if (ranges.isEmpty() && !isLiveSource() && isSeekable()) ranges.addInterval(0, duration()); #ifdef DEBUG_PLAYBIN qDebug() << ranges; #endif return ranges; } int QGstreamerPlayerSession::activeStream(QMediaStreamsControl::StreamType streamType) const { int streamNumber = -1; if (m_playbin) { switch (streamType) { case QMediaStreamsControl::AudioStream: g_object_get(G_OBJECT(m_playbin), "current-audio", &streamNumber, nullptr); break; case QMediaStreamsControl::VideoStream: g_object_get(G_OBJECT(m_playbin), "current-video", &streamNumber, nullptr); break; case QMediaStreamsControl::SubPictureStream: g_object_get(G_OBJECT(m_playbin), "current-text", &streamNumber, nullptr); break; default: break; } } if (streamNumber >= 0) streamNumber += m_playbin2StreamOffset.value(streamType,0); return streamNumber; } void QGstreamerPlayerSession::setActiveStream(QMediaStreamsControl::StreamType streamType, int streamNumber) { #ifdef DEBUG_PLAYBIN qDebug() << Q_FUNC_INFO << streamType << streamNumber; #endif if (streamNumber >= 0) streamNumber -= m_playbin2StreamOffset.value(streamType,0); if (m_playbin) { switch (streamType) { case QMediaStreamsControl::AudioStream: g_object_set(G_OBJECT(m_playbin), "current-audio", streamNumber, nullptr); break; case QMediaStreamsControl::VideoStream: g_object_set(G_OBJECT(m_playbin), "current-video", streamNumber, nullptr); break; case QMediaStreamsControl::SubPictureStream: g_object_set(G_OBJECT(m_playbin), "current-text", streamNumber, nullptr); break; default: break; } } } int QGstreamerPlayerSession::volume() const { return m_volume; } bool QGstreamerPlayerSession::isMuted() const { return m_muted; } bool QGstreamerPlayerSession::isAudioAvailable() const { return m_audioAvailable; } #if GST_CHECK_VERSION(1,0,0) static GstPadProbeReturn block_pad_cb(GstPad *pad, GstPadProbeInfo *info, gpointer user_data) #else static void block_pad_cb(GstPad *pad, gboolean blocked, gpointer user_data) #endif { Q_UNUSED(pad); #if GST_CHECK_VERSION(1,0,0) Q_UNUSED(info); Q_UNUSED(user_data); return GST_PAD_PROBE_OK; #else #ifdef DEBUG_PLAYBIN qDebug() << "block_pad_cb, blocked:" << blocked; #endif if (blocked && user_data) { QGstreamerPlayerSession *session = reinterpret_cast(user_data); QMetaObject::invokeMethod(session, "finishVideoOutputChange", Qt::QueuedConnection); } #endif } void QGstreamerPlayerSession::updateVideoRenderer() { #ifdef DEBUG_PLAYBIN qDebug() << "Video sink has chaged, reload video output"; #endif if (m_videoOutput) setVideoRenderer(m_videoOutput); } void QGstreamerPlayerSession::setVideoRenderer(QObject *videoOutput) { #ifdef DEBUG_PLAYBIN qDebug() << Q_FUNC_INFO; #endif if (m_videoOutput != videoOutput) { if (m_videoOutput) { disconnect(m_videoOutput, SIGNAL(sinkChanged()), this, SLOT(updateVideoRenderer())); disconnect(m_videoOutput, SIGNAL(readyChanged(bool)), this, SLOT(updateVideoRenderer())); m_busHelper->removeMessageFilter(m_videoOutput); } m_videoOutput = videoOutput; if (m_videoOutput) { connect(m_videoOutput, SIGNAL(sinkChanged()), this, SLOT(updateVideoRenderer())); connect(m_videoOutput, SIGNAL(readyChanged(bool)), this, SLOT(updateVideoRenderer())); m_busHelper->installMessageFilter(m_videoOutput); } } m_renderer = qobject_cast(videoOutput); emit rendererChanged(); // No sense to continue if custom pipeline requested. if (!m_playbin) return; GstElement *videoSink = 0; if (m_renderer && m_renderer->isReady()) videoSink = m_renderer->videoSink(); if (!videoSink) videoSink = m_nullVideoSink; #ifdef DEBUG_PLAYBIN qDebug() << "Set video output:" << videoOutput; qDebug() << "Current sink:" << (m_videoSink ? GST_ELEMENT_NAME(m_videoSink) : "") << m_videoSink << "pending:" << (m_pendingVideoSink ? GST_ELEMENT_NAME(m_pendingVideoSink) : "") << m_pendingVideoSink << "new sink:" << (videoSink ? GST_ELEMENT_NAME(videoSink) : "") << videoSink; #endif if (m_pendingVideoSink == videoSink || (m_pendingVideoSink == 0 && m_videoSink == videoSink)) { #ifdef DEBUG_PLAYBIN qDebug() << "Video sink has not changed, skip video output reconfiguration"; #endif return; } #ifdef DEBUG_PLAYBIN qDebug() << "Reconfigure video output"; #endif if (m_state == QMediaPlayer::StoppedState) { #ifdef DEBUG_PLAYBIN qDebug() << "The pipeline has not started yet, pending state:" << m_pendingState; #endif //the pipeline has not started yet flushVideoProbes(); m_pendingVideoSink = 0; gst_element_set_state(m_videoSink, GST_STATE_NULL); gst_element_set_state(m_playbin, GST_STATE_NULL); #if !GST_CHECK_VERSION(1,0,0) if (m_usingColorspaceElement) { gst_element_unlink(m_colorSpace, m_videoSink); gst_bin_remove(GST_BIN(m_videoOutputBin), m_colorSpace); } else { gst_element_unlink(m_videoIdentity, m_videoSink); } #endif removeVideoBufferProbe(); gst_bin_remove(GST_BIN(m_videoOutputBin), m_videoSink); m_videoSink = videoSink; gst_bin_add(GST_BIN(m_videoOutputBin), m_videoSink); bool linked = gst_element_link(m_videoIdentity, m_videoSink); #if !GST_CHECK_VERSION(1,0,0) m_usingColorspaceElement = false; if (!linked) { m_usingColorspaceElement = true; #ifdef DEBUG_PLAYBIN qDebug() << "Failed to connect video output, inserting the colorspace element."; #endif gst_bin_add(GST_BIN(m_videoOutputBin), m_colorSpace); linked = gst_element_link_many(m_videoIdentity, m_colorSpace, m_videoSink, nullptr); } #endif if (!linked) qWarning() << "Linking video output element failed"; if (g_object_class_find_property(G_OBJECT_GET_CLASS(m_videoSink), "show-preroll-frame") != 0) { gboolean value = m_displayPrerolledFrame; g_object_set(G_OBJECT(m_videoSink), "show-preroll-frame", value, nullptr); } addVideoBufferProbe(); switch (m_pendingState) { case QMediaPlayer::PausedState: gst_element_set_state(m_playbin, GST_STATE_PAUSED); break; case QMediaPlayer::PlayingState: gst_element_set_state(m_playbin, GST_STATE_PLAYING); break; default: break; } resumeVideoProbes(); } else { if (m_pendingVideoSink) { #ifdef DEBUG_PLAYBIN qDebug() << "already waiting for pad to be blocked, just change the pending sink"; #endif m_pendingVideoSink = videoSink; return; } m_pendingVideoSink = videoSink; #ifdef DEBUG_PLAYBIN qDebug() << "Blocking the video output pad..."; #endif //block pads, async to avoid locking in paused state GstPad *srcPad = gst_element_get_static_pad(m_videoIdentity, "src"); #if GST_CHECK_VERSION(1,0,0) this->pad_probe_id = gst_pad_add_probe(srcPad, (GstPadProbeType)(GST_PAD_PROBE_TYPE_BUFFER | GST_PAD_PROBE_TYPE_BLOCKING), block_pad_cb, this, nullptr); #else gst_pad_set_blocked_async(srcPad, true, &block_pad_cb, this); #endif gst_object_unref(GST_OBJECT(srcPad)); //Unpause the sink to avoid waiting until the buffer is processed //while the sink is paused. The pad will be blocked as soon as the current //buffer is processed. if (m_state == QMediaPlayer::PausedState) { #ifdef DEBUG_PLAYBIN qDebug() << "Starting video output to avoid blocking in paused state..."; #endif gst_element_set_state(m_videoSink, GST_STATE_PLAYING); } } } void QGstreamerPlayerSession::finishVideoOutputChange() { if (!m_playbin || !m_pendingVideoSink) return; #ifdef DEBUG_PLAYBIN qDebug() << "finishVideoOutputChange" << m_pendingVideoSink; #endif GstPad *srcPad = gst_element_get_static_pad(m_videoIdentity, "src"); if (!gst_pad_is_blocked(srcPad)) { //pad is not blocked, it's possible to swap outputs only in the null state qWarning() << "Pad is not blocked yet, could not switch video sink"; GstState identityElementState = GST_STATE_NULL; gst_element_get_state(m_videoIdentity, &identityElementState, nullptr, GST_CLOCK_TIME_NONE); if (identityElementState != GST_STATE_NULL) { gst_object_unref(GST_OBJECT(srcPad)); return; //can't change vo yet, received async call from the previous change } } if (m_pendingVideoSink == m_videoSink) { qDebug() << "Abort, no change"; //video output was change back to the current one, //no need to torment the pipeline, just unblock the pad if (gst_pad_is_blocked(srcPad)) #if GST_CHECK_VERSION(1,0,0) gst_pad_remove_probe(srcPad, this->pad_probe_id); #else gst_pad_set_blocked_async(srcPad, false, &block_pad_cb, 0); #endif m_pendingVideoSink = 0; gst_object_unref(GST_OBJECT(srcPad)); return; } #if !GST_CHECK_VERSION(1,0,0) if (m_usingColorspaceElement) { gst_element_set_state(m_colorSpace, GST_STATE_NULL); gst_element_set_state(m_videoSink, GST_STATE_NULL); gst_element_unlink(m_colorSpace, m_videoSink); gst_bin_remove(GST_BIN(m_videoOutputBin), m_colorSpace); } else { #else { #endif gst_element_set_state(m_videoSink, GST_STATE_NULL); gst_element_unlink(m_videoIdentity, m_videoSink); } removeVideoBufferProbe(); gst_bin_remove(GST_BIN(m_videoOutputBin), m_videoSink); m_videoSink = m_pendingVideoSink; m_pendingVideoSink = 0; gst_bin_add(GST_BIN(m_videoOutputBin), m_videoSink); addVideoBufferProbe(); bool linked = gst_element_link(m_videoIdentity, m_videoSink); #if !GST_CHECK_VERSION(1,0,0) m_usingColorspaceElement = false; if (!linked) { m_usingColorspaceElement = true; #ifdef DEBUG_PLAYBIN qDebug() << "Failed to connect video output, inserting the colorspace element."; #endif gst_bin_add(GST_BIN(m_videoOutputBin), m_colorSpace); linked = gst_element_link_many(m_videoIdentity, m_colorSpace, m_videoSink, nullptr); } #endif if (!linked) qWarning() << "Linking video output element failed"; #ifdef DEBUG_PLAYBIN qDebug() << "notify the video connector it has to emit a new segment message..."; #endif #if !GST_CHECK_VERSION(1,0,0) //it's necessary to send a new segment event just before //the first buffer pushed to the new sink g_signal_emit_by_name(m_videoIdentity, "resend-new-segment", true //emit connection-failed signal //to have a chance to insert colorspace element ); #endif GstState state = GST_STATE_VOID_PENDING; switch (m_pendingState) { case QMediaPlayer::StoppedState: state = GST_STATE_NULL; break; case QMediaPlayer::PausedState: state = GST_STATE_PAUSED; break; case QMediaPlayer::PlayingState: state = GST_STATE_PLAYING; break; } #if !GST_CHECK_VERSION(1,0,0) if (m_usingColorspaceElement) gst_element_set_state(m_colorSpace, state); #endif gst_element_set_state(m_videoSink, state); if (state == GST_STATE_NULL) flushVideoProbes(); // Set state change that was deferred due the video output // change being pending gst_element_set_state(m_playbin, state); if (state != GST_STATE_NULL) resumeVideoProbes(); //don't have to wait here, it will unblock eventually if (gst_pad_is_blocked(srcPad)) #if GST_CHECK_VERSION(1,0,0) gst_pad_remove_probe(srcPad, this->pad_probe_id); #else gst_pad_set_blocked_async(srcPad, false, &block_pad_cb, 0); #endif gst_object_unref(GST_OBJECT(srcPad)); } #if !GST_CHECK_VERSION(1,0,0) void QGstreamerPlayerSession::insertColorSpaceElement(GstElement *element, gpointer data) { #ifdef DEBUG_PLAYBIN qDebug() << Q_FUNC_INFO; #endif Q_UNUSED(element); QGstreamerPlayerSession* session = reinterpret_cast(data); if (session->m_usingColorspaceElement) return; session->m_usingColorspaceElement = true; #ifdef DEBUG_PLAYBIN qDebug() << "Failed to connect video output, inserting the colorspace elemnt."; qDebug() << "notify the video connector it has to emit a new segment message..."; #endif //it's necessary to send a new segment event just before //the first buffer pushed to the new sink g_signal_emit_by_name(session->m_videoIdentity, "resend-new-segment", false // don't emit connection-failed signal ); gst_element_unlink(session->m_videoIdentity, session->m_videoSink); gst_bin_add(GST_BIN(session->m_videoOutputBin), session->m_colorSpace); gst_element_link_many(session->m_videoIdentity, session->m_colorSpace, session->m_videoSink, nullptr); GstState state = GST_STATE_VOID_PENDING; switch (session->m_pendingState) { case QMediaPlayer::StoppedState: state = GST_STATE_NULL; break; case QMediaPlayer::PausedState: state = GST_STATE_PAUSED; break; case QMediaPlayer::PlayingState: state = GST_STATE_PLAYING; break; } gst_element_set_state(session->m_colorSpace, state); } #endif bool QGstreamerPlayerSession::isVideoAvailable() const { return m_videoAvailable; } bool QGstreamerPlayerSession::isSeekable() const { return m_seekable; } bool QGstreamerPlayerSession::play() { #if GST_CHECK_VERSION(1,0,0) static bool dumpDot = qEnvironmentVariableIsSet("GST_DEBUG_DUMP_DOT_DIR"); if (dumpDot) gst_debug_bin_to_dot_file_with_ts(GST_BIN(m_pipeline), GstDebugGraphDetails(GST_DEBUG_GRAPH_SHOW_ALL), "gst.play"); #endif #ifdef DEBUG_PLAYBIN qDebug() << Q_FUNC_INFO; #endif m_everPlayed = false; if (m_pipeline) { m_pendingState = QMediaPlayer::PlayingState; if (gst_element_set_state(m_pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) { qWarning() << "GStreamer; Unable to play -" << m_request.url().toString(); m_pendingState = m_state = QMediaPlayer::StoppedState; emit stateChanged(m_state); } else { resumeVideoProbes(); return true; } } return false; } bool QGstreamerPlayerSession::pause() { #ifdef DEBUG_PLAYBIN qDebug() << Q_FUNC_INFO; #endif if (m_pipeline) { m_pendingState = QMediaPlayer::PausedState; if (m_pendingVideoSink != 0) return true; if (gst_element_set_state(m_pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE) { qWarning() << "GStreamer; Unable to pause -" << m_request.url().toString(); m_pendingState = m_state = QMediaPlayer::StoppedState; emit stateChanged(m_state); } else { resumeVideoProbes(); return true; } } return false; } void QGstreamerPlayerSession::stop() { #ifdef DEBUG_PLAYBIN qDebug() << Q_FUNC_INFO; #endif m_everPlayed = false; if (m_pipeline) { if (m_renderer) m_renderer->stopRenderer(); flushVideoProbes(); gst_element_set_state(m_pipeline, GST_STATE_NULL); m_lastPosition = 0; QMediaPlayer::State oldState = m_state; m_pendingState = m_state = QMediaPlayer::StoppedState; finishVideoOutputChange(); //we have to do it here, since gstreamer will not emit bus messages any more setSeekable(false); if (oldState != m_state) emit stateChanged(m_state); } } bool QGstreamerPlayerSession::seek(qint64 ms) { #ifdef DEBUG_PLAYBIN qDebug() << Q_FUNC_INFO << ms; #endif //seek locks when the video output sink is changing and pad is blocked if (m_pipeline && !m_pendingVideoSink && m_state != QMediaPlayer::StoppedState && m_seekable) { ms = qMax(ms,qint64(0)); qint64 from = m_playbackRate > 0 ? ms : 0; qint64 to = m_playbackRate > 0 ? duration() : ms; bool isSeeking = gst_element_seek(m_pipeline, m_playbackRate, GST_FORMAT_TIME, GstSeekFlags(GST_SEEK_FLAG_FLUSH), GST_SEEK_TYPE_SET, from * 1000000, GST_SEEK_TYPE_SET, to * 1000000); if (isSeeking) m_lastPosition = ms; return isSeeking; } return false; } void QGstreamerPlayerSession::setVolume(int volume) { #ifdef DEBUG_PLAYBIN qDebug() << Q_FUNC_INFO << volume; #endif if (m_volume != volume) { m_volume = volume; if (m_volumeElement) g_object_set(G_OBJECT(m_volumeElement), "volume", m_volume / 100.0, nullptr); emit volumeChanged(m_volume); } } void QGstreamerPlayerSession::setMuted(bool muted) { #ifdef DEBUG_PLAYBIN qDebug() << Q_FUNC_INFO << muted; #endif if (m_muted != muted) { m_muted = muted; if (m_volumeElement) g_object_set(G_OBJECT(m_volumeElement), "mute", m_muted ? TRUE : FALSE, nullptr); emit mutedStateChanged(m_muted); } } void QGstreamerPlayerSession::setSeekable(bool seekable) { #ifdef DEBUG_PLAYBIN qDebug() << Q_FUNC_INFO << seekable; #endif if (seekable != m_seekable) { m_seekable = seekable; emit seekableChanged(m_seekable); } } bool QGstreamerPlayerSession::processBusMessage(const QGstreamerMessage &message) { GstMessage* gm = message.rawMessage(); if (gm) { //tag message comes from elements inside playbin, not from playbin itself if (GST_MESSAGE_TYPE(gm) == GST_MESSAGE_TAG) { GstTagList *tag_list; gst_message_parse_tag(gm, &tag_list); QMap newTags = QGstUtils::gstTagListToMap(tag_list); QMap::const_iterator it = newTags.constBegin(); for ( ; it != newTags.constEnd(); ++it) m_tags.insert(it.key(), it.value()); // overwrite existing tags gst_tag_list_free(tag_list); emit tagsChanged(); } else if (GST_MESSAGE_TYPE(gm) == GST_MESSAGE_DURATION) { updateDuration(); } #ifdef DEBUG_PLAYBIN if (m_sourceType == MMSSrc && qstrcmp(GST_OBJECT_NAME(GST_MESSAGE_SRC(gm)), "source") == 0) { qDebug() << "Message from MMSSrc: " << GST_MESSAGE_TYPE(gm); } else if (m_sourceType == RTSPSrc && qstrcmp(GST_OBJECT_NAME(GST_MESSAGE_SRC(gm)), "source") == 0) { qDebug() << "Message from RTSPSrc: " << GST_MESSAGE_TYPE(gm); } else { qDebug() << "Message from " << GST_OBJECT_NAME(GST_MESSAGE_SRC(gm)) << ":" << GST_MESSAGE_TYPE(gm); } #endif if (GST_MESSAGE_TYPE(gm) == GST_MESSAGE_BUFFERING) { int progress = 0; gst_message_parse_buffering(gm, &progress); emit bufferingProgressChanged(progress); } bool handlePlaybin2 = false; if (GST_MESSAGE_SRC(gm) == GST_OBJECT_CAST(m_pipeline)) { switch (GST_MESSAGE_TYPE(gm)) { case GST_MESSAGE_STATE_CHANGED: { GstState oldState; GstState newState; GstState pending; gst_message_parse_state_changed(gm, &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") }; qDebug() << QStringLiteral("state changed: old: %1 new: %2 pending: %3") \ .arg(states[oldState]) \ .arg(states[newState]) \ .arg(states[pending]); #endif switch (newState) { case GST_STATE_VOID_PENDING: case GST_STATE_NULL: setSeekable(false); finishVideoOutputChange(); if (m_state != QMediaPlayer::StoppedState) emit stateChanged(m_state = QMediaPlayer::StoppedState); break; case GST_STATE_READY: setSeekable(false); if (m_state != QMediaPlayer::StoppedState) emit stateChanged(m_state = QMediaPlayer::StoppedState); break; case GST_STATE_PAUSED: { QMediaPlayer::State prevState = m_state; m_state = QMediaPlayer::PausedState; //check for seekable if (oldState == GST_STATE_READY) { if (m_sourceType == SoupHTTPSrc || m_sourceType == MMSSrc) { //since udpsrc is a live source, it is not applicable here m_everPlayed = true; } getStreamsInfo(); updateVideoResolutionTag(); //gstreamer doesn't give a reliable indication the duration //information is ready, GST_MESSAGE_DURATION is not sent by most elements //the duration is queried up to 5 times with increasing delay m_durationQueries = 5; // This should also update the seekable flag. updateDuration(); if (!qFuzzyCompare(m_playbackRate, qreal(1.0))) { qreal rate = m_playbackRate; m_playbackRate = 1.0; setPlaybackRate(rate); } } if (m_state != prevState) emit stateChanged(m_state); break; } case GST_STATE_PLAYING: m_everPlayed = true; if (m_state != QMediaPlayer::PlayingState) { emit stateChanged(m_state = QMediaPlayer::PlayingState); // For rtsp streams duration information might not be available // until playback starts. if (m_duration <= 0) { m_durationQueries = 5; updateDuration(); } } break; } } break; case GST_MESSAGE_EOS: emit playbackFinished(); break; case GST_MESSAGE_TAG: case GST_MESSAGE_STREAM_STATUS: case GST_MESSAGE_UNKNOWN: break; case GST_MESSAGE_ERROR: { GError *err; gchar *debug; gst_message_parse_error(gm, &err, &debug); if (err->domain == GST_STREAM_ERROR && err->code == GST_STREAM_ERROR_CODEC_NOT_FOUND) processInvalidMedia(QMediaPlayer::FormatError, tr("Cannot play stream of type: ")); else processInvalidMedia(QMediaPlayer::ResourceError, QString::fromUtf8(err->message)); qWarning() << "Error:" << QString::fromUtf8(err->message); g_error_free(err); g_free(debug); } break; case GST_MESSAGE_WARNING: { GError *err; gchar *debug; gst_message_parse_warning (gm, &err, &debug); qWarning() << "Warning:" << QString::fromUtf8(err->message); g_error_free (err); g_free (debug); } break; case GST_MESSAGE_INFO: #ifdef DEBUG_PLAYBIN { GError *err; gchar *debug; gst_message_parse_info (gm, &err, &debug); qDebug() << "Info:" << QString::fromUtf8(err->message); g_error_free (err); g_free (debug); } #endif break; case GST_MESSAGE_BUFFERING: case GST_MESSAGE_STATE_DIRTY: case GST_MESSAGE_STEP_DONE: case GST_MESSAGE_CLOCK_PROVIDE: case GST_MESSAGE_CLOCK_LOST: case GST_MESSAGE_NEW_CLOCK: case GST_MESSAGE_STRUCTURE_CHANGE: case GST_MESSAGE_APPLICATION: case GST_MESSAGE_ELEMENT: break; case GST_MESSAGE_SEGMENT_START: { const GstStructure *structure = gst_message_get_structure(gm); qint64 position = g_value_get_int64(gst_structure_get_value(structure, "position")); position /= 1000000; m_lastPosition = position; emit positionChanged(position); } break; case GST_MESSAGE_SEGMENT_DONE: break; case GST_MESSAGE_LATENCY: #if GST_CHECK_VERSION(0,10,13) case GST_MESSAGE_ASYNC_START: break; case GST_MESSAGE_ASYNC_DONE: { gint64 position = 0; if (qt_gst_element_query_position(m_pipeline, GST_FORMAT_TIME, &position)) { position /= 1000000; m_lastPosition = position; emit positionChanged(position); } break; } #if GST_CHECK_VERSION(0,10,23) case GST_MESSAGE_REQUEST_STATE: #endif #endif case GST_MESSAGE_ANY: break; default: break; } } else if (GST_MESSAGE_TYPE(gm) == GST_MESSAGE_ERROR) { GError *err; gchar *debug; gst_message_parse_error(gm, &err, &debug); // If the source has given up, so do we. if (qstrcmp(GST_OBJECT_NAME(GST_MESSAGE_SRC(gm)), "source") == 0) { bool everPlayed = m_everPlayed; // Try and differentiate network related resource errors from the others if (!m_request.url().isRelative() && m_request.url().scheme().compare(QLatin1String("file"), Qt::CaseInsensitive) != 0 ) { if (everPlayed || (err->domain == GST_RESOURCE_ERROR && ( err->code == GST_RESOURCE_ERROR_BUSY || err->code == GST_RESOURCE_ERROR_OPEN_READ || err->code == GST_RESOURCE_ERROR_READ || err->code == GST_RESOURCE_ERROR_SEEK || err->code == GST_RESOURCE_ERROR_SYNC))) { processInvalidMedia(QMediaPlayer::NetworkError, QString::fromUtf8(err->message)); } else { processInvalidMedia(QMediaPlayer::ResourceError, QString::fromUtf8(err->message)); } } else processInvalidMedia(QMediaPlayer::ResourceError, QString::fromUtf8(err->message)); } else if (err->domain == GST_STREAM_ERROR && (err->code == GST_STREAM_ERROR_DECRYPT || err->code == GST_STREAM_ERROR_DECRYPT_NOKEY)) { processInvalidMedia(QMediaPlayer::AccessDeniedError, QString::fromUtf8(err->message)); } else { handlePlaybin2 = true; } if (!handlePlaybin2) qWarning() << "Error:" << QString::fromUtf8(err->message); g_error_free(err); g_free(debug); } else if (GST_MESSAGE_TYPE(gm) == GST_MESSAGE_ELEMENT && qstrcmp(GST_OBJECT_NAME(GST_MESSAGE_SRC(gm)), "source") == 0 && m_sourceType == UDPSrc && gst_structure_has_name(gst_message_get_structure(gm), "GstUDPSrcTimeout")) { //since udpsrc will not generate an error for the timeout event, //we need to process its element message here and treat it as an error. processInvalidMedia(m_everPlayed ? QMediaPlayer::NetworkError : QMediaPlayer::ResourceError, tr("UDP source timeout")); } else { handlePlaybin2 = true; } if (handlePlaybin2) { if (GST_MESSAGE_TYPE(gm) == GST_MESSAGE_WARNING) { GError *err; gchar *debug; gst_message_parse_warning(gm, &err, &debug); if (err->domain == GST_STREAM_ERROR && err->code == GST_STREAM_ERROR_CODEC_NOT_FOUND) emit error(int(QMediaPlayer::FormatError), tr("Cannot play stream of type: ")); // GStreamer shows warning for HTTP playlists if (err && err->message) qWarning() << "Warning:" << QString::fromUtf8(err->message); g_error_free(err); g_free(debug); } else if (GST_MESSAGE_TYPE(gm) == GST_MESSAGE_ERROR) { GError *err; gchar *debug; gst_message_parse_error(gm, &err, &debug); // Nearly all errors map to ResourceError QMediaPlayer::Error qerror = QMediaPlayer::ResourceError; if (err->domain == GST_STREAM_ERROR && (err->code == GST_STREAM_ERROR_DECRYPT || err->code == GST_STREAM_ERROR_DECRYPT_NOKEY)) { qerror = QMediaPlayer::AccessDeniedError; } processInvalidMedia(qerror, QString::fromUtf8(err->message)); if (err && err->message) qWarning() << "Error:" << QString::fromUtf8(err->message); g_error_free(err); g_free(debug); } } } return false; } void QGstreamerPlayerSession::getStreamsInfo() { if (!m_playbin) return; QList< QMap > oldProperties = m_streamProperties; QList oldTypes = m_streamTypes; QMap oldOffset = m_playbin2StreamOffset; //check if video is available: bool haveAudio = false; bool haveVideo = false; m_streamProperties.clear(); m_streamTypes.clear(); m_playbin2StreamOffset.clear(); gint audioStreamsCount = 0; gint videoStreamsCount = 0; gint textStreamsCount = 0; g_object_get(G_OBJECT(m_playbin), "n-audio", &audioStreamsCount, nullptr); g_object_get(G_OBJECT(m_playbin), "n-video", &videoStreamsCount, nullptr); g_object_get(G_OBJECT(m_playbin), "n-text", &textStreamsCount, nullptr); haveAudio = audioStreamsCount > 0; haveVideo = videoStreamsCount > 0; m_playbin2StreamOffset[QMediaStreamsControl::AudioStream] = 0; m_playbin2StreamOffset[QMediaStreamsControl::VideoStream] = audioStreamsCount; m_playbin2StreamOffset[QMediaStreamsControl::SubPictureStream] = audioStreamsCount+videoStreamsCount; for (int i=0; i streamProperties; int streamIndex = i - m_playbin2StreamOffset[streamType]; GstTagList *tags = 0; switch (streamType) { case QMediaStreamsControl::AudioStream: g_signal_emit_by_name(G_OBJECT(m_playbin), "get-audio-tags", streamIndex, &tags); break; case QMediaStreamsControl::VideoStream: g_signal_emit_by_name(G_OBJECT(m_playbin), "get-video-tags", streamIndex, &tags); break; case QMediaStreamsControl::SubPictureStream: g_signal_emit_by_name(G_OBJECT(m_playbin), "get-text-tags", streamIndex, &tags); break; default: break; } #if GST_CHECK_VERSION(1,0,0) if (tags && GST_IS_TAG_LIST(tags)) { #else if (tags && gst_is_tag_list(tags)) { #endif gchar *languageCode = 0; if (gst_tag_list_get_string(tags, GST_TAG_LANGUAGE_CODE, &languageCode)) streamProperties[QMediaMetaData::Language] = QString::fromUtf8(languageCode); //qDebug() << "language for setream" << i << QString::fromUtf8(languageCode); g_free (languageCode); gst_tag_list_free(tags); } m_streamProperties.append(streamProperties); } bool emitAudioChanged = (haveAudio != m_audioAvailable); bool emitVideoChanged = (haveVideo != m_videoAvailable); m_audioAvailable = haveAudio; m_videoAvailable = haveVideo; if (emitAudioChanged) { emit audioAvailableChanged(m_audioAvailable); } if (emitVideoChanged) { emit videoAvailableChanged(m_videoAvailable); } if (oldProperties != m_streamProperties || oldTypes != m_streamTypes || oldOffset != m_playbin2StreamOffset) emit streamsChanged(); } void QGstreamerPlayerSession::updateVideoResolutionTag() { if (!m_videoIdentity) return; #ifdef DEBUG_PLAYBIN qDebug() << Q_FUNC_INFO; #endif QSize size; QSize aspectRatio; GstPad *pad = gst_element_get_static_pad(m_videoIdentity, "src"); GstCaps *caps = qt_gst_pad_get_current_caps(pad); if (caps) { const GstStructure *structure = gst_caps_get_structure(caps, 0); gst_structure_get_int(structure, "width", &size.rwidth()); gst_structure_get_int(structure, "height", &size.rheight()); gint aspectNum = 0; gint aspectDenum = 0; if (!size.isEmpty() && gst_structure_get_fraction( structure, "pixel-aspect-ratio", &aspectNum, &aspectDenum)) { if (aspectDenum > 0) aspectRatio = QSize(aspectNum, aspectDenum); } gst_caps_unref(caps); } gst_object_unref(GST_OBJECT(pad)); QSize currentSize = m_tags.value("resolution").toSize(); QSize currentAspectRatio = m_tags.value("pixel-aspect-ratio").toSize(); if (currentSize != size || currentAspectRatio != aspectRatio) { if (aspectRatio.isEmpty()) m_tags.remove("pixel-aspect-ratio"); if (size.isEmpty()) { m_tags.remove("resolution"); } else { m_tags.insert("resolution", QVariant(size)); if (!aspectRatio.isEmpty()) m_tags.insert("pixel-aspect-ratio", QVariant(aspectRatio)); } emit tagsChanged(); } } void QGstreamerPlayerSession::updateDuration() { gint64 gstDuration = 0; qint64 duration = 0; if (m_pipeline && qt_gst_element_query_duration(m_pipeline, GST_FORMAT_TIME, &gstDuration)) duration = gstDuration / 1000000; if (m_duration != duration) { m_duration = duration; emit durationChanged(m_duration); } gboolean seekable = false; if (m_duration > 0) { m_durationQueries = 0; GstQuery *query = gst_query_new_seeking(GST_FORMAT_TIME); if (gst_element_query(m_pipeline, query)) gst_query_parse_seeking(query, 0, &seekable, 0, 0); gst_query_unref(query); } setSeekable(seekable); if (m_durationQueries > 0) { //increase delay between duration requests int delay = 25 << (5 - m_durationQueries); QTimer::singleShot(delay, this, SLOT(updateDuration())); m_durationQueries--; } #ifdef DEBUG_PLAYBIN qDebug() << Q_FUNC_INFO << m_duration; #endif } void QGstreamerPlayerSession::playbinNotifySource(GObject *o, GParamSpec *p, gpointer d) { Q_UNUSED(p); GstElement *source = 0; g_object_get(o, "source", &source, nullptr); if (source == 0) return; #ifdef DEBUG_PLAYBIN qDebug() << "Playbin source added:" << G_OBJECT_CLASS_NAME(G_OBJECT_GET_CLASS(source)); #endif // Set Headers const QByteArray userAgentString("User-Agent"); QGstreamerPlayerSession *self = reinterpret_cast(d); // User-Agent - special case, souphhtpsrc will always set something, even if // defined in extra-headers if (g_object_class_find_property(G_OBJECT_GET_CLASS(source), "user-agent") != 0) { g_object_set(G_OBJECT(source), "user-agent", self->m_request.rawHeader(userAgentString).constData(), nullptr); } // The rest if (g_object_class_find_property(G_OBJECT_GET_CLASS(source), "extra-headers") != 0) { GstStructure *extras = qt_gst_structure_new_empty("extras"); const auto rawHeaderList = self->m_request.rawHeaderList(); for (const QByteArray &rawHeader : rawHeaderList) { if (rawHeader == userAgentString) // Filter User-Agent continue; else { GValue headerValue; memset(&headerValue, 0, sizeof(GValue)); g_value_init(&headerValue, G_TYPE_STRING); g_value_set_string(&headerValue, self->m_request.rawHeader(rawHeader).constData()); gst_structure_set_value(extras, rawHeader.constData(), &headerValue); } } if (gst_structure_n_fields(extras) > 0) g_object_set(G_OBJECT(source), "extra-headers", extras, nullptr); gst_structure_free(extras); } //set timeout property to 30 seconds const int timeout = 30; if (qstrcmp(G_OBJECT_CLASS_NAME(G_OBJECT_GET_CLASS(source)), "GstUDPSrc") == 0) { quint64 convertedTimeout = timeout; #if GST_CHECK_VERSION(1,0,0) // Gst 1.x -> nanosecond convertedTimeout *= 1000000000; #else // Gst 0.10 -> microsecond convertedTimeout *= 1000000; #endif g_object_set(G_OBJECT(source), "timeout", convertedTimeout, nullptr); self->m_sourceType = UDPSrc; //The udpsrc is always a live source. self->m_isLiveSource = true; QUrlQuery query(self->m_request.url()); const QString var = QLatin1String("udpsrc.caps"); if (query.hasQueryItem(var)) { GstCaps *caps = gst_caps_from_string(query.queryItemValue(var).toLatin1().constData()); g_object_set(G_OBJECT(source), "caps", caps, nullptr); gst_caps_unref(caps); } } else if (qstrcmp(G_OBJECT_CLASS_NAME(G_OBJECT_GET_CLASS(source)), "GstSoupHTTPSrc") == 0) { //souphttpsrc timeout unit = second g_object_set(G_OBJECT(source), "timeout", guint(timeout), nullptr); self->m_sourceType = SoupHTTPSrc; //since gst_base_src_is_live is not reliable, so we check the source property directly gboolean isLive = false; g_object_get(G_OBJECT(source), "is-live", &isLive, nullptr); self->m_isLiveSource = isLive; } else if (qstrcmp(G_OBJECT_CLASS_NAME(G_OBJECT_GET_CLASS(source)), "GstMMSSrc") == 0) { self->m_sourceType = MMSSrc; self->m_isLiveSource = gst_base_src_is_live(GST_BASE_SRC(source)); g_object_set(G_OBJECT(source), "tcp-timeout", G_GUINT64_CONSTANT(timeout*1000000), nullptr); } else if (qstrcmp(G_OBJECT_CLASS_NAME(G_OBJECT_GET_CLASS(source)), "GstRTSPSrc") == 0) { //rtspsrc acts like a live source and will therefore only generate data in the PLAYING state. self->m_sourceType = RTSPSrc; self->m_isLiveSource = true; g_object_set(G_OBJECT(source), "buffer-mode", 1, nullptr); } else { self->m_sourceType = UnknownSrc; self->m_isLiveSource = gst_base_src_is_live(GST_BASE_SRC(source)); } #ifdef DEBUG_PLAYBIN if (self->m_isLiveSource) qDebug() << "Current source is a live source"; else qDebug() << "Current source is a non-live source"; #endif if (self->m_videoSink) g_object_set(G_OBJECT(self->m_videoSink), "sync", !self->m_isLiveSource, nullptr); gst_object_unref(source); } bool QGstreamerPlayerSession::isLiveSource() const { return m_isLiveSource; } void QGstreamerPlayerSession::handleVolumeChange(GObject *o, GParamSpec *p, gpointer d) { Q_UNUSED(o); Q_UNUSED(p); QGstreamerPlayerSession *session = reinterpret_cast(d); QMetaObject::invokeMethod(session, "updateVolume", Qt::QueuedConnection); } void QGstreamerPlayerSession::updateVolume() { double volume = 1.0; g_object_get(m_playbin, "volume", &volume, nullptr); if (m_volume != int(volume*100 + 0.5)) { m_volume = int(volume*100 + 0.5); #ifdef DEBUG_PLAYBIN qDebug() << Q_FUNC_INFO << m_volume; #endif emit volumeChanged(m_volume); } } void QGstreamerPlayerSession::handleMutedChange(GObject *o, GParamSpec *p, gpointer d) { Q_UNUSED(o); Q_UNUSED(p); QGstreamerPlayerSession *session = reinterpret_cast(d); QMetaObject::invokeMethod(session, "updateMuted", Qt::QueuedConnection); } void QGstreamerPlayerSession::updateMuted() { gboolean muted = FALSE; g_object_get(G_OBJECT(m_playbin), "mute", &muted, nullptr); if (m_muted != muted) { m_muted = muted; #ifdef DEBUG_PLAYBIN qDebug() << Q_FUNC_INFO << m_muted; #endif emit mutedStateChanged(muted); } } #if !GST_CHECK_VERSION(0, 10, 33) static gboolean factory_can_src_any_caps (GstElementFactory *factory, const GstCaps *caps) { GList *templates; g_return_val_if_fail(factory != nullptr, FALSE); g_return_val_if_fail(caps != nullptr, FALSE); templates = factory->staticpadtemplates; while (templates) { GstStaticPadTemplate *templ = (GstStaticPadTemplate *)templates->data; if (templ->direction == GST_PAD_SRC) { GstCaps *templcaps = gst_static_caps_get(&templ->static_caps); if (qt_gst_caps_can_intersect(caps, templcaps)) { gst_caps_unref(templcaps); return TRUE; } gst_caps_unref(templcaps); } templates = g_list_next(templates); } return FALSE; } #endif GstAutoplugSelectResult QGstreamerPlayerSession::handleAutoplugSelect(GstBin *bin, GstPad *pad, GstCaps *caps, GstElementFactory *factory, QGstreamerPlayerSession *session) { Q_UNUSED(bin); Q_UNUSED(pad); Q_UNUSED(caps); GstAutoplugSelectResult res = GST_AUTOPLUG_SELECT_TRY; // if VAAPI is available and can be used to decode but the current video sink cannot handle // the decoded format, don't use it const gchar *factoryName = gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(factory)); if (g_str_has_prefix(factoryName, "vaapi")) { GstPad *sinkPad = gst_element_get_static_pad(session->m_videoSink, "sink"); #if GST_CHECK_VERSION(1,0,0) GstCaps *sinkCaps = gst_pad_query_caps(sinkPad, nullptr); #else GstCaps *sinkCaps = gst_pad_get_caps(sinkPad); #endif #if !GST_CHECK_VERSION(0, 10, 33) if (!factory_can_src_any_caps(factory, sinkCaps)) #else if (!gst_element_factory_can_src_any_caps(factory, sinkCaps)) #endif res = GST_AUTOPLUG_SELECT_SKIP; gst_object_unref(sinkPad); gst_caps_unref(sinkCaps); } return res; } void QGstreamerPlayerSession::handleElementAdded(GstBin *bin, GstElement *element, QGstreamerPlayerSession *session) { Q_UNUSED(bin); //we have to configure queue2 element to enable media downloading //and reporting available ranges, //but it's added dynamically to playbin2 gchar *elementName = gst_element_get_name(element); if (g_str_has_prefix(elementName, "queue2")) { // Disable on-disk buffering. g_object_set(G_OBJECT(element), "temp-template", nullptr, nullptr); } else if (g_str_has_prefix(elementName, "uridecodebin") || #if GST_CHECK_VERSION(1,0,0) g_str_has_prefix(elementName, "decodebin")) { #else g_str_has_prefix(elementName, "decodebin2")) { if (g_str_has_prefix(elementName, "uridecodebin")) { // Add video/x-surface (VAAPI) to default raw formats g_object_set(G_OBJECT(element), "caps", gst_static_caps_get(&static_RawCaps), nullptr); // listen for uridecodebin autoplug-select to skip VAAPI usage when the current // video sink doesn't support it g_signal_connect(element, "autoplug-select", G_CALLBACK(handleAutoplugSelect), session); } #endif //listen for queue2 element added to uridecodebin/decodebin2 as well. //Don't touch other bins since they may have unrelated queues g_signal_connect(element, "element-added", G_CALLBACK(handleElementAdded), session); } g_free(elementName); } void QGstreamerPlayerSession::handleStreamsChange(GstBin *bin, gpointer user_data) { Q_UNUSED(bin); QGstreamerPlayerSession* session = reinterpret_cast(user_data); QMetaObject::invokeMethod(session, "getStreamsInfo", Qt::QueuedConnection); } //doing proper operations when detecting an invalidMedia: change media status before signal the erorr void QGstreamerPlayerSession::processInvalidMedia(QMediaPlayer::Error errorCode, const QString& errorString) { #ifdef DEBUG_PLAYBIN qDebug() << Q_FUNC_INFO; #endif emit invalidMedia(); stop(); emit error(int(errorCode), errorString); } void QGstreamerPlayerSession::showPrerollFrames(bool enabled) { #ifdef DEBUG_PLAYBIN qDebug() << Q_FUNC_INFO << enabled; #endif if (enabled != m_displayPrerolledFrame && m_videoSink && g_object_class_find_property(G_OBJECT_GET_CLASS(m_videoSink), "show-preroll-frame") != 0) { gboolean value = enabled; g_object_set(G_OBJECT(m_videoSink), "show-preroll-frame", value, nullptr); m_displayPrerolledFrame = enabled; } } void QGstreamerPlayerSession::addProbe(QGstreamerVideoProbeControl* probe) { Q_ASSERT(!m_videoProbe); m_videoProbe = probe; addVideoBufferProbe(); } void QGstreamerPlayerSession::removeProbe(QGstreamerVideoProbeControl* probe) { Q_ASSERT(m_videoProbe == probe); removeVideoBufferProbe(); m_videoProbe = 0; } void QGstreamerPlayerSession::addProbe(QGstreamerAudioProbeControl* probe) { Q_ASSERT(!m_audioProbe); m_audioProbe = probe; addAudioBufferProbe(); } void QGstreamerPlayerSession::removeProbe(QGstreamerAudioProbeControl* probe) { Q_ASSERT(m_audioProbe == probe); removeAudioBufferProbe(); m_audioProbe = 0; } // This function is similar to stop(), // but does not set m_everPlayed, m_lastPosition, // and setSeekable() values. void QGstreamerPlayerSession::endOfMediaReset() { if (m_renderer) m_renderer->stopRenderer(); flushVideoProbes(); gst_element_set_state(m_pipeline, GST_STATE_PAUSED); QMediaPlayer::State oldState = m_state; m_pendingState = m_state = QMediaPlayer::StoppedState; finishVideoOutputChange(); if (oldState != m_state) emit stateChanged(m_state); } void QGstreamerPlayerSession::removeVideoBufferProbe() { if (!m_videoProbe) return; GstPad *pad = gst_element_get_static_pad(m_videoSink, "sink"); if (pad) { m_videoProbe->removeProbeFromPad(pad); gst_object_unref(GST_OBJECT(pad)); } } void QGstreamerPlayerSession::addVideoBufferProbe() { if (!m_videoProbe) return; GstPad *pad = gst_element_get_static_pad(m_videoSink, "sink"); if (pad) { m_videoProbe->addProbeToPad(pad); gst_object_unref(GST_OBJECT(pad)); } } void QGstreamerPlayerSession::removeAudioBufferProbe() { if (!m_audioProbe) return; GstPad *pad = gst_element_get_static_pad(m_audioSink, "sink"); if (pad) { m_audioProbe->removeProbeFromPad(pad); gst_object_unref(GST_OBJECT(pad)); } } void QGstreamerPlayerSession::addAudioBufferProbe() { if (!m_audioProbe) return; GstPad *pad = gst_element_get_static_pad(m_audioSink, "sink"); if (pad) { m_audioProbe->addProbeToPad(pad); gst_object_unref(GST_OBJECT(pad)); } } void QGstreamerPlayerSession::flushVideoProbes() { if (m_videoProbe) m_videoProbe->startFlushing(); } void QGstreamerPlayerSession::resumeVideoProbes() { if (m_videoProbe) m_videoProbe->stopFlushing(); } QT_END_NAMESPACE