iphone - Cómo lanzar correctamente un AVCaptureSession
multithreading ios (7)
Estoy usando las clases de AV Foundation para capturar la transmisión de video en vivo desde la cámara y procesar las muestras de video. Esto funciona bien Sin embargo, tengo problemas para liberar correctamente las instancias de la base AV (sesión de captura, capa de vista previa, entrada y salida) una vez que he terminado.
Cuando ya no necesito la sesión y todos los objetos asociados, detengo la sesión de captura y la lanzo. Esto funciona la mayor parte del tiempo. Sin embargo, a veces la aplicación se bloquea con una señal EXEC_BAD_ACCESS generada en el segundo hilo creado por la cola de envío (y donde se procesan las muestras de video). El bloqueo se debe principalmente a mi propia instancia de clase, que sirve como delegado del buffer de muestra y se libera después de detener la sesión de captura.
La documentación de Apple menciona el problema: detener la sesión de captura es una operación asíncrona. Es decir: no sucede de inmediato. En particular, el segundo hilo continúa procesando muestras de video y accediendo a diferentes instancias como la sesión de captura o los dispositivos de entrada y salida.
Entonces, ¿cómo liberar correctamente la sesión AVCaptureSession y todas las instancias relacionadas? ¿Hay alguna notificación que me indique de forma fiable que AVCaptureSession ha finalizado?
Aquí está mi código:
Declaraciones:
AVCaptureSession* session;
AVCaptureVideoPreviewLayer* previewLayer;
UIView* view;
Configuración de instancias:
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];
Limpiar:
[previewLayer removeFromSuperlayer];
[previewLayer release];
[session stopRunning];
[session release];
Con los finalizadores de cola, puede usar un dispatch_semaphore para cada cola y luego continuar con su rutina de limpieza una vez que haya terminado.
#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];
Recuerde que debe configurar los delegados del búfer de muestra de los objetos AVCaptureVideoDataOutput / AVCaptureAudioDataOutput en nil o nunca liberarán sus colas asociadas y, por lo tanto, nunca llamarán a sus finalizadores cuando libere su AVCaptureSession.
[avs removeOutput:vOut];
[vOut setSampleBufferDelegate:nil queue:NULL];
Después de la asignación de AVCaptureSession puede utilizar:
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];
Están devolviendo los métodos relevantes a session.stopRunning, session.startRunning, etc.
Allí también debe implementar algún bloque de limpieza no documentado:
AVCaptureInput* input = [session.inputs objectAtIndex:0];
[session removeInput:input];
AVCaptureVideoDataOutput* output = (AVCaptureVideoDataOutput*)[session.outputs objectAtIndex:0];
[session removeOutput:output];
Sin embargo, lo que me pareció confuso es que al llamar a seesion.stopRunning, onVideoStop: ¡se llama de forma síncrona! a pesar de la asunción asincrónica de Apple en el caso.
Está funcionando, pero por favor avíseme en caso de que vea algún truco. Preferiría trabajar con él de forma asíncrona.
Gracias
Esta es la mejor solución que he encontrado hasta ahora. La idea básica es utilizar el finalizador de la cola de envío. Cuando la cola de despacho se cierra, podemos estar seguros de que no habrá más acción en el segundo hilo donde se procesan los búferes de muestra.
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];
...
Desafortunadamente, ahora tengo que dejar de capturar explícitamente. De lo contrario, liberar mi instancia no lo liberará porque el segundo hilo ahora también incrementa y disminuye el contador.
Otro problema es que mi clase ahora se libera de dos subprocesos diferentes. ¿Es esto confiable o es el siguiente problema que causa fallas?
He publicado una pregunta muy similar en el Foro de desarrolladores de Apple y recibí una respuesta de un empleado de Apple. Él dice que es un problema conocido:
Este es un problema con AVCaptureSession / VideoDataOutput en iOS 4.0-4.1 que se ha solucionado y aparecerá en una futura actualización. Por el momento, puede solucionarlo esperando un corto período de tiempo después de detener AVCaptureSession, por ejemplo, medio segundo, antes de deshacerse de la sesión y la salida de datos.
Él / ella propone el siguiente código:
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.
}
);
Todavía me gusta más el enfoque con el finalizador de cola de envío porque este código simplemente adivina cuándo el segundo hilo podría haber terminado.
Resuelto Tal vez sea la secuencia de acciones en la inicialización de la sesión. Esta funciona para mí:
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];
Por cierto, esta secuencia parece resolver el problema de las notificaciones síncronas :)
Según los documentos actuales de Apple ( 1 ) [AVCaptureSession stopRunning]
es una operación sincrónica que bloquea hasta que el receptor haya dejado de funcionar por completo. Así que todas estas cuestiones no deberían suceder más.
-(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;
}
Llamé a esta función antes de abrir y presionar cualquier otra vista. Solucionó mi problema de advertencia de memoria baja.