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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With