如何正确释放AVCaptureSession

34
我正在使用AVFoundation类来捕获相机的实时视频流并处理视频样本。这个功能运作得很好。然而,一旦完成操作,我确实有问题释放AVFoundation实例(捕获会话、预览层、输入和输出)。

当我不再需要该会话和所有相关对象时,我停止捕获会话并将其释放。这在大多数情况下都有效。但是,有时应用程序会崩溃,并在由调度队列创建的第二个线程中引发 EXEC_BAD_ACCESS 信号(此线程处理视频样本)。崩溃主要是由于我的类实例造成的,它充当样本缓冲区委托,并在我停止捕获会话后被释放。

苹果文档提到了这个问题:停止捕获会话是一个异步操作。也就是说,它不会立即发生。特别是第二个线程继续处理视频样本并访问不同的实例,如捕获会话或输入和输出设备。

那么,我该如何正确释放 AVCaptureSession 和所有相关实例?是否有一种可靠的通知告诉我 AVCaptureSession 已经完成?

以下是我的代码:

声明:

AVCaptureSession* session;
AVCaptureVideoPreviewLayer* previewLayer;
UIView* view;

实例的设置:

AVCaptureDevice* camera = [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo];
session = [[AVCaptureSession alloc] init];

AVCaptureDeviceInput* input = [AVCaptureDeviceInput deviceInputWithDevice: camera error: &error];
[session addInput: input];
AVCaptureVideoDataOutput* output = [[[AVCaptureVideoDataOutput alloc] init] autorelease];
[session addOutput: output];

dispatch_queue_t queue = dispatch_queue_create("augm_reality", NULL);
[output setSampleBufferDelegate: self queue: queue];
dispatch_release(queue);

previewLayer = [[AVCaptureVideoPreviewLayer layerWithSession: session] retain];
previewLayer.frame = view.bounds;
[view.layer addSublayer: previewLayer];

[session startRunning];

清理:

[previewLayer removeFromSuperlayer];
[previewLayer release];
[session stopRunning];
[session release];
7个回答

18

这是我目前发现的最佳解决方案。基本思路是使用调度队列的终结器。当调度队列退出时,我们可以确信在处理样本缓冲区的第二个线程中不会有任何其他操作。

static void capture_cleanup(void* p)
{
    AugmReality* ar = (AugmReality *)p; // cast to original context instance
    [ar release];  // releases capture session if dealloc is called
}

...

dispatch_queue_t queue = dispatch_queue_create("augm_reality", NULL);
dispatch_set_context(queue, self);
dispatch_set_finalizer_f(queue, capture_cleanup);
[output setSampleBufferDelegate: self queue: queue];
dispatch_release(queue);
[self retain];

...

不幸的是,我现在必须明确停止捕获。否则释放我的实例将不会释放它,因为第二个线程现在也会增加和减少计数器。

另一个问题是,我的类现在从两个不同的线程中释放。这可靠吗?还是下一个问题会导致崩溃?


在capture_cleanup函数中,AugmReality是什么?我不太明白那个东西。 - NiravPatel
AugmReality 是我应用程序的自定义类,实现了示例缓冲区委托。因此,变量 p(或 ar)指的是我想要释放的实例,但在捕获会话完全停止之前无法释放。 - Codo

4
我曾在Apple开发者论坛上发布过一个非常类似的问题,并收到了一位苹果员工的答复。他表示这是一个已知的问题:

这是一个AVCaptureSession/VideoDataOutput在iOS 4.0-4.1中存在的问题,已经得到修复并将在未来的更新中呈现。 目前,您可以解决它,方法是在停止AVCaptureSession后等待短时间(例如半秒钟)再处置会话和数据输出。

他/她建议使用以下代码:
dispatch_after(
    dispatch_time(0, 500000000),
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), // or main queue, or your own
    ^{
        // Do your work here.
        [session release];
        // etc.
    }
);

我仍然更喜欢使用调度队列终结器的方法,因为这段代码只是猜测第二个线程可能何时完成。


3
根据苹果当前文档(1),[AVCaptureSession stopRunning] 是一个同步操作,会阻塞直到接收器完全停止运行。因此,所有这些问题都不应该再发生。

1
它们似乎正在发生在我身上,iOS 10,Swift 3,Xcode 9。 - Brian Ogden

2
解决了! 也许是在初始化会话时的操作顺序引起的。这个对我有用:
NSError *error = nil;

if(session)
    [session release];

// Create the session
session = [[AVCaptureSession alloc] init];


// Configure the session to produce lower resolution video frames, if your 
// processing algorithm can cope. We'll specify medium quality for the
// chosen device.
session.sessionPreset = AVCaptureSessionPresetMedium;

// Find a suitable AVCaptureDevice
AVCaptureDevice *device = [AVCaptureDevice
                           defaultDeviceWithMediaType:AVMediaTypeVideo];

// Create a device input with the device and add it to the session.
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device 
                                                                    error:&error];
if (!input) {
    // Handling the error appropriately.
}
[session addInput:input];

// Create a VideoDataOutput and add it to the session
AVCaptureVideoDataOutput *output = [[[AVCaptureVideoDataOutput alloc] init] autorelease];
[session addOutput:output];


// Configure your output.
dispatch_queue_t queue = dispatch_queue_create("myQueue", NULL);
[output setSampleBufferDelegate:self queue:queue];
dispatch_release(queue);

// Specify the pixel format
output.videoSettings = 
[NSDictionary dictionaryWithObject:
 [NSNumber numberWithInt:kCVPixelFormatType_32BGRA] 
                            forKey:(id)kCVPixelBufferPixelFormatTypeKey];

// If you wish to cap the frame rate to a known value, such as 15 fps, set 
// minFrameDuration.
output.minFrameDuration = CMTimeMake(1, 15);

previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:session];
[delegate layerArrived:previewLayer];

NSNotificationCenter *notify =
[NSNotificationCenter defaultCenter];
[notify addObserver: self
            selector: @selector(onVideoError:)
            name: AVCaptureSessionRuntimeErrorNotification
            object: session];
[notify addObserver: self
            selector: @selector(onVideoStart:)
            name: AVCaptureSessionDidStartRunningNotification
            object: session];
[notify addObserver: self
            selector: @selector(onVideoStop:)
            name: AVCaptureSessionDidStopRunningNotification
            object: session];
[notify addObserver: self
            selector: @selector(onVideoStop:)
            name: AVCaptureSessionWasInterruptedNotification
            object: session];
[notify addObserver: self
            selector: @selector(onVideoStart:)
            name: AVCaptureSessionInterruptionEndedNotification
            object: session];

// Start the session running to start the flow of data
[session startRunning];

顺便提一下,这个序列似乎解决了同步通知问题 :)

3
很抱歉,但这并没有任何区别,它仍然会崩溃。那么如何解决通知问题呢?难道通知现在要延迟到第二个线程完成之后才能解决吗?与此同时,我已经找到了适合我的解决方案(请看我的回答)。 - Codo

2
使用队列终结器,您可以为每个队列使用一个dispatch_semaphore,然后在完成后继续进行清理例程。请保留HTML标签。
#define GCD_TIME(delayInSeconds) dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC)

static void vQueueCleanup(void* context) {
  VideoRecordingViewController *vc = (VideoRecordingViewController*)context;
  if (vc.vSema) dispatch_semaphore_signal(vc.vSema);
}

static void aQueueCleanup(void* context) {
  VideoRecordingViewController *vc = (VideoRecordingViewController*)context;
  if (vc.aSema) dispatch_semaphore_signal(vc.aSema);
}

//In your cleanup method:
vSema = dispatch_semaphore_create(0);
aSema = dispatch_semaphore_create(0);
self.avSession = nil;
if (vSema) dispatch_semaphore_wait(vSema, GCD_TIME(0.5));
if (aSema) dispatch_semaphore_wait(aSema, GCD_TIME(0.5));
[self.navigationController popViewControllerAnimated:YES];

请记住,您必须将AVCaptureVideoDataOutput/AVCaptureAudioDataOutput对象的样本缓冲区委托设置为nil,否则它们将永远不会释放其关联的队列,因此在释放AVCaptureSession时也永远不会调用它们的终结器。

[avs removeOutput:vOut];
[vOut setSampleBufferDelegate:nil queue:NULL];

2
 -(void)deallocSession
{
[captureVideoPreviewLayer removeFromSuperlayer];
for(AVCaptureInput *input1 in session.inputs) {
    [session removeInput:input1];
}

for(AVCaptureOutput *output1 in session.outputs) {
    [session removeOutput:output1];
}
[session stopRunning];
session=nil;
outputSettings=nil;
device=nil;
input=nil;
captureVideoPreviewLayer=nil;
stillImageOutput=nil;
self.vImagePreview=nil;

}

在弹出和推入任何其他视图之前,我调用了这个函数。它解决了我遇到的低内存警告问题。


我遇到了相机卡顿的问题,在接听电话后,如何重新启动我的相机预览? - Mr.G

1

在AVCaptureSession分配之后,您可以使用:

NSNotificationCenter *notify =
[NSNotificationCenter defaultCenter];
[notify addObserver: self
            selector: @selector(onVideoError:)
            name: AVCaptureSessionRuntimeErrorNotification
            object: session];
[notify addObserver: self
            selector: @selector(onVideoStart:)
            name: AVCaptureSessionDidStartRunningNotification
            object: session];
[notify addObserver: self
            selector: @selector(onVideoStop:)
            name: AVCaptureSessionDidStopRunningNotification
            object: session];
[notify addObserver: self
            selector: @selector(onVideoStop:)
            name: AVCaptureSessionWasInterruptedNotification
            object: session];
[notify addObserver: self
            selector: @selector(onVideoStart:)
            name: AVCaptureSessionInterruptionEndedNotification
            object: session];

这些在session.stopRunning、session.startRunning等方法调用时会回调相关方法。

此外,您还应该实现一些未记录的清理块:

AVCaptureInput* input = [session.inputs objectAtIndex:0];
[session removeInput:input];
AVCaptureVideoDataOutput* output = (AVCaptureVideoDataOutput*)[session.outputs objectAtIndex:0];
[session removeOutput:output];  

我发现令人困惑的是,在调用session.stopRunning时,onVideoStop:同步地被调用了!尽管苹果在这种情况下假定是异步的。

它能工作,但如果您发现任何技巧,请告诉我。我更喜欢异步地处理它。

谢谢


1
我已经尝试使用通知并发现与您相同:通知是在_session.stopRunning_返回之前立即发送的,而第二个线程仍在运行。因此,应用程序仍然会不时崩溃。我将尝试建议的清理代码,但我只会将其放在_session.stopRunning_之后。或者这真的有任何区别吗? - Codo
很遗憾,你的解决方案还不可行。它仍然会偶尔崩溃,因为第二个线程不会立即退出并访问已释放的实例。 - Codo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接