iOS:自定义相机

Page content

iOS中的自定义相机

前言

虽然系统相机功能多且强大,但很多项目中,系统相机的功能不切合需求,不可避免的用到自定义相机,所以我们来对相机做一个自定义。

自定义相机,使用的是Apple提供的AVFoundation框架;

AVFoundation是一个功能非常强大且全面的框架,包括但不限于音频,视频,录制,播放,编码解码等等,它提供了相对底层的API,允许我们自定义这些功能。

界面

以下截图,为自定义相机的界面(UI)部分,其中手势添加于containerView上:

自定义相机界面

实现

设计

我们将设备相关分拆为设备类PhotoTakeDevice,控制器类PhotoTakeViewController则负责处理逻辑和功能。

这里我们需要:

  1. 获取输入设备AVCaptureDevice,根据device创建设备输入AVCaptureDeviceInput;
  2. 使用AVCaptureStillImageOutput作为照片输出;
  3. 创建AVCaptureSession并关联设备输入和输出,它负责输入和输出设置之间的数据传递;
  4. 创建AVCaptureVideoPreviewLayer作为预览层,使用户实时获取摄像头摄入的信息;
  5. 至于设置闪光灯,聚焦,切换摄像头,拍摄的调用及回调,在代码中有详细的注释。

设备类PhotoTakeDevice

接口:
~~~objective-c
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>

@interface PhotoTakeDevice : NSObject
/// AVCaptureSession 负责输入和输出设置之间的数据传递
@property (nonatomic, strong) AVCaptureSession * session;
/// AVCaptureDeviceInput 负责从AVCaptureDevice获得输入数据
@property (nonatomic, strong) AVCaptureDeviceInput * deviceInput;
/// AVCaptureStillImageOutput 照片输出流
@property (nonatomic, strong) AVCaptureStillImageOutput * stillImageOutPut;
/// AVCaptureVideoPreviewLayer 拍摄预览图层
@property (nonatomic, strong) AVCaptureVideoPreviewLayer * videoPreviewLayer;
/**
 * 配置预览层,将视频预览层添加到界面中
 */
- (void)configPreviewLayerInView:(UIView *)view belowView:(UIView *)belowView;
/**
 * 开启会话
 */
- (void)deviceStart;
/**
 * 停止会话
 */
- (void)deviceStop;
/**
 *  设置闪光灯模式
 *
 *  @param flashMode 闪光灯模式
 */
- (void)setFlashMode:(AVCaptureFlashMode )flashMode;
/**
 *  获取闪光灯状态
 */
- (void)getFlashMode:(void(^)(BOOL flashAvailable, AVCaptureFlashMode flashMode))flashModeCallback;
/**
 *  点按手势,点按时聚焦
 */
- (void)tapScreenPoint:(CGPoint)point;
/**
 * 切换摄像头
 */
- (void)cameraToggle;
/**
 * 拍照
 */
- (void)cameraTakePhotoIsSave:(BOOL)isSaveAlbum Callback:(void(^)(UIImage * image, NSData * imgData, NSError * error))photoTokenCallback;
@end
~~~
属性懒加载:
~~~objective-c
#pragma mark - Lazy Loading
- (AVCaptureSession *)session {
    if (!_session) {
        /// 初始化会话
        _session = [[AVCaptureSession alloc] init];
        /// 设置分辨率
        if ([_session canSetSessionPreset:AVCaptureSessionPresetPhoto]) {
            _session.sessionPreset = AVCaptureSessionPresetPhoto;
        }
    }
    return _session;
}

- (AVCaptureDeviceInput *)deviceInput {
    if (!_deviceInput) {
        /// 获得输入设备 取得后置摄像头
        AVCaptureDevice * captureDevice = [self getCameraDeviceWithPosition:AVCaptureDevicePositionBack];
        if (!captureDevice) { NSLog(@"取得后置摄像头出错"); }
        /// 根据输入设备初始化设备输入对象,用于获得输入数据
        NSError *error=nil;
        _deviceInput = [[AVCaptureDeviceInput alloc]initWithDevice:captureDevice error:&error];
        if (error) { NSLog(@"取得设备输入对象时出错,错误原因:%@",error.localizedDescription); }
    }
    return _deviceInput;
}

- (AVCaptureStillImageOutput *)stillImageOutPut {
    if (!_stillImageOutPut) {
        /// 初始化设备输出对象,用于获得输出数据
        _stillImageOutPut=[[AVCaptureStillImageOutput alloc]init];
        NSDictionary *outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG}; //输出设置
        [_stillImageOutPut setOutputSettings:outputSettings];
    }
    return _stillImageOutPut;
}

- (AVCaptureVideoPreviewLayer *)videoPreviewLayer {
    if (!_videoPreviewLayer) {
        /// 创建视频预览层,用于实时展示摄像头状态
        _videoPreviewLayer =[[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session];
        /// 填充模式
        _videoPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
    }
    return _videoPreviewLayer;
}
~~~
通知:
~~~objective-c
#pragma mark - 通知
/**
 *  给输入设备添加通知
 */
- (void)addNotificationToCaptureDevice:(AVCaptureDevice *)captureDevice {
    //注意添加区域改变捕获通知必须首先设置设备允许捕获
    [self changeDeviceProperty:^(AVCaptureDevice *captureDevice) {
        captureDevice.subjectAreaChangeMonitoringEnabled=YES;
    }];
    NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter];
    //捕获区域发生改变
    [notificationCenter addObserver:self selector:@selector(areaChange:) name:AVCaptureDeviceSubjectAreaDidChangeNotification object:captureDevice];
}

- (void)removeNotificationFromCaptureDevice:(AVCaptureDevice *)captureDevice {
    NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter];
    [notificationCenter removeObserver:self name:AVCaptureDeviceSubjectAreaDidChangeNotification object:captureDevice];
}

/**
 *  移除所有通知
 */
- (void)removeNotification {
    NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter];
    [notificationCenter removeObserver:self];
}
- (void)addNotificationToCaptureSession:(AVCaptureSession *)captureSession {
    NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter];
    //会话出错
    [notificationCenter addObserver:self selector:@selector(sessionRuntimeError:) name:AVCaptureSessionRuntimeErrorNotification object:captureSession];
}

/**
 *  设备连接成功
 *
 *  @param notification 通知对象
 */
- (void)deviceConnected:(NSNotification *)notification {
    NSLog(@"设备已连接...");
}
/**
 *  设备连接断开
 *
 *  @param notification 通知对象
 */
- (void)deviceDisconnected:(NSNotification *)notification {
    NSLog(@"设备已断开.");
}
/**
 *  捕获区域改变
 *
 *  @param notification 通知对象
 */
- (void)areaChange:(NSNotification *)notification {
    NSLog(@"捕获区域改变...");
}
/**
 *  会话出错
 *
 *  @param notification 通知对象
 */
- (void)sessionRuntimeError:(NSNotification *)notification {
    NSLog(@"会话发生错误.");
}
~~~
初始化:
~~~objective-c
- (instancetype)init {
    if (self = [super init]) {
        [self configSession];
    }
    return self;
}

- (void)dealloc {
    [self removeNotification];
}

- (void)configSession {
    // 将设备输入对象添加到会话中
    if ([self.session canAddInput:self.deviceInput]) {
        [self.session addInput:self.deviceInput];
    }
    // 将设备输出对象添加到会话中
    if ([self.session canAddOutput:self.stillImageOutPut]) {
        [self.session addOutput:self.stillImageOutPut];
    }
    AVCaptureDevice * captureDevice = [self.deviceInput device];
    [self addNotificationToCaptureDevice:captureDevice];
}
~~~
PublicMethods:
~~~objective-c
#pragma mark - Public
/**
 * 配置预览层,将视频预览层添加到界面中
 */
- (void)configPreviewLayerInView:(UIView *)view belowView:(UIView *)belowView {
    CALayer *layer = view.layer;
    layer.masksToBounds=YES;
    self.videoPreviewLayer.frame=layer.bounds;
    [layer insertSublayer:self.videoPreviewLayer below:belowView.layer];
}
/**
 * 开启会话
 */
- (void)deviceStart {
    [self.session startRunning];
}
/**
 * 停止会话
 */
- (void)deviceStop {
    [self.session stopRunning];
}
/**
 *  设置闪光灯模式
 *
 *  @param flashMode 闪光灯模式
 */
- (void)setFlashMode:(AVCaptureFlashMode )flashMode {
    [self changeDeviceProperty:^(AVCaptureDevice *captureDevice) {
        if ([captureDevice isFlashModeSupported:flashMode]) {
            [captureDevice setFlashMode:flashMode];
        }
    }];
}
/**
 *  设置闪光灯按钮状态
 */
- (void)getFlashMode:(void(^)(BOOL flashAvailable, AVCaptureFlashMode flashMode))flashModeCallback {
    AVCaptureDevice *captureDevice = [self.deviceInput device];
    AVCaptureFlashMode flashMode = captureDevice.flashMode;
    flashModeCallback([captureDevice isFlashAvailable], flashMode);
}
/**
 *  点按手势,点按时聚焦
 */
- (void)tapScreenPoint:(CGPoint)point {
    //将UI坐标转化为摄像头坐标
    CGPoint cameraPoint = [self.videoPreviewLayer captureDevicePointOfInterestForPoint:point];
    [self focusWithMode:AVCaptureFocusModeAutoFocus exposureMode:AVCaptureExposureModeAutoExpose atPoint:cameraPoint];
}
/**
 * 切换摄像头
 */
- (void)cameraToggle {
    AVCaptureDevice *currentDevice=[self.deviceInput device];
    AVCaptureDevicePosition currentPosition=[currentDevice position];
    [self removeNotificationFromCaptureDevice:currentDevice];
    AVCaptureDevice *toChangeDevice;
    AVCaptureDevicePosition toChangePosition=AVCaptureDevicePositionFront;
    
    if (currentPosition==AVCaptureDevicePositionUnspecified||currentPosition==AVCaptureDevicePositionFront) {
        toChangePosition=AVCaptureDevicePositionBack;
    }
    
    toChangeDevice=[self getCameraDeviceWithPosition:toChangePosition];
    [self addNotificationToCaptureDevice:toChangeDevice];
    //获得要调整的设备输入对象
    AVCaptureDeviceInput *toChangeDeviceInput=[[AVCaptureDeviceInput alloc]initWithDevice:toChangeDevice error:nil];
    
    //改变会话的配置前一定要先开启配置,配置完成后提交配置改变
    [self.session beginConfiguration];
    //移除原有输入对象
    [self.session removeInput:self.deviceInput];
    //添加新的输入对象
    if ([self.session canAddInput:toChangeDeviceInput]) {
        [self.session addInput:toChangeDeviceInput];
        self.deviceInput=toChangeDeviceInput;
    }
    //提交会话配置
    [self.session commitConfiguration];
}
/**
 * 拍照
 */
- (void)cameraTakePhotoIsSave:(BOOL)isSaveAlbum Callback:(void(^)(UIImage * image, NSData * imgData, NSError * error))photoTokenCallback {
    //根据设备输出获得连接
    AVCaptureConnection *captureConnection=[self.stillImageOutPut connectionWithMediaType:AVMediaTypeVideo];
    //根据连接取得设备输出的数据
    __weak typeof(self) weakSelf = self;
    [self.stillImageOutPut captureStillImageAsynchronouslyFromConnection:captureConnection completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) {
        
        __strong typeof(self) strongSelf = weakSelf;
        if (imageDataSampleBuffer) {
            NSData * imageData=[AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];
            UIImage * image=[UIImage imageWithData:imageData];
            if (isSaveAlbum) {
                UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);
            }
            [strongSelf.session stopRunning];
            photoTokenCallback(image, imageData, error);
        } else {
            photoTokenCallback(nil, nil, error);
        }
        
    }];
}
~~~
PrivateMethods:
~~~objective-c
#pragma mark - 私有方法
/**
 *  取得指定位置的摄像头
 *
 *  @param position 摄像头位置
 *
 *  @return 摄像头设备
 */
- (AVCaptureDevice *)getCameraDeviceWithPosition:(AVCaptureDevicePosition )position {
    NSArray *cameras= [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    for (AVCaptureDevice *camera in cameras) {
        if ([camera position]==position) {
            return camera;
        }
    }
    return nil;
}
/**
 *  改变设备属性的统一操作方法
 *
 *  @param propertyChange 属性改变操作
 */
- (void)changeDeviceProperty:(void(^)(AVCaptureDevice *captureDevice))propertyChange {
    AVCaptureDevice *captureDevice= [self.deviceInput device];
    NSError *error;
    //注意改变设备属性前一定要首先调用lockForConfiguration:调用完之后使用unlockForConfiguration方法解锁
    if ([captureDevice lockForConfiguration:&error]) {
        propertyChange(captureDevice);
        [captureDevice unlockForConfiguration];
    } else {
        NSLog(@"设置设备属性过程发生错误,错误信息:%@",error.localizedDescription);
    }
}
/**
 *  设置聚焦模式
 *
 *  @param focusMode 聚焦模式
 */
- (void)setFocusMode:(AVCaptureFocusMode )focusMode {
    [self changeDeviceProperty:^(AVCaptureDevice *captureDevice) {
        if ([captureDevice isFocusModeSupported:focusMode]) {
            [captureDevice setFocusMode:focusMode];
        }
    }];
}
/**
 *  设置曝光模式
 *
 *  @param exposureMode 曝光模式
 */
- (void)setExposureMode:(AVCaptureExposureMode)exposureMode {
    [self changeDeviceProperty:^(AVCaptureDevice *captureDevice) {
        if ([captureDevice isExposureModeSupported:exposureMode]) {
            [captureDevice setExposureMode:exposureMode];
        }
    }];
}
/**
 *  设置聚焦点
 *
 *  @param point 聚焦点
 */
- (void)focusWithMode:(AVCaptureFocusMode)focusMode exposureMode:(AVCaptureExposureMode)exposureMode atPoint:(CGPoint)point {
    [self changeDeviceProperty:^(AVCaptureDevice *captureDevice) {
        if ([captureDevice isFocusModeSupported:focusMode]) {
            [captureDevice setFocusMode:AVCaptureFocusModeAutoFocus];
        }
        if ([captureDevice isFocusPointOfInterestSupported]) {
            [captureDevice setFocusPointOfInterest:point];
        }
        if ([captureDevice isExposureModeSupported:exposureMode]) {
            [captureDevice setExposureMode:AVCaptureExposureModeAutoExpose];
        }
        if ([captureDevice isExposurePointOfInterestSupported]) {
            [captureDevice setExposurePointOfInterest:point];
        }
    }];
}
~~~

控制器PhotoTakeViewController

接口:
~~~objective-c
#import <UIKit/UIKit.h>
@protocol PhotoTakeDelegate <NSObject>
- (void)photoTokenImage:(UIImage *)image ImgData:(NSData *)imgData ErrorMsg:(NSString *)errorMsg;
@end
@interface PhotoTakeViewController : UIViewController
@property (nonatomic, weak) id<PhotoTakeDelegate> delegate;
@property (nonatomic, assign) BOOL isSaveAlbum;
@end
~~~
匿名类别:
~~~objective-c
#import "PhotoTakeViewController.h"
#import <AVFoundation/AVFoundation.h>
#import <AssetsLibrary/AssetsLibrary.h>
#import "PhotoTakeDevice.h"
@interface PhotoTakeViewController ()
{
    /// 闪光相关灯按钮
    __weak IBOutlet UIButton * _flashLightAutoButton;
    __weak IBOutlet UIButton * _flashLightOnButton;
    __weak IBOutlet UIButton * _flashLightOffButton;
    /// 拍照按钮
    __weak IBOutlet UIButton * _takePicButton;
    /// 取消按钮
    __weak IBOutlet UIButton * _cancelButton;
    /// 使用照片按钮
    __weak IBOutlet UIButton * _usePicButton;
    /// 图像预览层
    __weak IBOutlet UIView * _containerView;
    /// 聚焦光标
    __weak IBOutlet UIImageView * _focusCursor;
    /// 拍摄拾取的照片
    NSData * _imageData;
    UIImage * _image;
}
/// 设备类
@property (nonatomic, strong) PhotoTakeDevice * photoTakeDevice;
@end
~~~
LifeCycle:
~~~objective-c
- (void)viewDidLoad {
    [super viewDidLoad];
    _photoTakeDevice = [[PhotoTakeDevice alloc] init];
    [_photoTakeDevice configPreviewLayerInView:_containerView belowView:_focusCursor];
    [self setFlashModeButtonStatus];
}
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [_photoTakeDevice deviceStart];
}
- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [_photoTakeDevice deviceStop];
}

/**
 *  设置闪光灯按钮状态
 */
- (void)setFlashModeButtonStatus
{
    [_photoTakeDevice getFlashMode:^(BOOL flashAvailable, AVCaptureFlashMode flashMode) {
        
        if(!flashAvailable) {
            _flashLightAutoButton.hidden=YES;
            _flashLightOnButton.hidden=YES;
            _flashLightOffButton.hidden=YES;
            return ;
        }
        _flashLightAutoButton.hidden=NO;
        _flashLightOnButton.hidden=NO;
        _flashLightOffButton.hidden=NO;
        _flashLightAutoButton.enabled=YES;
        _flashLightOnButton.enabled=YES;
        _flashLightOffButton.enabled=YES;
        
        switch (flashMode) {
            case AVCaptureFlashModeAuto:
                _flashLightAutoButton.enabled=NO;
                break;
            case AVCaptureFlashModeOn:
                _flashLightOnButton.enabled=NO;
                break;
            case AVCaptureFlashModeOff:
                _flashLightOffButton.enabled=NO;
                break;
            default:
                break;
        }
    }];
}
/**
 *  设置聚焦光标位置
 *
 *  @param point 光标位置
 */
- (void)setFocusCursorWithPoint:(CGPoint)point {
    _focusCursor.center=point;
    _focusCursor.transform=CGAffineTransformMakeScale(1.5, 1.5);
    _focusCursor.alpha=1.0;
    [UIView animateWithDuration:1.0 animations:^{
        _focusCursor.transform=CGAffineTransformIdentity;
    } completion:^(BOOL finished) {
        _focusCursor.alpha=0;
        
    }];
}
~~~
响应:
~~~objective-c
/**
 * 退出
 */
- (IBAction)exitTakeCamera {
    [self dismissViewControllerAnimated:true completion:nil];
}
/**
 * 闪光灯切换
 */
- (IBAction)flashLightChanged:(UIButton *)sender {
    switch (sender.tag) {
        case 0: //关闭闪光灯
        {
            [_photoTakeDevice setFlashMode:AVCaptureFlashModeOff];
            [self setFlashModeButtonStatus];
            break;
        }
        case 1: //打开闪光灯
        {
            [_photoTakeDevice setFlashMode:AVCaptureFlashModeOn];
            [self setFlashModeButtonStatus];
            break;
        }
        case 2: //自动闪光灯开启
        {
            [_photoTakeDevice setFlashMode:AVCaptureFlashModeAuto];
            [self setFlashModeButtonStatus];
            break;
        }
    }
}
/**
 * 摄像头切换
 */
- (IBAction)toggleCamera:(UIButton *)sender {
    [_photoTakeDevice cameraToggle];
    [self setFlashModeButtonStatus];
}
/**
 * 拍照
 */
- (IBAction)takePicture {
    [_photoTakeDevice cameraTakePhotoIsSave:false Callback:^(UIImage *image, NSData *imgData, NSError *error) {
        if (error) {
            BOOL existDelegate = [self.delegate respondsToSelector:@selector(photoTokenImage:ImgData:ErrorMsg:)];
            if (!existDelegate)
            {
                return ;
            }
            [self.delegate photoTokenImage:image ImgData:imgData ErrorMsg:error.localizedDescription];
            [self exitTakeCamera];
        } else {
            _image = image;
            _imageData = imgData;
            _takePicButton.hidden = true;
            _cancelButton.hidden = false;
            _usePicButton.hidden = false;
        }
    }];
}
/**
 * 拍照后使用照片
 */
- (IBAction)usePicture {
    if ([self.delegate respondsToSelector:@selector(photoTokenImage:ImgData:ErrorMsg:)]) {
        [self.delegate photoTokenImage:_image ImgData:_imageData ErrorMsg:nil];
        [self exitTakeCamera];
    }
}
/**
 * 拍照后取消
 */
- (IBAction)cancelPicture {
    _takePicButton.hidden = false;
    _cancelButton.hidden = true;
    _usePicButton.hidden = true;
    [_photoTakeDevice deviceStart];
}
/**
 *  点按手势,点按时聚焦
 */
- (IBAction)tapScreen:(UITapGestureRecognizer *)tapGesture {
    CGPoint point= [tapGesture locationInView:_containerView];
    [self setFocusCursorWithPoint:point];
    [_photoTakeDevice tapScreenPoint:point];
}
~~~

使用这个封装好的相机控制器进行摄像

~~~objective-c
#import "ViewController.h"
#import "PhotoTakeViewController.h"
@interface ViewController ()
<PhotoTakeDelegate>
{
    __weak IBOutlet UIImageView *_imgView;
}
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
}
- (IBAction)toLyraPhotoTakeVC:(id)sender {
    PhotoTakeViewController * cameraVC = [[PhotoTakeViewController alloc] init];
    cameraVC.delegate = self;
    [self presentViewController:cameraVC animated:true completion:nil];
}
- (void)photoTokenImage:(UIImage *)image ImgData:(NSData *)imgData ErrorMsg:(NSString *)errorMsg {
    if (errorMsg) {
        NSLog(@"%@", errorMsg);
    } else {
        _imgView.image = image;
    }
}
@end
~~~