How to Troubleshoot Apps for the Modern Connected Worker
Ā
Building a Native Camera Access Library - Part IV - Transcript.pdf
1. Building Native Camera Access - Part IV
In this part we continue the iOS Port work starting with CameraKitView
2. #import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
@interface CameraKitView : UIView {
AVCaptureVideoPreviewLayer *innerLayer;
}
-(void)layoutSubviews;
-(void)setLayer:(AVCaptureVideoPreviewLayer*)l;
@end
CameraKitView.h
Before I go into the 4 new update methods I'd like to detour through the CameraKitView class first.
This is pretty standard. We just derive from UIView which is the standard component type for iOS UI's
4. #import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
@interface CameraKitView : UIView {
AVCaptureVideoPreviewLayer *innerLayer;
}
-(void)layoutSubviews;
-(void)setLayer:(AVCaptureVideoPreviewLayer*)l;
@end
CameraKitView.h
This is a method we're overriding from UIView it's invoked when the UIView is arranged by iOS which also has something similar to a layout manager
6. #import <Foundation/Foundation.h>
#import "CameraKitView.h"
@implementation CameraKitView
-(void)setLayer:(AVCaptureVideoPreviewLayer*)l {
innerLayer = l;
}
-(void)layoutSubviews {
innerLayer.frame = self.bounds;
}
@end
CameraKitView.m
The implementation of the class is even simpler. We store the layer into the member field in set layer.
7. #import <Foundation/Foundation.h>
#import "CameraKitView.h"
@implementation CameraKitView
-(void)setLayer:(AVCaptureVideoPreviewLayer*)l {
innerLayer = l;
}
-(void)layoutSubviews {
innerLayer.frame = self.bounds;
}
@end
CameraKitView.m
When laying out the view I update the fame based on the bounds. Both represent the physical location of the view on the screen.āØ
Codename One positions the UIView automatically but the CALayer within is positioned by this class. So when Codename One places the UIView based on the layout
manager, the bounds of the UIView are copied into the layer so it shows in the same place
8. -(void)updateDirection {
[previewLayer removeFromSuperlayer];
[container setLayer:nil];
[captureSession stopRunning];
[previewLayer release];
[captureSession release];
[self lazyInitPostAuthorization];
}
com_codename1_camerakit_impl_CameraNativeAccessImpl.m
Now that this is out of the way lets go into the update methods. Before I go into the ones we already saw there is a hidden one which is invoked when a user invokes
setFacing to change the direction. It's not in the code from before as the functionality was embedded into that code.
When we change the camera direction we need to pick a new device and eļ¬ectively start over again which is what lazyInitPostAuthorization does. Here we discard the
preview layer & stop the capture session
9. -(void)updateDirection {
[previewLayer removeFromSuperlayer];
[container setLayer:nil];
[captureSession stopRunning];
[previewLayer release];
[captureSession release];
[self lazyInitPostAuthorization];
}
com_codename1_camerakit_impl_CameraNativeAccessImpl.m
Objective-C uses reference counting, we need to release objects we allocated. This is handled automatically by ARC normally but ARC collides with the GC
10. -(void)updateDirection {
[previewLayer removeFromSuperlayer];
[container setLayer:nil];
[captureSession stopRunning];
[previewLayer release];
[captureSession release];
[self lazyInitPostAuthorization];
}
com_codename1_camerakit_impl_CameraNativeAccessImpl.m
Because this is essentially recreating the UI we go through the second part of initialization over again
12. -(void)updateVideoQuality {
if(device == nil) {
return;
}
[device lockForConfiguration:nil];
switch(videoQuality) {
case VIDEO_QUALITY_480P:
captureSession.sessionPreset = AVCaptureSessionPreset640x480;
break;
case VIDEO_QUALITY_720P:
captureSession.sessionPreset = AVCaptureSessionPreset1280x720;
break;
case VIDEO_QUALITY_LOWEST:
case VIDEO_QUALITY_QVGA:
captureSession.sessionPreset = AVCaptureSessionPresetLow;
break;
case VIDEO_QUALITY_1080P:
captureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
break;
case VIDEO_QUALITY_HIGHEST:
case VIDEO_QUALITY_2160P:
captureSession.sessionPreset = AVCaptureSessionPreset3840x2160;
break;
}
[device unlockForConfiguration];
}
com_codename1_camerakit_impl_CameraNativeAccessImpl.m
Moving on lets look at the updateVideoQuality method. While it's a bit bigger the core concepts are relatively simple.
If this is invoked before start() it's totally fine, this method will be invoked again when start() is invoked.
13. -(void)updateVideoQuality {
if(device == nil) {
return;
}
[device lockForConfiguration:nil];
switch(videoQuality) {
case VIDEO_QUALITY_480P:
captureSession.sessionPreset = AVCaptureSessionPreset640x480;
break;
case VIDEO_QUALITY_720P:
captureSession.sessionPreset = AVCaptureSessionPreset1280x720;
break;
case VIDEO_QUALITY_LOWEST:
case VIDEO_QUALITY_QVGA:
captureSession.sessionPreset = AVCaptureSessionPresetLow;
break;
case VIDEO_QUALITY_1080P:
captureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
break;
case VIDEO_QUALITY_HIGHEST:
case VIDEO_QUALITY_2160P:
captureSession.sessionPreset = AVCaptureSessionPreset3840x2160;
break;
}
[device unlockForConfiguration];
}
com_codename1_camerakit_impl_CameraNativeAccessImpl.m
When we manipulate some configurations we need to acquire a device lock to prevent concurrent modifications
14. -(void)updateVideoQuality {
if(device == nil) {
return;
}
[device lockForConfiguration:nil];
switch(videoQuality) {
case VIDEO_QUALITY_480P:
captureSession.sessionPreset = AVCaptureSessionPreset640x480;
break;
case VIDEO_QUALITY_720P:
captureSession.sessionPreset = AVCaptureSessionPreset1280x720;
break;
case VIDEO_QUALITY_LOWEST:
case VIDEO_QUALITY_QVGA:
captureSession.sessionPreset = AVCaptureSessionPresetLow;
break;
case VIDEO_QUALITY_1080P:
captureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
break;
case VIDEO_QUALITY_HIGHEST:
case VIDEO_QUALITY_2160P:
captureSession.sessionPreset = AVCaptureSessionPreset3840x2160;
break;
}
[device unlockForConfiguration];
}
com_codename1_camerakit_impl_CameraNativeAccessImpl.m
The rest is a standard switch case to map the standard constants to iOS constants.
15. -(void)updateFlash {
if(device == nil) {
return;
}
[device lockForConfiguration:nil];
switch(flash) {
case FLASH_ON:
if([device isFlashModeSupported:AVCaptureFlashModeOn]) {
[device setFlashMode:AVCaptureFlashModeOn];
}
break;
case FLASH_OFF:
if([device isFlashModeSupported:AVCaptureFlashModeOff]) {
[device setFlashMode:AVCaptureFlashModeOff];
}
break;
case FLASH_AUTO:
if([device isFlashModeSupported:AVCaptureFlashModeAuto]) {
[device setFlashMode:AVCaptureFlashModeAuto];
}
break;
}
[device unlockForConfiguration];
}
com_codename1_camerakit_impl_CameraNativeAccessImpl.m
This was pretty simple. Next on the line is updateFlash which is also as simple. I can go over the method but there is really nothing here that we didn't discuss in the
previous method. We return for null device, we lock for configuration & we convert the constant type.
16. -(void)updateFocus {
if(device == nil) {
return;
}
[device lockForConfiguration:nil];
switch(focus) {
case FOCUS_OFF:
[device setFocusMode:AVCaptureFocusModeLocked];
break;
case FOCUS_CONTINUOUS:
[device setFocusMode:AVCaptureFocusModeContinuousAutoFocus];
break;
case FOCUS_TAP_WITH_MARKER:
case FOCUS_TAP:
[device setFocusMode:AVCaptureFocusModeContinuousAutoFocus];
UITapGestureRecognizer *tapGR = [[UITapGestureRecognizer alloc]
initWithTarget:self
action:@selector(tapToFocus:)];
[tapGR setNumberOfTapsRequired:1];
[tapGR setNumberOfTouchesRequired:1];
[container addGestureRecognizer:tapGR];
break;
}
[device unlockForConfiguration];
}
com_codename1_camerakit_impl_CameraNativeAccessImpl.m
Surprisingly updateFocus does have something new to say despite being pretty identical to the first two. I'll skip the identical part and discuss the final section.
There is no builtin focus on tap in iOS so we need to use some code to do this
17. -(void)updateFocus {
if(device == nil) {
return;
}
[device lockForConfiguration:nil];
switch(focus) {
case FOCUS_OFF:
[device setFocusMode:AVCaptureFocusModeLocked];
break;
case FOCUS_CONTINUOUS:
[device setFocusMode:AVCaptureFocusModeContinuousAutoFocus];
break;
case FOCUS_TAP_WITH_MARKER:
case FOCUS_TAP:
[device setFocusMode:AVCaptureFocusModeContinuousAutoFocus];
UITapGestureRecognizer *tapGR = [[UITapGestureRecognizer alloc]
initWithTarget:self
action:@selector(tapToFocus:)];
[tapGR setNumberOfTapsRequired:1];
[tapGR setNumberOfTouchesRequired:1];
[container addGestureRecognizer:tapGR];
break;
}
[device unlockForConfiguration];
}
com_codename1_camerakit_impl_CameraNativeAccessImpl.m
This invoked the tapToFocus method on self (which is this object in Objective-C) when a user taps container
18. // converted from this answer: https://stackoverflow.com/a/17025083/756809
-(void)tapToFocus:(UITapGestureRecognizer *)singleTap {
CGPoint touchPoint = [singleTap locationInView:container];
CGPoint convertedPoint = [previewLayer
captureDevicePointOfInterestForPoint:touchPoint];
if([device isFocusPointOfInterestSupported] &&
[device isFocusModeSupported:AVCaptureFocusModeAutoFocus]){
NSError *error = nil;
[device lockForConfiguration:&error];
if(!error){
[device setFocusPointOfInterest:convertedPoint];
[device setFocusMode:AVCaptureFocusModeAutoFocus];
[device unlockForConfiguration];
}
}
}
com_codename1_camerakit_impl_CameraNativeAccessImpl.m
So lets look at the tapToFocus method. Notice that this logic isn't something I came up with. I took it from a stackoverflow answer which is very convenient for
implementing these sort of things.
A tap returns a point on the screen which we can convert to a point relative to the coordinate space of the previewLayer
19. // converted from this answer: https://stackoverflow.com/a/17025083/756809
-(void)tapToFocus:(UITapGestureRecognizer *)singleTap {
CGPoint touchPoint = [singleTap locationInView:container];
CGPoint convertedPoint = [previewLayer
captureDevicePointOfInterestForPoint:touchPoint];
if([device isFocusPointOfInterestSupported] &&
[device isFocusModeSupported:AVCaptureFocusModeAutoFocus]){
NSError *error = nil;
[device lockForConfiguration:&error];
if(!error){
[device setFocusPointOfInterest:convertedPoint];
[device setFocusMode:AVCaptureFocusModeAutoFocus];
[device unlockForConfiguration];
}
}
}
com_codename1_camerakit_impl_CameraNativeAccessImpl.m
If we can declare a focus point of interest we can just set the focus to the right point. It's not necessarily trivial but mostly boilerplate
20. -(void)updateZoom {
if(device == nil) {
return;
}
[device lockForConfiguration:nil];
device.videoZoomFactor = MAX(1.0,
MIN(zoom, device.activeFormat.videoMaxZoomFactor));
[device unlockForConfiguration];
}
com_codename1_camerakit_impl_CameraNativeAccessImpl.m
This brings us to the last and simplest of these methods. This is mostly a rehash of the other methods so I won't discuss it. Notice it guards against zooming too much or
too little.
And that's it, with these changes camera will basically work! We just need to fill in a few more "details" which are mostly boilerplate.