iOS開發 AVFoundation自定義相機

敖老黑發表於2017-12-21

直插正題!

首先匯入一個標頭檔案

#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可以管理多個輸入輸出裝置,如下圖所示

輸入輸出裝置之間的關係(圖片來自蘋果官方開發文件)

接下來初始化所有物件,下面這個方法的呼叫我放到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的關係如下圖

圖片來自蘋果官方開發文件

接下來搞一個獲取裝置方向的方法,再配置圖片輸出的時候需要使用

     -(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"];
複製程式碼

第一次寫東西哈,寫的不好見諒有錯誤請指出。~~~

相關文章