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
avfmediaassetwriter.mm
Go to the documentation of this file.
1// Copyright (C) 2016 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 <camera/avfcamerarenderer_p.h>
7#include <camera/avfcameraservice_p.h>
8#include <camera/avfcamerasession_p.h>
9#include <camera/avfmediaencoder_p.h>
10#include <common/avfmetadata_p.h>
11#include <qdarwinformatsinfo_p.h>
12
13#include <QtMultimedia/private/qavfcameradebug_p.h>
14#include <QtCore/qatomic.h>
15#include <QtCore/qmetaobject.h>
16#include <QtCore/private/qcore_mac_p.h>
17
18QT_USE_NAMESPACE
19
20namespace {
21
23{
24 if (!service || !service->session())
25 return false;
26
27 AVFCameraSession *session = service->session();
28 if (!session->captureSession())
29 return false;
30
31 if (!session->videoInput() && !session->audioInput())
32 return false;
33
34 return true;
35}
36
44
45using AVFAtomicInt64 = QAtomicInteger<qint64>;
46
47} // unnamed namespace
48
49@interface QT_MANGLE_NAMESPACE(AVFMediaAssetWriter) (PrivateAPI)
50- (bool)addWriterInputs;
51- (void)setQueues;
52- (void)updateDuration:(CMTime)newTimeStamp;
53- (QCFType<CMSampleBufferRef>)adjustTime:(const QCFType<CMSampleBufferRef> &)sample
54 by:(CMTime)offset;
55@end
56
57@implementation QT_MANGLE_NAMESPACE(AVFMediaAssetWriter)
58{
59@private
60 AVFCameraService *m_service;
61
62 AVFScopedPointer<AVAssetWriterInput> m_cameraWriterInput;
63 AVFScopedPointer<AVAssetWriterInput> m_audioWriterInput;
64
65 // Pending audio buffer waiting for format stabilization:
66 QCFType<CMSampleBufferRef> m_pendingAudioBuffer;
67 bool m_audioFormatStabilized;
68
69 // Queue to write sample buffers:
70 AVFScopedPointer<dispatch_queue_t> m_writerQueue;
71 // High priority serial queue for video output:
72 AVFScopedPointer<dispatch_queue_t> m_videoQueue;
73 // Serial queue for audio output:
74 AVFScopedPointer<dispatch_queue_t> m_audioQueue;
75
76 AVFScopedPointer<AVAssetWriter> m_assetWriter;
77
78 AVFMediaEncoder *m_delegate;
79
80 bool m_setStartTime;
81
82 QAtomicInt m_state;
83
84 bool m_writeFirstAudioBuffer;
85
86 CMTime m_startTime;
87 CMTime m_lastTimeStamp;
88 CMTime m_lastVideoTimestamp;
89 CMTime m_lastAudioTimestamp;
90 CMTime m_timeOffset;
91 bool m_adjustTime;
92
93 NSDictionary *m_audioSettings;
94 NSDictionary *m_videoSettings;
95
96 AVFAtomicInt64 m_durationInMs;
97}
98
99- (id)initWithDelegate:(AVFMediaEncoder *)delegate
100{
101 Q_ASSERT(delegate);
102
103 if (self = [super init]) {
104 m_delegate = delegate;
105 m_setStartTime = true;
106 m_state.storeRelaxed(WriterStateIdle);
107 }
108
109 return self;
110}
111
112- (bool)setupWithFileURL:(NSURL *)fileURL
113 cameraService:(AVFCameraService *)service
114 audioSettings:(NSDictionary *)audioSettings
115 videoSettings:(NSDictionary *)videoSettings
116 fileFormat:(QMediaFormat::FileFormat)fileFormat
117 transform:(CGAffineTransform)transform
118{
119 Q_ASSERT(fileURL);
120
121 if (!qt_capture_session_isValid(service)) {
122 qCDebug(qLcCamera) << Q_FUNC_INFO << "invalid capture session";
123 return false;
124 }
125
126 m_service = service;
127 m_audioSettings = audioSettings;
128 m_videoSettings = videoSettings;
129
130 AVFCameraSession *session = m_service->session();
131
132 m_writerQueue.reset(dispatch_queue_create("asset-writer-queue", DISPATCH_QUEUE_SERIAL));
133 if (!m_writerQueue) {
134 qCDebug(qLcCamera) << Q_FUNC_INFO << "failed to create an asset writer's queue";
135 return false;
136 }
137
138 m_videoQueue.reset();
139 if (session->videoInput() && session->videoOutput() && session->videoOutput()->videoDataOutput()) {
140 m_videoQueue.reset(dispatch_queue_create("video-output-queue", DISPATCH_QUEUE_SERIAL));
141 if (!m_videoQueue) {
142 qCDebug(qLcCamera) << Q_FUNC_INFO << "failed to create video queue";
143 return false;
144 }
145 dispatch_set_target_queue(m_videoQueue, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0));
146 }
147
148 m_audioQueue.reset();
149 if (session->audioInput() && session->audioOutput()) {
150 m_audioQueue.reset(dispatch_queue_create("audio-output-queue", DISPATCH_QUEUE_SERIAL));
151 if (!m_audioQueue) {
152 qCDebug(qLcCamera) << Q_FUNC_INFO << "failed to create audio queue";
153 if (!m_videoQueue)
154 return false;
155 // But we still can write video!
156 }
157 }
158
159 auto fileType = QDarwinFormatInfo::avFileTypeForContainerFormat(fileFormat);
160 m_assetWriter.reset([[AVAssetWriter alloc] initWithURL:fileURL
161 fileType:fileType
162 error:nil]);
163 if (!m_assetWriter) {
164 qCDebug(qLcCamera) << Q_FUNC_INFO << "failed to create asset writer";
165 return false;
166 }
167
168 if (!m_videoQueue)
169 m_writeFirstAudioBuffer = true;
170
171 if (![self addWriterInputs]) {
172 m_assetWriter.reset();
173 return false;
174 }
175
176 if (m_cameraWriterInput)
177 m_cameraWriterInput.data().transform = transform;
178
179 [self setMetaData:fileType];
180
181 // Ready to start ...
182 return true;
183}
184
185- (void)setMetaData:(AVFileType)fileType
186{
187 m_assetWriter.data().metadata = AVFMetaData::toAVMetadataForFormat(m_delegate->metaData(), fileType);
188}
189
190- (void)start
191{
192 [self setQueues];
193
194 m_setStartTime = true;
195 m_audioFormatStabilized = false;
196 m_pendingAudioBuffer = nullptr;
197
198 m_state.storeRelease(WriterStateActive);
199
200 [m_assetWriter startWriting];
201 AVCaptureSession *session = m_service->session()->captureSession();
202 if (!session.running)
203 [session startRunning];
204}
205
206- (void)stop
207{
208 if (m_state.loadAcquire() != WriterStateActive && m_state.loadAcquire() != WriterStatePaused)
209 return;
210
211 if ([m_assetWriter status] != AVAssetWriterStatusWriting
212 && [m_assetWriter status] != AVAssetWriterStatusFailed)
213 return;
214
215 // Do this here so that -
216 // 1. '-abort' should not try calling finishWriting again and
217 // 2. async block (see below) will know if recorder control was deleted
218 // before the block's execution:
219 m_state.storeRelease(WriterStateIdle);
220 // Now, since we have to ensure no sample buffers are
221 // appended after a call to finishWriting, we must
222 // ensure writer's queue sees this change in m_state
223 // _before_ we call finishWriting:
224 dispatch_sync(m_writerQueue, ^{});
225 // Done, but now we also want to prevent video queue
226 // from updating our viewfinder:
227 if (m_videoQueue)
228 dispatch_sync(m_videoQueue, ^{});
229
230 // Now we're safe to stop:
231 [m_assetWriter finishWritingWithCompletionHandler:^{
232 // This block is async, so by the time it's executed,
233 // it's possible that render control was deleted already ...
234 if (m_state.loadAcquire() == WriterStateAborted)
235 return;
236
237 AVCaptureSession *session = m_service->session()->captureSession();
238 if (session.running)
239 [session stopRunning];
240 QMetaObject::invokeMethod(m_delegate, "assetWriterFinished", Qt::QueuedConnection);
241 }];
242}
243
244- (void)abort
245{
246 // -abort is to be called from recorder control's dtor.
247
248 if (m_state.fetchAndStoreRelease(WriterStateAborted) != WriterStateActive) {
249 // Not recording, nothing to stop.
250 return;
251 }
252
253 // From Apple's docs:
254 // "To guarantee that all sample buffers are successfully written,
255 // you must ensure that all calls to appendSampleBuffer: and
256 // appendPixelBuffer:withPresentationTime: have returned before
257 // invoking this method."
258 //
259 // The only way we can ensure this is:
260 dispatch_sync(m_writerQueue, ^{});
261 // At this point next block (if any) on the writer's queue
262 // will see m_state preventing it from any further processing.
263 if (m_videoQueue)
264 dispatch_sync(m_videoQueue, ^{});
265 // After this point video queue will not try to modify our
266 // viewfider, so we're safe to delete now.
267
268 [m_assetWriter finishWritingWithCompletionHandler:^{
269 }];
270}
271
272- (void)pause
273{
274 if (m_state.loadAcquire() != WriterStateActive)
275 return;
276 if ([m_assetWriter status] != AVAssetWriterStatusWriting)
277 return;
278
279 m_state.storeRelease(WriterStatePaused);
280 m_adjustTime = true;
281}
282
283- (void)resume
284{
285 if (m_state.loadAcquire() != WriterStatePaused)
286 return;
287 if ([m_assetWriter status] != AVAssetWriterStatusWriting)
288 return;
289
290 m_state.storeRelease(WriterStateActive);
291}
292
293- (void)setStartTimeFrom:(CMSampleBufferRef)sampleBuffer
294{
295 // Writer's queue only.
296 Q_ASSERT(m_setStartTime);
297 Q_ASSERT(sampleBuffer);
298
299 if (m_state.loadAcquire() != WriterStateActive)
300 return;
301
302 QMetaObject::invokeMethod(m_delegate, "assetWriterStarted", Qt::QueuedConnection);
303
304 m_durationInMs.storeRelease(0);
305 m_startTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
306 m_lastTimeStamp = m_startTime;
307 [m_assetWriter startSessionAtSourceTime:m_startTime];
308 m_setStartTime = false;
309}
310
311- (QCFType<CMSampleBufferRef>)adjustTime:(const QCFType<CMSampleBufferRef> &)sample
312 by:(CMTime)offset
313{
314 CMItemCount count;
315 CMSampleBufferGetSampleTimingInfoArray(sample, 0, nil, &count);
316 CMSampleTimingInfo* timingInfo = (CMSampleTimingInfo*) malloc(sizeof(CMSampleTimingInfo) * count);
317 CMSampleBufferGetSampleTimingInfoArray(sample, count, timingInfo, &count);
318 for (CMItemCount i = 0; i < count; i++)
319 {
320 timingInfo[i].decodeTimeStamp = CMTimeSubtract(timingInfo[i].decodeTimeStamp, offset);
321 timingInfo[i].presentationTimeStamp = CMTimeSubtract(timingInfo[i].presentationTimeStamp, offset);
322 }
323 CMSampleBufferRef updatedBuffer;
324 CMSampleBufferCreateCopyWithNewTiming(kCFAllocatorDefault, sample, count, timingInfo, &updatedBuffer);
325 free(timingInfo);
326 return updatedBuffer;
327}
328
329- (void)writeVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer
330{
331 // This code is executed only on a writer's queue.
332 Q_ASSERT(sampleBuffer);
333
334 if (m_state.loadAcquire() == WriterStateActive) {
335 if (m_setStartTime)
336 [self setStartTimeFrom:sampleBuffer];
337
338 if (m_cameraWriterInput.data().readyForMoreMediaData) {
339 [self updateDuration:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
340 [m_cameraWriterInput appendSampleBuffer:sampleBuffer];
341 }
342 }
343}
344
345- (void)writeAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer
346{
347 Q_ASSERT(sampleBuffer);
348
349 // This code is executed only on a writer's queue.
350 if (m_state.loadAcquire() == WriterStateActive) {
351 if (m_setStartTime)
352 [self setStartTimeFrom:sampleBuffer];
353
354 // On macOS, AVCaptureSession may deliver the first audio buffer(s) in a
355 // transient format that differs from the stable format the device settles
356 // on shortly after. Appending a transient-format buffer configures
357 // AVAssetWriterInput's internal AudioConverter for the wrong format,
358 // causing error -12737 or audible noise once subsequent buffers arrive in
359 // the real (stable) format. This has been observed with built-in, USB,
360 // and Bluetooth microphones.
361 // To avoid this, we wait for format stabilization: the first buffer whose
362 // CMFormatDescription matches the previous one is considered stable.
363 // At that point we append both the held buffer and the current one.
364 // See: QTBUG-127444, FB16500782.
365 if (!m_audioFormatStabilized) {
366 if (!m_pendingAudioBuffer) {
367 m_pendingAudioBuffer = QCFType<CMSampleBufferRef>::constructFromGet(sampleBuffer);
368 return;
369 }
370
371 CMFormatDescriptionRef pendingFormat =
372 CMSampleBufferGetFormatDescription(m_pendingAudioBuffer);
373 CMFormatDescriptionRef currentFormat = CMSampleBufferGetFormatDescription(sampleBuffer);
374
375 if (pendingFormat && currentFormat
376 && CMFormatDescriptionEqual(pendingFormat, currentFormat)) {
377 m_audioFormatStabilized = true;
378 // Append the held buffer first, then fall through to append
379 // the current one.
380 if (m_audioWriterInput.data().readyForMoreMediaData) {
381 [self updateDuration:CMSampleBufferGetPresentationTimeStamp(
382 m_pendingAudioBuffer)];
383 [m_audioWriterInput appendSampleBuffer:m_pendingAudioBuffer];
384 }
385 m_pendingAudioBuffer = nullptr;
386 } else {
387 qCDebug(qLcCamera) << "Audio format changed, discarding pending buffer";
388 m_pendingAudioBuffer = QCFType<CMSampleBufferRef>::constructFromGet(sampleBuffer);
389 return;
390 }
391 }
392
393 if (m_audioWriterInput.data().readyForMoreMediaData) {
394 [self updateDuration:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
395 [m_audioWriterInput appendSampleBuffer:sampleBuffer];
396 }
397 }
398}
399
400- (void)captureOutput:(AVCaptureOutput *)captureOutput
401 didOutputSampleBuffer:(CMSampleBufferRef)buffer
402 fromConnection:(AVCaptureConnection *)connection
403{
404 Q_UNUSED(connection);
405 Q_ASSERT(m_service && m_service->session());
406
407 if (m_state.loadAcquire() != WriterStateActive && m_state.loadAcquire() != WriterStatePaused)
408 return;
409
410 if ([m_assetWriter status] != AVAssetWriterStatusWriting) {
411 if ([m_assetWriter status] == AVAssetWriterStatusFailed) {
412 NSError *error = [m_assetWriter error];
413 NSString *failureReason = error.localizedFailureReason;
414 NSString *suggestion = error.localizedRecoverySuggestion;
415 NSString *errorString = suggestion ? [failureReason stringByAppendingString:suggestion] : failureReason;
416 QMetaObject::invokeMethod(m_delegate, "assetWriterError",
417 Qt::QueuedConnection,
418 Q_ARG(QString, QString::fromNSString(errorString)));
419 }
420 return;
421 }
422
423 if (!CMSampleBufferDataIsReady(buffer)) {
424 qWarning() << Q_FUNC_INFO << "sample buffer is not ready, skipping.";
425 return;
426 }
427
428 // take ownership
429 auto sampleBuffer = QCFType<CMSampleBufferRef>::constructFromGet(buffer);
430
431 bool isVideoBuffer = true;
432 isVideoBuffer = (captureOutput != m_service->session()->audioOutput());
433 if (isVideoBuffer) {
434 // Find renderercontrol's delegate and invoke its method to
435 // show updated viewfinder's frame.
436 if (m_service->session()->videoOutput()) {
437 NSObject<AVCaptureVideoDataOutputSampleBufferDelegate> *vfDelegate =
438 (NSObject<AVCaptureVideoDataOutputSampleBufferDelegate> *)m_service->session()->videoOutput()->captureDelegate();
439 if (vfDelegate) {
440 AVCaptureOutput *output = nil;
441 AVCaptureConnection *connection = nil;
442 [vfDelegate captureOutput:output didOutputSampleBuffer:sampleBuffer fromConnection:connection];
443 }
444 }
445 } else {
446 if (m_service->session()->audioOutput()) {
447 NSObject<AVCaptureAudioDataOutputSampleBufferDelegate> *audioPreviewDelegate =
448 (NSObject<AVCaptureAudioDataOutputSampleBufferDelegate> *)m_service->session()->audioPreviewDelegate();
449 if (audioPreviewDelegate) {
450 AVCaptureOutput *output = nil;
451 AVCaptureConnection *connection = nil;
452 [audioPreviewDelegate captureOutput:output didOutputSampleBuffer:sampleBuffer fromConnection:connection];
453 }
454 }
455 }
456
457 if (m_state.loadAcquire() != WriterStateActive)
458 return;
459
460 if (m_adjustTime) {
461 CMTime currentTimestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
462 CMTime lastTimestamp = isVideoBuffer ? m_lastVideoTimestamp : m_lastAudioTimestamp;
463
464 if (!CMTIME_IS_INVALID(lastTimestamp)) {
465 if (!CMTIME_IS_INVALID(m_timeOffset))
466 currentTimestamp = CMTimeSubtract(currentTimestamp, m_timeOffset);
467
468 CMTime pauseDuration = CMTimeSubtract(currentTimestamp, lastTimestamp);
469
470 if (m_timeOffset.value == 0)
471 m_timeOffset = pauseDuration;
472 else
473 m_timeOffset = CMTimeAdd(m_timeOffset, pauseDuration);
474 }
475 m_lastVideoTimestamp = kCMTimeInvalid;
476 m_adjustTime = false;
477 }
478
479 if (m_timeOffset.value > 0) {
480 sampleBuffer = [self adjustTime:sampleBuffer by:m_timeOffset];
481 }
482
483 CMTime currentTimestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
484 CMTime currentDuration = CMSampleBufferGetDuration(sampleBuffer);
485 if (currentDuration.value > 0)
486 currentTimestamp = CMTimeAdd(currentTimestamp, currentDuration);
487
488 if (isVideoBuffer)
489 {
490 m_lastVideoTimestamp = currentTimestamp;
491 dispatch_async(m_writerQueue, ^{
492 [self writeVideoSampleBuffer:sampleBuffer];
493 m_writeFirstAudioBuffer = true;
494 });
495 } else if (m_writeFirstAudioBuffer) {
496 m_lastAudioTimestamp = currentTimestamp;
497 dispatch_async(m_writerQueue, ^{
498 [self writeAudioSampleBuffer:sampleBuffer];
499 });
500 }
501}
502
503- (bool)addWriterInputs
504{
505 Q_ASSERT(m_service && m_service->session());
506 Q_ASSERT(m_assetWriter.data());
507
508 AVFCameraSession *session = m_service->session();
509
510 m_cameraWriterInput.reset();
511 if (m_videoQueue)
512 {
513 Q_ASSERT(session->videoCaptureDevice() && session->videoOutput() && session->videoOutput()->videoDataOutput());
514 @try {
515 m_cameraWriterInput.reset([[AVAssetWriterInput alloc]
516 initWithMediaType:AVMediaTypeVideo
517 outputSettings:m_videoSettings
518 sourceFormatHint:session->videoCaptureDevice()
519 .activeFormat.formatDescription]);
520 } @catch (NSException *exception) {
521 qCWarning(qLcCamera) << Q_FUNC_INFO << "Failed to create video writer input:"
522 << QString::fromNSString(exception.reason);
523 m_cameraWriterInput.reset();
524 return false;
525 }
526
527 @try {
528 if (m_cameraWriterInput && [m_assetWriter canAddInput:m_cameraWriterInput]) {
529 [m_assetWriter addInput:m_cameraWriterInput];
530 } else {
531 qCDebug(qLcCamera) << Q_FUNC_INFO << "failed to add camera writer input";
532 m_cameraWriterInput.reset();
533 return false;
534 }
535 } @catch (NSException *exception) {
536 qCWarning(qLcCamera) << Q_FUNC_INFO << "Failed to add video input:"
537 << QString::fromNSString(exception.reason);
538 m_cameraWriterInput.reset();
539 return false;
540 }
541
542 m_cameraWriterInput.data().expectsMediaDataInRealTime = YES;
543 }
544
545 m_audioWriterInput.reset();
546 if (m_audioQueue) {
547 @try {
548 m_audioWriterInput.reset([[AVAssetWriterInput alloc]
549 initWithMediaType:AVMediaTypeAudio
550 outputSettings:m_audioSettings]);
551 } @catch (NSException *exception) {
552 qCWarning(qLcCamera) << Q_FUNC_INFO << "Failed to create audio writer input:"
553 << QString::fromNSString(exception.reason);
554 m_audioWriterInput.reset();
555 // But we still can record video.
556 if (!m_cameraWriterInput)
557 return false;
558 }
559 if (!m_audioWriterInput) {
560 qWarning() << Q_FUNC_INFO << "failed to create audio writer input";
561 // But we still can record video.
562 if (!m_cameraWriterInput)
563 return false;
564 } else {
565 @try {
566 if ([m_assetWriter canAddInput:m_audioWriterInput]) {
567 [m_assetWriter addInput:m_audioWriterInput];
568 m_audioWriterInput.data().expectsMediaDataInRealTime = YES;
569 } else {
570 qWarning() << Q_FUNC_INFO << "failed to add audio writer input";
571 m_audioWriterInput.reset();
572 if (!m_cameraWriterInput)
573 return false;
574 // We can (still) write video though ...
575 }
576 } @catch (NSException *exception) {
577 qCWarning(qLcCamera)
578 << Q_FUNC_INFO
579 << "Failed to add audio input:" << QString::fromNSString(exception.reason);
580 m_audioWriterInput.reset();
581 if (!m_cameraWriterInput)
582 return false;
583 // We can (still) write video though ...
584 }
585 }
586 }
587
588 return true;
589}
590
591- (void)setQueues
592{
593 Q_ASSERT(m_service && m_service->session());
594 AVFCameraSession *session = m_service->session();
595
596 if (m_videoQueue) {
597 Q_ASSERT(session->videoOutput() && session->videoOutput()->videoDataOutput());
598 [session->videoOutput()->videoDataOutput() setSampleBufferDelegate:self queue:m_videoQueue];
599 }
600
601 if (m_audioQueue) {
602 Q_ASSERT(session->audioOutput());
603 [session->audioOutput() setSampleBufferDelegate:self queue:m_audioQueue];
604 }
605}
606
607- (void)updateDuration:(CMTime)newTimeStamp
608{
609 Q_ASSERT(CMTIME_IS_VALID(m_startTime));
610 Q_ASSERT(CMTIME_IS_VALID(m_lastTimeStamp));
611 if (CMTimeCompare(newTimeStamp, m_lastTimeStamp) > 0) {
612
613 const CMTime duration = CMTimeSubtract(newTimeStamp, m_startTime);
614 if (CMTIME_IS_INVALID(duration))
615 return;
616
617 m_durationInMs.storeRelease(CMTimeGetSeconds(duration) * 1000);
618 m_lastTimeStamp = newTimeStamp;
619
620 m_delegate->updateDuration([self durationInMs]);
621 }
622}
623
624- (qint64)durationInMs
625{
626 return m_durationInMs.loadAcquire();
627}
628
629@end
AVFCameraSession * session() const
bool qt_capture_session_isValid(AVFCameraService *service)