Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Locking exposureMode with key value observer causes crash

I am doing some motion detection on an area of screen. Before starting the detection I want to set focus and exposure and lock them so they don't trigger a false motion. I am therefore sending a AVCaptureFocusModeAutoFocus and AVCaptureExposureModeAutoExpose to the device and add a KeyvalueObserver. When the observer says that it has finished focusing and changing exposure it locks them (and starts the motion detection). Everything works fine with the focus, but locking the exposure crashes the app within a few seconds", despite having identical code in both cases.

static void * const MyAdjustingFocusObservationContext = (void*)&MyAdjustingFocusObservationContext;
static void * const MyAdjustingExposureObservationContext = (void*)&MyAdjustingExposureObservationContext;

-(void)focusAtPoint{

   CGPoint point;
   if(fromRight) point.x = 450.0/480.0;
   else point.x = 30.0/480.0;
   point.y = 245.0/320.0;

   AVCaptureDevice *device =[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

   if(device != nil) {
       NSError *error;
       if([device lockForConfiguration:&error]){

          if([device isExposureModeSupported:AVCaptureFocusModeContinuousAutoFocus] && [device isFocusPointOfInterestSupported]) {
             [device setFocusPointOfInterest:point];
             [device setFocusMode:AVCaptureFocusModeContinuousAutoFocus];
             [device addObserver:self forKeyPath:@"adjustingFocus" options:NSKeyValueObservingOptionNew context:MyAdjustingFocusObservationContext];
             NSLog(@"focus now");
          }

          if([device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure] && [device isExposurePointOfInterestSupported]) {
             [device setExposurePointOfInterest:point];
             [device setExposureMode:AVCaptureExposureModeContinuousAutoExposure];
             [device addObserver:self forKeyPath:@"adjustingExposure" options:NSKeyValueObservingOptionNew context:MyAdjustingExposureObservationContext];
             NSLog(@"expose now");
          }

          [device unlockForConfiguration];
      }else{
        NSLog(@"Error in Focus Mode");
      }        
  }
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {

   AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
  NSError *error;

  if([keyPath isEqualToString:@"adjustingFocus"]){   
    if(![object isAdjustingFocus]){
       [device removeObserver:self forKeyPath:keyPath context:context];
       if([device isFocusModeSupported:AVCaptureFocusModeLocked]) {
          [device lockForConfiguration:&error];
          device.focusMode = AVCaptureFocusModeLocked;
          [device unlockForConfiguration];
          NSLog(@" focus locked");
       }
    }
  }

  if([keyPath isEqualToString:@"adjustingExposure"]){    
    if(![object isAdjustingExposure]){
       [device removeObserver:self forKeyPath:keyPath context:context];
       if([device isExposureModeSupported:AVCaptureExposureModeLocked]) {
          [device lockForConfiguration:&error];
          device.exposureMode=AVCaptureExposureModeLocked; //causes the crash
          [device unlockForConfiguration];
          NSLog(@" exposure locked");
       }
    }
  }

If I comment out the line "device.exposureMode=AVCaptureExposureModeLocked" everything works fine (except that the focus doesn't lock). If I move the line to the focus observer everything works fine (except that the exposure sometimes locks before it is set correctly). If I lock the exposure some other way, e.g. via a timer, it works.

The crash log doesn't help me much (hopefully someone can interpret it)

Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x00000000
Crashed Thread:  0

Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   Foundation                      0x3209d5e2 NSKVOPendingNotificationRelease + 6
1   CoreFoundation                  0x317b21c8 __CFArrayReleaseValues + 352
2   CoreFoundation                  0x317419f8 _CFArrayReplaceValues + 308
3   CoreFoundation                  0x3174391c CFArrayRemoveValueAtIndex + 80
4   Foundation                      0x3209d6b6 NSKeyValuePopPendingNotificationPerThread + 38
5   Foundation                      0x32090328 NSKeyValueDidChange + 356
6   Foundation                      0x3206a6ce -[NSObject(NSKeyValueObserverNotification) didChangeValueForKey:] + 90
7   AVFoundation                    0x30989fd0 -[AVCaptureFigVideoDevice handleNotification:payload:] + 1668
8   AVFoundation                    0x30983f60 -[AVCaptureDeviceInput handleNotification:payload:] + 84
9   AVFoundation                    0x3098fc64 avcaptureSessionFigRecorderNotification + 924
10  AVFoundation                    0x309b1c64 AVCMNotificationDispatcherCallback + 188
11  CoreFoundation                  0x317cee22 __CFNotificationCenterAddObserver_block_invoke_0 + 122
12  CoreFoundation                  0x31753034 _CFXNotificationPost + 1424
13  CoreFoundation                  0x3175460c CFNotificationCenterPostNotification + 100
14  CoreMedia                       0x31d3db8e CMNotificationCenterPostNotification + 114
15  Celestial                       0x34465aa4 FigRecorderRemoteCallbacksServer_NotificationIsPending + 628
16  Celestial                       0x34465826 _XNotificationIsPending + 66
17  Celestial                       0x344657dc figrecordercallbacks_server + 96
18  Celestial                       0x34465028 remrec_ClientPortCallBack + 172
19  CoreFoundation                  0x317cc5d8 __CFMachPortPerform + 116
20  CoreFoundation                  0x317d7170    __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 32
21  CoreFoundation                  0x317d7112 __CFRunLoopDoSource1 + 134
22  CoreFoundation                  0x317d5f94 __CFRunLoopRun + 1380
23  CoreFoundation                  0x31748eb8 CFRunLoopRunSpecific + 352
24  CoreFoundation                  0x31748d44 CFRunLoopRunInMode + 100
25  GraphicsServices                0x3530c2e6 GSEventRunModal + 70
26  UIKit                           0x3365e2fc UIApplicationMain + 1116
27  ShootKing                       0x000ed304 main (main.m:16)
28  ShootKing                       0x000ed28c start + 36
like image 796
Sten Avatar asked Jun 13 '13 21:06

Sten


1 Answers

You won't find this in any of the documentation (i.e. I don't have "proof"), but I can tell you from painful, personal experience consisting of many days (if not weeks) of debugging, that this kind of crash is caused by adding/removing observers for a property inside a KVO notification handler for that property. (The presence of NSKeyValuePopPendingNotificationPerThread in the stack trace is the "smoking gun" in my experience.) I've also empirically observed that the order in which observers of a given property are notified is non-deterministic, so even if adding or removing observers inside notification handlers works some of the time, it can arbitrarily fail under different circumstances. (I'm assuming there's an unordered data structure down in the guts of KVO somewhere that can get enumerated in different orders, perhaps based on the numerical value of a pointer or something arbitrary like that.) In the past, I've worked around this by posting an NSNotification immediately before/after setting the property to give observers an opportunity to add/remove themselves. It's a clunky pattern, but it's better than crashing (and allows me to continue using other things that rely on KVO, like bindings.)

Also, just as an aside, I notice in the code you posted that you're not using contexts to identify your observations, and you're not calling super in your observeValueForKeyPath:... implementation. Both of these things can lead to subtle, hard-to-diagnose bugs. A more bullet-proof pattern for KVO looks like this:

static void * const MyAdjustingFocusObservationContext = (void*)&MyAdjustingFocusObservationContext;
static void * const MyAdjustingExposureObservationContext = (void*)&MyAdjustingExposureObservationContext;

- (void)focusAtPoint
{
    // ... other stuff ...
    [device addObserver:self forKeyPath:@"adjustingFocus" options:NSKeyValueObservingOptionNew context:MyAdjustingFocusObservationContext];
    [device addObserver:self forKeyPath:@"adjustingExposure" options:NSKeyValueObservingOptionNew context:MyAdjustingExposureObservationContext];
    // ... other stuff ...
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 
{
    if (context == MyAdjustingFocusObservationContext)
    {
        // Do stuff
    }
    else if (context == MyAdjustingExposureObservationContext)
    {
        // Do other stuff
    }
    else
    {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

EDIT: I wanted to follow up to see if I could help more with this specific situation. I gather from the code and from your comments that you're looking for these observations to effectively be one-shots. I see two ways to do this:

The more straightforward and bulletproof approach would be for this object to always observe the capture device (i.e. addObserver:... when you init, removeObserver:... when you dealloc) but then "gate" the behavior using a couple of ivars called waitingForFocus and waitingForExposure. In -focusAtPoint where you currently addObserver:... instead set the ivars to YES. Then in observeValueForKeyPath:... only take action if those ivars are YES and then instead of removeObserver:... just set the ivars to NO. This should have the desired effect without requiring you to add and remove the observation each time.

The other approach I thought of would be to call removeObserver:... "later" using GCD. So you would change the removeObserver:... like this:

    dispatch_async(dispatch_get_main_queue(), ^{ [device removeObserver:self forKeyPath:keyPath context:context]; });

This will cause that call to be made elsewhere in the run loop, after the notification process has finished. This is slightly less bulletproof because there's nothing that guarantees that the notification won't be fired a second time before the delayed removal call occurs. In that regard the first approach is more rigorously "correct" in achieving the desired one-shot behavior.

EDIT 2: I just couldn't let it go. :) I figured out why you're crashing. I observe that setting exposureMode while in a KVO handler for adjustingExposure ends up causing another notification for adjustingExposure, and so the stack blows up until your process gets killed. I was able to get it working by wrapping the portion of observeValueForKeyPath:... that handles changes to adjustingExposure in a dispatch_async(dispatch_get_main_queue(), ^{...}); (including the eventual removeObserver:... call). After this it worked for me, and was definitely locking exposure and focus. That said, like I mentioned about above, this would arguably be better handled with ivars to prevent the recursion and not a arbitrarily-delayed dispatch_async().

Hope that helps.

like image 87
ipmcc Avatar answered Nov 16 '22 20:11

ipmcc