Revision control
Copy as Markdown
// 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 <chrono>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
QT_BEGIN_NAMESPACE
static const char tempFile[] = "encoded.wav";
constexpr int dequeueTimeout = 5000;
Q_STATIC_LOGGING_CATEGORY(adLogger, "QAndroidAudioDecoder");
using namespace Qt::Literals;
using namespace std::chrono_literals;
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() : u""_s;
if (!mime.isEmpty() && !mime.contains(u"audio"_s, 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)) {
Q_EMIT error(QAudioDecoder::ResourceError, tr("Cannot open the file"));
return;
}
const int fd = file.handle();
if (fd < 0) {
Q_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()
{
using namespace std::chrono;
// 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;
}
Q_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);
Q_EMIT durationChanged(round<milliseconds>(microseconds{ durationUs }));
// 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()
{
using namespace std::chrono;
if (!m_formatError.isEmpty()) {
Q_EMIT error(QAudioDecoder::FormatError, m_formatError);
return;
}
if (!m_extractor) {
Q_EMIT error(QAudioDecoder::ResourceError, tr("Cannot decode, source not set."));
return;
}
createDecoder();
if (!m_codec) {
Q_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) {
Q_EMIT error(QAudioDecoder::ResourceError, tr("Audio Decoder failed configuration."));
return;
}
status = AMediaCodec_start(m_codec);
if (status != AMEDIA_OK) {
Q_EMIT error(QAudioDecoder::ResourceError, tr("Audio Decoder failed to start."));
return;
}
AMediaExtractor_selectTrack(m_extractor, 0);
Q_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)
Q_EMIT positionChanged(std::move(audioBuffer),
round<milliseconds>(microseconds{ presentationTimeUs }));
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;
}
}
Q_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;
Q_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;
if (m_device && (!m_device->isOpen() || !m_device->isReadable())) {
Q_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() && position() < 0ms && duration() < 0ms)
return;
m_decoder->stop();
m_audioBuffer.clear();
setIsDecoding(false);
QPlatformAudioDecoder::positionChanged(kInvalidPosition);
durationChanged(kInvalidDuration);
Q_EMIT bufferAvailableChanged(false);
}
QAudioBuffer QAndroidAudioDecoder::read()
{
if (!m_audioBuffer.isEmpty()) {
auto buffer = m_audioBuffer.takeFirst();
QPlatformAudioDecoder::positionChanged(buffer.second);
return buffer.first;
}
return {};
}
bool QAndroidAudioDecoder::bufferAvailable() const
{
return m_audioBuffer.size() > 0;
}
void QAndroidAudioDecoder::positionChanged(QAudioBuffer audioBuffer, std::chrono::milliseconds position)
{
m_audioBuffer.append({ audioBuffer, position });
QPlatformAudioDecoder::positionChanged(position);
Q_EMIT bufferReady();
}
void QAndroidAudioDecoder::durationChanged(std::chrono::milliseconds duration)
{
QPlatformAudioDecoder::durationChanged(duration);
}
void QAndroidAudioDecoder::error(const QAudioDecoder::Error err, const QString &errorString)
{
stop();
Q_EMIT QPlatformAudioDecoder::error(err, errorString);
}
void QAndroidAudioDecoder::finished()
{
Q_EMIT bufferAvailableChanged(m_audioBuffer.size() > 0);
if (QPlatformAudioDecoder::duration() >= 0ms)
QPlatformAudioDecoder::durationChanged(QPlatformAudioDecoder::duration());
// remove temp file when decoding is finished
QFile(QString(QDir::tempPath()).append(QString::fromUtf8(tempFile))).remove();
Q_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)
Q_EMIT error(QAudioDecoder::ResourceError, tr("Error opening temporary file: %1").arg(file.errorString()));
success &= (file.write(m_deviceBuffer) == m_deviceBuffer.size());
if (!success)
Q_EMIT error(QAudioDecoder::ResourceError, tr("Error while writing data to temporary file"));
file.close();
m_deviceBuffer.clear();
if (success)
m_decoder->setSource(QUrl::fromLocalFile(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"