Как правильно освободить AVCaptureSession
Я использую классы AV Foundation для захвата живого видеопотока с камеры и обработки образцов видео. Это хорошо работает. Тем не менее, у меня возникают проблемы с тем, чтобы правильно освободить экземпляры баз данных AV (сеанс захвата, уровень предварительного просмотра, ввод и вывод), как только я закончу.
Когда мне больше не нужен сеанс и все связанные объекты, я останавливаю сеанс захвата и отпускаю его. Это работает большую часть времени. Однако иногда приложение выходит из строя с сигналом EXEC_BAD_ACCESS, поднятым во втором потоке, который был создан диспетчерской очередью (и где обрабатываются образцы видео). Авария в основном связана с моим собственным экземпляром класса, который служит делегатом примера буфера и освобождается после того, как я остановил сеанс захвата.
В документации Apple упоминается проблема: остановка сеанса захвата - асинхронная операция. То есть: это происходит не сразу. В частности, второй поток продолжает обрабатывать образцы видео и получать доступ к различным экземплярам, таким как сеанс захвата или устройства ввода и вывода.
Итак, как правильно освободить 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];
Ответы
Ответ 1
Вот лучшее решение, которое я нашел до сих пор. Основная идея заключается в использовании финализатора очереди отправки. Когда очередь отправки завершается, мы можем быть уверены, что во втором потоке, где обрабатываются буферы выборки, больше не будет действий.
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];
...
К сожалению, теперь я должен явно остановить захват. В противном случае освобождение моего экземпляра не освободит его, потому что второй поток теперь увеличивает и уменьшает счетчик.
Еще одна проблема заключается в том, что мой класс теперь выпущен из двух разных потоков. Является ли это надежным или это следующая проблема, вызывающая сбои?
Ответ 2
Я опубликовал очень похожий вопрос на форуме разработчиков Apple и получил ответ от сотрудника 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
С помощью финализаторов очереди вы можете использовать dispatch_semaphore для каждой очереди, а затем продолжить свою процедуру очистки после выполнения.
#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, или они никогда не выпустят связанные очереди и, таким образом, никогда не вызывают их финализаторы при отпускании вашего AVCaptureSession.
[avs removeOutput:vOut];
[vOut setSampleBufferDelegate:nil queue:NULL];
Ответ 4
-(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;
}
Я вызывал эту функцию перед тем, как выскочить и нажать любое другое представление. Это решило мою проблему с предупреждением о низкой памяти.
Ответ 5
В соответствии с текущими документами Apple (1) [AVCaptureSession stopRunning]
- это синхронная операция, которая блокируется до тех пор, пока ресивер полностью не остановится. Поэтому все эти проблемы больше не должны возникать.
Ответ 6
После выделения 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];
То, что я сбила с толку, заключается в том, что при вызове seeion.stopRunning, onVideoStop: называется синхронно! несмотря на асинхронное предположение Apple о случае.
Его работа, но, пожалуйста, дайте мне знать, если вы увидите какой-либо трюк. Я бы предпочел работать с ним асинхронно.
Спасибо
Ответ 7
Решено!
Возможно, это последовательность айонов при инициализации сеанса. Это работает для меня:
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];
Btw эта последовательность, похоже, разрешает проблему синхронных уведомлений:)