AVFMediaAssetWriter - fix potential race condition(s)

1. m_writerQueue is now shared by recorder control and asset writer
   to ensure it lives long enough.

2. m_delegate->method() calls from async block can be dangerous, since by the time
   this block is actually executed, delegate can be deleted already. This fix uses
   Q_INVOKABLE and invokeMethod with QueuedConnection instead.

3. -finishWritingWithCompletionHandler: is async and when the block finally gets
   executed, lock and 'if aborted' test are still needed.

4. Simplify the logic and reduce locking.

Change-Id: If23daf2fe22043244033427a7f6517a0fe3f23d1
Reviewed-by: Yoann Lopes <yoann.lopes@qt.io>
This commit is contained in:
Timur Pocheptsov
2016-04-19 15:21:50 +02:00
parent ffe61fd516
commit f97e1988a6
4 changed files with 69 additions and 107 deletions

View File

@@ -44,19 +44,9 @@
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
class AVFMediaRecorderControlIOS;
class AVFCameraService; class AVFCameraService;
class AVFMediaAssetWriterDelegate
{
public:
virtual ~AVFMediaAssetWriterDelegate();
virtual void assetWriterStarted() = 0;
virtual void assetWriterFailedToStart() = 0;
virtual void assetWriterFailedToStop() = 0;
virtual void assetWriterFinished() = 0;
};
typedef QAtomicInteger<bool> AVFAtomicBool; typedef QAtomicInteger<bool> AVFAtomicBool;
typedef QAtomicInteger<qint64> AVFAtomicInt64; typedef QAtomicInteger<qint64> AVFAtomicInt64;
@@ -80,18 +70,15 @@ QT_END_NAMESPACE
// Serial queue for audio output: // Serial queue for audio output:
QT_PREPEND_NAMESPACE(AVFScopedPointer)<dispatch_queue_t> m_audioQueue; QT_PREPEND_NAMESPACE(AVFScopedPointer)<dispatch_queue_t> m_audioQueue;
// Queue to write sample buffers: // Queue to write sample buffers:
dispatch_queue_t m_writerQueue; QT_PREPEND_NAMESPACE(AVFScopedPointer)<dispatch_queue_t> m_writerQueue;
QT_PREPEND_NAMESPACE(AVFScopedPointer)<AVAssetWriter> m_assetWriter; QT_PREPEND_NAMESPACE(AVFScopedPointer)<AVAssetWriter> m_assetWriter;
// Delegate's queue.
dispatch_queue_t m_delegateQueue;
QT_PREPEND_NAMESPACE(AVFMediaAssetWriterDelegate) *m_delegate; QT_PREPEND_NAMESPACE(AVFMediaRecorderControlIOS) *m_delegate;
bool m_setStartTime; bool m_setStartTime;
QT_PREPEND_NAMESPACE(AVFAtomicBool) m_stopped; QT_PREPEND_NAMESPACE(AVFAtomicBool) m_stopped;
bool m_stoppedInternal; QT_PREPEND_NAMESPACE(AVFAtomicBool) m_aborted;
bool m_aborted;
QT_PREPEND_NAMESPACE(QMutex) m_writerMutex; QT_PREPEND_NAMESPACE(QMutex) m_writerMutex;
@public @public
@@ -102,8 +89,7 @@ QT_END_NAMESPACE
} }
- (id)initWithQueue:(dispatch_queue_t)writerQueue - (id)initWithQueue:(dispatch_queue_t)writerQueue
delegate:(QT_PREPEND_NAMESPACE(AVFMediaAssetWriterDelegate) *)delegate delegate:(QT_PREPEND_NAMESPACE(AVFMediaRecorderControlIOS) *)delegate;
delegateQueue:(dispatch_queue_t)delegateQueue;
- (bool)setupWithFileURL:(NSURL *)fileURL - (bool)setupWithFileURL:(NSURL *)fileURL
cameraService:(QT_PREPEND_NAMESPACE(AVFCameraService) *)service; cameraService:(QT_PREPEND_NAMESPACE(AVFCameraService) *)service;

View File

@@ -32,6 +32,7 @@
****************************************************************************/ ****************************************************************************/
#include "avfaudioinputselectorcontrol.h" #include "avfaudioinputselectorcontrol.h"
#include "avfmediarecordercontrol_ios.h"
#include "avfcamerarenderercontrol.h" #include "avfcamerarenderercontrol.h"
#include "avfmediaassetwriter.h" #include "avfmediaassetwriter.h"
#include "avfcameraservice.h" #include "avfcameraservice.h"
@@ -39,6 +40,7 @@
#include "avfcameradebug.h" #include "avfcameradebug.h"
//#include <QtCore/qmutexlocker.h> //#include <QtCore/qmutexlocker.h>
#include <QtCore/qmetaobject.h>
#include <QtCore/qsysinfo.h> #include <QtCore/qsysinfo.h>
QT_USE_NAMESPACE QT_USE_NAMESPACE
@@ -65,11 +67,7 @@ bool qt_camera_service_isValid(AVFCameraService *service)
return true; return true;
} }
} } // unnamed namespace
AVFMediaAssetWriterDelegate::~AVFMediaAssetWriterDelegate()
{
}
@interface QT_MANGLE_NAMESPACE(AVFMediaAssetWriter) (PrivateAPI) @interface QT_MANGLE_NAMESPACE(AVFMediaAssetWriter) (PrivateAPI)
- (bool)addAudioCapture; - (bool)addAudioCapture;
@@ -83,21 +81,20 @@ AVFMediaAssetWriterDelegate::~AVFMediaAssetWriterDelegate()
@implementation QT_MANGLE_NAMESPACE(AVFMediaAssetWriter) @implementation QT_MANGLE_NAMESPACE(AVFMediaAssetWriter)
- (id)initWithQueue:(dispatch_queue_t)writerQueue - (id)initWithQueue:(dispatch_queue_t)writerQueue
delegate:(AVFMediaAssetWriterDelegate *)delegate delegate:(AVFMediaRecorderControlIOS *)delegate
delegateQueue:(dispatch_queue_t)delegateQueue
{ {
Q_ASSERT(writerQueue); Q_ASSERT(writerQueue);
Q_ASSERT(delegate); Q_ASSERT(delegate);
Q_ASSERT(delegateQueue);
if (self = [super init]) { if (self = [super init]) {
m_writerQueue = writerQueue; // "Shared" queue:
dispatch_retain(writerQueue);
m_writerQueue.reset(writerQueue);
m_delegate = delegate; m_delegate = delegate;
m_delegateQueue = delegateQueue;
m_setStartTime = true; m_setStartTime = true;
m_stopped.store(true); m_stopped.store(true);
m_stoppedInternal = false; m_aborted.store(false);
m_aborted = false;
m_startTime = kCMTimeInvalid; m_startTime = kCMTimeInvalid;
m_lastTimeStamp = kCMTimeInvalid; m_lastTimeStamp = kCMTimeInvalid;
m_durationInMs.store(0); m_durationInMs.store(0);
@@ -160,14 +157,13 @@ AVFMediaAssetWriterDelegate::~AVFMediaAssetWriterDelegate()
{ {
// To be executed on a writer's queue. // To be executed on a writer's queue.
const QMutexLocker lock(&m_writerMutex); const QMutexLocker lock(&m_writerMutex);
if (m_aborted) if (m_aborted.load())
return; return;
[self setQueues]; [self setQueues];
m_setStartTime = true; m_setStartTime = true;
m_stopped.store(false); m_stopped.store(false);
m_stoppedInternal = false;
[m_assetWriter startWriting]; [m_assetWriter startWriting];
AVCaptureSession *session = m_service->session()->captureSession(); AVCaptureSession *session = m_service->session()->captureSession();
if (!session.running) if (!session.running)
@@ -177,40 +173,41 @@ AVFMediaAssetWriterDelegate::~AVFMediaAssetWriterDelegate()
- (void)stop - (void)stop
{ {
// To be executed on a writer's queue. // To be executed on a writer's queue.
{
const QMutexLocker lock(&m_writerMutex); const QMutexLocker lock(&m_writerMutex);
if (m_aborted) if (m_aborted.load())
return; return;
if (m_stopped.load()) { 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; return;
}
m_stopped.store(true); m_stopped.store(true);
m_stoppedInternal = true; }
[m_assetWriter finishWritingWithCompletionHandler:^{ [m_assetWriter finishWritingWithCompletionHandler:^{
// TODO: make sure the session exist and we can call stop/remove on it. // This block is async, so by the time it's executed,
// it's possible that render control was deleted already ...
const QMutexLocker lock(&m_writerMutex);
if (m_aborted.load())
return;
AVCaptureSession *session = m_service->session()->captureSession(); AVCaptureSession *session = m_service->session()->captureSession();
[session stopRunning]; [session stopRunning];
[session removeOutput:m_audioOutput]; [session removeOutput:m_audioOutput];
[session removeInput:m_audioInput]; [session removeInput:m_audioInput];
dispatch_async(m_delegateQueue, ^{ QMetaObject::invokeMethod(m_delegate, "assetWriterFinished", Qt::QueuedConnection);
m_delegate->assetWriterFinished();
});
}]; }];
} }
- (void)abort - (void)abort
{ {
// To be executed on any thread, prevents writer from // To be executed on any thread (presumably, it's the main thread),
// accessing any external object (probably deleted by this time) // prevents writer from accessing any shared object.
const QMutexLocker lock(&m_writerMutex); const QMutexLocker lock(&m_writerMutex);
m_aborted = true; m_aborted.store(true);
if (m_stopped.load()) if (m_stopped.load())
return; return;
[m_assetWriter finishWritingWithCompletionHandler:^{ [m_assetWriter finishWritingWithCompletionHandler:^{
}]; }];
} }
@@ -221,9 +218,11 @@ AVFMediaAssetWriterDelegate::~AVFMediaAssetWriterDelegate()
Q_ASSERT(m_setStartTime); Q_ASSERT(m_setStartTime);
Q_ASSERT(sampleBuffer); Q_ASSERT(sampleBuffer);
dispatch_async(m_delegateQueue, ^{ const QMutexLocker lock(&m_writerMutex);
m_delegate->assetWriterStarted(); if (m_aborted.load() || m_stopped.load())
}); return;
QMetaObject::invokeMethod(m_delegate, "assetWriterStarted", Qt::QueuedConnection);
m_durationInMs.store(0); m_durationInMs.store(0);
m_startTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); m_startTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
@@ -236,22 +235,18 @@ AVFMediaAssetWriterDelegate::~AVFMediaAssetWriterDelegate()
{ {
Q_ASSERT(sampleBuffer); Q_ASSERT(sampleBuffer);
// This code is executed only on a writer's queue, but // This code is executed only on a writer's queue.
// it can access potentially deleted objects, so we if (!m_aborted.load() && !m_stopped.load()) {
// need a lock and m_aborted flag test. if (m_setStartTime)
{ [self setStartTimeFrom:sampleBuffer];
const QMutexLocker lock(&m_writerMutex);
if (!m_aborted && !m_stoppedInternal) {
if (m_setStartTime)
[self setStartTimeFrom:sampleBuffer];
if (m_cameraWriterInput.data().readyForMoreMediaData) { if (m_cameraWriterInput.data().readyForMoreMediaData) {
[self updateDuration:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)]; [self updateDuration:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
[m_cameraWriterInput appendSampleBuffer:sampleBuffer]; [m_cameraWriterInput appendSampleBuffer:sampleBuffer];
}
} }
} }
CFRelease(sampleBuffer); CFRelease(sampleBuffer);
} }
@@ -261,16 +256,13 @@ AVFMediaAssetWriterDelegate::~AVFMediaAssetWriterDelegate()
// it does not touch any shared/external data. // it does not touch any shared/external data.
Q_ASSERT(sampleBuffer); Q_ASSERT(sampleBuffer);
{ if (!m_aborted.load() && !m_stopped.load()) {
const QMutexLocker lock(&m_writerMutex); if (m_setStartTime)
if (!m_aborted && !m_stoppedInternal) { [self setStartTimeFrom:sampleBuffer];
if (m_setStartTime)
[self setStartTimeFrom:sampleBuffer];
if (m_audioWriterInput.data().readyForMoreMediaData) { if (m_audioWriterInput.data().readyForMoreMediaData) {
[self updateDuration:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)]; [self updateDuration:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
[m_audioWriterInput appendSampleBuffer:sampleBuffer]; [m_audioWriterInput appendSampleBuffer:sampleBuffer];
}
} }
} }
@@ -283,13 +275,12 @@ AVFMediaAssetWriterDelegate::~AVFMediaAssetWriterDelegate()
{ {
Q_UNUSED(connection) Q_UNUSED(connection)
// This method can be called on either video or audio queue, never on a writer's // This method can be called on either video or audio queue,
// queue - it does not access any shared data except this atomic flag below. // never on a writer's queue, it needs access to a shared data, so
// lock is required.
if (m_stopped.load()) if (m_stopped.load())
return; return;
// Even if we are stopped now, we still do not access any data.
if (!CMSampleBufferDataIsReady(sampleBuffer)) { if (!CMSampleBufferDataIsReady(sampleBuffer)) {
qDebugCamera() << Q_FUNC_INFO << "sample buffer is not ready, skipping."; qDebugCamera() << Q_FUNC_INFO << "sample buffer is not ready, skipping.";
return; return;
@@ -298,21 +289,18 @@ AVFMediaAssetWriterDelegate::~AVFMediaAssetWriterDelegate()
CFRetain(sampleBuffer); CFRetain(sampleBuffer);
if (captureOutput != m_audioOutput.data()) { if (captureOutput != m_audioOutput.data()) {
{ const QMutexLocker lock(&m_writerMutex);
const QMutexLocker lock(&m_writerMutex); if (m_aborted.load() || m_stopped.load()) {
if (m_aborted || m_stoppedInternal) { CFRelease(sampleBuffer);
CFRelease(sampleBuffer); return;
return; }
} // Find renderercontrol's delegate and invoke its method to
// show updated viewfinder's frame.
// Find renderercontrol's delegate and invoke its method to if (m_service && m_service->videoOutput()) {
// show updated viewfinder's frame. NSObject<AVCaptureVideoDataOutputSampleBufferDelegate> *vfDelegate =
if (m_service && m_service->videoOutput()) { (NSObject<AVCaptureVideoDataOutputSampleBufferDelegate> *)m_service->videoOutput()->captureDelegate();
NSObject<AVCaptureVideoDataOutputSampleBufferDelegate> *vfDelegate = if (vfDelegate)
(NSObject<AVCaptureVideoDataOutputSampleBufferDelegate> *)m_service->videoOutput()->captureDelegate(); [vfDelegate captureOutput:nil didOutputSampleBuffer:sampleBuffer fromConnection:nil];
if (vfDelegate)
[vfDelegate captureOutput:nil didOutputSampleBuffer:sampleBuffer fromConnection:nil];
}
} }
dispatch_async(m_writerQueue, ^{ dispatch_async(m_writerQueue, ^{

View File

@@ -51,7 +51,7 @@ class AVFCameraService;
class QString; class QString;
class QUrl; class QUrl;
class AVFMediaRecorderControlIOS : public QMediaRecorderControl, public AVFMediaAssetWriterDelegate class AVFMediaRecorderControlIOS : public QMediaRecorderControl
{ {
Q_OBJECT Q_OBJECT
public: public:
@@ -76,13 +76,10 @@ public Q_SLOTS:
void setMuted(bool muted) Q_DECL_OVERRIDE; void setMuted(bool muted) Q_DECL_OVERRIDE;
void setVolume(qreal volume) Q_DECL_OVERRIDE; void setVolume(qreal volume) Q_DECL_OVERRIDE;
// Writer delegate:
private: private:
void assetWriterStarted() Q_DECL_OVERRIDE; Q_INVOKABLE void assetWriterStarted();
void assetWriterFailedToStart() Q_DECL_OVERRIDE; Q_INVOKABLE void assetWriterFinished();
void assetWriterFailedToStop() Q_DECL_OVERRIDE;
void assetWriterFinished() Q_DECL_OVERRIDE;
private Q_SLOTS: private Q_SLOTS:
void captureModeChanged(QCamera::CaptureModes); void captureModeChanged(QCamera::CaptureModes);

View File

@@ -86,8 +86,7 @@ AVFMediaRecorderControlIOS::AVFMediaRecorderControlIOS(AVFCameraService *service
return; return;
} }
m_writer.reset([[QT_MANGLE_NAMESPACE(AVFMediaAssetWriter) alloc] initWithQueue:m_writerQueue m_writer.reset([[QT_MANGLE_NAMESPACE(AVFMediaAssetWriter) alloc] initWithQueue:m_writerQueue delegate:this]);
delegate:this delegateQueue:dispatch_get_main_queue()]);
if (!m_writer) { if (!m_writer) {
qDebugCamera() << Q_FUNC_INFO << "failed to create an asset writer"; qDebugCamera() << Q_FUNC_INFO << "failed to create an asset writer";
return; return;
@@ -259,14 +258,6 @@ void AVFMediaRecorderControlIOS::assetWriterStarted()
Q_EMIT statusChanged(QMediaRecorder::RecordingStatus); Q_EMIT statusChanged(QMediaRecorder::RecordingStatus);
} }
void AVFMediaRecorderControlIOS::assetWriterFailedToStart()
{
}
void AVFMediaRecorderControlIOS::assetWriterFailedToStop()
{
}
void AVFMediaRecorderControlIOS::assetWriterFinished() void AVFMediaRecorderControlIOS::assetWriterFinished()
{ {
AVFCameraControl *cameraControl = m_service->cameraControl(); AVFCameraControl *cameraControl = m_service->cameraControl();