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