Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
Loading...
Searching...
No Matches
avfaudiodecoder.mm
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "avfaudiodecoder_p.h"
5
6#include <QtCore/qmutex.h>
7#include <QtCore/qiodevice.h>
8#include <QMimeDatabase>
9#include <QThread>
10#include "private/qcoreaudioutils_p.h"
11#include <QtCore/qloggingcategory.h>
12
13#include <AVFoundation/AVFoundation.h>
14
16
17static Q_LOGGING_CATEGORY(qLcAVFAudioDecoder, "qt.multimedia.darwin.AVFAudioDecoder")
18constexpr static int MAX_BUFFERS_IN_QUEUE = 5;
19
20QAudioBuffer handleNextSampleBuffer(CMSampleBufferRef sampleBuffer)
21{
22 if (!sampleBuffer)
23 return {};
24
25 // Check format
26 CMFormatDescriptionRef formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer);
27 if (!formatDescription)
28 return {};
29 const AudioStreamBasicDescription* const asbd = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription);
31 if (qtFormat.sampleFormat() == QAudioFormat::Unknown && asbd->mBitsPerChannel == 8)
32 qtFormat.setSampleFormat(QAudioFormat::UInt8);
33 if (!qtFormat.isValid())
34 return {};
35
36 // Get the required size to allocate to audioBufferList
37 size_t audioBufferListSize = 0;
38 OSStatus err = CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer,
39 &audioBufferListSize,
40 NULL,
41 0,
42 NULL,
43 NULL,
44 kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment,
45 NULL);
46 if (err != noErr)
47 return {};
48
49 CMBlockBufferRef blockBuffer = NULL;
50 AudioBufferList* audioBufferList = (AudioBufferList*) malloc(audioBufferListSize);
51 // This ensures the buffers placed in audioBufferList are contiguous
52 err = CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer,
53 NULL,
54 audioBufferList,
55 audioBufferListSize,
56 NULL,
57 NULL,
58 kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment,
59 &blockBuffer);
60 if (err != noErr) {
61 free(audioBufferList);
62 return {};
63 }
64
65 QByteArray abuf;
66 for (UInt32 i = 0; i < audioBufferList->mNumberBuffers; i++)
67 {
68 AudioBuffer audioBuffer = audioBufferList->mBuffers[i];
69 abuf.push_back(QByteArray((const char*)audioBuffer.mData, audioBuffer.mDataByteSize));
70 }
71
72 free(audioBufferList);
73 CFRelease(blockBuffer);
74
75 CMTime sampleStartTime = (CMSampleBufferGetPresentationTimeStamp(sampleBuffer));
76 float sampleStartTimeSecs = CMTimeGetSeconds(sampleStartTime);
77
78 return QAudioBuffer(abuf, qtFormat, qint64(sampleStartTimeSecs * 1000000));
79}
80
81@interface AVFResourceReaderDelegate : NSObject <AVAssetResourceLoaderDelegate> {
84}
85
86- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader
87 shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest;
88
89@end
90
91@implementation AVFResourceReaderDelegate
92
93- (id)initWithDecoder:(AVFAudioDecoder *)decoder
94{
95 if (!(self = [super init]))
96 return nil;
97
98 m_decoder = decoder;
99
100 return self;
101}
102
103-(BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader
104 shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
105{
106 Q_UNUSED(resourceLoader);
107
108 if (![loadingRequest.request.URL.scheme isEqualToString:@"iodevice"])
109 return NO;
110
111 QMutexLocker locker(&m_mutex);
112
114 if (!device)
115 return NO;
116
117 device->seek(loadingRequest.dataRequest.requestedOffset);
118 if (loadingRequest.contentInformationRequest) {
119 loadingRequest.contentInformationRequest.contentLength = device->size();
120 loadingRequest.contentInformationRequest.byteRangeAccessSupported = YES;
121 }
122
123 if (loadingRequest.dataRequest) {
124 NSInteger requestedLength = loadingRequest.dataRequest.requestedLength;
125 int maxBytes = qMin(32 * 1024, int(requestedLength));
126 char buffer[maxBytes];
127 NSInteger submitted = 0;
128 while (submitted < requestedLength) {
129 qint64 len = device->read(buffer, maxBytes);
130 if (len < 1)
131 break;
132
133 [loadingRequest.dataRequest respondWithData:[NSData dataWithBytes:buffer length:len]];
134 submitted += len;
135 }
136
137 // Finish loading even if not all bytes submitted.
138 [loadingRequest finishLoading];
139 }
140
141 return YES;
142}
143
144@end
145
146namespace {
147
148NSDictionary *av_audio_settings_for_format(const QAudioFormat &format)
149{
150 float sampleRate = format.sampleRate();
151 int nChannels = format.channelCount();
152 int sampleSize = format.bytesPerSample() * 8;
153 BOOL isFloat = format.sampleFormat() == QAudioFormat::Float;
154
155 NSDictionary *audioSettings = [NSDictionary dictionaryWithObjectsAndKeys:
156 [NSNumber numberWithInt:kAudioFormatLinearPCM], AVFormatIDKey,
157 [NSNumber numberWithFloat:sampleRate], AVSampleRateKey,
158 [NSNumber numberWithInt:nChannels], AVNumberOfChannelsKey,
159 [NSNumber numberWithInt:sampleSize], AVLinearPCMBitDepthKey,
160 [NSNumber numberWithBool:isFloat], AVLinearPCMIsFloatKey,
161 [NSNumber numberWithBool:NO], AVLinearPCMIsNonInterleaved,
162 [NSNumber numberWithBool:NO], AVLinearPCMIsBigEndianKey,
163 nil];
164
165 return audioSettings;
166}
167
168QAudioFormat qt_format_for_audio_track(AVAssetTrack *track)
169{
171 CMFormatDescriptionRef desc = (__bridge CMFormatDescriptionRef)track.formatDescriptions[0];
172 const AudioStreamBasicDescription* const asbd =
173 CMAudioFormatDescriptionGetStreamBasicDescription(desc);
175 // AudioStreamBasicDescription's mBitsPerChannel is 0 for compressed formats
176 // In this case set default Int16 sample format
177 if (asbd->mBitsPerChannel == 0)
178 format.setSampleFormat(QAudioFormat::Int16);
179 return format;
180}
181
182}
183
185{
186 AVAssetReader *m_reader = nullptr;
187 AVAssetReaderTrackOutput *m_readerOutput = nullptr;
188
190 {
191 if (m_reader) {
192 [m_reader cancelReading];
193 [m_reader release];
194 }
195
196 [m_readerOutput release];
197 }
198};
199
202{
203 m_readingQueue = dispatch_queue_create("reader_queue", DISPATCH_QUEUE_SERIAL);
204 m_decodingQueue = dispatch_queue_create("decoder_queue", DISPATCH_QUEUE_SERIAL);
205
206 m_readerDelegate = [[AVFResourceReaderDelegate alloc] initWithDecoder:this];
207}
208
210{
211 stop();
212
213 [m_readerDelegate release];
214 [m_asset release];
215
216 dispatch_release(m_readingQueue);
217 dispatch_release(m_decodingQueue);
218}
219
221{
222 return m_source;
223}
224
226{
227 if (!m_device && m_source == fileName)
228 return;
229
230 stop();
231 m_device = nullptr;
232 [m_asset release];
233 m_asset = nil;
234
235 m_source = fileName;
236
237 if (!m_source.isEmpty()) {
238 NSURL *nsURL = m_source.toNSURL();
239 m_asset = [[AVURLAsset alloc] initWithURL:nsURL options:nil];
240 }
241
243}
244
246{
247 return m_device;
248}
249
251{
252 if (m_device == device && m_source.isEmpty())
253 return;
254
255 stop();
256 m_source.clear();
257 [m_asset release];
258 m_asset = nil;
259
260 m_device = device;
261
262 if (m_device) {
263 const QString ext = QMimeDatabase().mimeTypeForData(m_device).preferredSuffix();
264 const QString url = "iodevice:///iodevice." + ext;
265 NSString *urlString = url.toNSString();
266 NSURL *nsURL = [NSURL URLWithString:urlString];
267
268 m_asset = [[AVURLAsset alloc] initWithURL:nsURL options:nil];
269
270 // use decoding queue instead of reading queue in order to fix random stucks.
271 // Anyway, decoding queue is empty in the moment.
272 [m_asset.resourceLoader setDelegate:m_readerDelegate queue:m_decodingQueue];
273 }
274
276}
277
279{
280 if (m_decodingContext) {
281 qCDebug(qLcAVFAudioDecoder()) << "AVFAudioDecoder has been already started";
282 return;
283 }
284
285 positionChanged(-1);
286
287 if (m_device && (!m_device->isOpen() || !m_device->isReadable())) {
288 processInvalidMedia(QAudioDecoder::ResourceError, tr("Unable to read from specified device"));
289 return;
290 }
291
292 m_decodingContext = std::make_shared<DecodingContext>();
293 std::weak_ptr<DecodingContext> weakContext(m_decodingContext);
294
295 auto handleLoadingResult = [=]() {
296 NSError *error = nil;
297 AVKeyValueStatus status = [m_asset statusOfValueForKey:@"tracks" error:&error];
298
299 if (status == AVKeyValueStatusFailed) {
300 if (error.domain == NSURLErrorDomain)
301 processInvalidMedia(QAudioDecoder::ResourceError,
302 QString::fromNSString(error.localizedDescription));
303 else
304 processInvalidMedia(QAudioDecoder::FormatError,
305 tr("Could not load media source's tracks"));
306 } else if (status != AVKeyValueStatusLoaded) {
307 qWarning() << "Unexpected AVKeyValueStatus:" << status;
308 stop();
309 }
310 else {
311 initAssetReader();
312 }
313 };
314
315 [m_asset loadValuesAsynchronouslyForKeys:@[ @"tracks" ]
316 completionHandler:[=]() {
317 invokeWithDecodingContext(weakContext, handleLoadingResult);
318 }];
319}
320
321void AVFAudioDecoder::decBuffersCounter(uint val)
322{
323 if (val) {
324 QMutexLocker locker(&m_buffersCounterMutex);
325 m_buffersCounter -= val;
326 }
327
328 Q_ASSERT(m_buffersCounter >= 0);
329
330 m_buffersCounterCondition.wakeAll();
331}
332
334{
335 qCDebug(qLcAVFAudioDecoder()) << "stop decoding";
336
337 m_decodingContext.reset();
338 decBuffersCounter(m_cachedBuffers.size());
339 m_cachedBuffers.clear();
340
342 positionChanged(-1);
343 durationChanged(-1);
344
345 onFinished();
346}
347
349{
350 return m_format;
351}
352
354{
355 if (m_format != format) {
356 m_format = format;
357 formatChanged(m_format);
358 }
359}
360
362{
363 if (m_cachedBuffers.empty())
364 return QAudioBuffer();
365
366 Q_ASSERT(m_cachedBuffers.size() > 0);
367 QAudioBuffer buffer = m_cachedBuffers.dequeue();
368 decBuffersCounter(1);
369
370 positionChanged(buffer.startTime() / 1000);
371 bufferAvailableChanged(!m_cachedBuffers.empty());
372 return buffer;
373}
374
375void AVFAudioDecoder::processInvalidMedia(QAudioDecoder::Error errorCode,
376 const QString &errorString)
377{
378 qCDebug(qLcAVFAudioDecoder()) << "Invalid media. Error code:" << errorCode
379 << "Description:" << errorString;
380
382
383 error(int(errorCode), errorString);
384
385 // TODO: may be check if decodingCondext was changed by
386 // user's action (restart) from the emitted error.
387 // We should handle it somehow (don't run stop, print warning or etc...)
388
389 stop();
390}
391
392void AVFAudioDecoder::onFinished()
393{
394 m_decodingContext.reset();
395
396 if (isDecoding())
397 finished();
398}
399
400void AVFAudioDecoder::initAssetReader()
401{
402 qCDebug(qLcAVFAudioDecoder()) << "Init asset reader";
403
404 Q_ASSERT(m_asset);
406
407 NSArray<AVAssetTrack *> *tracks = [m_asset tracksWithMediaType:AVMediaTypeAudio];
408 if (!tracks.count) {
409 processInvalidMedia(QAudioDecoder::FormatError, tr("No audio tracks found"));
410 return;
411 }
412
413 AVAssetTrack *track = [tracks objectAtIndex:0];
414 QAudioFormat format = m_format.isValid() ? m_format : qt_format_for_audio_track(track);
415 if (!format.isValid()) {
416 processInvalidMedia(QAudioDecoder::FormatError, tr("Unsupported source format"));
417 return;
418 }
419
420 durationChanged(CMTimeGetSeconds(track.timeRange.duration) * 1000);
421
422 NSError *error = nil;
423 NSDictionary *audioSettings = av_audio_settings_for_format(format);
424
425 AVAssetReaderTrackOutput *readerOutput =
426 [[AVAssetReaderTrackOutput alloc] initWithTrack:track outputSettings:audioSettings];
427 AVAssetReader *reader = [[AVAssetReader alloc] initWithAsset:m_asset error:&error];
428 if (error) {
429 processInvalidMedia(QAudioDecoder::ResourceError, QString::fromNSString(error.localizedDescription));
430 return;
431 }
432 if (![reader canAddOutput:readerOutput]) {
433 processInvalidMedia(QAudioDecoder::ResourceError, tr("Failed to add asset reader output"));
434 return;
435 }
436
437 [reader addOutput:readerOutput];
438
439 Q_ASSERT(m_decodingContext);
440 m_decodingContext->m_reader = reader;
441 m_decodingContext->m_readerOutput = readerOutput;
442
443 startReading();
444}
445
446void AVFAudioDecoder::startReading()
447{
448 Q_ASSERT(m_decodingContext);
449 Q_ASSERT(m_decodingContext->m_reader);
451
452 // Prepares the receiver for obtaining sample buffers from the asset.
453 if (![m_decodingContext->m_reader startReading]) {
454 processInvalidMedia(QAudioDecoder::ResourceError, tr("Could not start reading"));
455 return;
456 }
457
458 setIsDecoding(true);
459
460 std::weak_ptr<DecodingContext> weakContext = m_decodingContext;
461
462 // Since copyNextSampleBuffer is synchronous, submit it to an async dispatch queue
463 // to run in a separate thread. Call the handleNextSampleBuffer "callback" on another
464 // thread when new audio sample is read.
465 auto copyNextSampleBuffer = [=]() {
466 auto decodingContext = weakContext.lock();
467 if (!decodingContext)
468 return false;
469
470 CMSampleBufferRef sampleBuffer = [decodingContext->m_readerOutput copyNextSampleBuffer];
471 if (!sampleBuffer)
472 return false;
473
474 dispatch_async(m_decodingQueue, [=]() {
475 if (!weakContext.expired() && CMSampleBufferDataIsReady(sampleBuffer)) {
476 auto audioBuffer = handleNextSampleBuffer(sampleBuffer);
477
478 if (audioBuffer.isValid())
479 invokeWithDecodingContext(weakContext,
480 [=]() { handleNewAudioBuffer(audioBuffer); });
481 }
482
483 CFRelease(sampleBuffer);
484 });
485
486 return true;
487 };
488
489 dispatch_async(m_readingQueue, [=]() {
490 qCDebug(qLcAVFAudioDecoder()) << "start reading thread";
491
492 do {
493 // Note, waiting here doesn't ensure strong contol of the counter.
494 // However, it doesn't affect the logic: the reading flow works fine
495 // even if the counter is time-to-time more than max value
496 waitUntilBuffersCounterLessMax();
497 } while (copyNextSampleBuffer());
498
499 // TODO: check m_reader.status == AVAssetReaderStatusFailed
500 invokeWithDecodingContext(weakContext, [this]() { onFinished(); });
501 });
502}
503
504void AVFAudioDecoder::waitUntilBuffersCounterLessMax()
505{
506 if (m_buffersCounter >= MAX_BUFFERS_IN_QUEUE) {
507 // the check avoids extra mutex lock.
508
509 QMutexLocker locker(&m_buffersCounterMutex);
510
511 while (m_buffersCounter >= MAX_BUFFERS_IN_QUEUE)
512 m_buffersCounterCondition.wait(&m_buffersCounterMutex);
513 }
514}
515
516void AVFAudioDecoder::handleNewAudioBuffer(QAudioBuffer buffer)
517{
518 m_cachedBuffers.enqueue(buffer);
519 ++m_buffersCounter;
520
521 Q_ASSERT(m_cachedBuffers.size() == m_buffersCounter);
522
524 bufferReady();
525}
526
527/*
528 * The method calls the passed functor in the thread of AVFAudioDecoder and guarantees that
529 * the passed decoding context is not expired. In other words, it helps avoiding all callbacks
530 * after stopping of the decoder.
531 */
532template<typename F>
533void AVFAudioDecoder::invokeWithDecodingContext(std::weak_ptr<DecodingContext> weakContext, F &&f)
534{
535 if (!weakContext.expired())
536 QMetaObject::invokeMethod(this, [=]() {
537 // strong check: compare with actual decoding context.
538 // Otherwise, the context can be temporary locked by one of dispatch queues.
539 if (auto context = weakContext.lock(); context && context == m_decodingContext)
540 f();
541 });
542}
543
544#include "moc_avfaudiodecoder_p.cpp"
QAudioBuffer handleNextSampleBuffer(CMSampleBufferRef sampleBuffer)
IOBluetoothDevice * device
QAudioBuffer read() override
void setAudioFormat(const QAudioFormat &format) override
AVFAudioDecoder(QAudioDecoder *parent)
QUrl source() const override
QIODevice * sourceDevice() const override
QAudioFormat audioFormat() const override
void setSource(const QUrl &fileName) override
void stop() override
void start() override
void setSourceDevice(QIODevice *device) override
virtual ~AVFAudioDecoder()
static Q_MULTIMEDIA_EXPORT QAudioFormat toQAudioFormat(const AudioStreamBasicDescription &streamFormat)
\inmodule QtMultimedia
The QAudioDecoder class implements decoding audio.
Error
Defines a media player error condition.
The QAudioFormat class stores audio stream parameter information.
constexpr bool isValid() const noexcept
Returns true if all of the parameters are valid.
\inmodule QtCore
Definition qbytearray.h:57
\inmodule QtCore \reentrant
Definition qiodevice.h:34
bool isOpen() const
Returns true if the device is open; otherwise returns false.
bool isReadable() const
Returns true if data can be read from the device; otherwise returns false.
qsizetype size() const noexcept
Definition qlist.h:397
bool empty() const noexcept
Definition qlist.h:685
void clear()
Definition qlist.h:434
\inmodule QtCore
QMimeType mimeTypeForData(const QByteArray &data) const
Returns a MIME type for data.
QString preferredSuffix
the preferred suffix for the MIME type
Definition qmimetype.h:38
\inmodule QtCore
Definition qmutex.h:313
\inmodule QtCore
Definition qmutex.h:281
QObject * parent() const
Returns a pointer to the parent object.
Definition qobject.h:346
QThread * thread() const
Returns the thread in which the object lives.
Definition qobject.cpp:1598
void durationChanged(qint64 duration)
void positionChanged(qint64 position)
void bufferAvailableChanged(bool available)
QAudioDecoder::Error error() const
void formatChanged(const QAudioFormat &format)
void setIsDecoding(bool running=true)
void enqueue(const T &t)
Adds value t to the tail of the queue.
Definition qqueue.h:18
T dequeue()
Removes the head item in the queue and returns it.
Definition qqueue.h:19
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:129
static QThread * currentThread()
Definition qthread.cpp:1039
\inmodule QtCore
Definition qurl.h:94
bool isEmpty() const
Returns true if the URL has no data; otherwise returns false.
Definition qurl.cpp:1896
void clear()
Resets the content of the QUrl.
Definition qurl.cpp:1909
bool wait(QMutex *, QDeadlineTimer=QDeadlineTimer(QDeadlineTimer::Forever))
AVFAudioDecoder * m_decoder
QImageReader reader("image.png")
[1]
static void * context
long NSInteger
typedef QByteArray(EGLAPIENTRYP PFNQGSGETDISPLAYSPROC)()
#define MAX_BUFFERS_IN_QUEUE
#define qWarning
Definition qlogging.h:166
#define Q_LOGGING_CATEGORY(name,...)
#define qCDebug(category,...)
constexpr const T & qMin(const T &a, const T &b)
Definition qminmax.h:40
GLenum GLuint id
[7]
GLfloat GLfloat f
GLenum GLuint buffer
GLint GLsizei GLsizei GLenum format
GLuint GLfloat * val
GLenum GLsizei len
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
#define tr(X)
static QT_BEGIN_NAMESPACE void init(QTextBoundaryFinder::BoundaryType type, QStringView str, QCharAttributes *attributes)
#define Q_UNUSED(x)
unsigned int uint
Definition qtypes.h:34
long long qint64
Definition qtypes.h:60
QUrl url("example.com")
[constructor-url-reference]
AVAssetReaderTrackOutput * m_readerOutput
static bool invokeMethod(QObject *obj, const char *member, Qt::ConnectionType, QGenericReturnArgument ret, QGenericArgument val0=QGenericArgument(nullptr), QGenericArgument val1=QGenericArgument(), QGenericArgument val2=QGenericArgument(), QGenericArgument val3=QGenericArgument(), QGenericArgument val4=QGenericArgument(), QGenericArgument val5=QGenericArgument(), QGenericArgument val6=QGenericArgument(), QGenericArgument val7=QGenericArgument(), QGenericArgument val8=QGenericArgument(), QGenericArgument val9=QGenericArgument())
\threadsafe This is an overloaded member function, provided for convenience. It differs from the abov...