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