天天看點

AVFoundation自定義相機AVFoundation自定義相機

AVFoundation自定義相機

一般需要使用相機時候, 調用系統的相機就可以了, 但是如果有複雜的自定義拍照需求的話, 使用更強大的AVFoundation就會很友善, 可以實作自定義拍照界面, 不顯示預覽層的盲拍, 以及不存儲手機相冊, 音量鍵拍照等功能都可以輕松實作.

導入依賴庫 AVFoundation.framework

首先導入一個頭檔案

#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鏡頭捕捉到得預覽圖層

接下來初始化所有對象,下面這個方法的調用我放到viewDidLoad裡面調用了

- (void)initAVCaptureSession{

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

NSError *error;

AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

//更改這個設定的時候必須先鎖定裝置,修改完後再解鎖,否則崩潰
[device lockForConfiguration:nil];
//設定閃光燈為自動
[device setFlashMode:AVCaptureFlashModeAuto];

//這句代碼是對夜間拍照時候的自動補光, 如果沒有這句代碼, 晚上拍照基本上是黑色的, 比蘋果系統的相機照片差很多
if (device.isLowLightBoostSupported) {
        device.automaticallyEnablesLowLightBoostWhenAvailable = YES;
    }

 [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];
 }
           

之後在viewWillAppear,viewDidDisappear方法裡開啟和關閉session

注意:session的開啟時很消耗CUP的  一直處于開啟狀态  會導緻手機發熱  不建議放在viewWillApper方法裡面開啟和viewDidDisappear裡面關閉

最好是拍照前開啟 拍照後關閉

- (void)viewWillAppear:(BOOL)animated{

    [super viewWillAppear:YES];

    if ([self.session isRunning]) {
        
    }else{
        [self.session startRunning];
    }
}


- (void)viewDidDisappear:(BOOL)animated{

   [super viewDidDisappear:YES];

   if ([self.session isRunning]) {
        [self.session stopRunning];

    }
}
           

到這裡所有的初始化工作基本完成,運作程式可以看到鏡頭捕捉到得畫面。接下來實作拍照按鈕

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

-(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) {

        }];         
        //以上是寫入手機相冊的代碼  如果有不寫入手機相冊的需求  注掉以下代碼 就好

       //這裡還有一個問題 就是如果旋轉手機後拍照後直接上傳到伺服器 伺服器收到的圖檔是反的(上下颠倒) 但是如果是在手機相冊檢視是正常的 是以這裡需要做一下處理

       UIImage *img = [UIImage imageWithData:jpegData];
       if (img.imageOrientation == UIImageOrientationDown) {
          NSLog(@"照片反了!!!!");
          img = [self normalizedImage:img];
       }
            
      if (img.imageOrientation == UIImageOrientationUp) {
          NSLog(@"照片正常!!");
       }

     //這裡寫上傳伺服器的代碼就好了 将img傳過去即可
}];

}
           
- (UIImage *)normalizedImage:(UIImage *)image {
    if (image.imageOrientation == UIImageOrientationUp){
        return image;
    }
    
    UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale);
    [image drawInRect:(CGRect){0, 0, image.size}];
    UIImage *normalizedImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    return normalizedImage;
}
           

至此相機的拍照功能已經完成

  • [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];
    }
               
    </pre><pre>
               

    閃光燈的設定非常簡單,隻需要修改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];
               
  • 如果是橫屏開發時候會出現預覽層貌似旋轉的效果
  • 解決方法:
  • 添加代碼
    self.previewLayer.orientation = AVCaptureVideoOrientationLandscapeRight;
               
    注意事項: 如果使用過程中出現 拍攝照片是黑色的(就像是在晚上拍的一樣)
  • 是以下代碼的使用問題
  • if (self.session) {
    
        [self.session startRunning];
        }
               
  • demo示範下載下傳位址
  • https://github.com/RockyAo/RACustomCamera

繼續閱讀