summaryrefslogtreecommitdiffstats
path: root/src/plugins/avfoundation/camera/avfmediaassetwriter.mm
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/avfoundation/camera/avfmediaassetwriter.mm')
-rw-r--r--src/plugins/avfoundation/camera/avfmediaassetwriter.mm474
1 files changed, 474 insertions, 0 deletions
diff --git a/src/plugins/avfoundation/camera/avfmediaassetwriter.mm b/src/plugins/avfoundation/camera/avfmediaassetwriter.mm
new file mode 100644
index 000000000..37004c1db
--- /dev/null
+++ b/src/plugins/avfoundation/camera/avfmediaassetwriter.mm
@@ -0,0 +1,474 @@
+/****************************************************************************
+**
+** Copyright (C) 2015 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL21$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 or version 3 as published by the Free
+** Software Foundation and appearing in the file LICENSE.LGPLv21 and
+** LICENSE.LGPLv3 included in the packaging of this file. Please review the
+** following information to ensure the GNU Lesser General Public License
+** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
+** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** As a special exception, The Qt Company gives you certain additional
+** rights. These rights are described in The Qt Company LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "avfaudioinputselectorcontrol.h"
+#include "avfcamerarenderercontrol.h"
+#include "avfmediaassetwriter.h"
+#include "avfcameraservice.h"
+#include "avfcamerasession.h"
+#include "avfcameradebug.h"
+
+//#include <QtCore/qmutexlocker.h>
+#include <QtCore/qsysinfo.h>
+
+QT_USE_NAMESPACE
+
+namespace {
+
+bool qt_camera_service_isValid(AVFCameraService *service)
+{
+ if (!service || !service->session())
+ return false;
+
+ AVFCameraSession *session = service->session();
+ if (!session->captureSession())
+ return false;
+
+ if (!session->videoInput())
+ return false;
+
+ if (!service->videoOutput()
+ || !service->videoOutput()->videoDataOutput()) {
+ return false;
+ }
+
+ return true;
+}
+
+}
+
+AVFMediaAssetWriterDelegate::~AVFMediaAssetWriterDelegate()
+{
+}
+
+@interface QT_MANGLE_NAMESPACE(AVFMediaAssetWriter) (PrivateAPI)
+- (bool)addAudioCapture;
+- (bool)addWriterInputs;
+- (void)setQueues;
+- (NSDictionary *)videoSettings;
+- (NSDictionary *)audioSettings;
+- (void)updateDuration:(CMTime)newTimeStamp;
+@end
+
+@implementation QT_MANGLE_NAMESPACE(AVFMediaAssetWriter)
+
+- (id)initWithQueue:(dispatch_queue_t)writerQueue
+ delegate:(AVFMediaAssetWriterDelegate *)delegate
+ delegateQueue:(dispatch_queue_t)delegateQueue
+{
+ Q_ASSERT(writerQueue);
+ Q_ASSERT(delegate);
+ Q_ASSERT(delegateQueue);
+
+ if (self = [super init]) {
+ m_writerQueue = writerQueue;
+ m_delegate = delegate;
+ m_delegateQueue = delegateQueue;
+ m_setStartTime = true;
+ m_stopped.store(true);
+ m_stoppedInternal = false;
+ m_aborted = false;
+ m_startTime = kCMTimeInvalid;
+ m_lastTimeStamp = kCMTimeInvalid;
+ m_durationInMs.store(0);
+ }
+
+ return self;
+}
+
+- (bool)setupWithFileURL:(NSURL *)fileURL
+ cameraService:(AVFCameraService *)service
+{
+ Q_ASSERT(fileURL);
+
+ if (!qt_camera_service_isValid(service)) {
+ qDebugCamera() << Q_FUNC_INFO << "invalid camera service";
+ return false;
+ }
+
+ m_service = service;
+
+ m_videoQueue.reset(dispatch_queue_create("video-output-queue", DISPATCH_QUEUE_SERIAL));
+ if (!m_videoQueue) {
+ qDebugCamera() << Q_FUNC_INFO << "failed to create video queue";
+ return false;
+ }
+ dispatch_set_target_queue(m_videoQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
+ m_audioQueue.reset(dispatch_queue_create("audio-output-queue", DISPATCH_QUEUE_SERIAL));
+ if (!m_audioQueue) {
+ qDebugCamera() << Q_FUNC_INFO << "failed to create audio queue";
+ // But we still can write video!
+ }
+
+ m_assetWriter.reset([[AVAssetWriter alloc] initWithURL:fileURL fileType:AVFileTypeQuickTimeMovie error:nil]);
+ if (!m_assetWriter) {
+ qDebugCamera() << Q_FUNC_INFO << "failed to create asset writer";
+ return false;
+ }
+
+ bool audioCaptureOn = false;
+
+ if (m_audioQueue)
+ audioCaptureOn = [self addAudioCapture];
+
+ if (![self addWriterInputs]) {
+ if (audioCaptureOn) {
+ AVCaptureSession *session = m_service->session()->captureSession();
+ [session removeOutput:m_audioOutput];
+ [session removeInput:m_audioInput];
+ m_audioOutput.reset();
+ m_audioInput.reset();
+ }
+ m_assetWriter.reset();
+ return false;
+ }
+ // Ready to start ...
+ return true;
+}
+
+- (void)start
+{
+ // To be executed on a writer's queue.
+ const QMutexLocker lock(&m_writerMutex);
+ if (m_aborted)
+ return;
+
+ [self setQueues];
+
+ m_setStartTime = true;
+ m_stopped.store(false);
+ m_stoppedInternal = false;
+ [m_assetWriter startWriting];
+ AVCaptureSession *session = m_service->session()->captureSession();
+ if (!session.running)
+ [session startRunning];
+}
+
+- (void)stop
+{
+ // To be executed on a writer's queue.
+ const QMutexLocker lock(&m_writerMutex);
+ if (m_aborted)
+ return;
+
+ if (m_stopped.load()) {
+ // Should never happen, but ...
+ // if something went wrong in a recorder control
+ // and we set state stopped without starting first ...
+ // m_stoppedIntenal will be false, but m_stopped - true.
+ return;
+ }
+
+ m_stopped.store(true);
+ m_stoppedInternal = true;
+ [m_assetWriter finishWritingWithCompletionHandler:^{
+ // TODO: make sure the session exist and we can call stop/remove on it.
+ AVCaptureSession *session = m_service->session()->captureSession();
+ [session stopRunning];
+ [session removeOutput:m_audioOutput];
+ [session removeInput:m_audioInput];
+ dispatch_async(m_delegateQueue, ^{
+ m_delegate->assetWriterFinished();
+ });
+ }];
+}
+
+- (void)abort
+{
+ // To be executed on any thread, prevents writer from
+ // accessing any external object (probably deleted by this time)
+ const QMutexLocker lock(&m_writerMutex);
+ m_aborted = true;
+ if (m_stopped.load())
+ return;
+ [m_assetWriter finishWritingWithCompletionHandler:^{
+ }];
+}
+
+- (void)setStartTimeFrom:(CMSampleBufferRef)sampleBuffer
+{
+ // Writer's queue only.
+ Q_ASSERT(m_setStartTime);
+ Q_ASSERT(sampleBuffer);
+
+ dispatch_async(m_delegateQueue, ^{
+ m_delegate->assetWriterStarted();
+ });
+
+ m_durationInMs.store(0);
+ m_startTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
+ m_lastTimeStamp = m_startTime;
+ [m_assetWriter startSessionAtSourceTime:m_startTime];
+ m_setStartTime = false;
+}
+
+- (void)writeVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer
+{
+ Q_ASSERT(sampleBuffer);
+
+ // This code is executed only on a writer's queue, but
+ // it can access potentially deleted objects, so we
+ // need a lock and m_aborted flag test.
+ {
+ const QMutexLocker lock(&m_writerMutex);
+ if (!m_aborted && !m_stoppedInternal) {
+ if (m_setStartTime)
+ [self setStartTimeFrom:sampleBuffer];
+
+ if (m_cameraWriterInput.data().readyForMoreMediaData) {
+ [self updateDuration:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
+ [m_cameraWriterInput appendSampleBuffer:sampleBuffer];
+ }
+ }
+ }
+
+ CFRelease(sampleBuffer);
+}
+
+- (void)writeAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer
+{
+ // This code is executed only on a writer's queue.
+ // it does not touch any shared/external data.
+ Q_ASSERT(sampleBuffer);
+
+ {
+ const QMutexLocker lock(&m_writerMutex);
+ if (!m_aborted && !m_stoppedInternal) {
+ if (m_setStartTime)
+ [self setStartTimeFrom:sampleBuffer];
+
+ if (m_audioWriterInput.data().readyForMoreMediaData) {
+ [self updateDuration:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
+ [m_audioWriterInput appendSampleBuffer:sampleBuffer];
+ }
+ }
+ }
+
+ CFRelease(sampleBuffer);
+}
+
+- (void)captureOutput:(AVCaptureOutput *)captureOutput
+ didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
+ fromConnection:(AVCaptureConnection *)connection
+{
+ Q_UNUSED(connection)
+
+ // This method can be called on either video or audio queue, never on a writer's
+ // queue - it does not access any shared data except this atomic flag below.
+ if (m_stopped.load())
+ return;
+
+ // Even if we are stopped now, we still do not access any data.
+
+ if (!CMSampleBufferDataIsReady(sampleBuffer)) {
+ qDebugCamera() << Q_FUNC_INFO << "sample buffer is not ready, skipping.";
+ return;
+ }
+
+ CFRetain(sampleBuffer);
+
+ if (captureOutput != m_audioOutput.data()) {
+ {
+ const QMutexLocker lock(&m_writerMutex);
+ if (m_aborted || m_stoppedInternal) {
+ CFRelease(sampleBuffer);
+ return;
+ }
+
+ // Find renderercontrol's delegate and invoke its method to
+ // show updated viewfinder's frame.
+ if (m_service && m_service->videoOutput()) {
+ NSObject<AVCaptureVideoDataOutputSampleBufferDelegate> *vfDelegate =
+ (NSObject<AVCaptureVideoDataOutputSampleBufferDelegate> *)m_service->videoOutput()->captureDelegate();
+ if (vfDelegate)
+ [vfDelegate captureOutput:nil didOutputSampleBuffer:sampleBuffer fromConnection:nil];
+ }
+ }
+
+ dispatch_async(m_writerQueue, ^{
+ [self writeVideoSampleBuffer:sampleBuffer];
+ });
+ } else {
+ dispatch_async(m_writerQueue, ^{
+ [self writeAudioSampleBuffer:sampleBuffer];
+ });
+ }
+}
+
+- (bool)addAudioCapture
+{
+ Q_ASSERT(m_service && m_service->session() && m_service->session()->captureSession());
+
+ if (!m_service->audioInputSelectorControl())
+ return false;
+
+ AVCaptureSession *captureSession = m_service->session()->captureSession();
+
+ AVCaptureDevice *audioDevice = m_service->audioInputSelectorControl()->createCaptureDevice();
+ if (!audioDevice) {
+ qWarning() << Q_FUNC_INFO << "no audio input device available";
+ return false;
+ } else {
+ NSError *error = nil;
+ m_audioInput.reset([[AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:&error] retain]);
+
+ if (!m_audioInput || error) {
+ qWarning() << Q_FUNC_INFO << "failed to create audio device input";
+ m_audioInput.reset();
+ return false;
+ } else if (![captureSession canAddInput:m_audioInput]) {
+ qWarning() << Q_FUNC_INFO << "could not connect the audio input";
+ m_audioInput.reset();
+ return false;
+ } else {
+ [captureSession addInput:m_audioInput];
+ }
+ }
+
+
+ m_audioOutput.reset([[AVCaptureAudioDataOutput alloc] init]);
+ if (m_audioOutput && [captureSession canAddOutput:m_audioOutput]) {
+ [captureSession addOutput:m_audioOutput];
+ } else {
+ qDebugCamera() << Q_FUNC_INFO << "failed to add audio output";
+ [captureSession removeInput:m_audioInput];
+ m_audioInput.reset();
+ m_audioOutput.reset();
+ return false;
+ }
+
+ return true;
+}
+
+- (bool)addWriterInputs
+{
+ Q_ASSERT(m_service && m_service->videoOutput()
+ && m_service->videoOutput()->videoDataOutput());
+ Q_ASSERT(m_assetWriter);
+
+ m_cameraWriterInput.reset([[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:[self videoSettings]]);
+ if (!m_cameraWriterInput) {
+ qDebugCamera() << Q_FUNC_INFO << "failed to create camera writer input";
+ return false;
+ }
+
+ if ([m_assetWriter canAddInput:m_cameraWriterInput]) {
+ [m_assetWriter addInput:m_cameraWriterInput];
+ } else {
+ qDebugCamera() << Q_FUNC_INFO << "failed to add camera writer input";
+ m_cameraWriterInput.reset();
+ return false;
+ }
+
+ m_cameraWriterInput.data().expectsMediaDataInRealTime = YES;
+
+ if (m_audioOutput) {
+ m_audioWriterInput.reset([[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:[self audioSettings]]);
+ if (!m_audioWriterInput) {
+ qDebugCamera() << Q_FUNC_INFO << "failed to create audio writer input";
+ // But we still can record video.
+ } else if ([m_assetWriter canAddInput:m_audioWriterInput]) {
+ [m_assetWriter addInput:m_audioWriterInput];
+ m_audioWriterInput.data().expectsMediaDataInRealTime = YES;
+ } else {
+ qDebugCamera() << Q_FUNC_INFO << "failed to add audio writer input";
+ m_audioWriterInput.reset();
+ // We can (still) write video though ...
+ }
+ }
+
+ return true;
+}
+
+- (void)setQueues
+{
+ Q_ASSERT(m_service && m_service->videoOutput() && m_service->videoOutput()->videoDataOutput());
+ Q_ASSERT(m_videoQueue);
+
+ [m_service->videoOutput()->videoDataOutput() setSampleBufferDelegate:self queue:m_videoQueue];
+
+ if (m_audioOutput) {
+ Q_ASSERT(m_audioQueue);
+ [m_audioOutput setSampleBufferDelegate:self queue:m_audioQueue];
+ }
+}
+
+
+- (NSDictionary *)videoSettings
+{
+ // TODO: these settings should be taken from
+ // the video encoding settings control.
+ // For now we either take recommended (iOS >= 7.0)
+ // or some hardcoded values - they are still better than nothing (nil).
+#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_7_0)
+ AVCaptureVideoDataOutput *videoOutput = m_service->videoOutput()->videoDataOutput();
+ if (QSysInfo::MacintoshVersion >= QSysInfo::MV_IOS_7_0 && videoOutput)
+ return [videoOutput recommendedVideoSettingsForAssetWriterWithOutputFileType:AVFileTypeQuickTimeMovie];
+#endif
+ NSDictionary *videoSettings = [NSDictionary dictionaryWithObjectsAndKeys:AVVideoCodecH264, AVVideoCodecKey,
+ [NSNumber numberWithInt:1280], AVVideoWidthKey,
+ [NSNumber numberWithInt:720], AVVideoHeightKey, nil];
+
+ return videoSettings;
+}
+
+- (NSDictionary *)audioSettings
+{
+ // TODO: these settings should be taken from
+ // the video/audio encoder settings control.
+ // For now we either take recommended (iOS >= 7.0)
+ // or nil - this seems to be good enough.
+#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_7_0)
+ if (QSysInfo::MacintoshVersion >= QSysInfo::MV_IOS_7_0 && m_audioOutput)
+ return [m_audioOutput recommendedAudioSettingsForAssetWriterWithOutputFileType:AVFileTypeQuickTimeMovie];
+#endif
+
+ return nil;
+}
+
+- (void)updateDuration:(CMTime)newTimeStamp
+{
+ Q_ASSERT(CMTimeCompare(m_startTime, kCMTimeInvalid));
+ Q_ASSERT(CMTimeCompare(m_lastTimeStamp, kCMTimeInvalid));
+ if (CMTimeCompare(newTimeStamp, m_lastTimeStamp) > 0) {
+
+ const CMTime duration = CMTimeSubtract(newTimeStamp, m_startTime);
+ if (!CMTimeCompare(duration, kCMTimeInvalid))
+ return;
+
+ m_durationInMs.store(CMTimeGetSeconds(duration) * 1000);
+ m_lastTimeStamp = newTimeStamp;
+ }
+}
+
+@end