summaryrefslogtreecommitdiffstats
path: root/src/plugins/multimedia/android/audio/qandroidaudiodecoder.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/multimedia/android/audio/qandroidaudiodecoder.cpp')
-rw-r--r--src/plugins/multimedia/android/audio/qandroidaudiodecoder.cpp437
1 files changed, 437 insertions, 0 deletions
diff --git a/src/plugins/multimedia/android/audio/qandroidaudiodecoder.cpp b/src/plugins/multimedia/android/audio/qandroidaudiodecoder.cpp
new file mode 100644
index 000000000..d200a72b5
--- /dev/null
+++ b/src/plugins/multimedia/android/audio/qandroidaudiodecoder.cpp
@@ -0,0 +1,437 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+#include "qandroidaudiodecoder_p.h"
+
+#include <QtCore/qcoreapplication.h>
+#include <QtCore/qjniobject.h>
+#include <QtCore/qjnienvironment.h>
+#include <QtCore/private/qandroidextras_p.h>
+#include <qloggingcategory.h>
+#include <QTimer>
+#include <QFile>
+#include <QDir>
+
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+QT_BEGIN_NAMESPACE
+
+static const char tempFile[] = "encoded.wav";
+constexpr int dequeueTimeout = 5000;
+static Q_LOGGING_CATEGORY(adLogger, "QAndroidAudioDecoder")
+
+Decoder::Decoder()
+ : m_format(AMediaFormat_new())
+{}
+
+Decoder::~Decoder()
+{
+ if (m_codec) {
+ AMediaCodec_delete(m_codec);
+ m_codec = nullptr;
+ }
+
+ if (m_extractor) {
+ AMediaExtractor_delete(m_extractor);
+ m_extractor = nullptr;
+ }
+
+ if (m_format) {
+ AMediaFormat_delete(m_format);
+ m_format = nullptr;
+ }
+}
+
+void Decoder::stop()
+{
+ if (!m_codec)
+ return;
+
+ const media_status_t err = AMediaCodec_stop(m_codec);
+ if (err != AMEDIA_OK)
+ qCWarning(adLogger) << "stop() error: " << err;
+}
+
+void Decoder::setSource(const QUrl &source)
+{
+ const QJniObject path = QJniObject::callStaticObjectMethod(
+ "org/qtproject/qt/android/multimedia/QtMultimediaUtils",
+ "getMimeType",
+ "(Landroid/content/Context;Ljava/lang/String;)Ljava/lang/String;",
+ QNativeInterface::QAndroidApplication::context().object(),
+ QJniObject::fromString(source.path()).object());
+
+ const QString mime = path.isValid() ? path.toString() : "";
+
+ if (!mime.isEmpty() && !mime.contains("audio", Qt::CaseInsensitive)) {
+ m_formatError = tr("Cannot set source, invalid mime type for the source provided.");
+ return;
+ }
+
+ if (!m_extractor)
+ m_extractor = AMediaExtractor_new();
+
+ QFile file(source.path());
+ if (!file.open(QFile::ReadOnly)) {
+ emit error(QAudioDecoder::ResourceError, tr("Cannot open the file"));
+ return;
+ }
+
+ const int fd = file.handle();
+
+ if (fd < 0) {
+ emit error(QAudioDecoder::ResourceError, tr("Invalid fileDescriptor for source."));
+ return;
+ }
+ const int size = file.size();
+ media_status_t status = AMediaExtractor_setDataSourceFd(m_extractor, fd, 0,
+ size > 0 ? size : LONG_MAX);
+ close(fd);
+
+ if (status != AMEDIA_OK) {
+ if (m_extractor) {
+ AMediaExtractor_delete(m_extractor);
+ m_extractor = nullptr;
+ }
+ m_formatError = tr("Setting source for Audio Decoder failed.");
+ }
+}
+
+void Decoder::createDecoder()
+{
+ // get encoded format for decoder
+ m_format = AMediaExtractor_getTrackFormat(m_extractor, 0);
+
+ const char *mime;
+ if (!AMediaFormat_getString(m_format, AMEDIAFORMAT_KEY_MIME, &mime)) {
+ if (m_extractor) {
+ AMediaExtractor_delete(m_extractor);
+ m_extractor = nullptr;
+ }
+ emit error(QAudioDecoder::FormatError, tr("Format not supported by Audio Decoder."));
+
+ return;
+ }
+
+ // get audio duration from source
+ int64_t durationUs;
+ AMediaFormat_getInt64(m_format, AMEDIAFORMAT_KEY_DURATION, &durationUs);
+ emit durationChanged(durationUs / 1000);
+
+ // set default output audio format from input file
+ if (!m_outputFormat.isValid()) {
+ int32_t sampleRate;
+ AMediaFormat_getInt32(m_format, AMEDIAFORMAT_KEY_SAMPLE_RATE, &sampleRate);
+ m_outputFormat.setSampleRate(sampleRate);
+ int32_t channelCount;
+ AMediaFormat_getInt32(m_format, AMEDIAFORMAT_KEY_CHANNEL_COUNT, &channelCount);
+ m_outputFormat.setChannelCount(channelCount);
+ m_outputFormat.setSampleFormat(QAudioFormat::Int16);
+ }
+
+ m_codec = AMediaCodec_createDecoderByType(mime);
+}
+
+void Decoder::doDecode()
+{
+ if (!m_formatError.isEmpty()) {
+ emit error(QAudioDecoder::FormatError, m_formatError);
+ return;
+ }
+
+ if (!m_extractor) {
+ emit error(QAudioDecoder::ResourceError, tr("Cannot decode, source not set."));
+ return;
+ }
+
+ createDecoder();
+
+ if (!m_codec) {
+ emit error(QAudioDecoder::ResourceError, tr("Audio Decoder could not be created."));
+ return;
+ }
+
+ media_status_t status = AMediaCodec_configure(m_codec, m_format, nullptr /* surface */,
+ nullptr /* crypto */, 0);
+
+ if (status != AMEDIA_OK) {
+ emit error(QAudioDecoder::ResourceError, tr("Audio Decoder failed configuration."));
+ return;
+ }
+
+ status = AMediaCodec_start(m_codec);
+ if (status != AMEDIA_OK) {
+ emit error(QAudioDecoder::ResourceError, tr("Audio Decoder failed to start."));
+ return;
+ }
+
+ AMediaExtractor_selectTrack(m_extractor, 0);
+
+ emit decodingChanged(true);
+ m_inputEOS = false;
+ while (!m_inputEOS) {
+ // handle input buffer
+ const ssize_t bufferIdx = AMediaCodec_dequeueInputBuffer(m_codec, dequeueTimeout);
+
+ if (bufferIdx >= 0) {
+ size_t bufferSize = {};
+ uint8_t *buffer = AMediaCodec_getInputBuffer(m_codec, bufferIdx, &bufferSize);
+ const int sample = AMediaExtractor_readSampleData(m_extractor, buffer, bufferSize);
+ if (sample < 0) {
+ m_inputEOS = true;
+ break;
+ }
+
+ const int64_t presentationTimeUs = AMediaExtractor_getSampleTime(m_extractor);
+ AMediaCodec_queueInputBuffer(m_codec, bufferIdx, 0, sample, presentationTimeUs,
+ m_inputEOS ? AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM : 0);
+ AMediaExtractor_advance(m_extractor);
+
+ // handle output buffer
+ AMediaCodecBufferInfo info;
+ ssize_t idx = AMediaCodec_dequeueOutputBuffer(m_codec, &info, dequeueTimeout);
+
+ while (idx == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED
+ || idx == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED) {
+ if (idx == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED)
+ qCWarning(adLogger) << "dequeueOutputBuffer() status: outputFormat changed";
+
+ idx = AMediaCodec_dequeueOutputBuffer(m_codec, &info, dequeueTimeout);
+ }
+
+ if (idx >= 0) {
+ if (info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM)
+ break;
+
+ if (info.size > 0) {
+ size_t bufferSize;
+ const uint8_t *bufferData = AMediaCodec_getOutputBuffer(m_codec, idx,
+ &bufferSize);
+ const QByteArray data((const char*)(bufferData + info.offset), info.size);
+ auto audioBuffer = QAudioBuffer(data, m_outputFormat, presentationTimeUs);
+ if (presentationTimeUs >= 0)
+ emit positionChanged(std::move(audioBuffer), presentationTimeUs / 1000);
+
+ AMediaCodec_releaseOutputBuffer(m_codec, idx, false);
+ }
+ } else if (idx == AMEDIACODEC_INFO_TRY_AGAIN_LATER) {
+ qCWarning(adLogger) << "dequeueOutputBuffer() status: try again later";
+ break;
+ } else {
+ qCWarning(adLogger) <<
+ "AMediaCodec_dequeueOutputBuffer() status: invalid buffer idx " << idx;
+ }
+ } else {
+ qCWarning(adLogger) << "dequeueInputBuffer() status: invalid buffer idx " << bufferIdx;
+ }
+ }
+ emit finished();
+}
+
+QAndroidAudioDecoder::QAndroidAudioDecoder(QAudioDecoder *parent)
+ : QPlatformAudioDecoder(parent),
+ m_decoder(new Decoder())
+{
+ connect(m_decoder, &Decoder::positionChanged, this, &QAndroidAudioDecoder::positionChanged);
+ connect(m_decoder, &Decoder::durationChanged, this, &QAndroidAudioDecoder::durationChanged);
+ connect(m_decoder, &Decoder::error, this, &QAndroidAudioDecoder::error);
+ connect(m_decoder, &Decoder::finished, this, &QAndroidAudioDecoder::finished);
+ connect(m_decoder, &Decoder::decodingChanged, this, &QPlatformAudioDecoder::setIsDecoding);
+ connect(this, &QAndroidAudioDecoder::setSourceUrl, m_decoder, & Decoder::setSource);
+}
+
+QAndroidAudioDecoder::~QAndroidAudioDecoder()
+{
+ m_decoder->thread()->quit();
+ m_decoder->thread()->wait();
+ delete m_threadDecoder;
+ delete m_decoder;
+}
+
+void QAndroidAudioDecoder::setSource(const QUrl &fileName)
+{
+ if (!requestPermissions())
+ return;
+
+ if (isDecoding())
+ return;
+
+ m_device = nullptr;
+ error(QAudioDecoder::NoError, QStringLiteral(""));
+
+ if (m_source != fileName) {
+ m_source = fileName;
+ emit setSourceUrl(m_source);
+ sourceChanged();
+ }
+}
+
+void QAndroidAudioDecoder::setSourceDevice(QIODevice *device)
+{
+ if (isDecoding())
+ return;
+
+ m_source.clear();
+ if (m_device != device) {
+ m_device = device;
+
+ if (!requestPermissions())
+ return;
+
+ sourceChanged();
+ }
+}
+
+void QAndroidAudioDecoder::start()
+{
+ if (isDecoding())
+ return;
+
+ m_position = -1;
+
+ if (m_device && (!m_device->isOpen() || !m_device->isReadable())) {
+ emit error(QAudioDecoder::ResourceError,
+ QString::fromUtf8("Unable to read from the specified device"));
+ return;
+ }
+
+ if (!m_threadDecoder) {
+ m_threadDecoder = new QThread(this);
+ m_decoder->moveToThread(m_threadDecoder);
+ m_threadDecoder->start();
+ }
+
+ decode();
+}
+
+void QAndroidAudioDecoder::stop()
+{
+ if (!isDecoding() && m_position < 0 && m_duration < 0)
+ return;
+
+ m_decoder->stop();
+ m_audioBuffer.clear();
+ m_position = -1;
+ m_duration = -1;
+ setIsDecoding(false);
+
+ emit bufferAvailableChanged(false);
+ emit QPlatformAudioDecoder::positionChanged(m_position);
+}
+
+QAudioBuffer QAndroidAudioDecoder::read()
+{
+ if (!m_audioBuffer.isEmpty()) {
+ QPair<QAudioBuffer, int> buffer = m_audioBuffer.takeFirst();
+ m_position = buffer.second;
+ emit QPlatformAudioDecoder::positionChanged(buffer.second);
+ return buffer.first;
+ }
+
+ // no buffers available
+ return {};
+}
+
+bool QAndroidAudioDecoder::bufferAvailable() const
+{
+ return m_audioBuffer.size() > 0;
+}
+
+qint64 QAndroidAudioDecoder::position() const
+{
+ return m_position;
+}
+
+qint64 QAndroidAudioDecoder::duration() const
+{
+ return m_duration;
+}
+
+void QAndroidAudioDecoder::positionChanged(QAudioBuffer audioBuffer, qint64 position)
+{
+ m_audioBuffer.append(QPair<QAudioBuffer, int>(audioBuffer, position));
+ m_position = position;
+ emit bufferReady();
+}
+
+void QAndroidAudioDecoder::durationChanged(qint64 duration)
+{
+ m_duration = duration;
+ emit QPlatformAudioDecoder::durationChanged(duration);
+}
+
+void QAndroidAudioDecoder::error(const QAudioDecoder::Error err, const QString &errorString)
+{
+ stop();
+ emit QPlatformAudioDecoder::error(err, errorString);
+}
+
+void QAndroidAudioDecoder::finished()
+{
+ emit bufferAvailableChanged(m_audioBuffer.size() > 0);
+
+ if (m_duration != -1)
+ emit durationChanged(m_duration);
+
+ // remove temp file when decoding is finished
+ QFile(QString(QDir::tempPath()).append(QString::fromUtf8(tempFile))).remove();
+ emit QPlatformAudioDecoder::finished();
+}
+
+bool QAndroidAudioDecoder::requestPermissions()
+{
+ const auto writeRes = QtAndroidPrivate::requestPermission(QStringLiteral("android.permission.WRITE_EXTERNAL_STORAGE"));
+ if (writeRes.result() == QtAndroidPrivate::Authorized)
+ return true;
+
+ return false;
+}
+
+void QAndroidAudioDecoder::decode()
+{
+ if (m_device) {
+ connect(m_device, &QIODevice::readyRead, this, &QAndroidAudioDecoder::readDevice);
+ if (m_device->bytesAvailable())
+ readDevice();
+ } else {
+ QTimer::singleShot(0, m_decoder, &Decoder::doDecode);
+ }
+}
+
+bool QAndroidAudioDecoder::createTempFile()
+{
+ QFile file = QFile(QDir::tempPath().append(QString::fromUtf8(tempFile)), this);
+
+ bool success = file.open(QIODevice::QIODevice::ReadWrite);
+ if (!success)
+ emit error(QAudioDecoder::ResourceError, tr("Error opening temporary file: %1").arg(file.errorString()));
+
+ success &= (file.write(m_deviceBuffer) == m_deviceBuffer.size());
+ if (!success)
+ emit error(QAudioDecoder::ResourceError, tr("Error while writing data to temporary file"));
+
+ file.close();
+ m_deviceBuffer.clear();
+ if (success)
+ m_decoder->setSource(file.fileName());
+
+ return success;
+}
+
+void QAndroidAudioDecoder::readDevice() {
+ m_deviceBuffer.append(m_device->readAll());
+ if (m_device->atEnd()) {
+ disconnect(m_device, &QIODevice::readyRead, this, &QAndroidAudioDecoder::readDevice);
+ if (!createTempFile()) {
+ m_deviceBuffer.clear();
+ stop();
+ return;
+ }
+ QTimer::singleShot(0, m_decoder, &Decoder::doDecode);
+ }
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qandroidaudiodecoder_p.cpp"