Revision control

++ // Copyright (C) 2020 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
++ //#define DEBUG_DECODER
++
++ #include <audio/qgstreameraudiodecoder_p.h>
++
++ #include <common/qgst_debug_p.h>
++ #include <common/qgstreamermessage_p.h>
++ #include <common/qgstutils_p.h>
++ #include <uri_handler/qgstreamer_qiodevice_handler_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>
++ #include <QtCore/qloggingcategory.h>
++
++ QT_BEGIN_NAMESPACE
++
++ Q_STATIC_LOGGING_CATEGORY(qLcGstreamerAudioDecoder, "qt.multimedia.gstreameraudiodecoder");
++
++ 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;
++
++
++ q23::expected<QPlatformAudioDecoder *, QString> QGstreamerAudioDecoder::create(QAudioDecoder *parent)
++ {
++ static const auto error = qGstErrorMessageIfElementsNotAvailable("audioconvert", "playbin");
++ if (error)
++ return q23::unexpected{ *error };
++
++ return new QGstreamerAudioDecoder(parent);
++ }
++
++ QGstreamerAudioDecoder::QGstreamerAudioDecoder(QAudioDecoder *parent)
++ : QPlatformAudioDecoder(parent),
++ m_playbin{
++ QGstPipeline::createFromFactory("playbin3", "playbin"),
++ },
++ m_audioConvert{
++ QGstElement::createFromFactory("audioconvert", "audioconvert"),
++ }
++ {
++ // Sort out messages
++ m_playbin.installMessageFilter(this);
++
++ // Set the rest of the pipeline up
++ setAudioFlags(true);
++
++ m_outputBin = QGstBin::create("audio-output-bin");
++ m_outputBin.add(m_audioConvert);
++
++ // add ghostpad
++ m_outputBin.addGhostPad(m_audioConvert, "sink");
++
++ g_object_set(m_playbin.object(), "audio-sink", m_outputBin.element(), NULL);
++
++ // Set volume to 100%
++ gdouble volume = 1.0;
++ m_playbin.set("volume", volume);
++ }
++
++ QGstreamerAudioDecoder::~QGstreamerAudioDecoder()
++ {
++ stop();
++
++ m_playbin.removeMessageFilter(this);
++ }
++
++ bool QGstreamerAudioDecoder::processBusMessage(const QGstreamerMessage &message)
++ {
++ qCDebug(qLcGstreamerAudioDecoder) << "received bus message:" << message;
++
++ switch (message.type()) {
++ case GST_MESSAGE_DURATION:
++ return processBusMessageDuration(message);
++
++ case GST_MESSAGE_ERROR:
++ return processBusMessageError(message);
++
++ case GST_MESSAGE_WARNING:
++ return processBusMessageWarning(message);
++
++ case GST_MESSAGE_INFO:
++ return processBusMessageInfo(message);
++
++ case GST_MESSAGE_EOS:
++ return processBusMessageEOS(message);
++
++ case GST_MESSAGE_STATE_CHANGED:
++ return processBusMessageStateChanged(message);
++
++ case GST_MESSAGE_STREAMS_SELECTED:
++ return processBusMessageStreamsSelected(message);
++
++ default:
++ return false;
++ }
++ }
++
++ bool QGstreamerAudioDecoder::canReadQrc() const
++ {
++ return true;
++ }
++
++ bool QGstreamerAudioDecoder::processBusMessageError(const QGstreamerMessage &message)
++ {
++ qCDebug(qLcGstreamerAudioDecoder) << " error" << QCompactGstMessageAdaptor(message);
++
++ QUniqueGErrorHandle err;
++ QGString debug;
++ gst_message_parse_error(message.message(), &err, &debug);
++
++ if (message.source() == m_playbin) {
++ if (err.get()->domain == GST_STREAM_ERROR
++ && err.get()->code == GST_STREAM_ERROR_CODEC_NOT_FOUND)
++ processInvalidMedia(QAudioDecoder::FormatError,
++ tr("Cannot play stream of type: <unknown>"));
++ else
++ processInvalidMedia(QAudioDecoder::ResourceError,
++ QString::fromUtf8(err.get()->message));
++ } else {
++ QAudioDecoder::Error qerror = QAudioDecoder::ResourceError;
++ if (err.get()->domain == GST_STREAM_ERROR) {
++ switch (err.get()->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.get()->domain == GST_CORE_ERROR) {
++ switch (err.get()->code) {
++ case GST_CORE_ERROR_MISSING_PLUGIN:
++ qerror = QAudioDecoder::FormatError;
++ break;
++ default:
++ break;
++ }
++ }
++
++ processInvalidMedia(qerror, QString::fromUtf8(err.get()->message));
++ }
++
++ return false;
++ }
++
++ bool QGstreamerAudioDecoder::processBusMessageDuration(const QGstreamerMessage &)
++ {
++ updateDuration();
++ return false;
++ }
++
++ bool QGstreamerAudioDecoder::processBusMessageWarning(const QGstreamerMessage &message)
++ {
++ qCWarning(qLcGstreamerAudioDecoder) << "Warning:" << QCompactGstMessageAdaptor(message);
++ return false;
++ }
++
++ bool QGstreamerAudioDecoder::processBusMessageInfo(const QGstreamerMessage &message)
++ {
++ if (qLcGstreamerAudioDecoder().isDebugEnabled())
++ qCWarning(qLcGstreamerAudioDecoder) << "Info:" << QCompactGstMessageAdaptor(message);
++ return false;
++ }
++
++ bool QGstreamerAudioDecoder::processBusMessageEOS(const QGstreamerMessage &)
++ {
++ m_playbin.setState(GST_STATE_NULL);
++ finished();
++ return false;
++ }
++
++ bool QGstreamerAudioDecoder::processBusMessageStateChanged(const QGstreamerMessage &message)
++ {
++ if (message.source() != m_playbin)
++ return false;
++
++ GstState oldState;
++ GstState newState;
++ GstState pending;
++
++ gst_message_parse_state_changed(message.message(), &oldState, &newState, &pending);
++
++ bool isDecoding = false;
++ switch (newState) {
++ case GST_STATE_VOID_PENDING:
++ case GST_STATE_NULL:
++ case GST_STATE_READY:
++ break;
++ case GST_STATE_PLAYING:
++ isDecoding = true;
++ break;
++ case GST_STATE_PAUSED:
++ isDecoding = true;
++
++ // 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;
++ }
++
++ setIsDecoding(isDecoding);
++ return false;
++ }
++
++ bool QGstreamerAudioDecoder::processBusMessageStreamsSelected(const QGstreamerMessage &message)
++ {
++ using namespace Qt::StringLiterals;
++
++ QGstStreamCollectionHandle collection;
++ gst_message_parse_streams_selected(const_cast<GstMessage *>(message.message()), &collection);
++
++ bool hasAudio = false;
++ qForeachStreamInCollection(collection, [&](GstStream *stream) {
++ GstStreamType type = gst_stream_get_stream_type(stream);
++ if (type == GstStreamType::GST_STREAM_TYPE_AUDIO)
++ hasAudio = true;
++ });
++
++ if (!hasAudio)
++ processInvalidMedia(QAudioDecoder::FormatError, u"No audio track in media"_s);
++
++ return false;
++ }
++
++ QUrl QGstreamerAudioDecoder::source() const
++ {
++ return mSource;
++ }
++
++ void QGstreamerAudioDecoder::setSource(const QUrl &fileName)
++ {
++ stop();
++ mDevice = nullptr;
++
++ bool isSignalRequired = (mSource != fileName);
++ mSource = fileName;
++ if (isSignalRequired)
++ sourceChanged();
++ }
++
++ QIODevice *QGstreamerAudioDecoder::sourceDevice() const
++ {
++ return mDevice;
++ }
++
++ void QGstreamerAudioDecoder::setSourceDevice(QIODevice *device)
++ {
++ stop();
++ mSource.clear();
++ bool isSignalRequired = (mDevice != device);
++ mDevice = device;
++ if (isSignalRequired)
++ sourceChanged();
++ }
++
++ void QGstreamerAudioDecoder::start()
++ {
++ addAppSink();
++
++ if (!mSource.isEmpty()) {
++ m_playbin.set("uri", mSource.toEncoded().constData());
++ } else if (mDevice) {
++ // make sure we can read from device
++ if (!mDevice->isOpen() || !mDevice->isReadable()) {
++ processInvalidMedia(QAudioDecoder::ResourceError, QLatin1String("Unable to read from specified device"));
++ return;
++ }
++
++ QUrl streamURL = qGstRegisterQIODevice(mDevice);
++ m_playbin.set("uri", streamURL.toEncoded().constData());
++ } else {
++ return;
++ }
++
++ // Set audio format
++ if (m_appSink) {
++ if (mFormat.isValid()) {
++ setAudioFlags(false);
++ auto caps = QGstUtils::capsForAudioFormat(mFormat);
++ m_appSink.setCaps(caps);
++ } else {
++ // We want whatever the native audio format is
++ setAudioFlags(true);
++ m_appSink.setCaps({});
++ }
++ }
++
++ if (m_playbin.setState(GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) {
++ qWarning() << "GStreamer; Unable to start decoding process";
++ m_playbin.dumpGraph("failed");
++ return;
++ }
++ }
++
++ void QGstreamerAudioDecoder::stop()
++ {
++ m_playbin.setState(GST_STATE_NULL);
++ m_currentSessionId += 1;
++ removeAppSink();
++
++ // GStreamer thread is stopped. Can safely access m_buffersAvailable
++ if (m_buffersAvailable != 0) {
++ m_buffersAvailable = 0;
++ bufferAvailableChanged(false);
++ }
++
++ positionChanged(kInvalidPosition);
++ durationChanged(kInvalidDuration);
++
++ setIsDecoding(false);
++ }
++
++ QAudioFormat QGstreamerAudioDecoder::audioFormat() const
++ {
++ return mFormat;
++ }
++
++ void QGstreamerAudioDecoder::setAudioFormat(const QAudioFormat &format)
++ {
++ if (mFormat != format) {
++ mFormat = format;
++ formatChanged(mFormat);
++ }
++ }
++
++ QAudioBuffer QGstreamerAudioDecoder::read()
++ {
++ using namespace std::chrono;
++
++ QAudioBuffer audioBuffer;
++
++ if (m_buffersAvailable == 0)
++ return audioBuffer;
++
++ m_buffersAvailable -= 1;
++
++ if (m_buffersAvailable == 0)
++ bufferAvailableChanged(false);
++
++ QGstSampleHandle sample = m_appSink.pullSample();
++ GstBuffer *buffer = gst_sample_get_buffer(sample.get());
++ GstMapInfo mapInfo;
++ gst_buffer_map(buffer, &mapInfo, GST_MAP_READ);
++ const char *bufferData = (const char *)mapInfo.data;
++ int bufferSize = mapInfo.size;
++ QAudioFormat format = QGstUtils::audioFormatForSample(sample.get());
++
++ 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.
++ nanoseconds position = getPositionFromBuffer(buffer);
++ // QAudioBuffer expects startTime in microseconds, but the platform
++ // QPlatformAudioDecoder stores/uses milliseconds resolution. Convert
++ // the nanoseconds position to microseconds for the buffer, and to
++ // milliseconds for the stored platform position.
++ const auto startUs = std::chrono::round<std::chrono::microseconds>(position);
++ audioBuffer = QAudioBuffer{ QByteArray(bufferData, bufferSize), format, startUs.count() };
++ positionChanged(std::chrono::round<std::chrono::milliseconds>(position));
++ }
++ gst_buffer_unmap(buffer, &mapInfo);
++
++ return audioBuffer;
++ }
++
++ void QGstreamerAudioDecoder::processInvalidMedia(QAudioDecoder::Error errorCode, const QString& errorString)
++ {
++ stop();
++ error(errorCode, errorString);
++ }
++
++ GstFlowReturn QGstreamerAudioDecoder::newSample(GstAppSink *)
++ {
++ // "Note that the preroll buffer will also be returned as the first buffer when calling
++ // gst_app_sink_pull_buffer()."
++
++ QMetaObject::invokeMethod(this, [this, sessionId = m_currentSessionId] {
++ if (sessionId != m_currentSessionId)
++ return; // stop()ed before message is executed
++
++ m_buffersAvailable += 1;
++ bufferAvailableChanged(true);
++ bufferReady();
++ });
++
++ return GST_FLOW_OK;
++ }
++
++ GstFlowReturn QGstreamerAudioDecoder::new_sample(GstAppSink *sink, gpointer user_data)
++ {
++ QGstreamerAudioDecoder *decoder = reinterpret_cast<QGstreamerAudioDecoder *>(user_data);
++ qCDebug(qLcGstreamerAudioDecoder) << "QGstreamerAudioDecoder::new_sample";
++ return decoder->newSample(sink);
++ }
++
++ void QGstreamerAudioDecoder::setAudioFlags(bool wantNativeAudio)
++ {
++ int flags = m_playbin.getInt("flags");
++ // 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;
++ m_playbin.set("flags", flags);
++ }
++
++ void QGstreamerAudioDecoder::addAppSink()
++ {
++ using namespace std::chrono_literals;
++
++ if (m_appSink)
++ return;
++
++ qCDebug(qLcGstreamerAudioDecoder) << "QGstreamerAudioDecoder::addAppSink";
++ m_appSink = QGstAppSink::create("decoderAppSink");
++ GstAppSinkCallbacks callbacks{};
++ callbacks.new_sample = new_sample;
++ m_appSink.setCallbacks(callbacks, this, nullptr);
++
++ #if GST_CHECK_VERSION(1, 24, 0)
++ static constexpr auto maxBufferTime = 500ms;
++ m_appSink.setMaxBufferTime(maxBufferTime);
++ #else
++ static constexpr int maxBuffers = 16;
++ m_appSink.setMaxBuffers(maxBuffers);
++ #endif
++
++ static constexpr bool sync = false;
++ m_appSink.setSync(sync);
++
++ m_audioConvert.src().modifyPipelineInIdleProbe([&] {
++ m_outputBin.add(m_appSink);
++ qLinkGstElements(m_audioConvert, m_appSink);
++ });
++ }
++
++ void QGstreamerAudioDecoder::removeAppSink()
++ {
++ if (!m_appSink)
++ return;
++
++ qCDebug(qLcGstreamerAudioDecoder) << "QGstreamerAudioDecoder::removeAppSink";
++
++ m_audioConvert.src().modifyPipelineInIdleProbe([&] {
++ qUnlinkGstElements(m_audioConvert, m_appSink);
++ m_outputBin.stopAndRemoveElements(m_appSink);
++ });
++
++ m_appSink = {};
++ }
++
++ void QGstreamerAudioDecoder::updateDuration()
++ {
++ std::optional<std::chrono::milliseconds> dur = m_playbin.durationInMs();
++ if (!dur)
++ dur = kInvalidDuration;
++
++ durationChanged(*dur);
++
++ if (dur->count() > 0)
++ m_durationQueries = 0;
++
++ if (m_durationQueries > 0) {
++ //increase delay between duration requests
++ int delay = 25 << (5 - m_durationQueries);
++ QTimer::singleShot(delay, this, &QGstreamerAudioDecoder::updateDuration);
++ m_durationQueries--;
++ }
++ }
++
++ std::chrono::nanoseconds QGstreamerAudioDecoder::getPositionFromBuffer(GstBuffer *buffer)
++ {
++ using namespace std::chrono;
++ using namespace std::chrono_literals;
++ nanoseconds position{ GST_BUFFER_TIMESTAMP(buffer) };
++ if (position >= 0ns)
++ return position;
++ else
++ return std::chrono::nanoseconds{ kInvalidPosition };
++ }
++
++ QT_END_NAMESPACE
++
++ #include "moc_qgstreameraudiodecoder_p.cpp"