Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
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
5
6#include <QtCore/qiodevice.h>
7#include <QtCore/qloggingcategory.h>
8#include <QtCore/qmimedatabase.h>
9#include <QtCore/qmutex.h>
10#include <QtCore/qthread.h>
11#include <QtCore/private/qcore_mac_p.h>
12
13#include "private/qcoreaudioutils_p.h"
14
15#import <AVFoundation/AVFoundation.h>
16
17QT_USE_NAMESPACE
18
19Q_STATIC_LOGGING_CATEGORY(qLcAVFAudioDecoder, "qt.multimedia.darwin.AVFAudioDecoder");
20constexpr static int MAX_BUFFERS_IN_QUEUE = 5;
21using namespace Qt::Literals;
22
23static QAudioBuffer handleNextSampleBuffer(QAudioFormat qtFormat, CMSampleBufferRef sampleBuffer)
24{
25 if (!sampleBuffer)
26 return {};
27
28 // Check format
29 auto validateFormat = [&] {
30 CMFormatDescriptionRef formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer);
31 if (!formatDescription)
32 return false;
33 const AudioStreamBasicDescription *const asbd =
34 CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription);
35 if (!asbd)
36 return false;
37 return qtFormat == QCoreAudioUtils::toPreferredQAudioFormat(*asbd);
38 };
39
40 Q_ASSERT(validateFormat());
41
42 // Get the required size to allocate to audioBufferList
43 size_t audioBufferListSize = 0;
44 OSStatus err = CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer,
45 &audioBufferListSize,
46 NULL,
47 0,
48 NULL,
49 NULL,
50 kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment,
51 NULL);
52 if (err != noErr)
53 return {};
54
55 CMBlockBufferRef blockBuffer = nullptr;
56 AudioBufferList* audioBufferList = (AudioBufferList*) malloc(audioBufferListSize);
57 // This ensures the buffers placed in audioBufferList are contiguous
58 err = CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer,
59 NULL,
60 audioBufferList,
61 audioBufferListSize,
62 NULL,
63 NULL,
64 kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment,
65 &blockBuffer);
66 if (err != noErr) {
67 free(audioBufferList);
68 return {};
69 }
70
71 QByteArray abuf;
72 for (UInt32 i = 0; i < audioBufferList->mNumberBuffers; i++)
73 {
74 AudioBuffer audioBuffer = audioBufferList->mBuffers[i];
75 abuf.push_back(QByteArray((const char*)audioBuffer.mData, audioBuffer.mDataByteSize));
76 }
77
78 free(audioBufferList);
79 CFRelease(blockBuffer);
80
81 CMTime sampleStartTime = (CMSampleBufferGetPresentationTimeStamp(sampleBuffer));
82 float sampleStartTimeSecs = CMTimeGetSeconds(sampleStartTime);
83
84 return QAudioBuffer(abuf, qtFormat, qint64(sampleStartTimeSecs * 1000000));
85}
86
87@interface AVFResourceReaderDelegate : NSObject <AVAssetResourceLoaderDelegate> {
88 AVFAudioDecoder *m_decoder;
89 QMutex m_mutex;
90}
91
92- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader
93 shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest;
94
95@end
96
97@implementation AVFResourceReaderDelegate
98
99- (id)initWithDecoder:(AVFAudioDecoder *)decoder
100{
101 if (!(self = [super init]))
102 return nil;
103
104 m_decoder = decoder;
105
106 return self;
107}
108
109-(BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader
110 shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
111{
112 Q_UNUSED(resourceLoader);
113
114 if (![loadingRequest.request.URL.scheme isEqualToString:@"iodevice"])
115 return NO;
116
117 QMutexLocker locker(&m_mutex);
118
119 QIODevice *device = m_decoder->sourceDevice();
120 if (!device)
121 return NO;
122
123 device->seek(loadingRequest.dataRequest.requestedOffset);
124 if (loadingRequest.contentInformationRequest) {
125 loadingRequest.contentInformationRequest.contentLength = device->size();
126 loadingRequest.contentInformationRequest.byteRangeAccessSupported = YES;
127 }
128
129 if (loadingRequest.dataRequest) {
130 NSInteger requestedLength = loadingRequest.dataRequest.requestedLength;
131 int maxBytes = qMin(32 * 1024, int(requestedLength));
132 QByteArray buffer;
133 buffer.resize(maxBytes);
134 NSInteger submitted = 0;
135 while (submitted < requestedLength) {
136 qint64 len = device->read(buffer.data(), maxBytes);
137 if (len < 1)
138 break;
139
140 [loadingRequest.dataRequest respondWithData:[NSData dataWithBytes:buffer length:len]];
141 submitted += len;
142 }
143
144 // Finish loading even if not all bytes submitted.
145 [loadingRequest finishLoading];
146 }
147
148 return YES;
149}
150
151@end
152
153namespace {
154
155NSDictionary *av_audio_settings_for_format(const QAudioFormat &format)
156{
157 float sampleRate = format.sampleRate();
158 int nChannels = format.channelCount();
159 int sampleSize = format.bytesPerSample() * 8;
160 BOOL isFloat = format.sampleFormat() == QAudioFormat::Float;
161
162 NSDictionary *audioSettings = [NSDictionary dictionaryWithObjectsAndKeys:
163 [NSNumber numberWithInt:kAudioFormatLinearPCM], AVFormatIDKey,
164 [NSNumber numberWithFloat:sampleRate], AVSampleRateKey,
165 [NSNumber numberWithInt:nChannels], AVNumberOfChannelsKey,
166 [NSNumber numberWithInt:sampleSize], AVLinearPCMBitDepthKey,
167 [NSNumber numberWithBool:isFloat], AVLinearPCMIsFloatKey,
168 [NSNumber numberWithBool:NO], AVLinearPCMIsNonInterleaved,
169 [NSNumber numberWithBool:NO], AVLinearPCMIsBigEndianKey,
170 nil];
171
172 return audioSettings;
173}
174
175QAudioFormat qt_format_for_audio_track(AVAssetTrack *track)
176{
177 CMFormatDescriptionRef desc = (__bridge CMFormatDescriptionRef)track.formatDescriptions[0];
178 const AudioStreamBasicDescription* const asbd =
179 CMAudioFormatDescriptionGetStreamBasicDescription(desc);
180 return QCoreAudioUtils::toPreferredQAudioFormat(*asbd);
181}
182
183} // namespace
184
185struct AVFAudioDecoder::DecodingContext
186{
189
191 {
192 if (m_reader) {
193 [m_reader cancelReading];
194 [m_reader release];
195 }
196
197 [m_readerOutput release];
198 }
199};
200
201AVFAudioDecoder::AVFAudioDecoder(QAudioDecoder *parent)
202 : QPlatformAudioDecoder(parent)
203{
204 m_readingQueue = dispatch_queue_create("reader_queue", DISPATCH_QUEUE_SERIAL);
205 m_decodingQueue = dispatch_queue_create("decoder_queue", DISPATCH_QUEUE_SERIAL);
206
207 m_readerDelegate = [[AVFResourceReaderDelegate alloc] initWithDecoder:this];
208}
209
210AVFAudioDecoder::~AVFAudioDecoder()
211{
212 stop();
213
214 [m_readerDelegate release];
215 [m_asset release];
216
217 dispatch_release(m_readingQueue);
218 dispatch_release(m_decodingQueue);
219}
220
221QUrl AVFAudioDecoder::source() const
222{
223 return m_source;
224}
225
226void AVFAudioDecoder::setSource(const QUrl &fileName)
227{
228 if (!m_device && m_source == fileName)
229 return;
230
231 stop();
232 m_device = nullptr;
233 [m_asset release];
234 m_asset = nil;
235
236 m_source = fileName;
237
238 if (!m_source.isEmpty()) {
239 NSURL *nsURL = m_source.toNSURL();
240 m_asset = [[AVURLAsset alloc] initWithURL:nsURL options:nil];
241 }
242
243 sourceChanged();
244}
245
246QIODevice *AVFAudioDecoder::sourceDevice() const
247{
248 return m_device;
249}
250
251void AVFAudioDecoder::setSourceDevice(QIODevice *device)
252{
253 if (m_device == device && m_source.isEmpty())
254 return;
255
256 stop();
257 m_source.clear();
258 [m_asset release];
259 m_asset = nil;
260
261 m_device = device;
262
263 if (m_device) {
264 const QString ext = QMimeDatabase().mimeTypeForData(m_device).preferredSuffix();
265 const QString url = u"iodevice:///iodevice."_s + ext;
266 NSString *urlString = url.toNSString();
267 NSURL *nsURL = [NSURL URLWithString:urlString];
268
269 m_asset = [[AVURLAsset alloc] initWithURL:nsURL options:nil];
270
271 // use decoding queue instead of reading queue in order to fix random stucks.
272 // Anyway, decoding queue is empty in the moment.
273 [m_asset.resourceLoader setDelegate:m_readerDelegate queue:m_decodingQueue];
274 }
275
276 sourceChanged();
277}
278
279void AVFAudioDecoder::start()
280{
281 if (m_decodingContext) {
282 qCDebug(qLcAVFAudioDecoder()) << "AVFAudioDecoder has been already started";
283 return;
284 }
285
286 positionChanged(-1);
287
288 if (m_device && (!m_device->isOpen() || !m_device->isReadable())) {
289 processInvalidMedia(QAudioDecoder::ResourceError, tr("Unable to read from specified device"));
290 return;
291 }
292
293 m_decodingContext = std::make_shared<DecodingContext>();
294 std::weak_ptr<DecodingContext> weakContext(m_decodingContext);
295
296 auto handleLoadingResult = [=, this]() {
297 NSError *error = nil;
298 AVKeyValueStatus status = [m_asset statusOfValueForKey:@"tracks" error:&error];
299
300 if (status == AVKeyValueStatusFailed) {
301 if (error.domain == NSURLErrorDomain)
302 processInvalidMedia(QAudioDecoder::ResourceError,
303 QString::fromNSString(error.localizedDescription));
304 else
305 processInvalidMedia(QAudioDecoder::FormatError,
306 tr("Could not load media source's tracks"));
307 } else if (status != AVKeyValueStatusLoaded) {
308 qWarning() << "Unexpected AVKeyValueStatus:" << status;
309 stop();
310 }
311 else {
312 initAssetReader();
313 }
314 };
315
316 [m_asset loadValuesAsynchronouslyForKeys:@[ @"tracks" ]
317 completionHandler:[=, this]() {
318 invokeWithDecodingContext(weakContext, handleLoadingResult);
319 }];
320}
321
322void AVFAudioDecoder::decBuffersCounter(uint val)
323{
324 if (val) {
325 QMutexLocker locker(&m_buffersCounterMutex);
326 m_buffersCounter -= val;
327 }
328
329 Q_ASSERT(m_buffersCounter >= 0);
330
331 m_buffersCounterCondition.wakeAll();
332}
333
334void AVFAudioDecoder::stop()
335{
336 qCDebug(qLcAVFAudioDecoder()) << "stop decoding";
337
338 m_decodingContext.reset();
339 decBuffersCounter(m_cachedBuffers.size());
340 m_cachedBuffers.clear();
341
342 bufferAvailableChanged(false);
343 positionChanged(-1);
344 durationChanged(-1);
345
346 onFinished();
347}
348
349QAudioFormat AVFAudioDecoder::audioFormat() const
350{
351 return m_format;
352}
353
354void AVFAudioDecoder::setAudioFormat(const QAudioFormat &format)
355{
356 if (m_format != format) {
357 m_format = format;
358 formatChanged(m_format);
359 }
360}
361
362QAudioBuffer AVFAudioDecoder::read()
363{
364 if (m_cachedBuffers.empty())
365 return QAudioBuffer();
366
367 Q_ASSERT(m_cachedBuffers.size() > 0);
368 QAudioBuffer buffer = m_cachedBuffers.dequeue();
369 decBuffersCounter(1);
370
371 positionChanged(buffer.startTime() / 1000);
372 bufferAvailableChanged(!m_cachedBuffers.empty());
373 return buffer;
374}
375
376void AVFAudioDecoder::processInvalidMedia(QAudioDecoder::Error errorCode,
377 const QString &errorString)
378{
379 qCDebug(qLcAVFAudioDecoder()) << "Invalid media. Error code:" << errorCode
380 << "Description:" << errorString;
381
382 Q_ASSERT(QThread::currentThread() == thread());
383
384 error(int(errorCode), errorString);
385
386 // TODO: may be check if decodingCondext was changed by
387 // user's action (restart) from the emitted error.
388 // We should handle it somehow (don't run stop, print warning or etc...)
389
390 stop();
391}
392
393void AVFAudioDecoder::onFinished()
394{
395 m_decodingContext.reset();
396
397 if (isDecoding())
398 finished();
399}
400
401void AVFAudioDecoder::initAssetReaderImpl(AVAssetTrack *track, NSError *error)
402{
403 Q_ASSERT(track != nullptr);
404
405 if (error) {
406 processInvalidMedia(QAudioDecoder::ResourceError, QString::fromNSString(error.localizedDescription));
407 return;
408 }
409
410 QAudioFormat format = m_format.isValid() ? m_format : qt_format_for_audio_track(track);
411 if (!format.isValid()) {
412 processInvalidMedia(QAudioDecoder::FormatError, tr("Unsupported source format"));
413 return;
414 }
415
416 durationChanged(CMTimeGetSeconds(track.timeRange.duration) * 1000);
417
418 NSDictionary *audioSettings = av_audio_settings_for_format(format);
419
420 AVAssetReaderTrackOutput *readerOutput =
421 [[AVAssetReaderTrackOutput alloc] initWithTrack:track outputSettings:audioSettings];
422 AVAssetReader *reader = [[AVAssetReader alloc] initWithAsset:m_asset error:&error];
423 auto cleanup = qScopeGuard([&] {
424 [readerOutput release];
425 [reader release];
426 });
427
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 cleanup.dismiss();
441
442 m_decodingContext->m_reader = reader;
443 m_decodingContext->m_readerOutput = readerOutput;
444
446}
447
448void AVFAudioDecoder::initAssetReader()
449{
450 qCDebug(qLcAVFAudioDecoder()) << "Init asset reader";
451
452 Q_ASSERT(m_asset);
453 Q_ASSERT(QThread::currentThread() == thread());
454
455#if defined(Q_OS_VISIONOS)
456 [m_asset loadTracksWithMediaType:AVMediaTypeAudio completionHandler:[=](NSArray<AVAssetTrack *> *tracks, NSError *error) {
457 if (tracks && tracks.count > 0) {
458 if (AVAssetTrack *track = [tracks objectAtIndex:0])
459 QMetaObject::invokeMethod(this, &AVFAudioDecoder::initAssetReaderImpl, Qt::QueuedConnection, track, error);
460 }
461 }];
462#else
463 NSArray<AVAssetTrack *> *tracks = [m_asset tracksWithMediaType:AVMediaTypeAudio];
464 if (tracks && tracks.count > 0) {
465 if (AVAssetTrack *track = [tracks objectAtIndex:0])
466 initAssetReaderImpl(track, nullptr /*error*/);
467 }
468#endif
469
470}
471
472void AVFAudioDecoder::startReading(QAudioFormat format)
473{
474 Q_ASSERT(m_decodingContext);
475 Q_ASSERT(m_decodingContext->m_reader);
476 Q_ASSERT(QThread::currentThread() == thread());
477
478 // Prepares the receiver for obtaining sample buffers from the asset.
479 if (![m_decodingContext->m_reader startReading]) {
480 processInvalidMedia(QAudioDecoder::ResourceError, tr("Could not start reading"));
481 return;
482 }
483
484 setIsDecoding(true);
485
486 std::weak_ptr<DecodingContext> weakContext = m_decodingContext;
487
488 // Since copyNextSampleBuffer is synchronous, submit it to an async dispatch queue
489 // to run in a separate thread. Call the handleNextSampleBuffer "callback" on another
490 // thread when new audio sample is read.
491 auto copyNextSampleBuffer = [=, this]() {
492 auto decodingContext = weakContext.lock();
493 if (!decodingContext)
494 return false;
495
496 QCFType<CMSampleBufferRef> sampleBuffer{
497 [decodingContext->m_readerOutput copyNextSampleBuffer],
498 };
499 if (!sampleBuffer)
500 return false;
501
502 dispatch_async(m_decodingQueue, [=, this]() {
503 if (!weakContext.expired() && CMSampleBufferDataIsReady(sampleBuffer)) {
504 auto audioBuffer = handleNextSampleBuffer(format, sampleBuffer);
505
506 if (audioBuffer.isValid())
507 invokeWithDecodingContext(weakContext, [=, this]() {
508 handleNewAudioBuffer(audioBuffer);
509 });
510 }
511 });
512
513 return true;
514 };
515
516 dispatch_async(m_readingQueue, [=, this]() {
517 qCDebug(qLcAVFAudioDecoder()) << "start reading thread";
518
519 do {
520 // Note, waiting here doesn't ensure strong contol of the counter.
521 // However, it doesn't affect the logic: the reading flow works fine
522 // even if the counter is time-to-time more than max value
523 waitUntilBuffersCounterLessMax();
524 } while (copyNextSampleBuffer());
525
526 // TODO: check m_reader.status == AVAssetReaderStatusFailed
527 invokeWithDecodingContext(weakContext, [this]() { onFinished(); });
528 });
529}
530
531void AVFAudioDecoder::waitUntilBuffersCounterLessMax()
532{
533 if (m_buffersCounter >= MAX_BUFFERS_IN_QUEUE) {
534 // the check avoids extra mutex lock.
535
536 QMutexLocker locker(&m_buffersCounterMutex);
537
538 while (m_buffersCounter >= MAX_BUFFERS_IN_QUEUE)
539 m_buffersCounterCondition.wait(&m_buffersCounterMutex);
540 }
541}
542
543void AVFAudioDecoder::handleNewAudioBuffer(QAudioBuffer buffer)
544{
545 m_cachedBuffers.enqueue(std::move(buffer));
546 ++m_buffersCounter;
547
548 Q_ASSERT(m_cachedBuffers.size() == m_buffersCounter);
549
550 bufferAvailableChanged(true);
551 bufferReady();
552}
553
554/*
555 * The method calls the passed functor in the thread of AVFAudioDecoder and guarantees that
556 * the passed decoding context is not expired. In other words, it helps avoiding all callbacks
557 * after stopping of the decoder.
558 */
559template<typename F>
560void AVFAudioDecoder::invokeWithDecodingContext(std::weak_ptr<DecodingContext> weakContext, F &&f)
561{
562 if (!weakContext.expired())
563 QMetaObject::invokeMethod(
564 this, [this, f = std::forward<F>(f), weakContext = std::move(weakContext)]() {
565 // strong check: compare with actual decoding context.
566 // Otherwise, the context can be temporary locked by one of dispatch queues.
567 if (auto context = weakContext.lock(); context && context == m_decodingContext)
568 f();
569 });
570}
571
572#include "moc_avfaudiodecoder_p.cpp"
static constexpr int MAX_BUFFERS_IN_QUEUE
static QAudioBuffer handleNextSampleBuffer(QAudioFormat qtFormat, CMSampleBufferRef sampleBuffer)
AVAssetReaderTrackOutput * m_readerOutput
QImage::Format format
Definition qimage_p.h:52