Osheep

时光不回头,当下最重要。

iOS开发--AVFoundation自定义相机

直插正题!

首先导入一个头文件

    #import <AVFoundation/AVFoundation.h>

由于后面我们需要将拍摄好的照片写入系统相册中,所以我们在这里还需要导入一个相册需要的头文件

   #import <AssetsLibrary/AssetsLibrary.h>

导入头文件后我们需要创建几个相机必须的属性

    /**
     *  AVCaptureSession对象来执行输入设备和输出设备之间的数据传递
     */
    @property (nonatomic, strong) AVCaptureSession* session;
      /**
       *  输入设备
       */
    @property (nonatomic, strong) AVCaptureDeviceInput* videoInput;
      /**
       *  照片输出流
       */
    @property (nonatomic, strong) AVCaptureStillImageOutput* stillImageOutput;
      /**
       *  预览图层
       */
    @property (nonatomic, strong) AVCaptureVideoPreviewLayer* previewLayer;

AVCaptureSession控制输入和输出设备之间的数据传递
AVCaptureDeviceInput调用所有的输入硬件。例如摄像头和麦克风
AVCaptureStillImageOutput用于输出图像
AVCaptureVideoPreviewLayer镜头捕捉到得预览图层

一个session可以管理多个输入输出设备,如下图所示

《iOS开发--AVFoundation自定义相机》

输入输出设备之间的关系(图片来自苹果官方开发文档)

接下来初始化所有对象,下面这个方法的调用我放到viewDidLoad里面调用了

- (void)initAVCaptureSession{

self.session = [[AVCaptureSession alloc] init];

NSError *error;

AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

//更改这个设置的时候必须先锁定设备,修改完后再解锁,否则崩溃
[device lockForConfiguration:nil];
//设置闪光灯为自动
[device setFlashMode:AVCaptureFlashModeAuto];
[device unlockForConfiguration];

self.videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error];
if (error) {
    NSLog(@"%@",error);
}
self.stillImageOutput = [[AVCaptureStillImageOutput alloc] init];
//输出设置。AVVideoCodecJPEG   输出jpeg格式图片
NSDictionary * outputSettings = [[NSDictionary alloc] initWithObjectsAndKeys:AVVideoCodecJPEG,AVVideoCodecKey, nil];
[self.stillImageOutput setOutputSettings:outputSettings];

if ([self.session canAddInput:self.videoInput]) {
    [self.session addInput:self.videoInput];
}
if ([self.session canAddOutput:self.stillImageOutput]) {
    [self.session addOutput:self.stillImageOutput];
}

//初始化预览图层
self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session];
[self.previewLayer setVideoGravity:AVLayerVideoGravityResizeAspect];
NSLog(@"%f",kMainScreenWidth);
self.previewLayer.frame = CGRectMake(0, 0,kMainScreenWidth, kMainScreenHeight - 64);
self.backView.layer.masksToBounds = YES;
[self.backView.layer addSublayer:self.previewLayer];
 }

上面代码中

//该代码可能导致预览界面无法全屏(在frame正确的情况下)
[self.previewLayer setVideoGravity:AVLayerVideoGravityResizeAspect];

修改为

self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;

感谢 @胖成桩桩 的提醒

之后在viewWillAppear,viewDidDisappear方法里开启和关闭session

- (void)viewWillAppear:(BOOL)animated{

    [super viewWillAppear:YES];

    if (self.session) {

    [self.session startRunning];
    }
}


- (void)viewDidDisappear:(BOOL)animated{

   [super viewDidDisappear:YES];

   if (self.session) {

     [self.session stopRunning];
   }
}

到这里所有的初始化工作基本完成,运行程序可以看到镜头捕捉到得画面。接下来实现拍照按钮。

输出图像的时候需要用到AVCaptureConnection这个类,session通过AVCaptureConnection连接AVCaptureStillImageOutput进行图片输出,输入输出与connection的关系如下图

《iOS开发--AVFoundation自定义相机》

图片来自苹果官方开发文档

接下来搞一个获取设备方向的方法,再配置图片输出的时候需要使用

 -(AVCaptureVideoOrientation)avOrientationForDeviceOrientation:(UIDeviceOrientation)deviceOrientation
 {
    AVCaptureVideoOrientation result = (AVCaptureVideoOrientation)deviceOrientation;
    if ( deviceOrientation == UIDeviceOrientationLandscapeLeft )
       result = AVCaptureVideoOrientationLandscapeRight;
    else if ( deviceOrientation == UIDeviceOrientationLandscapeRight )
       result = AVCaptureVideoOrientationLandscapeLeft;
    return result;
}

下面是拍照按钮方法

 - (IBAction)takePhotoButtonClick:(UIBarButtonItem *)sender {

    AVCaptureConnection *stillImageConnection = [self.stillImageOutput        connectionWithMediaType:AVMediaTypeVideo];
    UIDeviceOrientation curDeviceOrientation = [[UIDevice currentDevice] orientation];
    AVCaptureVideoOrientation avcaptureOrientation = [self avOrientationForDeviceOrientation:curDeviceOrientation];
   [stillImageConnection setVideoOrientation:avcaptureOrientation];
   [stillImageConnection setVideoScaleAndCropFactor:1];

   [self.stillImageOutput captureStillImageAsynchronouslyFromConnection:stillImageConnection completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) {

        NSData *jpegData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];
        CFDictionaryRef attachments = CMCopyDictionaryOfAttachments(kCFAllocatorDefault,
                                                                imageDataSampleBuffer,
                                                                kCMAttachmentMode_ShouldPropagate);

        ALAuthorizationStatus author = [ALAssetsLibrary authorizationStatus];
        if (author == ALAuthorizationStatusRestricted || author == ALAuthorizationStatusDenied){
            //无权限
            return ;
        }
        ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
        [library writeImageDataToSavedPhotosAlbum:jpegData metadata:(__bridge id)attachments completionBlock:^(NSURL *assetURL, NSError *error) {

        }];

   }];
}

至此相机的拍照功能已经完成
注:

  • [stillImageConnection setVideoScaleAndCropFactor:1];这个方法是控制焦距用的暂时先固定为1,后边写手势缩放焦距的时候会修改这里
  • 照片写入相册之前需要进行旋转(我在代码里并没有进行旋转)
  • 写入相册之前需要判断用户是否允许了程序访问相册,否则程序会崩溃,包括在开启相机的时候和拍摄按钮点击的时候都需要做安全验证,验证设别是否支持拍照,用户是否允许程序访问相机。

接下来完成闪光灯

- (IBAction)flashButtonClick:(UIBarButtonItem *)sender {

   NSLog(@"flashButtonClick");

   AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

   //修改前必须先锁定
   [device lockForConfiguration:nil];
   //必须判定是否有闪光灯,否则如果没有闪光灯会崩溃
   if ([device hasFlash]) {

      if (device.flashMode == AVCaptureFlashModeOff) {
        device.flashMode = AVCaptureFlashModeOn;

          [sender setTitle:@"flashOn"];
      } else if (device.flashMode == AVCaptureFlashModeOn) {
          device.flashMode = AVCaptureFlashModeAuto;
          [sender setTitle:@"flashAuto"];
      } else if (device.flashMode == AVCaptureFlashModeAuto) {
          device.flashMode = AVCaptureFlashModeOff;
          [sender setTitle:@"flashOff"];
      }

   } else {

      NSLog(@"设备不支持闪光灯");
   }
   [device unlockForConfiguration];
}

闪光灯的设置非常简单,只需要修改device的flashMode属性即可,这里需要注意的是,修改device时候需要先锁住,修改完成后再解锁,否则会崩溃,设置闪光灯的时候也需要做安全判断,验证设备是否支持闪光灯,有些iOS设备是没有闪光灯的,如果不做判断还是会crash掉 T_T

剩下一个小功能就是切回镜头了,方法如下

- (IBAction)switchCameraSegmentedControlClick:(UISegmentedControl *)sender {

     NSLog(@"%ld",(long)sender.selectedSegmentIndex);

    AVCaptureDevicePosition desiredPosition;
    if (isUsingFrontFacingCamera){
       desiredPosition = AVCaptureDevicePositionBack;
    }else{
       desiredPosition = AVCaptureDevicePositionFront;
    }

    for (AVCaptureDevice *d in [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) {
        if ([d position] == desiredPosition) {
            [self.previewLayer.session beginConfiguration];
            AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:d error:nil];
            for (AVCaptureInput *oldInput in self.previewLayer.session.inputs) {
               [[self.previewLayer session] removeInput:oldInput];
            }
            [self.previewLayer.session addInput:input];
            [self.previewLayer.session commitConfiguration];
            break;
        }
    }

    isUsingFrontFacingCamera = !isUsingFrontFacingCamera;
}

isUsingFrontFacingCamera这个属性是个BOOL值变量,前面忘记写这个属性了。用于防止重复切换统一摄像头,调用这个点击方法的控件是个segement,文章最后我会附上demo地址。

最后一步就是加入手势缩放,手动调节相机焦距。
加入两个属性,并遵守这个协议<UIGestureRecognizerDelegate>

      /**
        *  记录开始的缩放比例
        */
      @property(nonatomic,assign)CGFloat beginGestureScale;
     /**
      * 最后的缩放比例
      */
      @property(nonatomic,assign)CGFloat effectiveScale;

这两个属性分别用于记录缩放的比例。相机支持的焦距是1.0~67.5,所以再控制器加载的时候分别给这两个属性附上一个初值 1.0。之后给view添加一个缩放手势,手势调用的方法如下

//缩放手势 用于调整焦距
- (void)handlePinchGesture:(UIPinchGestureRecognizer *)recognizer{

  BOOL allTouchesAreOnThePreviewLayer = YES;
  NSUInteger numTouches = [recognizer numberOfTouches], i;
  for ( i = 0; i < numTouches; ++i ) {
      CGPoint location = [recognizer locationOfTouch:i inView:self.backView];
      CGPoint convertedLocation = [self.previewLayer convertPoint:location fromLayer:self.previewLayer.superlayer];
      if ( ! [self.previewLayer containsPoint:convertedLocation] ) {
          allTouchesAreOnThePreviewLayer = NO;
          break;
       }
}

   if ( allTouchesAreOnThePreviewLayer ) {


       self.effectiveScale = self.beginGestureScale * recognizer.scale;
       if (self.effectiveScale < 1.0){
          self.effectiveScale = 1.0;
       }

       NSLog(@"%f-------------->%f------------recognizerScale%f",self.effectiveScale,self.beginGestureScale,recognizer.scale);

       CGFloat maxScaleAndCropFactor = [[self.stillImageOutput connectionWithMediaType:AVMediaTypeVideo] videoMaxScaleAndCropFactor];

       NSLog(@"%f",maxScaleAndCropFactor);
       if (self.effectiveScale > maxScaleAndCropFactor)
        self.effectiveScale = maxScaleAndCropFactor;

       [CATransaction begin];
       [CATransaction setAnimationDuration:.025];
       [self.previewLayer setAffineTransform:CGAffineTransformMakeScale(self.effectiveScale, self.effectiveScale)];
       [CATransaction commit];

    }

}

这样之再实现一个delegate

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
   if ( [gestureRecognizer isKindOfClass:[UIPinchGestureRecognizer class]] ) {
       self.beginGestureScale = self.effectiveScale;
   }
   return YES;
}

在每次手势开始的时候把上一次实际缩放值赋给初始缩放值,如果不这么做的话你会发现每次手势开始的时候界面都会跳来跳去的(非常性感)。一个简单功能的相机基本上完成了,最后一步就是之前我们在拍照的方法里写死了一个1.0,我们还需要修改一下它,,否则虽然你看到的界面焦距改变了,但是实际拍出来的照片是没有变化的。找到拍照方法里的

[stillImageConnection setVideoScaleAndCropFactor:1.0];

修改为

[stillImageConnection setVideoScaleAndCropFactor:self.effectiveScale];

至此大功告成。

文章演示demo下载
(https://github.com/RockyAo/RACustomCamera)

苹果官方演示demo下载
(https://github.com/RockyAo/CameraDemo)

官方的演示demo里面还有个面部识别。

最后如果你想监听相机的对焦事件的话
再viewWillApper里面添加个监听

AVCaptureDevice *camDevice =[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
int flags =NSKeyValueObservingOptionNew;
[camDevice addObserver:self forKeyPath:@"adjustingFocus" options:flags context:nil];

然后实现下面方法

-(void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context {..........//在这里做你想做的事~~~}

最后别忘了再viewDidDisapper方法里移除监听

AVCaptureDevice*camDevice =[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
[camDevice removeObserver:self forKeyPath:@"adjustingFocus"];

第一次写东西哈,写的不好见谅有错误请指出。~~~

点赞