7

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
Sten
  • 3,624
  • 1
  • 27
  • 26

1 Answers1

19

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.

ipmcc
  • 29,581
  • 5
  • 84
  • 147
  • Thanks for your answer. You are probably right that using the context is better, even if it didn't help in this case. I have updated my question. You are absolutely right in that there is something strange going on when I try to remove the observer (it start sending values at a fantastic rate until it crashes). I have tried change when and in what order I add the KVO:s, without any luck. I am not sure how to test your suggestion to post a NSNotification since adjustingFocus is read only (or I might have misunderstood you). – Sten Jun 18 '13 at 08:38
  • Unfortunately I was running the KVO continuously to begin with, and it didn't works either (I have tested again to be sure). The problem seems to be that AVCaptureExposureModeLocked causes the KVO to run wild, independent of if you remove the observer or not. Whereas AVCaptureFocusModeLocked behaves as expected. So it is something in the combination of Exposure and KVO that causes the problem. I really appreciate you effort, but I probably have to find another way to implement it. – Sten Jun 19 '13 at 09:59
  • Now we are making progress :) This does seem to work. Thanks, and an extra point to your persistence. – Sten Jun 20 '13 at 08:36
  • @ipmcc - GREAT ANSWER!!!! I'm struggling with trying to add observers for both exposure and focus. I'm going to try your suggestions and if I still have problems, then I'll add a question. But kudos to you for all your information. VERY HELPFUL!! +1 – Patricia Feb 25 '14 at 18:26
  • @ipmcc - I'm still have issues. This works for me but not in the order I want it to. Nobody has answered my question yet. If you get this message, would you take a look? http://stackoverflow.com/questions/22029381/avfoundation-how-to-control-focus-and-exposure-in-that-order – Patricia Feb 26 '14 at 12:18
  • Wrapping my KVO code in a `dispatch_async` fixed it. I've dealt with this before and clearly forgot all about this. Thanks for the reminder! PS - this only seemed to happen when running from xcode. – Brenden Feb 10 '16 at 03:06
  • As I mentioned, there's non-determinism in KVO. Debug code vs Release code is a non-trivial perturbation, which can change things like enumeration order in "unordered" data structures. Always better safe than sorry. – ipmcc Feb 10 '16 at 12:01