blob: d6b8ad759143ad05c06bfc186c828f793df253f3 [file] [log] [blame]
/****************************************************************************
**
** 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