IOS音視訊(二)AVFoundation視訊捕捉

孔雨露發表於2020-04-05

@TOC

1. 媒體捕捉概念

  • 理解捕捉媒體,需要先了解一些基本概念:
  • 捕捉會話

AVCaptureSession 是管理捕獲活動並協調從輸入裝置到捕獲輸出的資料流的物件。 AVCaptureSession 用於連線輸入和輸出的資源,從物理裝置如攝像頭和麥克風等獲取資料流,輸出到一個或多個目的地。 AVCaptureSession 可以額外配置一個會話預設值(session preset),用於控制捕捉資料的格式和質量,預設值預設值為 AVCaptureSessionPresetHigh。

要執行實時捕獲,需要例項化AVCaptureSession物件並新增適當的輸入和輸出。下面的程式碼片段演示瞭如何配置捕獲裝置來錄製音訊。

// Create the capture session.
let captureSession = AVCaptureSession()

// Find the default audio device.
guard let audioDevice = AVCaptureDevice.default(for: .audio) else { return }

do {
    // Wrap the audio device in a capture device input.
    let audioInput = try AVCaptureDeviceInput(device: audioDevice)
    // If the input can be added, add it to the session.
    if captureSession.canAddInput(audioInput) {
        captureSession.addInput(audioInput)
    }
} catch {
    // Configuration failed. Handle error.
}
複製程式碼

您可以呼叫startRunning()來啟動從輸入到輸出的資料流,並呼叫stopRunning()來停止該流。

注意:startRunning()方法是一個阻塞呼叫,可能會花費一些時間,因此應該在序列佇列上執行會話設定,以免阻塞主佇列(這使UI保持響應)。參見AVCam:構建攝像機應用程式的實現示例

  • 捕捉裝置:

AVCaptureDevice 是為捕獲會話提供輸入(如音訊或視訊)併為特定於硬體的捕獲特性提供控制的裝置。它為物理裝置定義統一介面,以及大量控制方法,獲取指定型別的預設裝置方法如下:self.activeVideoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

  1. 一個 AVCaptureDevice 物件表示一個物理捕獲裝置和與該裝置相關聯的屬性。您可以使用捕獲裝置來配置底層硬體的屬性。捕獲裝置還向AVCaptureSession物件提供輸入資料(如音訊或視訊)。
  • 捕捉裝置的輸入:

不能直接將 AVCaptureDevice 加入到 AVCaptureSession 中,需要封裝為 AVCaptureDeviceInput

 self.captureVideoInput = [AVCaptureDeviceInput deviceInputWithDevice:self.activeVideoDevice error:&videoError];
    if (self.captureVideoInput) {
        if ([self.captureSession canAddInput:self.captureVideoInput]){
            [self.captureSession addInput:self.captureVideoInput];
        }
    } else if (videoError) {
    }
複製程式碼
  • 捕捉輸出 :

AVCaptureOutput 作為抽象基類提供了捕捉會話資料流的輸出目的地,同時定義了此抽象類的高階擴充套件類。

  1. AVCaptureStillImageOutput - 靜態照片( 在ios10後被廢棄,使用AVCapturePhotoOutput代替)
  2. AVCaptureMovieFileOutput - 視訊,
  3. AVCaptureAudioFileOutput - 音訊
  4. AVCaptureAudioDataOutput - 音訊底層數字樣本
  5. AVCaptureVideoDataOutput - 視訊底層數字樣本
  • 捕捉連線

AVCaptureConnection :捕獲會話中捕獲輸入和捕獲輸出物件的特定對之間的連線。AVCaptureConnection 用於確定哪些輸入產生視訊,哪些輸入產生音訊,能夠禁用特定連線或訪問單獨的音訊軌道。

  1. 捕獲輸入有一個或多個輸入埠(avcaptureinpu . port的例項)。捕獲輸出可以接受來自一個或多個源的資料(例如,AVCaptureMovieFileOutput物件同時接受視訊和音訊資料)。 只有在canAddConnection(:)方法返回true時,才可以使用addConnection(:)方法將AVCaptureConnection例項新增到會話中。當使用addInput(:)或addOutput(:)方法時,會話自動在所有相容的輸入和輸出之間形成連線。在新增沒有連線的輸入或輸出時,只需手動新增連線。您還可以使用連線來啟用或禁用來自給定輸入或到給定輸出的資料流。
  • 捕捉預覽 :

AVCaptureVideoPreviewLayer 是一個 CALayer 的子類,可以對捕捉視訊資料進行實時預覽。

2. 視訊捕捉例項

  • 這個例項的專案程式碼點選這裡下載:OC 視訊捕獲相機Demo
  • 專案是OC編寫的,主要功能實現在THCameraController中,如下圖:
    視訊捕獲功能實現類
  • 主要介面變數在標頭檔案THCameraController.h裡面:

#import <AVFoundation/AVFoundation.h>

extern NSString *const THThumbnailCreatedNotification;

@protocol THCameraControllerDelegate <NSObject>

// 1發生錯誤事件是,需要在物件委託上呼叫一些方法來處理
- (void)deviceConfigurationFailedWithError:(NSError *)error;
- (void)mediaCaptureFailedWithError:(NSError *)error;
- (void)assetLibraryWriteFailedWithError:(NSError *)error;
@end

@interface THCameraController : NSObject

@property (weak, nonatomic) id<THCameraControllerDelegate> delegate;
@property (nonatomic, strong, readonly) AVCaptureSession *captureSession;


// 2 用於設定、配置視訊捕捉會話
- (BOOL)setupSession:(NSError **)error;
- (void)startSession;
- (void)stopSession;

// 3 切換不同的攝像頭
- (BOOL)switchCameras;
- (BOOL)canSwitchCameras;
@property (nonatomic, readonly) NSUInteger cameraCount;
@property (nonatomic, readonly) BOOL cameraHasTorch; //手電筒
@property (nonatomic, readonly) BOOL cameraHasFlash; //閃光燈
@property (nonatomic, readonly) BOOL cameraSupportsTapToFocus; //聚焦
@property (nonatomic, readonly) BOOL cameraSupportsTapToExpose;//曝光
@property (nonatomic) AVCaptureTorchMode torchMode; //手電筒模式
@property (nonatomic) AVCaptureFlashMode flashMode; //閃光燈模式

// 4 聚焦、曝光、重設聚焦、曝光的方法
- (void)focusAtPoint:(CGPoint)point;
- (void)exposeAtPoint:(CGPoint)point;
- (void)resetFocusAndExposureModes;

// 5 實現捕捉靜態圖片 & 視訊的功能

//捕捉靜態圖片
- (void)captureStillImage;

//視訊錄製
//開始錄製
- (void)startRecording;

//停止錄製
- (void)stopRecording;

//獲取錄製狀態
- (BOOL)isRecording;

//錄製時間
- (CMTime)recordedDuration;

@end

複製程式碼
  • 我們需要新增訪問許可權,如果沒有獲取到相機和麥克風許可權,在設定 captureVideoInput 時就會出錯。
/// 檢測 AVAuthorization 許可權
/// 傳入待檢查的 AVMediaType,AVMediaTypeVideo or AVMediaTypeAudio
/// 返回是否許可權可用
- (BOOL)ifAVAuthorizationValid:(NSString *)targetAVMediaType grantedCallback:(void (^)())grantedCallback
{
    NSString *mediaType = targetAVMediaType;
    BOOL result = NO;
    if ([AVCaptureDevice respondsToSelector:@selector(authorizationStatusForMediaType:)]) {
        AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:mediaType];
        switch (authStatus) {
            case AVAuthorizationStatusNotDetermined: { // 尚未請求授權
                [AVCaptureDevice requestAccessForMediaType:targetAVMediaType completionHandler:^(BOOL granted) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        if (granted) {
                            grantedCallback();
                        }
                    });
                }];
                break;
            }
            case AVAuthorizationStatusDenied: { // 明確拒絕
                if ([mediaType isEqualToString:AVMediaTypeVideo]) {
                    [METSettingPermissionAlertView showAlertViewWithPermissionType:METSettingPermissionTypeCamera];// 申請相機許可權
                } else if ([mediaType isEqualToString:AVMediaTypeAudio]) {
                    [METSettingPermissionAlertView showAlertViewWithPermissionType:METSettingPermissionTypeMicrophone];// 申請麥克風許可權
                }
                break;
            }
            case AVAuthorizationStatusRestricted: { // 限制許可權更改
                break;
            }
            case AVAuthorizationStatusAuthorized: { // 已授權
                result = YES;
                break;
            }
            default: // 兜底
                break;
        }
    }
    return result;
}
複製程式碼

2.1 建立預覽檢視

    self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] init];
    [self.previewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
    [self.previewLayer setSession:self.cameraHelper.captureSession];
    self.previewLayer.frame = CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT - 50);
    [self.previewImageView.layer addSublayer:self.previewLayer];
複製程式碼
  • 也可以通過 view 的類方法直接換掉 view 的 CALayer 例項:
+ (Class)layerClass {
    return [AVCaptureVideoPreviewLayer class];
}

- (AVCaptureSession*)session {
    return [(AVCaptureVideoPreviewLayer*)self.layer session];
}

- (void)setSession:(AVCaptureSession *)session {
    [(AVCaptureVideoPreviewLayer*)self.layer setSession:session];
}
複製程式碼
  • AVCaptureVideoPreviewLayer 定義了兩個方法用於在螢幕座標系和裝置座標系之間轉換,裝置座標系規定左上角為 (0,0),右下角為(1,1)。
  1. (CGPoint)captureDevicePointOfInterestForPoint:(CGPoint)pointInLayer 從螢幕座標系的點轉換為裝置座標系
  2. (CGPoint)pointForCaptureDevicePointOfInterest:(CGPoint)captureDevicePointOfInterest 從裝置座標系的點轉換為螢幕座標系

2.2 設定捕捉會話

  • 首先是初始化捕捉會話:
    self.captureSession = [[AVCaptureSession alloc]init];
    [self.captureSession setSessionPreset:(self.isVideoMode)?AVCaptureSessionPreset1280x720:AVCaptureSessionPresetPhoto];
複製程式碼
  • 根據拍攝視訊還是拍攝照片選擇不同的預設值,然後設定會話輸入:
- (void)configSessionInput
{
    // 攝像頭輸入
    NSError *videoError = [[NSError alloc] init];
    self.activeVideoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    self.flashMode = self.activeVideoDevice.flashMode;
    self.captureVideoInput = [AVCaptureDeviceInput deviceInputWithDevice:self.activeVideoDevice error:&videoError];
    if (self.captureVideoInput) {
        if ([self.captureSession canAddInput:self.captureVideoInput]){
            [self.captureSession addInput:self.captureVideoInput];
        }
    } else if (videoError) {
    }
    
    if (self.isVideoMode) {
        // 麥克風輸入
        NSError *audioError = [[NSError alloc] init];
        AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio] error:&audioError];
        if (audioInput) {
            if ([self.captureSession canAddInput:audioInput]) {
                [self.captureSession addInput:audioInput];
            }
        } else if (audioError) {
        }
    }
}
複製程式碼
  • 對攝像頭和麥克風裝置均封裝為 AVCaptureDeviceInput 後加入到會話中。然後配置會話輸出:
- (void)configSessionOutput
{
    if (self.isVideoMode) {
        // 視訊輸出
        self.movieFileOutput = [[AVCaptureMovieFileOutput alloc] init];
        if ([self.captureSession canAddOutput:self.movieFileOutput]) {
            [self.captureSession addOutput:self.movieFileOutput];
        }
    } else {
        // 圖片輸出
        self.imageOutput = [[AVCaptureStillImageOutput alloc] init];
        self.imageOutput.outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG};// 配置 outputSetting 屬性,表示希望捕捉 JPEG 格式的圖片
        if ([self.captureSession canAddOutput:self.imageOutput]) {
            [self.captureSession addOutput:self.imageOutput];
        }
    }
}
複製程式碼
  • 當然你也可以合成在一個方法裡面直接設定捕獲會話
- (BOOL)setupSession:(NSError **)error {

    
    //建立捕捉會話。AVCaptureSession 是捕捉場景的中心樞紐
    self.captureSession = [[AVCaptureSession alloc]init];
    
    /*
     AVCaptureSessionPresetHigh
     AVCaptureSessionPresetMedium
     AVCaptureSessionPresetLow
     AVCaptureSessionPreset640x480
     AVCaptureSessionPreset1280x720
     AVCaptureSessionPresetPhoto
     */
    //設定影像的解析度
    self.captureSession.sessionPreset = AVCaptureSessionPresetHigh;
    
    //拿到預設視訊捕捉裝置 iOS系統返回後置攝像頭
    AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    
    //將捕捉裝置封裝成AVCaptureDeviceInput
    //注意:為會話新增捕捉裝置,必須將裝置封裝成AVCaptureDeviceInput物件
    AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:error];
    
    //判斷videoInput是否有效
    if (videoInput)
    {
        //canAddInput:測試是否能被新增到會話中
        if ([self.captureSession canAddInput:videoInput])
        {
            //將videoInput 新增到 captureSession中
            [self.captureSession addInput:videoInput];
            self.activeVideoInput = videoInput;
        }
    }else
    {
        return NO;
    }
    
    //選擇預設音訊捕捉裝置 即返回一個內建麥克風
    AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
    
    //為這個裝置建立一個捕捉裝置輸入
    AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:error];
   
    //判斷audioInput是否有效
    if (audioInput) {
        
        //canAddInput:測試是否能被新增到會話中
        if ([self.captureSession canAddInput:audioInput])
        {
            //將audioInput 新增到 captureSession中
            [self.captureSession addInput:audioInput];
        }
    }else
    {
        return NO;
    }

    //AVCaptureStillImageOutput 例項 從攝像頭捕捉靜態圖片
    self.imageOutput = [[AVCaptureStillImageOutput alloc]init];
    
    //配置字典:希望捕捉到JPEG格式的圖片
    self.imageOutput.outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG};
    
    //輸出連線 判斷是否可用,可用則新增到輸出連線中去
    if ([self.captureSession canAddOutput:self.imageOutput])
    {
        [self.captureSession addOutput:self.imageOutput];
        
    }
    
    
    //建立一個AVCaptureMovieFileOutput 例項,用於將Quick Time 電影錄製到檔案系統
    self.movieOutput = [[AVCaptureMovieFileOutput alloc]init];
    
    //輸出連線 判斷是否可用,可用則新增到輸出連線中去
    if ([self.captureSession canAddOutput:self.movieOutput])
    {
        [self.captureSession addOutput:self.movieOutput];
    }
    
    
    self.videoQueue = dispatch_queue_create("com.kongyulu.VideoQueue", NULL);
    
    return YES;
}
複製程式碼

2.3 啟動, 停止會話

  • 可以在一個 VC 的生命週期內啟動和停止會話,由於這個操作是比較耗時的同步操作,因此建議在非同步執行緒裡執行此方法。如下:
- (void)startSession {

    //檢查是否處於執行狀態
    if (![self.captureSession isRunning])
    {
        //使用同步呼叫會損耗一定的時間,則用非同步的方式處理
        dispatch_async(self.videoQueue, ^{
            [self.captureSession startRunning];
        });
    }
}

- (void)stopSession {
    
    //檢查是否處於執行狀態
    if ([self.captureSession isRunning])
    {
        //使用非同步方式,停止執行
        dispatch_async(self.videoQueue, ^{
            [self.captureSession stopRunning];
        });
    }
}

複製程式碼

2.4 切換攝像頭

  • 大多數 ios 裝置都有前後兩個攝像頭,標識前後攝像頭需要用到 AVCaptureDevicePosition 列舉類:
typedef NS_ENUM(NSInteger, AVCaptureDevicePosition) {
    AVCaptureDevicePositionUnspecified = 0, // 未知
    AVCaptureDevicePositionBack        = 1, // 後置攝像頭
    AVCaptureDevicePositionFront       = 2, // 前置攝像頭
}
複製程式碼
  • 接下來獲取當前活躍的裝置,沒有啟用的裝置:
- (AVCaptureDevice *)activeCamera {
    //返回當前捕捉會話對應的攝像頭的device 屬性
    return self.activeVideoInput.device;
}

//返回當前未啟用的攝像頭
- (AVCaptureDevice *)inactiveCamera {

    //通過查詢當前啟用攝像頭的反向攝像頭獲得,如果裝置只有1個攝像頭,則返回nil
       AVCaptureDevice *device = nil;
      if (self.cameraCount > 1)
      {
          if ([self activeCamera].position == AVCaptureDevicePositionBack) {
               device = [self cameraWithPosition:AVCaptureDevicePositionFront];
         }else
         {
             device = [self cameraWithPosition:AVCaptureDevicePositionBack];
         }
     }

    return device;
}

複製程式碼
  • 判斷是否有超過1個攝像頭可用
//判斷是否有超過1個攝像頭可用
- (BOOL)canSwitchCameras {
    return self.cameraCount > 1;
}

複製程式碼
  • 可用視訊捕捉裝置的數量:
//可用視訊捕捉裝置的數量
- (NSUInteger)cameraCount {
     return [[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count];
}
複製程式碼
  • 然後從 AVCaptureDeviceInput 就可以獲取到當前活躍的 device,然後找到與其相對的裝置:
#pragma mark - Device Configuration   配置攝像頭支援的方法

- (AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position {
    
    //獲取可用視訊裝置
    NSArray *devicess = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    
    //遍歷可用的視訊裝置 並返回position 引數值
    for (AVCaptureDevice *device in devicess)
    {
        if (device.position == position) {
            return device;
        }
    }
    return nil;
}

複製程式碼
  • 切換攝像頭,切換前首先要判斷能否切換:
//切換攝像頭
- (BOOL)switchCameras {

    //判斷是否有多個攝像頭
    if (![self canSwitchCameras])
    {
        return NO;
    }
    
    //獲取當前裝置的反向裝置
    NSError *error;
    AVCaptureDevice *videoDevice = [self inactiveCamera];
    
    //將輸入裝置封裝成AVCaptureDeviceInput
    AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
    
    //判斷videoInput 是否為nil
    if (videoInput)
    {
        //標註原配置變化開始
        [self.captureSession beginConfiguration];
        
        //將捕捉會話中,原本的捕捉輸入裝置移除
        [self.captureSession removeInput:self.activeVideoInput];
        
        //判斷新的裝置是否能加入
        if ([self.captureSession canAddInput:videoInput])
        {
            //能加入成功,則將videoInput 作為新的視訊捕捉裝置
            [self.captureSession addInput:videoInput];
            
            //將獲得裝置 改為 videoInput
            self.activeVideoInput = videoInput;
        }else
        {
            //如果新裝置,無法加入。則將原本的視訊捕捉裝置重新加入到捕捉會話中
            [self.captureSession addInput:self.activeVideoInput];
        }
        
        //配置完成後, AVCaptureSession commitConfiguration 會分批的將所有變更整合在一起。
        [self.captureSession commitConfiguration];
    }else
    {
        //建立AVCaptureDeviceInput 出現錯誤,則通知委託來處理該錯誤
        [self.delegate deviceConfigurationFailedWithError:error];
        return NO;
    }
    
    return YES;
}
複製程式碼

注意:

  1. AVCapture Device 定義了很多方法,讓開發者控制ios裝置上的攝像頭。可以獨立調整和鎖定攝像頭的焦距、曝光、白平衡。對焦和曝光可以基於特定的興趣點進行設定,使其在應用中實現點選對焦、點選曝光的功能。 還可以讓你控制裝置的LED作為拍照的閃光燈或手電筒的使用
  2. 每當修改攝像頭裝置時,一定要先測試修改動作是否能被裝置支援。並不是所有的攝像頭都支援所有功能,例如牽制攝像頭就不支援對焦操作,因為它和目標距離一般在一臂之長的距離。但大部分後置攝像頭是可以支援全尺寸對焦。嘗試應用一個不被支援的動作,會導致異常崩潰。所以修改攝像頭裝置前,需要判斷是否支援
  • 獲取到對應的 device 後就可以封裝為 AVCaptureInput 物件,然後進行配置:

//這裡 beginConfiguration 和 commitConfiguration 可以使修改操作成為原子性操作,保證裝置執行安全。
            [self.captureSession beginConfiguration];// 開始配置新的視訊輸入
            [self.captureSession removeInput:self.captureVideoInput]; // 首先移除舊的 input,才能加入新的 input
            if ([self.captureSession canAddInput:newInput]) {
                [self.captureSession addInput:newInput];
                self.activeVideoDevice = newActiveDevice;
                self.captureVideoInput = newInput;
            } else {
                [self.captureSession addInput:self.captureVideoInput];
            }
            [self.captureSession commitConfiguration];
複製程式碼

2.5 調整焦距和曝光, 閃光燈和手電筒模式

2.5.1 對焦

  • 對焦時,isFocusPointOfInterestSupported 用於判斷裝置是否支援興趣點對焦,isFocusModeSupported 判斷是否支援某種對焦模式,AVCaptureFocusModeAutoFocus 即自動對焦,然後進行對焦設定。程式碼如下:
#pragma mark - Focus Methods 點選聚焦方法的實現

- (BOOL)cameraSupportsTapToFocus {
    
    //詢問啟用中的攝像頭是否支援興趣點對焦
    return [[self activeCamera]isFocusPointOfInterestSupported];
}

- (void)focusAtPoint:(CGPoint)point {
    
    AVCaptureDevice *device = [self activeCamera];
    
    //是否支援興趣點對焦 & 是否自動對焦模式
    if (device.isFocusPointOfInterestSupported && [device isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {
        
        NSError *error;
        //鎖定裝置準備配置,如果獲得了鎖
        if ([device lockForConfiguration:&error]) {
            
            //將focusPointOfInterest屬性設定CGPoint
            device.focusPointOfInterest = point;
            
            //focusMode 設定為AVCaptureFocusModeAutoFocus
            device.focusMode = AVCaptureFocusModeAutoFocus;
            
            //釋放該鎖定
            [device unlockForConfiguration];
        }else{
            //錯誤時,則返回給錯誤處理代理
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}
複製程式碼

2.5.2 曝光

  • 先詢問裝置是否支援對一個興趣點進行曝光
- (BOOL)cameraSupportsTapToExpose {
    
    //詢問裝置是否支援對一個興趣點進行曝光
    return [[self activeCamera] isExposurePointOfInterestSupported];
}
複製程式碼
  • 曝光與對焦非常類似,核心方法如下:
static const NSString *THCameraAdjustingExposureContext;

- (void)exposeAtPoint:(CGPoint)point {

    AVCaptureDevice *device = [self activeCamera];
    
    AVCaptureExposureMode exposureMode =AVCaptureExposureModeContinuousAutoExposure;
    
    //判斷是否支援 AVCaptureExposureModeContinuousAutoExposure 模式
    if (device.isExposurePointOfInterestSupported && [device isExposureModeSupported:exposureMode]) {
        
        [device isExposureModeSupported:exposureMode];
        
        NSError *error;
        
        //鎖定裝置準備配置
        if ([device lockForConfiguration:&error])
        {
            //配置期望值
            device.exposurePointOfInterest = point;
            device.exposureMode = exposureMode;
            
            //判斷裝置是否支援鎖定曝光的模式。
            if ([device isExposureModeSupported:AVCaptureExposureModeLocked]) {
                
                //支援,則使用kvo確定裝置的adjustingExposure屬性的狀態。
                [device addObserver:self forKeyPath:@"adjustingExposure" options:NSKeyValueObservingOptionNew context:&THCameraAdjustingExposureContext];
                
            }
            
            //釋放該鎖定
            [device unlockForConfiguration];
            
        }else
        {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}

複製程式碼

2.5.3 閃光燈

  • 處理對焦,我們還可以很方便的調整閃光燈,開啟手電筒模式。
  • 閃光燈(flash)和手電筒(torch)是兩個不同的模式,分別定義如下:
typedef NS_ENUM(NSInteger, AVCaptureFlashMode) {
    AVCaptureFlashModeOff  = 0,
    AVCaptureFlashModeOn   = 1,
    AVCaptureFlashModeAuto = 2,
}

typedef NS_ENUM(NSInteger, AVCaptureTorchMode) {
    AVCaptureTorchModeOff  = 0,
    AVCaptureTorchModeOn   = 1,
    AVCaptureTorchModeAuto = 2,
}
複製程式碼
  • 通常在拍照時需要設定閃光燈,而拍視訊時需要設定手電筒。具體配置模式程式碼如下:
  • 判斷是否有閃光燈:
//判斷是否有閃光燈
- (BOOL)cameraHasFlash {
    return [[self activeCamera] hasFlash];
}
複製程式碼
//閃光燈模式
- (AVCaptureFlashMode)flashMode {
    return [[self activeCamera] flashMode];
}

//設定閃光燈
- (void)setFlashMode:(AVCaptureFlashMode)flashMode {

    //獲取會話
    AVCaptureDevice *device = [self activeCamera];
    
    //判斷是否支援閃光燈模式
    if ([device isFlashModeSupported:flashMode]) {
    
        //如果支援,則鎖定裝置
        NSError *error;
        if ([device lockForConfiguration:&error]) {

            //修改閃光燈模式
            device.flashMode = flashMode;
            //修改完成,解鎖釋放裝置
            [device unlockForConfiguration];
            
        }else
        {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
        
    }

}

複製程式碼

2.5.4 手電筒

  • 是否支援手電筒:
//是否支援手電筒
- (BOOL)cameraHasTorch {

    return [[self activeCamera]hasTorch];
}
複製程式碼
  • 切換為手電筒模式,開啟手電筒
//手電筒模式
- (AVCaptureTorchMode)torchMode {

    return [[self activeCamera]torchMode];
}


//設定是否開啟手電筒
- (void)setTorchMode:(AVCaptureTorchMode)torchMode {

    AVCaptureDevice *device = [self activeCamera];
    
    if ([device isTorchModeSupported:torchMode]) {
        
        NSError *error;
        if ([device lockForConfiguration:&error]) {
            
            device.torchMode = torchMode;
            [device unlockForConfiguration];
        }else
        {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}
複製程式碼

2.6 拍攝靜態圖片

  • 設定捕捉會話時我們將 AVCaptureStillImageOutput (注意 :AVCaptureStillImageOutput 在IOS10 之後被廢棄了,使用AVCapturePhotoOutput 代替)例項加入到會話中,這個會話可以用來拍攝靜態圖片。如下程式碼:
    AVCaptureConnection *connection = [self.cameraHelper.imageOutput connectionWithMediaType:AVMediaTypeVideo];
    if ([connection isVideoOrientationSupported]) {
        [connection setVideoOrientation:self.cameraHelper.videoOrientation];
    }
    if (!connection.enabled || !connection.isActive) { // connection 不可用
        // 處理非法情況
        return;
    }
複製程式碼
  1. 通過監聽重力感應器修改 orientation
  2. 通過 UIDevice 獲取
  • 通過監聽重力感應器修改 orientation:
    // 監測重力感應器並調整 orientation
    CMMotionManager *motionManager = [[CMMotionManager alloc] init];
    motionManager.deviceMotionUpdateInterval = 1/15.0;
    if (motionManager.deviceMotionAvailable) {
        [motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue]
                                           withHandler: ^(CMDeviceMotion *motion, NSError *error){
                                               double x = motion.gravity.x;
                                               double y = motion.gravity.y;
                                               if (fabs(y) >= fabs(x)) { // y 軸分量大於 x 軸
                                                   if (y >= 0) { // 頂部向下
                                                       self.videoOrientation = AVCaptureVideoOrientationPortraitUpsideDown; // UIDeviceOrientationPortraitUpsideDown;
                                                   } else { // 頂部向上
                                                       self.videoOrientation = AVCaptureVideoOrientationPortrait; // UIDeviceOrientationPortrait;
                                                   }
                                               } else {
                                                   if (x >= 0) { // 頂部向右
                                                       self.videoOrientation = AVCaptureVideoOrientationLandscapeLeft; // UIDeviceOrientationLandscapeRight;
                                                   } else { // 頂部向左
                                                       self.videoOrientation = AVCaptureVideoOrientationLandscapeRight; // UIDeviceOrientationLandscapeLeft;
                                                   }
                                               }
                                           }];
        self.motionManager = motionManager;
    } else {
        self.videoOrientation = AVCaptureVideoOrientationPortrait;
    }
複製程式碼
  • 然後我們呼叫方法來獲取 CMSampleBufferRef(CMSampleBufferRef 是一個 Core Media 定義的 Core Foundation 物件),可以通過 AVCaptureStillImageOutput 的 jpegStillImageNSDataRepresentation 類方法將其轉化為 NSData 型別。如下程式碼:
    @weakify(self)
    [self.cameraHelper.imageOutput captureStillImageAsynchronouslyFromConnection:connection completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) {
        @strongify(self)
        if (!error && imageDataSampleBuffer) {
            NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];
            if (!imageData) {return;}
            UIImage *image = [UIImage imageWithData:imageData];
            if (!image) {return;}
    }];
複製程式碼
  • 最後,我們可以直接將得到的圖片儲存存檔案形式,注意:Assets Library 在 ios 8 以後已經被 PHPhotoLibrary 替代,這裡用 PHPhotoLibrary 實現儲存圖片的功能。程式碼如下:
    [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
        PHAssetChangeRequest *changeRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:targetImage];
        NSString *imageIdentifier = changeRequest.placeholderForCreatedAsset.localIdentifier;
    } completionHandler:^( BOOL success, NSError * _Nullable error ) {
    }];
複製程式碼
  • 我們可以通過儲存時返回的 imageIdentifier 從相簿裡找到這個圖片。

  • 完整捕獲靜態圖片的程式碼如下:

#pragma mark - Image Capture Methods 拍攝靜態圖片
/*
    AVCaptureStillImageOutput 是AVCaptureOutput的子類。用於捕捉圖片
 */
- (void)captureStillImage {
    
    //獲取連線
    AVCaptureConnection *connection = [self.imageOutput connectionWithMediaType:AVMediaTypeVideo];
    
    //程式只支援縱向,但是如果使用者橫向拍照時,需要調整結果照片的方向
    //判斷是否支援設定視訊方向
    if (connection.isVideoOrientationSupported) {
        
        //獲取方向值
        connection.videoOrientation = [self currentVideoOrientation];
    }
    
    //定義一個handler 塊,會返回1個圖片的NSData資料
    id handler = ^(CMSampleBufferRef sampleBuffer,NSError *error)
                {
                    if (sampleBuffer != NULL) {
                        NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:sampleBuffer];
                        UIImage *image = [[UIImage alloc]initWithData:imageData];
                        
                        //重點:捕捉圖片成功後,將圖片傳遞出去
                        [self writeImageToAssetsLibrary:image];
                    }else
                    {
                        NSLog(@"NULL sampleBuffer:%@",[error localizedDescription]);
                    }
                        
                };
    
    //捕捉靜態圖片
    [self.imageOutput captureStillImageAsynchronouslyFromConnection:connection completionHandler:handler];
    
    
    
}

//獲取方向值
- (AVCaptureVideoOrientation)currentVideoOrientation {
    
    AVCaptureVideoOrientation orientation;
    
    //獲取UIDevice 的 orientation
    switch ([UIDevice currentDevice].orientation) {
        case UIDeviceOrientationPortrait:
            orientation = AVCaptureVideoOrientationPortrait;
            break;
        case UIDeviceOrientationLandscapeRight:
            orientation = AVCaptureVideoOrientationLandscapeLeft;
            break;
        case UIDeviceOrientationPortraitUpsideDown:
            orientation = AVCaptureVideoOrientationPortraitUpsideDown;
            break;
        default:
            orientation = AVCaptureVideoOrientationLandscapeRight;
            break;
    }
    
    return orientation;

    return 0;
}


/*
    Assets Library 框架 
    用來讓開發者通過程式碼方式訪問iOS photo
    注意:會訪問到相簿,需要修改plist 許可權。否則會導致專案崩潰
 */

- (void)writeImageToAssetsLibrary:(UIImage *)image {

    //建立ALAssetsLibrary  例項
    ALAssetsLibrary *library = [[ALAssetsLibrary alloc]init];
    
    //引數1:圖片(引數為CGImageRef 所以image.CGImage)
    //引數2:方向引數 轉為NSUInteger
    //引數3:寫入成功、失敗處理
    [library writeImageToSavedPhotosAlbum:image.CGImage
                             orientation:(NSUInteger)image.imageOrientation
                         completionBlock:^(NSURL *assetURL, NSError *error) {
                             //成功後,傳送捕捉圖片通知。用於繪製程式的左下角的縮圖
                             if (!error)
                             {
                                 [self postThumbnailNotifification:image];
                             }else
                             {
                                 //失敗列印錯誤資訊
                                 id message = [error localizedDescription];
                                 NSLog(@"%@",message);
                             }
                         }];
}

//傳送縮圖通知
- (void)postThumbnailNotifification:(UIImage *)image {
    
    //回到主佇列
    dispatch_async(dispatch_get_main_queue(), ^{
        //傳送請求
        NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
        [nc postNotificationName:THThumbnailCreatedNotification object:image];
    });
}

複製程式碼

2.7 視訊捕捉

  • QuickTime 格式的影片,後設資料處於影片檔案的開頭位置,這樣可以幫助視訊播放器快速讀取標頭檔案來確定檔案內容、結構和樣本位置,但是錄製時需要等所有樣本捕捉完成才能建立頭資料並將其附在檔案結尾處。這樣一來,如果錄製時發生崩潰或中斷就會導致無法建立影片頭,從而在磁碟生成一個不可讀的檔案。

  • 因此 AVFoundationAVCaptureMovieFileOutput 類就提供了分段捕捉能力,錄製開始時生成最小化的頭資訊,錄製進行中,片段間隔一定週期再次建立頭資訊,從而逐步完成建立。預設狀態下每 10s 寫入一個片段,可以通過 movieFragmentInterval 屬性來修改。

  • 首先是開啟視訊拍攝:

    AVCaptureConnection *videoConnection = [self.cameraHelper.movieFileOutput connectionWithMediaType:AVMediaTypeVideo];
    if ([videoConnection isVideoOrientationSupported]) {
        [videoConnection setVideoOrientation:self.cameraHelper.videoOrientation];
    }
    
    if ([videoConnection isVideoStabilizationSupported]) {
        [videoConnection setPreferredVideoStabilizationMode:AVCaptureVideoStabilizationModeAuto];
    }
    
    [videoConnection setVideoScaleAndCropFactor:1.0];
    if (![self.cameraHelper.movieFileOutput isRecording] && videoConnection.isActive && videoConnection.isEnabled) {
        // 判斷視訊連線是否可用
        self.countTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(refreshTimeLabel) userInfo:nil repeats:YES];
        NSString *urlString = [NSTemporaryDirectory() stringByAppendingString:[NSString stringWithFormat:@"%.0f.mov", [[NSDate date] timeIntervalSince1970] * 1000]];
        NSURL *url = [NSURL fileURLWithPath:urlString];
        [self.cameraHelper.movieFileOutput startRecordingToOutputFileURL:url recordingDelegate:self];
        [self.captureButton setTitle:@"結束" forState:UIControlStateNormal];
    } else {
    }
複製程式碼
  • 設定 PreferredVideoStabilizationMode 可以支援視訊拍攝時的穩定性和拍攝質量,但是這一穩定效果只會在拍攝的視訊中感受到,預覽視訊時無法感知。
  • 我們將視訊檔案臨時寫入到臨時檔案中,等待拍攝結束時會呼叫 AVCaptureFileOutputRecordingDelegate 的 (void)captureOutput:(AVCaptureFileOutput *)captureOutput didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray *)connections error:(NSError *)error 方法。此時可以進行儲存視訊和生成視訊縮圖的操作。
- (void)saveVideo:(NSURL *)videoURL
{
    __block NSString *imageIdentifier;
    @weakify(self)
    [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
        // 儲存視訊
        PHAssetChangeRequest *changeRequest = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:videoURL];
        imageIdentifier = changeRequest.placeholderForCreatedAsset.localIdentifier;
    } completionHandler:^( BOOL success, NSError * _Nullable error ) {
        @strongify(self)
        dispatch_async(dispatch_get_main_queue(), ^{
            @strongify(self)
            [self resetTimeCounter];
            if (!success) {
                // 錯誤處理
            } else {
                PHAsset *asset = [PHAsset fetchAssetsWithLocalIdentifiers:@[imageIdentifier] options:nil].firstObject;
                if (asset && asset.mediaType == PHAssetMediaTypeVideo) {
                    PHVideoRequestOptions *options = [[PHVideoRequestOptions alloc] init];
                    options.version = PHImageRequestOptionsVersionCurrent;
                    options.deliveryMode = PHVideoRequestOptionsDeliveryModeAutomatic;
                    [[PHImageManager defaultManager] requestAVAssetForVideo:asset options:options resultHandler:^(AVAsset * _Nullable obj, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) {
                        @strongify(self)
                        [self resolveAVAsset:obj identifier:asset.localIdentifier];
                    }];
                }
            }
        });
    }];
}
    
- (void)resolveAVAsset:(AVAsset *)asset identifier:(NSString *)identifier
{
    if (!asset) {
        return;
    }
    if (![asset isKindOfClass:[AVURLAsset class]]) {
        return;
    }
    AVURLAsset *urlAsset = (AVURLAsset *)asset;
    NSURL *url = urlAsset.URL;
    NSData *data = [NSData dataWithContentsOfURL:url];
    
    AVAssetImageGenerator *generator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];
    generator.appliesPreferredTrackTransform = YES; //捕捉縮圖時考慮視訊 orientation 變化,避免錯誤的縮圖方向
    CMTime snaptime = kCMTimeZero;
    CGImageRef cgImageRef = [generator copyCGImageAtTime:snaptime actualTime:NULL error:nil];
    UIImage *assetImage = [UIImage imageWithCGImage:cgImageRef];
    CGImageRelease(cgImageRef);
}
複製程式碼
  • 梳理一下視訊捕獲的流程
  • (1)判斷是否錄製狀態
//判斷是否錄製狀態
- (BOOL)isRecording {

    return self.movieOutput.isRecording;
}
複製程式碼
  • (2)開始錄製
//開始錄製
- (void)startRecording {

    if (![self isRecording]) {
        
        //獲取當前視訊捕捉連線資訊,用於捕捉視訊資料配置一些核心屬性
        AVCaptureConnection * videoConnection = [self.movieOutput connectionWithMediaType:AVMediaTypeVideo];
        
        //判斷是否支援設定videoOrientation 屬性。
        if([videoConnection isVideoOrientationSupported])
        {
            //支援則修改當前視訊的方向
            videoConnection.videoOrientation = [self currentVideoOrientation];
            
        }
        
        //判斷是否支援視訊穩定 可以顯著提高視訊的質量。只會在錄製視訊檔案涉及
        if([videoConnection isVideoStabilizationSupported])
        {
            videoConnection.enablesVideoStabilizationWhenAvailable = YES;
        }
        
        AVCaptureDevice *device = [self activeCamera];
        
        //攝像頭可以進行平滑對焦模式操作。即減慢攝像頭鏡頭對焦速度。當使用者移動拍攝時攝像頭會嘗試快速自動對焦。
        if (device.isSmoothAutoFocusEnabled) {
            NSError *error;
            if ([device lockForConfiguration:&error]) {
                
                device.smoothAutoFocusEnabled = YES;
                [device unlockForConfiguration];
            }else
            {
                [self.delegate deviceConfigurationFailedWithError:error];
            }
        }
        
        //查詢寫入捕捉視訊的唯一檔案系統URL.
        self.outputURL = [self uniqueURL];
        
        //在捕捉輸出上呼叫方法 引數1:錄製儲存路徑  引數2:代理
        [self.movieOutput startRecordingToOutputFileURL:self.outputURL recordingDelegate:self];
        
    }
}

- (CMTime)recordedDuration {
    return self.movieOutput.recordedDuration;
}


//寫入視訊唯一檔案系統URL
- (NSURL *)uniqueURL {

    NSFileManager *fileManager = [NSFileManager defaultManager];
    
    //temporaryDirectoryWithTemplateString  可以將檔案寫入的目的建立一個唯一命名的目錄;
    NSString *dirPath = [fileManager temporaryDirectoryWithTemplateString:@"kamera.XXXXXX"];
    
    if (dirPath) {
        NSString *filePath = [dirPath stringByAppendingPathComponent:@"kamera_movie.mov"];
        return  [NSURL fileURLWithPath:filePath];
    }
    return nil;
}
複製程式碼
  • (3)停止錄製
//停止錄製
- (void)stopRecording {

    //是否正在錄製
    if ([self isRecording]) {
        [self.movieOutput stopRecording];
    }
}
複製程式碼
  • (4)捕獲視訊回撥函式AVCaptureFileOutputRecordingDelegate
#pragma mark - AVCaptureFileOutputRecordingDelegate

- (void)captureOutput:(AVCaptureFileOutput *)captureOutput
didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
      fromConnections:(NSArray *)connections
                error:(NSError *)error {

    //錯誤
    if (error) {
        [self.delegate mediaCaptureFailedWithError:error];
    }else
    {
        //寫入
        [self writeVideoToAssetsLibrary:[self.outputURL copy]];
        
    }
    
    self.outputURL = nil;
}
複製程式碼
  • (5)將得到的視訊資料儲存寫入視訊檔案
//寫入捕捉到的視訊
- (void)writeVideoToAssetsLibrary:(NSURL *)videoURL {
    
    //ALAssetsLibrary 例項 提供寫入視訊的介面
    ALAssetsLibrary *library = [[ALAssetsLibrary alloc]init];
    
    //寫資源庫寫入前,檢查視訊是否可被寫入 (寫入前儘量養成判斷的習慣)
    if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:videoURL]) {
        
        //建立block塊
        ALAssetsLibraryWriteVideoCompletionBlock completionBlock;
        completionBlock = ^(NSURL *assetURL,NSError *error)
        {
            if (error) {
                
                [self.delegate assetLibraryWriteFailedWithError:error];
            }else
            {
                //用於介面展示視訊縮圖
                [self generateThumbnailForVideoAtURL:videoURL];
            }
        };
        
        //執行實際寫入資源庫的動作
        [library writeVideoAtPathToSavedPhotosAlbum:videoURL completionBlock:completionBlock];
    }
}

複製程式碼
  • (6)獲取視訊縮圖
//獲取視訊左下角縮圖
- (void)generateThumbnailForVideoAtURL:(NSURL *)videoURL {

    //在videoQueue 上,
    dispatch_async(self.videoQueue, ^{
        
        //建立新的AVAsset & AVAssetImageGenerator
        AVAsset *asset = [AVAsset assetWithURL:videoURL];
        
        AVAssetImageGenerator *imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];
        
        //設定maximumSize 寬為100,高為0 根據視訊的寬高比來計算圖片的高度
        imageGenerator.maximumSize = CGSizeMake(100.0f, 0.0f);
        
        //捕捉視訊縮圖會考慮視訊的變化(如視訊的方向變化),如果不設定,縮圖的方向可能出錯
        imageGenerator.appliesPreferredTrackTransform = YES;
        
        //獲取CGImageRef圖片 注意需要自己管理它的建立和釋放
        CGImageRef imageRef = [imageGenerator copyCGImageAtTime:kCMTimeZero actualTime:NULL error:nil];
        
        //將圖片轉化為UIImage
        UIImage *image = [UIImage imageWithCGImage:imageRef];
        
        //釋放CGImageRef imageRef 防止記憶體洩漏
        CGImageRelease(imageRef);
        
        //回到主執行緒
        dispatch_async(dispatch_get_main_queue(), ^{
            
            //傳送通知,傳遞最新的image
            [self postThumbnailNotifification:image];
            
        });
        
    });
    
}
複製程式碼

2.8 視訊縮放

  • iOS 7.0 為 AVCaptureDevice 提供了一個 videoZoomFactor 屬性用於對視訊輸出和捕捉提供縮放效果,這個屬性的最小值為 1.0,最大值由下面的方法提供:self.cameraHelper.activeVideoDevice.activeFormat.videoMaxZoomFactor;
  • 因而判斷一個裝置能否進行縮放也可以通過判斷這一屬性來獲知:
- (BOOL)cameraSupportsZoom
{
    return self.cameraHelper.activeVideoDevice.activeFormat.videoMaxZoomFactor > 1.0f;
}
複製程式碼
  • 裝置執行縮放效果是通過居中裁剪由攝像頭感測器捕捉到的圖片實現的,也可以通過 videoZoomFactorUpscaleThreshold 來設定具體的放大中心。當 zoom factors 縮放因子比較小的時候,裁剪的圖片剛好等於或者大於輸出尺寸(考慮與抗邊緣畸變有關),則無需放大就可以返回。但是當 zoom factors 比較大時,裝置必須縮放裁剪圖片以符合輸出尺寸,從而導致圖片質量上的丟失。具體的臨界點由 videoZoomFactorUpscaleThreshold 值來確定。
// 在 iphone6s 和 iphone8plus 上測試得到此值為 2.0左右
self.cameraHelper.activeVideoDevice.activeFormat.videoZoomFactorUpscaleThreshold;
複製程式碼
  • 可以通過一個變化值從 0.0 到 1.0 的 UISlider 來實現對縮放值的控制。程式碼如下:
{
    [self.slider addTarget:self action:@selector(sliderValueChange:) forControlEvents:UIControlEventValueChanged];
}

- (void)sliderValueChange:(id)sender
{
    UISlider *slider = (UISlider *)sender;
    [self setZoomValue:slider.value];
}

- (CGFloat)maxZoomFactor
{
    return MIN(self.cameraHelper.activeVideoDevice.activeFormat.videoMaxZoomFactor, 4.0f);
}

- (void)setZoomValue:(CGFloat)zoomValue
{
    if (!self.cameraHelper.activeVideoDevice.isRampingVideoZoom) {
        NSError *error;
        if ([self.cameraHelper.activeVideoDevice lockForConfiguration:&error]) {
            CGFloat zoomFactor = pow([self maxZoomFactor], zoomValue);
            self.cameraHelper.activeVideoDevice.videoZoomFactor = zoomFactor;
            [self.cameraHelper.activeVideoDevice unlockForConfiguration];
        }
    }
}    
複製程式碼
  • 首先注意在進行配置屬性前需要進行裝置的鎖定,否則會引發異常。其次,插值縮放是一個指數形式的增長,傳入的 slider 值是線性的,需要進行一次 pow 運算得到需要縮放的值。另外,videoMaxZoomFactor 的值可能會非常大,在 iphone8p 上這一個值是 16,縮放到這麼大的影像是沒有太大意義的,因此需要人為設定一個最大縮放值,這裡選擇 4.0。

  • 當然這裡進行的縮放是立即生效的,下面的方法可以以一個速度平滑縮放到一個縮放因子上:

- (void)rampZoomToValue:(CGFloat)zoomValue {
    CGFloat zoomFactor = pow([self maxZoomFactor], zoomValue);
    NSError *error;
    if ([self.activeCamera lockForConfiguration:&error]) {
        [self.activeCamera rampToVideoZoomFactor:zoomFactor
                                        withRate:THZoomRate];
        [self.activeCamera unlockForConfiguration];
    } else {
    }
}

- (void)cancelZoom {
    NSError *error;
    if ([self.activeCamera lockForConfiguration:&error]) {
        [self.activeCamera cancelVideoZoomRamp];
        [self.activeCamera unlockForConfiguration];
    } else {
    }
}
複製程式碼
  • 當然我們還可以監聽裝置的 videoZoomFactor 可以獲知當前的縮放值:
    [RACObserve(self, activeVideoDevice.videoZoomFactor) subscribeNext:^(id x) {
        NSLog(@"videoZoomFactor: %f", self.activeVideoDevice.videoZoomFactor);
    }];
複製程式碼
  • 還可以監聽裝置的 rampingVideoZoom 可以獲知裝置是否正在平滑縮放:
    [RACObserve(self, activeVideoDevice.rampingVideoZoom) subscribeNext:^(id x) {
        NSLog(@"rampingVideoZoom : %@", (self.activeVideoDevice.rampingVideoZoom)?@"true":@"false");
    }];
複製程式碼

2.9 視訊編輯

  • AVCaptureMovieFileOutput 可以簡單地捕捉視訊,但是不能進行視訊資料互動,因此需要使用 AVCaptureVideoDataOutput 類。AVCaptureVideoDataOutput 是一個 AVCaptureOutput 的子類,可以直接訪問攝像頭感測器捕捉到的視訊幀。與之對應的是處理音訊輸入的 AVCaptureAudioDataOutput 類。

  • AVCaptureVideoDataOutput 有一個遵循 AVCaptureVideoDataOutputSampleBufferDelegate 協議的委託物件,它有下面兩個主要方法:

- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection; // 有新的視訊幀寫入時呼叫,資料會基於 output 的 videoSetting 進行解碼或重新編碼
- (void)captureOutput:(AVCaptureOutput *)output didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection; // 有遲到的視訊幀被丟棄時呼叫,通常是因為在上面一個方法裡進行了比較耗時的操作
複製程式碼
  • CMSampleBufferRef 是一個由 Core Media 框架提供的 Core Foundation 風格的物件,用於在媒體管道中傳輸數字樣本。這樣我們可以對 CMSampleBufferRef 的每一個 Core Video 視訊幀進行處理,如下程式碼:
    int BYTES_PER_PIXEL = 4;
    CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); //CVPixelBufferRef 在主記憶體中儲存畫素資料
    CVPixelBufferLockBaseAddress(pixelBuffer, 0); // 獲取相應記憶體塊的鎖
    size_t bufferWidth = CVPixelBufferGetWidth(pixelBuffer);
    size_t bufferHeight = CVPixelBufferGetHeight(pixelBuffer);// 獲取畫素寬高
    unsigned char *pixel = (unsigned char *)CVPixelBufferGetBaseAddress(pixelBuffer); // 獲取畫素 buffer 的起始位置
    unsigned char grayPixel;
    for (int row = 0; row < bufferHeight; row++) {
        for (int column = 0; column < bufferWidth; column ++) { // 遍歷每一個畫素點
            grayPixel = (pixel[0] + pixel[1] + pixel[2])/3.0;
            pixel[0] = pixel[1] = pixel[2] = grayPixel;
            pixel += BYTES_PER_PIXEL;
        }
    }
    CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer]; // 通過 buffer 生成對應的 CIImage
    CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); // 解除鎖
複製程式碼
  • CMSampleBufferRef 還提供了每一幀資料的格式資訊,CMFormatDescription.h 標頭檔案定義了大量函式來獲取各種資訊。
    CMFormatDescriptionRef formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer);
    CMMediaType mediaType = CMFormatDescriptionGetMediaType(formatDescription);
複製程式碼
  • 還可以修改時間資訊:
    CMTime presentation = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); // 獲取幀樣本的原始時間戳
    CMTime decode = CMSampleBufferGetDecodeTimeStamp(sampleBuffer); // 獲取幀樣本的解碼時間戳
複製程式碼
  • 可以附加後設資料:
    CFDictionaryRef exif = (CFDictionaryRef)CMGetAttachment(sampleBuffer, kCGImagePropertyExifDictionary, NULL);
複製程式碼
  • AVCaptureVideoDataOutput 的配置與 AVCaptureMovieFileOutput 大致相同,但要指明它的委託物件和回撥佇列。為了確保視訊幀按順序傳遞,佇列要求必須是序列佇列。
    self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
    self.videoDataOutput.videoSettings = @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)}; // 攝像頭的初始格式為雙平面 420v,這是一個 YUV 格式,而 OpenGL ES 常用 BGRA 格式
    if ([self.captureSession canAddOutput:self.videoDataOutput]) {
        [self.captureSession addOutput:self.videoDataOutput];
        [self.videoDataOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];
    }
複製程式碼

2.10 高幀率捕捉

  • 除了上面介紹的普通視訊捕捉外,我們還可以使用高頻捕捉功能。高幀率捕獲視訊是在 iOS 7 以後加入的,具有更逼真的效果和更好的清晰度,對於細節的加強和動作流暢度的提升非常明顯,尤其是錄製快速移動的內容時更為明顯,也可以實現高質量的慢動作視訊效果。
  • 實現高幀率捕捉的基本思路是:首先通過裝置的 formats 屬性獲取所有支援的格式,也就是 AVCaptureDeviceFormat 物件;然後根據物件的 videoSupportedFrameRateRanges 屬性,這樣可以獲知其所支援的最小幀率、最大幀率及時長資訊;然後手動設定裝置的格式和幀時長。
  • 具體實現如下:
  • 首先寫一個 AVCaptureDevice 的 category,獲取支援格式的最大幀率的方法如下:
    AVCaptureDeviceFormat *maxFormat = nil;
    AVFrameRateRange *maxFrameRateRange = nil;
    for (AVCaptureDeviceFormat *format in self.formats) {
        FourCharCode codecType = CMVideoFormatDescriptionGetCodecType(format.formatDescription);
        //codecType 是一個無符號32位的資料型別,但是是由四個字元對應的四個位元組組成,一般可能值為 "420v" 或 "420f",這裡選取 420v 格式來配置。
        if (codecType == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) {
            NSArray *frameRateRanges = format.videoSupportedFrameRateRanges;
            for (AVFrameRateRange *range in frameRateRanges) {
                if (range.maxFrameRate > maxFrameRateRange.maxFrameRate) {
                    maxFormat = format;
                    maxFrameRateRange = range;
                }
            }
        } else {
        }
    }
複製程式碼
  • 我們可以通過判斷最大幀率是否大於 30,來判斷裝置是否支援高幀率:
- (BOOL)isHighFrameRate {
    return self.frameRateRange.maxFrameRate > 30.0f;
}
複製程式碼
  • 接下來我們就可以進行配置了:
    if ([self hasMediaType:AVMediaTypeVideo] && [self lockForConfiguration:error] && [self.activeCamera supportsHighFrameRateCapture]) {
        CMTime minFrameDuration = self.frameRateRange.minFrameDuration;
        self.activeFormat = self.format;
        self.activeVideoMinFrameDuration = minFrameDuration;
        self.activeVideoMaxFrameDuration = minFrameDuration;
        [self unlockForConfiguration];
    }
複製程式碼
  • 這裡首先鎖定了裝置,然後將最小幀時長和最大幀時長都設定成 minFrameDuration,幀時長與幀率是倒數關係,所以最大幀率對應最小幀時長。
  • 播放時可以針對 AVPlayer 設定不同的 rate 實現變速播放,在 iphone8plus 上實測,如果 rate 在 0 到 0.5 之間, 則實際播放速率仍為 0.5。
  • 另外要注意設定 AVPlayerItem 的 audioTimePitchAlgorithm 屬性,這個屬性允許你指定當視訊正在各種幀率下播放的時候如何播放音訊,通常選擇 AVAudioTimePitchAlgorithmSpectralAVAudioTimePitchAlgoruthmTimeDomain 即可。:
  1. AVAudioTimePitchAlgorithmLowQualityZeroLatency 質量低,適合快進,快退或低質量語音
  2. AVAudioTimePitchAlgoruthmTimeDomain 質量適中,計算成本較低,適合語音
  3. AVAudioTimePitchAlgorithmSpectral 最高質量,最昂貴的計算,保留了原來的專案間距
  4. AVAudioTimePitchAlgorithmVarispeed 高品質的播放沒有音高校正
  • 此外AVFoundation 提供了人臉識別,二維碼識別功能。

2.11 人臉識別

  • 人臉識別需要用到 AVCaptureMetadataOutput 作為輸出,首先將其加入到捕捉會話中:
    self.metaDataOutput = [[AVCaptureMetadataOutput alloc] init];
    if ([self.captureSession canAddOutput:self.metaDataOutput]) {
        [self.captureSession addOutput:self.metaDataOutput];
        NSArray *metaDataObjectType = @[AVMetadataObjectTypeFace];
        self.metaDataOutput.metadataObjectTypes = metaDataObjectType;
        [self.metaDataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
    }
複製程式碼
  • 可以看到這裡需要指定 AVCaptureMetadataOutput 的 metadataObjectTypes 屬性,將其設定為 AVMetadataObjectTypeFace 的陣列,它代表著人臉後設資料物件。然後設定其遵循 AVCaptureMetadataOutputObjectsDelegate 協議的委託物件及回撥執行緒,當檢測到人臉時就會呼叫下面的方法:
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    if (self.detectFaces) {
        self.detectFaces(metadataObjects);
    }
}
複製程式碼
  • 其中 metadataObjects 是一個包含了許多 AVMetadataObject 物件的陣列,這裡則可以認為都是 AVMetadataObject 的子類 AVMetadataFaceObject。對於 AVMetadataFaceObject 物件,有四個重要的屬性:
  1. faceID,用於標識檢測到的每一個 face
  2. rollAngle,用於標識人臉斜傾角,即人的頭部向肩膀方便的側傾角度
  3. yawAngle,偏轉角,即人臉繞 y 軸旋轉的角度
  4. bounds,標識檢測到的人臉區域
        @weakify(self)
        self.cameraHelper.detectFaces = ^(NSArray *faces) {
            @strongify(self)
            NSMutableArray *transformedFaces = [NSMutableArray array];
            for (AVMetadataFaceObject *face in faces) {
                AVMetadataObject *transformedFace = [self.previewLayer transformedMetadataObjectForMetadataObject:face];
                [transformedFaces addObject:transformedFace];
            }
            NSMutableArray *lostFaces = [self.faceLayers.allKeys mutableCopy];
            for (AVMetadataFaceObject *face in transformedFaces) {
                NSNumber *faceId = @(face.faceID);
                [lostFaces removeObject:faceId];
                
                CALayer *layer = self.faceLayers[faceId];
                if (!layer) {
                    layer = [CALayer layer];
                    layer.borderWidth = 5.0f;
                    layer.borderColor = [UIColor colorWithRed:0.188 green:0.517 blue:0.877 alpha:1.000].CGColor;
                    [self.previewLayer addSublayer:layer];
                    self.faceLayers[faceId] = layer;
                }
                layer.transform = CATransform3DIdentity;
                layer.frame = face.bounds;
                
                if (face.hasRollAngle) {
                    layer.transform = CATransform3DConcat(layer.transform, [self transformForRollAngle:face.rollAngle]);
                }
                
                if (face.hasYawAngle) {
                    NSLog(@"%f", face.yawAngle);
                    layer.transform = CATransform3DConcat(layer.transform, [self transformForYawAngle:face.yawAngle]);
                }
            }
            
            for (NSNumber *faceID in lostFaces) {
                CALayer *layer = self.faceLayers[faceID];
                [layer removeFromSuperlayer];
                [self.faceLayers removeObjectForKey:faceID];
            }
        };
        
// Rotate around Z-axis
- (CATransform3D)transformForRollAngle:(CGFloat)rollAngleInDegrees {        // 3
    CGFloat rollAngleInRadians = THDegreesToRadians(rollAngleInDegrees);
    return CATransform3DMakeRotation(rollAngleInRadians, 0.0f, 0.0f, 1.0f);
}

// Rotate around Y-axis
- (CATransform3D)transformForYawAngle:(CGFloat)yawAngleInDegrees {          // 5
    CGFloat yawAngleInRadians = THDegreesToRadians(yawAngleInDegrees);
    
    CATransform3D yawTransform = CATransform3DMakeRotation(yawAngleInRadians, 0.0f, -1.0f, 0.0f);
    
    return CATransform3DConcat(yawTransform, [self orientationTransform]);
}

- (CATransform3D)orientationTransform {                                     // 6
    CGFloat angle = 0.0;
    switch ([UIDevice currentDevice].orientation) {
        case UIDeviceOrientationPortraitUpsideDown:
            angle = M_PI;
            break;
        case UIDeviceOrientationLandscapeRight:
            angle = -M_PI / 2.0f;
            break;
        case UIDeviceOrientationLandscapeLeft:
            angle = M_PI / 2.0f;
            break;
        default: // as UIDeviceOrientationPortrait
            angle = 0.0;
            break;
    }
    return CATransform3DMakeRotation(angle, 0.0f, 0.0f, 1.0f);
}

static CGFloat THDegreesToRadians(CGFloat degrees) {
    return degrees * M_PI / 180;
}
複製程式碼
  • 我們用一個字典來管理每一個展示一個 face 物件的 layer,它的 key 值即 faceID,回撥時更新當前已存在的 faceLayer,移除不需要的 faceLayer。其次對每一個 face,根據其 rollAngle 和 yawAngle 要通過 transfor 來變換展示的矩陣。

  • 還要注意一點,transformedMetadataObjectForMetadataObject 方法可以將裝置座標系上的資料轉換到檢視座標系上,裝置座標系的範圍是 (0, 0) 到 (1,1)。

2.12 二維碼識別

  • 機器可讀程式碼包括一維條碼和二維碼等,AVFoundation 支援多種一維碼和三種二維碼,其中最常見的是 QR 碼,也即二維碼。
  • 掃碼仍然需要用到 AVMetadataObject 物件,首先加入到捕捉會話中。
    self.metaDataOutput = [[AVCaptureMetadataOutput alloc] init];
    if ([self.captureSession canAddOutput:self.metaDataOutput]) {
        [self.captureSession addOutput:self.metaDataOutput];
        [self.metaDataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
        NSArray *types = @[AVMetadataObjectTypeQRCode];
        self.metaDataOutput.metadataObjectTypes = types;
    }
複製程式碼
  • 然後實現委託方法:
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    [metadataObjects enumerateObjectsUsingBlock:^(__kindof AVMetadataObject * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([obj isKindOfClass:[AVMetadataMachineReadableCodeObject class]]) {
            NSLog(@"%@", ((AVMetadataMachineReadableCodeObject*)obj).stringValue);
        }
    }];
}
複製程式碼
  1. stringValue,用於表示二維碼編碼資訊
  2. bounds,用於表示二維碼的矩形邊界
  3. corners,一個角點字典表示的陣列,比 bounds 表示的二維碼區域更精確
  • 我們可以通過以上屬性,在 UI 介面上對二維碼區域進行高亮展示。
  • 首先需要注意,一個從 captureSession 獲得的 AVMetadataMachineReadableCodeObject,其座標是裝置座標系下的座標,需要進行座標轉換:
- (NSArray *)transformedCodesFromCodes:(NSArray *)codes {
    NSMutableArray *transformedCodes = [NSMutableArray array];
    [codes enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        AVMetadataObject *transformedCode = [self.previewLayer transformedMetadataObjectForMetadataObject:obj];
        [transformedCodes addObject:transformedCode];
    }];
    return [transformedCodes copy];
}
複製程式碼
- (UIBezierPath *)bezierPathForBounds:(CGRect)bounds {
    return [UIBezierPath bezierPathWithRect:bounds];
}
複製程式碼
  • 而 corners 屬性是一個字典,需要手動生成 CGPoint,然後進行連線操作,生成 UIBezierPath 物件:
- (UIBezierPath *)bezierPathForCorners:(NSArray *)corners {
    UIBezierPath *path = [UIBezierPath bezierPath];
    for (int i = 0; i < corners.count; i++) {
        CGPoint point = [self pointForCorner:corners[I]];
        if (i == 0) {
            [path moveToPoint:point];
        } else {
            [path addLineToPoint:point];
        }
    }
    [path closePath];
    return path;
}

- (CGPoint)pointForCorner:(NSDictionary *)corner {
    CGPoint point;
    CGPointMakeWithDictionaryRepresentation((CFDictionaryRef)corner, &point);
    return point;
}
複製程式碼
  • corners 字典的形式大致如下所示,可以呼叫 CGPointMakeWithDictionaryRepresentation 便捷函式將其轉換為 CGPoint 形式。一般來說一個 corners 裡會包含 4 個 corner 字典。獲取到每一個 code 對應的兩個 UIBezierPath 物件後,就可以在檢視上新增相應的 CALayer 來顯示高亮區域了。

3. 例項

3.1 捕捉照片和錄製視訊Demo Swift版本

  • 此demo 來自蘋果官方文件,詳情參考蘋果官方文件:AVCam: Building a Camera App 章節,這個Demo主要是用深度資料捕捉照片,並使用前後的iPhone和iPad攝像頭錄製視訊。這個Demo使用最新的IOS SDK 要求執行在IOS 13.0以上版本。
  • iOS攝像頭應用程式允許你從前後攝像頭捕捉照片和電影。根據您的裝置,相機應用程式還支援深度資料的靜態捕獲、人像效果和實時照片。
  • 這個示例程式碼專案AVCam向您展示瞭如何在自己的相機應用程式中實現這些捕獲功能。它利用了內建的iPhone和iPad前後攝像頭的基本功能。
  • 要使用AVCam,你需要一個執行ios13或更高版本的iOS裝置。由於Xcode無法訪問裝置攝像頭,因此此示例無法在模擬器中工作。AVCam隱藏了當前裝置不支援的模式按鈕,比如iPhone 7 Plus上的人像效果曝光傳送。
  • 專案程式碼結構如下圖:
    Swift專案工程程式碼結構

3.1.1 配置捕獲會話

  • AVCaptureSession接受來自攝像頭和麥克風等捕獲裝置的輸入資料。在接收到輸入後, AVCaptureSession將資料封送到適當的輸出進行處理,最終生成一個電影檔案或靜態照片。配置捕獲會話的輸入和輸出之後,您將告訴它開始捕獲,然後停止捕獲。
 private let session = AVCaptureSession()
複製程式碼
  • AVCam預設選擇後攝像頭,並配置攝像頭捕獲會話以將內容流到視訊預覽檢視。PreviewView是一個由AVCaptureVideoPreviewLayer支援的自定義UIView子類。AVFoundation沒有PreviewView類,但是示例程式碼建立了一個類來促進會話管理。

  • 下圖顯示了會話如何管理輸入裝置和捕獲輸出:

    會話如何管理輸入裝置和捕獲輸出

  • 將與avcapturesessiessie的任何互動(包括它的輸入和輸出)委託給一個專門的序列排程佇列(sessionQueue),這樣互動就不會阻塞主佇列。在單獨的排程佇列上執行任何涉及更改會話拓撲或中斷其正在執行的視訊流的配置,因為會話配置總是阻塞其他任務的執行,直到佇列處理更改為止。類似地,樣例程式碼將其他任務分派給會話佇列,比如恢復中斷的會話、切換捕獲模式、切換攝像機、將媒體寫入檔案,這樣它們的處理就不會阻塞或延遲使用者與應用程式的互動。

  • 相反,程式碼將影響UI的任務(比如更新預覽檢視)分派給主佇列,因為AVCaptureVideoPreviewLayer是CALayer的一個子類,是示例預覽檢視的支援層。您必須在主執行緒上操作UIView子類,以便它們以及時的、互動的方式顯示。

  • 在viewDidLoad中,AVCam建立一個會話並將其分配給preview檢視:previewView.session = session

  • 有關配置影像捕獲會話的更多資訊,請參見設定捕獲會話

    配置影像捕獲會話

3.1.2 請求訪問輸入裝置的授權

  • 配置會話之後,它就可以接受輸入了。每個avcapturedevice—不管是照相機還是麥克風—都需要使用者授權訪問。AVFoundation使用AVAuthorizationStatus列舉授權狀態,該狀態通知應用程式使用者是否限制或拒絕訪問捕獲裝置。
  • 有關準備應用程式資訊的更多資訊。有關自定義授權請求,請參閱iOS上的媒體捕獲請求授權

3.1.3 在前後攝像頭之間切換

  • changeCamera方法在使用者點選UI中的按鈕時處理相機之間的切換。它使用一個發現會話,該會話按優先順序列出可用的裝置型別,並接受它的裝置陣列中的第一個裝置。例如,AVCam中的videoDeviceDiscoverySession查詢應用程式所執行的裝置,查詢可用的輸入裝置。此外,如果使用者的裝置有一個壞了的攝像頭,它將不能在裝置陣列中使用。
switch currentPosition {
case .unspecified, .front:
    preferredPosition = .back
    preferredDeviceType = .builtInDualCamera
    
case .back:
    preferredPosition = .front
    preferredDeviceType = .builtInTrueDepthCamera
    
@unknown default:
    print("Unknown capture position. Defaulting to back, dual-camera.")
    preferredPosition = .back
    preferredDeviceType = .builtInDualCamera
}
複製程式碼
  • changeCamera方法處理相機之間的切換,如果發現會話發現相機處於適當的位置,它將從捕獲會話中刪除以前的輸入,並將新相機新增為輸入。
// Remove the existing device input first, because AVCaptureSession doesn't support
// simultaneous use of the rear and front cameras.
self.session.removeInput(self.videoDeviceInput)

if self.session.canAddInput(videoDeviceInput) {
    NotificationCenter.default.removeObserver(self, name: .AVCaptureDeviceSubjectAreaDidChange, object: currentVideoDevice)
    NotificationCenter.default.addObserver(self, selector: #selector(self.subjectAreaDidChange), name: .AVCaptureDeviceSubjectAreaDidChange, object: videoDeviceInput.device)
    
    self.session.addInput(videoDeviceInput)
    self.videoDeviceInput = videoDeviceInput
} else {
    self.session.addInput(self.videoDeviceInput)
}
複製程式碼

3.1.4 處理中斷和錯誤

  • 在捕獲會話期間,可能會出現諸如電話呼叫、其他應用程式通知和音樂播放等中斷。通過新增觀察者來處理這些干擾,以偵聽AVCaptureSessionWasInterrupted:
NotificationCenter.default.addObserver(self,
                                       selector: #selector(sessionWasInterrupted),
                                       name: .AVCaptureSessionWasInterrupted,
                                       object: session)
NotificationCenter.default.addObserver(self,
                                       selector: #selector(sessionInterruptionEnded),
                                       name: .AVCaptureSessionInterruptionEnded,
                                       object: session)
複製程式碼
  • 當AVCam接收到中斷通知時,它可以暫停或掛起會話,並提供一個在中斷結束時恢復活動的選項。AVCam將sessionwas註冊為接收通知的處理程式,當捕獲會話出現中斷時通知使用者:
if reason == .audioDeviceInUseByAnotherClient || reason == .videoDeviceInUseByAnotherClient {
    showResumeButton = true
} else if reason == .videoDeviceNotAvailableWithMultipleForegroundApps {
    // Fade-in a label to inform the user that the camera is unavailable.
    cameraUnavailableLabel.alpha = 0
    cameraUnavailableLabel.isHidden = false
    UIView.animate(withDuration: 0.25) {
        self.cameraUnavailableLabel.alpha = 1
    }
} else if reason == .videoDeviceNotAvailableDueToSystemPressure {
    print("Session stopped running due to shutdown system pressure level.")
}
複製程式碼
  • 攝像頭檢視控制器觀察AVCaptureSessionRuntimeError,當錯誤發生時接收通知:
NotificationCenter.default.addObserver(self,
                                       selector: #selector(sessionRuntimeError),
                                       name: .AVCaptureSessionRuntimeError,
                                       object: session)
複製程式碼
  • 當執行時錯誤發生時,重新啟動捕獲會話:
// If media services were reset, and the last start succeeded, restart the session.
if error.code == .mediaServicesWereReset {
    sessionQueue.async {
        if self.isSessionRunning {
            self.session.startRunning()
            self.isSessionRunning = self.session.isRunning
        } else {
            DispatchQueue.main.async {
                self.resumeButton.isHidden = false
            }
        }
    }
} else {
    resumeButton.isHidden = false
}
複製程式碼
  • 如果裝置承受系統壓力,比如過熱,捕獲會話也可能停止。相機本身不會降低拍攝質量或減少幀數;為了避免讓你的使用者感到驚訝,你可以讓你的應用手動降低幀速率,關閉深度,或者根據AVCaptureDevice.SystemPressureState:的反饋來調整效能。
let pressureLevel = systemPressureState.level
if pressureLevel == .serious || pressureLevel == .critical {
    if self.movieFileOutput == nil || self.movieFileOutput?.isRecording == false {
        do {
            try self.videoDeviceInput.device.lockForConfiguration()
            print("WARNING: Reached elevated system pressure level: \(pressureLevel). Throttling frame rate.")
            self.videoDeviceInput.device.activeVideoMinFrameDuration = CMTime(value: 1, timescale: 20)
            self.videoDeviceInput.device.activeVideoMaxFrameDuration = CMTime(value: 1, timescale: 15)
            self.videoDeviceInput.device.unlockForConfiguration()
        } catch {
            print("Could not lock device for configuration: \(error)")
        }
    }
} else if pressureLevel == .shutdown {
    print("Session stopped running due to shutdown system pressure level.")
}
複製程式碼

3.1.5 捕捉一張照片

  • 在會話佇列上拍照。該過程首先更新AVCapturePhotoOutput連線以匹配視訊預覽層的視訊方向。這使得相機能夠準確地捕捉到使用者在螢幕上看到的內容:
if let photoOutputConnection = self.photoOutput.connection(with: .video) {
    photoOutputConnection.videoOrientation = videoPreviewLayerOrientation!
}
複製程式碼
  • 對齊輸出後,AVCam繼續建立AVCapturePhotoSettings來配置捕獲引數,如焦點、flash和解析度:
var photoSettings = AVCapturePhotoSettings()

// Capture HEIF photos when supported. Enable auto-flash and high-resolution photos.
if  self.photoOutput.availablePhotoCodecTypes.contains(.hevc) {
    photoSettings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.hevc])
}

if self.videoDeviceInput.device.isFlashAvailable {
    photoSettings.flashMode = .auto
}

photoSettings.isHighResolutionPhotoEnabled = true
if !photoSettings.__availablePreviewPhotoPixelFormatTypes.isEmpty {
    photoSettings.previewPhotoFormat = [kCVPixelBufferPixelFormatTypeKey as String: photoSettings.__availablePreviewPhotoPixelFormatTypes.first!]
}
// Live Photo capture is not supported in movie mode.
if self.livePhotoMode == .on && self.photoOutput.isLivePhotoCaptureSupported {
    let livePhotoMovieFileName = NSUUID().uuidString
    let livePhotoMovieFilePath = (NSTemporaryDirectory() as NSString).appendingPathComponent((livePhotoMovieFileName as NSString).appendingPathExtension("mov")!)
    photoSettings.livePhotoMovieFileURL = URL(fileURLWithPath: livePhotoMovieFilePath)
}

photoSettings.isDepthDataDeliveryEnabled = (self.depthDataDeliveryMode == .on
    && self.photoOutput.isDepthDataDeliveryEnabled)

photoSettings.isPortraitEffectsMatteDeliveryEnabled = (self.portraitEffectsMatteDeliveryMode == .on
    && self.photoOutput.isPortraitEffectsMatteDeliveryEnabled)

if photoSettings.isDepthDataDeliveryEnabled {
    if !self.photoOutput.availableSemanticSegmentationMatteTypes.isEmpty {
        photoSettings.enabledSemanticSegmentationMatteTypes = self.selectedSemanticSegmentationMatteTypes
    }
}

photoSettings.photoQualityPrioritization = self.photoQualityPrioritizationMode
複製程式碼
  • 該示例使用一個單獨的物件PhotoCaptureProcessor作為照片捕獲委託,以隔離每個捕獲生命週期。對於實時照片來說,這種清晰的捕獲週期分離是必要的,因為單個捕獲週期可能涉及多個幀的捕獲。
  • 每次使用者按下中央快門按鈕時,AVCam都會通過呼叫capturePhoto(帶有:delegate:)來使用之前配置的設定捕捉照片:
self.photoOutput.capturePhoto(with: photoSettings, delegate: photoCaptureProcessor)
複製程式碼
  • capturePhoto方法接受兩個引數:
  1. 一個avcapturephotoset物件,它封裝了使用者通過應用配置的設定,比如曝光、閃光、對焦和手電筒。
  2. 一個符合AVCapturePhotoCaptureDelegate協議的委託,以響應系統在捕獲照片期間傳遞的後續回撥。
  • 一旦應用程式呼叫capturePhoto(帶有:delegate:),開始拍照的過程就結束了。此後,對單個照片捕獲的操作將在委託回撥中發生。

3.1.6 通過照片捕獲委託跟蹤結果

  • capturePhoto方法只是開始拍照的過程。剩下的過程發生在應用程式實現的委託方法中。

    照片捕獲流程

  • 當你呼叫capturePhoto時,photoOutput(_:willBeginCaptureFor:)首先到達。解析的設定表示相機將為即將到來的照片應用的實際設定。AVCam僅將此方法用於特定於活動照片的行為。AVCam通過檢查livephotomovieviedimensions尺寸來判斷照片是否為活動照片;如果照片是活動照片,AVCam會增加一個計數來跟蹤活動中的照片:

self.sessionQueue.async {
    if capturing {
        self.inProgressLivePhotoCapturesCount += 1
    } else {
        self.inProgressLivePhotoCapturesCount -= 1
    }
    
    let inProgressLivePhotoCapturesCount = self.inProgressLivePhotoCapturesCount
    DispatchQueue.main.async {
        if inProgressLivePhotoCapturesCount > 0 {
            self.capturingLivePhotoLabel.isHidden = false
        } else if inProgressLivePhotoCapturesCount == 0 {
            self.capturingLivePhotoLabel.isHidden = true
        } else {
            print("Error: In progress Live Photo capture count is less than 0.")
        }
    }
}
複製程式碼
  • photoOutput(_:willCapturePhotoFor:)正好在系統播放快門聲之後到達。AVCam利用這個機會來閃爍螢幕,提醒使用者照相機捕獲了一張照片。示例程式碼通過將預覽檢視層的不透明度從0調整到1來實現此flash。
// Flash the screen to signal that AVCam took a photo.
DispatchQueue.main.async {
    self.previewView.videoPreviewLayer.opacity = 0
    UIView.animate(withDuration: 0.25) {
        self.previewView.videoPreviewLayer.opacity = 1
    }
}
複製程式碼
  • photoOutput(_:didFinishProcessingPhoto:error:)在系統完成深度資料處理和人像效果處理後到達。AVCam檢查肖像效果,曝光和深度後設資料在這個階段:
self.sessionQueue.async {
    self.inProgressPhotoCaptureDelegates[photoCaptureProcessor.requestedPhotoSettings.uniqueID] = nil
}
複製程式碼
  • 您可以在此委託方法中應用其他視覺效果,例如動畫化捕獲照片的預覽縮圖。
  • 有關通過委託回撥跟蹤照片進度的更多資訊,請參見跟蹤照片捕獲進度

捕捉攝像頭拍照一個iOS裝置是一個複雜的過程,涉及物理相機機制、影像訊號處理、作業系統和應用程式。雖然你的應用有可能忽略許多階段,這個過程,只是等待最終的結果,您可以建立一個更具響應性相機介面通過監控每一步。 在呼叫capturePhoto(帶有:delegate:)之後,您的委派物件可以遵循該過程中的五個主要步驟(或者更多,取決於您的照片設定)。根據您的捕獲工作流和您想要建立的捕獲UI,您的委託可以處理以下部分或全部步驟:

捕獲照片流程
捕獲系統在這個過程的每一步都提供一個avcaptureresolvedphotoset物件。由於多個捕獲可以同時進行,因此每個解析後的照片設定物件都有一個uniqueID,其值與您用於拍攝照片的avcapturephotos的uniqueID相匹配。

3.1.7 捕捉實時的照片

  • 當您啟用實時照片捕捉功能時,相機會在捕捉瞬間拍攝一張靜止影像和一段短視訊。該應用程式以與靜態照片捕獲相同的方式觸發實時照片捕獲:通過對capturePhotoWithSettings的單個呼叫,您可以通過livePhotoMovieFileURL屬性傳遞實時照片短視訊的URL。您可以在AVCapturePhotoOutput級別啟用活動照片,也可以在每次捕獲的基礎上在avcapturephotoset級別配置活動照片。

  • 由於Live Photo capture建立了一個簡短的電影檔案,AVCam必須表示將電影檔案儲存為URL的位置。此外,由於實時照片捕捉可能會重疊,因此程式碼必須跟蹤正在進行的實時照片捕捉的數量,以確保實時照片標籤在這些捕捉期間保持可見。上一節中的photoOutput(_:willBeginCaptureFor:)委託方法實現了這個跟蹤計數器。

    捕捉實時的照片流程

  • photoOutput(_:didFinishRecordingLivePhotoMovieForEventualFileAt:resolvedSettings:)在錄製短片結束時觸發。AVCam取消了這裡的活動標誌。因為攝像機已經完成了短片的錄製,AVCam執行Live Photo處理器遞減完成計數器:livePhotoCaptureHandler(false)

  • photoOutput(_:didFinishProcessingLivePhotoToMovieFileAt:duration:photoDisplayTime:resolvedSettings:error:)最後觸發,表示影片已完全寫入磁碟,可以使用了。AVCam利用這個機會來顯示任何捕獲錯誤,並將儲存的檔案URL重定向到它的最終輸出位置:

if error != nil {
    print("Error processing Live Photo companion movie: \(String(describing: error))")
    return
}
livePhotoCompanionMovieURL = outputFileURL
複製程式碼

3.1.8 捕獲深度資料和人像效果曝光

  • 使用AVCapturePhotoOutput, AVCam查詢捕獲裝置,檢視其配置是否可以將深度資料和人像效果傳送到靜態影像。如果輸入裝置支援這兩種模式中的任何一種,並且您在捕獲設定中啟用了它們,則相機將深度和人像效果作為輔助後設資料附加到每張照片請求的基礎上。如果裝置支援深度資料、人像效果或實時照片的傳輸,應用程式會顯示一個按鈕,用來切換啟用或禁用該功能的設定。
if self.photoOutput.isDepthDataDeliverySupported {
               self.photoOutput.isDepthDataDeliveryEnabled = true
               
               DispatchQueue.main.async {
                   self.depthDataDeliveryButton.isEnabled = true
               }
           }
           
           if self.photoOutput.isPortraitEffectsMatteDeliverySupported {
               self.photoOutput.isPortraitEffectsMatteDeliveryEnabled = true
               
               DispatchQueue.main.async {
                   self.portraitEffectsMatteDeliveryButton.isEnabled = true
               }
           }
           
           if !self.photoOutput.availableSemanticSegmentationMatteTypes.isEmpty {
self.photoOutput.enabledSemanticSegmentationMatteTypes = self.photoOutput.availableSemanticSegmentationMatteTypes
               self.selectedSemanticSegmentationMatteTypes = self.photoOutput.availableSemanticSegmentationMatteTypes
               
               DispatchQueue.main.async {
                   self.semanticSegmentationMatteDeliveryButton.isEnabled = (self.depthDataDeliveryMode == .on) ? true : false
               }
           }
           
           DispatchQueue.main.async {
               self.livePhotoModeButton.isHidden = false
               self.depthDataDeliveryButton.isHidden = false
               self.portraitEffectsMatteDeliveryButton.isHidden = false
               self.semanticSegmentationMatteDeliveryButton.isHidden = false
               self.photoQualityPrioritizationSegControl.isHidden = false
               self.photoQualityPrioritizationSegControl.isEnabled = true
           }
複製程式碼
  • 相機儲存深度和人像效果的曝光後設資料作為輔助影像,可通過影像I/O API發現和定址。AVCam通過搜尋kCGImageAuxiliaryDataTypePortraitEffectsMatte型別的輔助影像來訪問這個後設資料:
if var portraitEffectsMatte = photo.portraitEffectsMatte {
    if let orientation = photo.metadata[String(kCGImagePropertyOrientation)] as? UInt32 {
        portraitEffectsMatte = portraitEffectsMatte.applyingExifOrientation(CGImagePropertyOrientation(rawValue: orientation)!)
    }
    let portraitEffectsMattePixelBuffer = portraitEffectsMatte.mattingImage
複製程式碼

在有後置雙攝像頭或前置真深度攝像頭的iOS裝置上,捕獲系統可以記錄深度資訊。深度圖就像一個影像;但是,它不是每個畫素提供一個顏色,而是表示從相機到影像的那一部分的距離(以絕對值表示,或與深度圖中的其他畫素相對)。 您可以使用一個深度地圖和照片一起建立影像處理效果,對前景和背景照片不同的元素,像iOS的豎屏模式相機應用。通過儲存顏色和深度資料分開,你甚至可以應用,改變這些影響長照片後被抓獲。

使用深度捕獲照片

3.1.9 捕捉語義分割

  • 使用AVCapturePhotoOutput, AVCam還可以捕獲語義分割影像,將一個人的頭髮、皮膚和牙齒分割成不同的影像。將這些輔助影像與你的主要照片一起捕捉,可以簡化照片效果的應用,比如改變一個人的頭髮顏色或讓他們的笑容更燦爛。 通過將照片輸出的enabledSemanticSegmentationMatteTypes屬性設定為首選值(頭髮、皮膚和牙齒),可以捕獲這些輔助影像。要捕獲所有受支援的型別,請設定此屬性以匹配照片輸出的availableSemanticSegmentationMatteTypes屬性。
// Capture all available semantic segmentation matte types.
photoOutput.enabledSemanticSegmentationMatteTypes = 
    photoOutput.availableSemanticSegmentationMatteTypes
複製程式碼
  • 當照片輸出完成捕獲一張照片時,您可以通過查詢照片的semanticSegmentationMatte(for:)方法來檢索相關的分割matte影像。此方法返回一個AVSemanticSegmentationMatte,其中包含matte影像和處理影像時可以使用的其他後設資料。示例應用程式將語義分割的matte影像資料新增到一個陣列中,這樣您就可以將其寫入使用者的照片庫。
// Find the semantic segmentation matte image for the specified type.
guard var segmentationMatte = photo.semanticSegmentationMatte(for: ssmType) else { return }

// Retrieve the photo orientation and apply it to the matte image.
if let orientation = photo.metadata[String(kCGImagePropertyOrientation)] as? UInt32,
    let exifOrientation = CGImagePropertyOrientation(rawValue: orientation) {
    // Apply the Exif orientation to the matte image.
    segmentationMatte = segmentationMatte.applyingExifOrientation(exifOrientation)
}

var imageOption: CIImageOption!

// Switch on the AVSemanticSegmentationMatteType value.
switch ssmType {
case .hair:
    imageOption = .auxiliarySemanticSegmentationHairMatte
case .skin:
    imageOption = .auxiliarySemanticSegmentationSkinMatte
case .teeth:
    imageOption = .auxiliarySemanticSegmentationTeethMatte
default:
    print("This semantic segmentation type is not supported!")
    return
}

guard let perceptualColorSpace = CGColorSpace(name: CGColorSpace.sRGB) else { return }

// Create a new CIImage from the matte's underlying CVPixelBuffer.
let ciImage = CIImage( cvImageBuffer: segmentationMatte.mattingImage,
                       options: [imageOption: true,
                                 .colorSpace: perceptualColorSpace])

// Get the HEIF representation of this image.
guard let imageData = context.heifRepresentation(of: ciImage,
                                                 format: .RGBA8,
                                                 colorSpace: perceptualColorSpace,
                                                 options: [.depthImage: ciImage]) else { return }

// Add the image data to the SSM data array for writing to the photo library.
semanticSegmentationMatteDataArray.append(imageData)
複製程式碼

3.1.10 儲存照片到使用者的照片庫

  • 在將影像或電影儲存到使用者的照片庫之前,必須首先請求訪問該庫。請求寫授權的過程映象捕獲裝置授權:使用Info.plist中提供的文字顯示警報。 AVCam在fileOutput(_:didFinishRecordingTo:from:error:)回撥方法中檢查授權,其中AVCaptureOutput提供了要儲存為輸出的媒體資料。PHPhotoLibrary.requestAuthorization { status in

  • 有關請求訪問使用者的照片庫的更多資訊,請參見請求訪問照片的授權

  1. 使用者必須明確授予您的應用程式訪問照片的許可權。通過提供調整字串來準備你的應用。調整字串是一個可本地化的訊息,你新增到你的應用程式的資訊。plist檔案,告訴使用者為什麼你的應用程式需要訪問使用者的照片庫。然後,當照片提示使用者授予訪問許可權時,警報將以使用者裝置上選擇的語言環境顯示您提供的調整字串。
  2. PHCollection,第一次您的應用程式使用PHAsset PHAssetCollection,從圖書館或PHCollectionList方法獲取內容,或使用一個照片庫中列出的方法應用更改請求更改庫內容,照片自動和非同步提示使用者請求授權。 系統使用者授予許可權後,記得將來使用的選擇在你的應用程式,但是使用者可以在任何時候改變這個選擇使用設定應用程式。如果使用者否認你的應用照片庫訪問,還沒有回覆許可權提示,或不能授予訪問許可權限制,任何試圖獲取照片庫內容將返回空PHFetchResult物件,和任何試圖更改照片庫將會失敗。如果這個方法返回PHAuthorizationStatus。您可以呼叫requestAuthorization(_:)方法來提示使用者訪問照片庫許可權。
  3. 使用與照片庫互動的類,如PHAsset、PHPhotoLibrary和PHImageManager(應用程式的資訊)。plist檔案必須包含面向使用者的NSPhotoLibraryUsageDescription鍵文字,系統在請求使用者訪問許可權時將顯示該文字。如果沒有這個鍵,iOS 10或之後的應用程式將會崩潰。
    修改許可權plist檔案

3.1.11 錄製視訊檔案

  • AVCam通過使用.video限定符查詢和新增輸入裝置來支援視訊捕獲。該應用程式預設為後雙攝像頭,但如果裝置沒有雙攝像頭,該應用程式預設為廣角攝像頭。
if let dualCameraDevice = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back) {
    defaultVideoDevice = dualCameraDevice
} else if let backCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
    // If a rear dual camera is not available, default to the rear wide angle camera.
    defaultVideoDevice = backCameraDevice
} else if let frontCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) {
    // If the rear wide angle camera isn't available, default to the front wide angle camera.
    defaultVideoDevice = frontCameraDevice
}
複製程式碼
  • 不像靜態照片那樣將設定傳遞給系統,而是像活動照片那樣傳遞輸出URL。委託回撥提供相同的URL,因此應用程式不需要將其儲存在中間變數中。
  • 一旦使用者點選記錄開始捕獲,AVCam呼叫startRecording(to:recordingDelegate:):
movieFileOutput.startRecording(to: URL(fileURLWithPath: outputFilePath), recordingDelegate: self)

複製程式碼
  • 與capturePhoto為still capture觸發委託回撥一樣,startRecording為影片錄製觸發一系列委託回撥。

錄製視訊流程

  • 通過委託回撥鏈跟蹤影片錄製的進度。與其實現AVCapturePhotoCaptureDelegate,不如實現AVCaptureFileOutputRecordingDelegate。由於影片錄製委託回撥需要與捕獲會話進行互動,因此AVCam將CameraViewController作為委託,而不是建立單獨的委託物件。
  • 當檔案輸出開始向檔案寫入資料時觸發fileOutput(_:didStartRecordingTo:from:)。AVCam利用這個機會將記錄按鈕更改為停止按鈕:
DispatchQueue.main.async {
    self.recordButton.isEnabled = true
    self.recordButton.setImage(#imageLiteral(resourceName: "CaptureStop"), for: [])
}
複製程式碼
  • fileOutput(_:didFinishRecordingTo:from:error:)最後觸發,表示影片已完全寫入磁碟,可以使用了。AVCam利用這個機會將臨時儲存的影片從給定的URL移動到使用者的照片庫或應用程式的文件資料夾:
PHPhotoLibrary.shared().performChanges({
    let options = PHAssetResourceCreationOptions()
    options.shouldMoveFile = true
    let creationRequest = PHAssetCreationRequest.forAsset()
    creationRequest.addResource(with: .video, fileURL: outputFileURL, options: options)
}, completionHandler: { success, error in
    if !success {
        print("AVCam couldn't save the movie to your photo library: \(String(describing: error))")
    }
    cleanup()
}
)
複製程式碼
  • 如果AVCam進入後臺——例如使用者接受來電時——應用程式必須獲得使用者的許可才能繼續錄製。AVCam通過後臺任務從系統請求時間來執行此儲存。這個後臺任務確保有足夠的時間將檔案寫入照片庫,即使AVCam退到後臺。為了結束後臺執行,AVCam在儲存記錄檔案後呼叫fileOutput(:didFinishRecordingTo:from:error:)中的endBackgroundTask(:)。
self.backgroundRecordingID = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)

複製程式碼

3.1.12 錄製視訊時要抓拍圖片

  • 與iOS攝像頭應用程式一樣,AVCam也可以在拍攝錄影的同時拍照。AVCam以與視訊相同的解析度捕捉這些照片。實現程式碼如下:
let movieFileOutput = AVCaptureMovieFileOutput()

if self.session.canAddOutput(movieFileOutput) {
    self.session.beginConfiguration()
    self.session.addOutput(movieFileOutput)
    self.session.sessionPreset = .high
    if let connection = movieFileOutput.connection(with: .video) {
        if connection.isVideoStabilizationSupported {
            connection.preferredVideoStabilizationMode = .auto
        }
    }
    self.session.commitConfiguration()
    
    DispatchQueue.main.async {
        captureModeControl.isEnabled = true
    }
    
    self.movieFileOutput = movieFileOutput
    
    DispatchQueue.main.async {
        self.recordButton.isEnabled = true
        
        /*
         For photo captures during movie recording, Speed quality photo processing is prioritized
         to avoid frame drops during recording.
         */
        self.photoQualityPrioritizationSegControl.selectedSegmentIndex = 0
        self.photoQualityPrioritizationSegControl.sendActions(for: UIControl.Event.valueChanged)
    }
}
複製程式碼

相關文章