| /**************************************************************************** |
| ** |
| ** 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$ |
| ** |
| ****************************************************************************/ |
| //#define DEBUG_DECODER |
| |
| #include "qgstreameraudiodecodersession.h" |
| #include <private/qgstreamerbushelper_p.h> |
| |
| #include <private/qgstutils_p.h> |
| |
| #include <gst/gstvalue.h> |
| #include <gst/base/gstbasesrc.h> |
| |
| #include <QtCore/qdatetime.h> |
| #include <QtCore/qdebug.h> |
| #include <QtCore/qsize.h> |
| #include <QtCore/qtimer.h> |
| #include <QtCore/qdebug.h> |
| #include <QtCore/qdir.h> |
| #include <QtCore/qstandardpaths.h> |
| #include <QtCore/qurl.h> |
| |
| #define MAX_BUFFERS_IN_QUEUE 4 |
| |
| QT_BEGIN_NAMESPACE |
| |
| 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; |
| |
| QGstreamerAudioDecoderSession::QGstreamerAudioDecoderSession(QObject *parent) |
| : QObject(parent), |
| m_state(QAudioDecoder::StoppedState), |
| m_pendingState(QAudioDecoder::StoppedState), |
| m_busHelper(0), |
| m_bus(0), |
| m_playbin(0), |
| m_outputBin(0), |
| m_audioConvert(0), |
| m_appSink(0), |
| #if QT_CONFIG(gstreamer_app) |
| m_appSrc(0), |
| #endif |
| mDevice(0), |
| m_buffersAvailable(0), |
| m_position(-1), |
| m_duration(-1), |
| m_durationQueries(0) |
| { |
| // Create pipeline here |
| m_playbin = gst_element_factory_make(QT_GSTREAMER_PLAYBIN_ELEMENT_NAME, NULL); |
| |
| if (m_playbin != 0) { |
| // Sort out messages |
| m_bus = gst_element_get_bus(m_playbin); |
| m_busHelper = new QGstreamerBusHelper(m_bus, this); |
| m_busHelper->installMessageFilter(this); |
| |
| // Set the rest of the pipeline up |
| setAudioFlags(true); |
| |
| m_audioConvert = gst_element_factory_make("audioconvert", NULL); |
| |
| m_outputBin = gst_bin_new("audio-output-bin"); |
| gst_bin_add(GST_BIN(m_outputBin), m_audioConvert); |
| |
| // add ghostpad |
| GstPad *pad = gst_element_get_static_pad(m_audioConvert, "sink"); |
| Q_ASSERT(pad); |
| gst_element_add_pad(GST_ELEMENT(m_outputBin), gst_ghost_pad_new("sink", pad)); |
| gst_object_unref(GST_OBJECT(pad)); |
| |
| g_object_set(G_OBJECT(m_playbin), "audio-sink", m_outputBin, NULL); |
| #if QT_CONFIG(gstreamer_app) |
| g_signal_connect(G_OBJECT(m_playbin), "deep-notify::source", (GCallback) &QGstreamerAudioDecoderSession::configureAppSrcElement, (gpointer)this); |
| #endif |
| |
| // Set volume to 100% |
| gdouble volume = 1.0; |
| g_object_set(G_OBJECT(m_playbin), "volume", volume, NULL); |
| } |
| } |
| |
| QGstreamerAudioDecoderSession::~QGstreamerAudioDecoderSession() |
| { |
| if (m_playbin) { |
| stop(); |
| |
| delete m_busHelper; |
| #if QT_CONFIG(gstreamer_app) |
| delete m_appSrc; |
| #endif |
| gst_object_unref(GST_OBJECT(m_bus)); |
| gst_object_unref(GST_OBJECT(m_playbin)); |
| } |
| } |
| |
| #if QT_CONFIG(gstreamer_app) |
| void QGstreamerAudioDecoderSession::configureAppSrcElement(GObject* object, GObject *orig, GParamSpec *pspec, QGstreamerAudioDecoderSession* self) |
| { |
| Q_UNUSED(object); |
| Q_UNUSED(pspec); |
| |
| // In case we switch from appsrc to not |
| if (!self->appsrc()) |
| return; |
| |
| GstElement *appsrc; |
| g_object_get(orig, "source", &appsrc, NULL); |
| |
| if (!self->appsrc()->setup(appsrc)) |
| qWarning()<<"Could not setup appsrc element"; |
| |
| g_object_unref(G_OBJECT(appsrc)); |
| } |
| #endif |
| |
| bool QGstreamerAudioDecoderSession::processBusMessage(const QGstreamerMessage &message) |
| { |
| GstMessage* gm = message.rawMessage(); |
| if (gm) { |
| if (GST_MESSAGE_TYPE(gm) == GST_MESSAGE_DURATION) { |
| updateDuration(); |
| } else if (GST_MESSAGE_SRC(gm) == GST_OBJECT_CAST(m_playbin)) { |
| 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_DECODER |
| QStringList states; |
| states << "GST_STATE_VOID_PENDING" << "GST_STATE_NULL" << "GST_STATE_READY" << "GST_STATE_PAUSED" << "GST_STATE_PLAYING"; |
| |
| qDebug() << QString("state changed: old: %1 new: %2 pending: %3") \ |
| .arg(states[oldState]) \ |
| .arg(states[newState]) \ |
| .arg(states[pending]) << "internal" << m_state; |
| #endif |
| |
| QAudioDecoder::State prevState = m_state; |
| |
| switch (newState) { |
| case GST_STATE_VOID_PENDING: |
| case GST_STATE_NULL: |
| m_state = QAudioDecoder::StoppedState; |
| break; |
| case GST_STATE_READY: |
| m_state = QAudioDecoder::StoppedState; |
| break; |
| case GST_STATE_PLAYING: |
| m_state = QAudioDecoder::DecodingState; |
| break; |
| case GST_STATE_PAUSED: |
| m_state = QAudioDecoder::DecodingState; |
| |
| //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; |
| updateDuration(); |
| break; |
| } |
| |
| if (prevState != m_state) |
| emit stateChanged(m_state); |
| } |
| break; |
| |
| case GST_MESSAGE_EOS: |
| m_pendingState = m_state = QAudioDecoder::StoppedState; |
| emit finished(); |
| emit stateChanged(m_state); |
| 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(QAudioDecoder::FormatError, tr("Cannot play stream of type: <unknown>")); |
| else |
| processInvalidMedia(QAudioDecoder::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; |
| #ifdef DEBUG_DECODER |
| case GST_MESSAGE_INFO: |
| { |
| GError *err; |
| gchar *debug; |
| gst_message_parse_info (gm, &err, &debug); |
| qDebug() << "Info:" << QString::fromUtf8(err->message); |
| g_error_free (err); |
| g_free (debug); |
| } |
| break; |
| #endif |
| default: |
| break; |
| } |
| } else if (GST_MESSAGE_TYPE(gm) == GST_MESSAGE_ERROR) { |
| GError *err; |
| gchar *debug; |
| gst_message_parse_error(gm, &err, &debug); |
| QAudioDecoder::Error qerror = QAudioDecoder::ResourceError; |
| if (err->domain == GST_STREAM_ERROR) { |
| switch (err->code) { |
| case GST_STREAM_ERROR_DECRYPT: |
| case GST_STREAM_ERROR_DECRYPT_NOKEY: |
| qerror = QAudioDecoder::AccessDeniedError; |
| break; |
| case GST_STREAM_ERROR_FORMAT: |
| case GST_STREAM_ERROR_DEMUX: |
| case GST_STREAM_ERROR_DECODE: |
| case GST_STREAM_ERROR_WRONG_TYPE: |
| case GST_STREAM_ERROR_TYPE_NOT_FOUND: |
| case GST_STREAM_ERROR_CODEC_NOT_FOUND: |
| qerror = QAudioDecoder::FormatError; |
| break; |
| default: |
| break; |
| } |
| } else if (err->domain == GST_CORE_ERROR) { |
| switch (err->code) { |
| case GST_CORE_ERROR_MISSING_PLUGIN: |
| qerror = QAudioDecoder::FormatError; |
| break; |
| default: |
| break; |
| } |
| } |
| |
| processInvalidMedia(qerror, QString::fromUtf8(err->message)); |
| g_error_free(err); |
| g_free(debug); |
| } |
| } |
| |
| return false; |
| } |
| |
| QString QGstreamerAudioDecoderSession::sourceFilename() const |
| { |
| return mSource; |
| } |
| |
| void QGstreamerAudioDecoderSession::setSourceFilename(const QString &fileName) |
| { |
| stop(); |
| mDevice = 0; |
| #if QT_CONFIG(gstreamer_app) |
| if (m_appSrc) |
| m_appSrc->deleteLater(); |
| m_appSrc = 0; |
| #endif |
| |
| bool isSignalRequired = (mSource != fileName); |
| mSource = fileName; |
| if (isSignalRequired) |
| emit sourceChanged(); |
| } |
| |
| QIODevice *QGstreamerAudioDecoderSession::sourceDevice() const |
| { |
| return mDevice; |
| } |
| |
| void QGstreamerAudioDecoderSession::setSourceDevice(QIODevice *device) |
| { |
| stop(); |
| mSource.clear(); |
| bool isSignalRequired = (mDevice != device); |
| mDevice = device; |
| if (isSignalRequired) |
| emit sourceChanged(); |
| } |
| |
| void QGstreamerAudioDecoderSession::start() |
| { |
| if (!m_playbin) { |
| processInvalidMedia(QAudioDecoder::ResourceError, "Playbin element is not valid"); |
| return; |
| } |
| |
| addAppSink(); |
| |
| if (!mSource.isEmpty()) { |
| g_object_set(G_OBJECT(m_playbin), "uri", QUrl::fromLocalFile(mSource).toEncoded().constData(), NULL); |
| } else if (mDevice) { |
| #if QT_CONFIG(gstreamer_app) |
| // make sure we can read from device |
| if (!mDevice->isOpen() || !mDevice->isReadable()) { |
| processInvalidMedia(QAudioDecoder::AccessDeniedError, "Unable to read from specified device"); |
| return; |
| } |
| |
| if (!m_appSrc) |
| m_appSrc = new QGstAppSrc(this); |
| m_appSrc->setStream(mDevice); |
| |
| g_object_set(G_OBJECT(m_playbin), "uri", "appsrc://", NULL); |
| #endif |
| } else { |
| return; |
| } |
| |
| // Set audio format |
| if (m_appSink) { |
| if (mFormat.isValid()) { |
| setAudioFlags(false); |
| GstCaps *caps = QGstUtils::capsForAudioFormat(mFormat); |
| gst_app_sink_set_caps(m_appSink, caps); |
| gst_caps_unref(caps); |
| } else { |
| // We want whatever the native audio format is |
| setAudioFlags(true); |
| gst_app_sink_set_caps(m_appSink, NULL); |
| } |
| } |
| |
| m_pendingState = QAudioDecoder::DecodingState; |
| if (gst_element_set_state(m_playbin, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) { |
| qWarning() << "GStreamer; Unable to start decoding process"; |
| m_pendingState = m_state = QAudioDecoder::StoppedState; |
| |
| emit stateChanged(m_state); |
| } |
| } |
| |
| void QGstreamerAudioDecoderSession::stop() |
| { |
| if (m_playbin) { |
| gst_element_set_state(m_playbin, GST_STATE_NULL); |
| removeAppSink(); |
| |
| QAudioDecoder::State oldState = m_state; |
| m_pendingState = m_state = QAudioDecoder::StoppedState; |
| |
| // GStreamer thread is stopped. Can safely access m_buffersAvailable |
| if (m_buffersAvailable != 0) { |
| m_buffersAvailable = 0; |
| emit bufferAvailableChanged(false); |
| } |
| |
| if (m_position != -1) { |
| m_position = -1; |
| emit positionChanged(m_position); |
| } |
| |
| if (m_duration != -1) { |
| m_duration = -1; |
| emit durationChanged(m_duration); |
| } |
| |
| if (oldState != m_state) |
| emit stateChanged(m_state); |
| } |
| } |
| |
| QAudioFormat QGstreamerAudioDecoderSession::audioFormat() const |
| { |
| return mFormat; |
| } |
| |
| void QGstreamerAudioDecoderSession::setAudioFormat(const QAudioFormat &format) |
| { |
| if (mFormat != format) { |
| mFormat = format; |
| emit formatChanged(mFormat); |
| } |
| } |
| |
| QAudioBuffer QGstreamerAudioDecoderSession::read() |
| { |
| QAudioBuffer audioBuffer; |
| |
| int buffersAvailable; |
| { |
| QMutexLocker locker(&m_buffersMutex); |
| buffersAvailable = m_buffersAvailable; |
| |
| // need to decrement before pulling a buffer |
| // to make sure assert in QGstreamerAudioDecoderSession::new_buffer works |
| m_buffersAvailable--; |
| } |
| |
| |
| if (buffersAvailable) { |
| if (buffersAvailable == 1) |
| emit bufferAvailableChanged(false); |
| |
| const char* bufferData = 0; |
| int bufferSize = 0; |
| |
| #if GST_CHECK_VERSION(1,0,0) |
| GstSample *sample = gst_app_sink_pull_sample(m_appSink); |
| GstBuffer *buffer = gst_sample_get_buffer(sample); |
| GstMapInfo mapInfo; |
| gst_buffer_map(buffer, &mapInfo, GST_MAP_READ); |
| bufferData = (const char*)mapInfo.data; |
| bufferSize = mapInfo.size; |
| QAudioFormat format = QGstUtils::audioFormatForSample(sample); |
| #else |
| GstBuffer *buffer = gst_app_sink_pull_buffer(m_appSink); |
| bufferData = (const char*)buffer->data; |
| bufferSize = buffer->size; |
| QAudioFormat format = QGstUtils::audioFormatForBuffer(buffer); |
| #endif |
| |
| if (format.isValid()) { |
| // XXX At the moment we have to copy data from GstBuffer into QAudioBuffer. |
| // We could improve performance by implementing QAbstractAudioBuffer for GstBuffer. |
| qint64 position = getPositionFromBuffer(buffer); |
| audioBuffer = QAudioBuffer(QByteArray((const char*)bufferData, bufferSize), format, position); |
| position /= 1000; // convert to milliseconds |
| if (position != m_position) { |
| m_position = position; |
| emit positionChanged(m_position); |
| } |
| } |
| #if GST_CHECK_VERSION(1,0,0) |
| gst_buffer_unmap(buffer, &mapInfo); |
| gst_sample_unref(sample); |
| #else |
| gst_buffer_unref(buffer); |
| #endif |
| } |
| |
| return audioBuffer; |
| } |
| |
| bool QGstreamerAudioDecoderSession::bufferAvailable() const |
| { |
| QMutexLocker locker(&m_buffersMutex); |
| return m_buffersAvailable > 0; |
| } |
| |
| qint64 QGstreamerAudioDecoderSession::position() const |
| { |
| return m_position; |
| } |
| |
| qint64 QGstreamerAudioDecoderSession::duration() const |
| { |
| return m_duration; |
| } |
| |
| void QGstreamerAudioDecoderSession::processInvalidMedia(QAudioDecoder::Error errorCode, const QString& errorString) |
| { |
| stop(); |
| emit error(int(errorCode), errorString); |
| } |
| |
| GstFlowReturn QGstreamerAudioDecoderSession::new_sample(GstAppSink *, gpointer user_data) |
| { |
| // "Note that the preroll buffer will also be returned as the first buffer when calling gst_app_sink_pull_buffer()." |
| QGstreamerAudioDecoderSession *session = reinterpret_cast<QGstreamerAudioDecoderSession*>(user_data); |
| |
| int buffersAvailable; |
| { |
| QMutexLocker locker(&session->m_buffersMutex); |
| buffersAvailable = session->m_buffersAvailable; |
| session->m_buffersAvailable++; |
| Q_ASSERT(session->m_buffersAvailable <= MAX_BUFFERS_IN_QUEUE); |
| } |
| |
| if (!buffersAvailable) |
| QMetaObject::invokeMethod(session, "bufferAvailableChanged", Qt::QueuedConnection, Q_ARG(bool, true)); |
| QMetaObject::invokeMethod(session, "bufferReady", Qt::QueuedConnection); |
| return GST_FLOW_OK; |
| } |
| |
| void QGstreamerAudioDecoderSession::setAudioFlags(bool wantNativeAudio) |
| { |
| int flags = 0; |
| if (m_playbin) { |
| g_object_get(G_OBJECT(m_playbin), "flags", &flags, NULL); |
| // make sure not to use GST_PLAY_FLAG_NATIVE_AUDIO unless desired |
| // it prevents audio format conversion |
| flags &= ~(GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_NATIVE_VIDEO | GST_PLAY_FLAG_TEXT | GST_PLAY_FLAG_VIS | GST_PLAY_FLAG_NATIVE_AUDIO); |
| flags |= GST_PLAY_FLAG_AUDIO; |
| if (wantNativeAudio) |
| flags |= GST_PLAY_FLAG_NATIVE_AUDIO; |
| g_object_set(G_OBJECT(m_playbin), "flags", flags, NULL); |
| } |
| } |
| |
| void QGstreamerAudioDecoderSession::addAppSink() |
| { |
| if (m_appSink) |
| return; |
| |
| m_appSink = (GstAppSink*)gst_element_factory_make("appsink", NULL); |
| |
| GstAppSinkCallbacks callbacks; |
| memset(&callbacks, 0, sizeof(callbacks)); |
| #if GST_CHECK_VERSION(1,0,0) |
| callbacks.new_sample = &new_sample; |
| #else |
| callbacks.new_buffer = &new_sample; |
| #endif |
| gst_app_sink_set_callbacks(m_appSink, &callbacks, this, NULL); |
| gst_app_sink_set_max_buffers(m_appSink, MAX_BUFFERS_IN_QUEUE); |
| gst_base_sink_set_sync(GST_BASE_SINK(m_appSink), FALSE); |
| |
| gst_bin_add(GST_BIN(m_outputBin), GST_ELEMENT(m_appSink)); |
| gst_element_link(m_audioConvert, GST_ELEMENT(m_appSink)); |
| } |
| |
| void QGstreamerAudioDecoderSession::removeAppSink() |
| { |
| if (!m_appSink) |
| return; |
| |
| gst_element_unlink(m_audioConvert, GST_ELEMENT(m_appSink)); |
| gst_bin_remove(GST_BIN(m_outputBin), GST_ELEMENT(m_appSink)); |
| |
| m_appSink = 0; |
| } |
| |
| void QGstreamerAudioDecoderSession::updateDuration() |
| { |
| gint64 gstDuration = 0; |
| int duration = -1; |
| |
| if (m_playbin && qt_gst_element_query_duration(m_playbin, GST_FORMAT_TIME, &gstDuration)) |
| duration = gstDuration / 1000000; |
| |
| if (m_duration != duration) { |
| m_duration = duration; |
| emit durationChanged(m_duration); |
| } |
| |
| if (m_duration > 0) |
| m_durationQueries = 0; |
| |
| if (m_durationQueries > 0) { |
| //increase delay between duration requests |
| int delay = 25 << (5 - m_durationQueries); |
| QTimer::singleShot(delay, this, SLOT(updateDuration())); |
| m_durationQueries--; |
| } |
| } |
| |
| qint64 QGstreamerAudioDecoderSession::getPositionFromBuffer(GstBuffer* buffer) |
| { |
| qint64 position = GST_BUFFER_TIMESTAMP(buffer); |
| if (position >= 0) |
| position = position / G_GINT64_CONSTANT(1000); // microseconds |
| else |
| position = -1; |
| return position; |
| } |
| |
| QT_END_NAMESPACE |