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

@TOC

1. 媒體捕捉概念

  • 理解捕捉媒體辐怕,需要先了解一些基本概念:
  • 捕捉會(huì)話

AVCaptureSession 是管理捕獲活動(dòng)并協(xié)調(diào)從輸入設(shè)備到捕獲輸出的數(shù)據(jù)流的對(duì)象。 AVCaptureSession 用于連接輸入和輸出的資源澈蝙,從物理設(shè)備如攝像頭和麥克風(fēng)等獲取數(shù)據(jù)流吓坚,輸出到一個(gè)或多個(gè)目的地。 AVCaptureSession 可以額外配置一個(gè)會(huì)話預(yù)設(shè)值(session preset)碉克,用于控制捕捉數(shù)據(jù)的格式和質(zhì)量凌唬,預(yù)設(shè)值默認(rèn)值為 AVCaptureSessionPresetHigh。

要執(zhí)行實(shí)時(shí)捕獲漏麦,需要實(shí)例化AVCaptureSession對(duì)象并添加適當(dāng)?shù)妮斎牒洼敵隹退啊O旅娴拇a片段演示了如何配置捕獲設(shè)備來(lái)錄制音頻。

// 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.
}

您可以調(diào)用startRunning()來(lái)啟動(dòng)從輸入到輸出的數(shù)據(jù)流撕贞,并調(diào)用stopRunning()來(lái)停止該流更耻。

注意:startRunning()方法是一個(gè)阻塞調(diào)用,可能會(huì)花費(fèi)一些時(shí)間捏膨,因此應(yīng)該在串行隊(duì)列上執(zhí)行會(huì)話設(shè)置秧均,以免阻塞主隊(duì)列(這使UI保持響應(yīng))。參見AVCam:構(gòu)建攝像機(jī)應(yīng)用程序的實(shí)現(xiàn)示例号涯。

  • 捕捉設(shè)備:

AVCaptureDevice 是為捕獲會(huì)話提供輸入(如音頻或視頻)并為特定于硬件的捕獲特性提供控制的設(shè)備目胡。它為物理設(shè)備定義統(tǒng)一接口,以及大量控制方法链快,獲取指定類型的默認(rèn)設(shè)備方法如下:self.activeVideoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

  1. 一個(gè) AVCaptureDevice 對(duì)象表示一個(gè)物理捕獲設(shè)備和與該設(shè)備相關(guān)聯(lián)的屬性誉己。您可以使用捕獲設(shè)備來(lái)配置底層硬件的屬性。捕獲設(shè)備還向AVCaptureSession對(duì)象提供輸入數(shù)據(jù)(如音頻或視頻)域蜗。
  • 捕捉設(shè)備的輸入:

不能直接將 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 作為抽象基類提供了捕捉會(huì)話數(shù)據(jù)流的輸出目的地霉祸,同時(shí)定義了此抽象類的高級(jí)擴(kuò)展類筑累。

  1. AVCaptureStillImageOutput - 靜態(tài)照片( 在ios10后被廢棄,使用AVCapturePhotoOutput代替)
  2. AVCaptureMovieFileOutput - 視頻,
  3. AVCaptureAudioFileOutput - 音頻
  4. AVCaptureAudioDataOutput - 音頻底層數(shù)字樣本
  5. AVCaptureVideoDataOutput - 視頻底層數(shù)字樣本
  • 捕捉連接

AVCaptureConnection :捕獲會(huì)話中捕獲輸入和捕獲輸出對(duì)象的特定對(duì)之間的連接丝蹭。AVCaptureConnection 用于確定哪些輸入產(chǎn)生視頻慢宗,哪些輸入產(chǎn)生音頻,能夠禁用特定連接或訪問單獨(dú)的音頻軌道。

  1. 捕獲輸入有一個(gè)或多個(gè)輸入端口(avcaptureinpu . port的實(shí)例)婆廊。捕獲輸出可以接受來(lái)自一個(gè)或多個(gè)源的數(shù)據(jù)(例如迅细,AVCaptureMovieFileOutput對(duì)象同時(shí)接受視頻和音頻數(shù)據(jù))。
    只有在canAddConnection(:)方法返回true時(shí)淘邻,才可以使用addConnection(:)方法將AVCaptureConnection實(shí)例添加到會(huì)話中茵典。當(dāng)使用addInput(:)或addOutput(:)方法時(shí),會(huì)話自動(dòng)在所有兼容的輸入和輸出之間形成連接宾舅。在添加沒有連接的輸入或輸出時(shí)统阿,只需手動(dòng)添加連接。您還可以使用連接來(lái)啟用或禁用來(lái)自給定輸入或到給定輸出的數(shù)據(jù)流筹我。
  • 捕捉預(yù)覽 :

AVCaptureVideoPreviewLayer 是一個(gè) CALayer 的子類扶平,可以對(duì)捕捉視頻數(shù)據(jù)進(jìn)行實(shí)時(shí)預(yù)覽。

2. 視頻捕捉實(shí)例

  • 這個(gè)實(shí)例的項(xiàng)目代碼點(diǎn)擊這里下載:OC 視頻捕獲相機(jī)Demo
  • 項(xiàng)目是OC編寫的蔬蕊,主要功能實(shí)現(xiàn)在THCameraController中结澄,如下圖:


    視頻捕獲功能實(shí)現(xiàn)類
  • 主要接口變量在頭文件THCameraController.h里面:

#import <AVFoundation/AVFoundation.h>

extern NSString *const THThumbnailCreatedNotification;

@protocol THCameraControllerDelegate <NSObject>

// 1發(fā)生錯(cuò)誤事件是,需要在對(duì)象委托上調(diào)用一些方法來(lái)處理
- (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 用于設(shè)置岸夯、配置視頻捕捉會(huì)話
- (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 聚焦麻献、曝光、重設(shè)聚焦猜扮、曝光的方法
- (void)focusAtPoint:(CGPoint)point;
- (void)exposeAtPoint:(CGPoint)point;
- (void)resetFocusAndExposureModes;

// 5 實(shí)現(xiàn)捕捉靜態(tài)圖片 & 視頻的功能

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

//視頻錄制
//開始錄制
- (void)startRecording;

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

//獲取錄制狀態(tài)
- (BOOL)isRecording;

//錄制時(shí)間
- (CMTime)recordedDuration;

@end

  • 我們需要添加訪問權(quán)限勉吻,如果沒有獲取到相機(jī)和麥克風(fēng)權(quán)限,在設(shè)置 captureVideoInput 時(shí)就會(huì)出錯(cuò)旅赢。
/// 檢測(cè) AVAuthorization 權(quán)限
/// 傳入待檢查的 AVMediaType齿桃,AVMediaTypeVideo or AVMediaTypeAudio
/// 返回是否權(quán)限可用
- (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: { // 尚未請(qǐng)求授權(quán)
                [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];// 申請(qǐng)相機(jī)權(quán)限
                } else if ([mediaType isEqualToString:AVMediaTypeAudio]) {
                    [METSettingPermissionAlertView showAlertViewWithPermissionType:METSettingPermissionTypeMicrophone];// 申請(qǐng)麥克風(fēng)權(quán)限
                }
                break;
            }
            case AVAuthorizationStatusRestricted: { // 限制權(quán)限更改
                break;
            }
            case AVAuthorizationStatusAuthorized: { // 已授權(quán)
                result = YES;
                break;
            }
            default: // 兜底
                break;
        }
    }
    return result;
}

2.1 創(chuàng)建預(yù)覽視圖

    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 實(shí)例:
+ (Class)layerClass {
    return [AVCaptureVideoPreviewLayer class];
}

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

- (void)setSession:(AVCaptureSession *)session {
    [(AVCaptureVideoPreviewLayer*)self.layer setSession:session];
}
  • AVCaptureVideoPreviewLayer 定義了兩個(gè)方法用于在屏幕坐標(biāo)系和設(shè)備坐標(biāo)系之間轉(zhuǎn)換,設(shè)備坐標(biāo)系規(guī)定左上角為 (0煮盼,0)短纵,右下角為(1,1)僵控。
  1. (CGPoint)captureDevicePointOfInterestForPoint:(CGPoint)pointInLayer 從屏幕坐標(biāo)系的點(diǎn)轉(zhuǎn)換為設(shè)備坐標(biāo)系
  2. (CGPoint)pointForCaptureDevicePointOfInterest:(CGPoint)captureDevicePointOfInterest 從設(shè)備坐標(biāo)系的點(diǎn)轉(zhuǎn)換為屏幕坐標(biāo)系

2.2 設(shè)置捕捉會(huì)話

  • 首先是初始化捕捉會(huì)話:
    self.captureSession = [[AVCaptureSession alloc]init];
    [self.captureSession setSessionPreset:(self.isVideoMode)?AVCaptureSessionPreset1280x720:AVCaptureSessionPresetPhoto];
  • 根據(jù)拍攝視頻還是拍攝照片選擇不同的預(yù)設(shè)值踩娘,然后設(shè)置會(huì)話輸入:
- (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) {
        // 麥克風(fēng)輸入
        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) {
        }
    }
}
  • 對(duì)攝像頭和麥克風(fēng)設(shè)備均封裝為 AVCaptureDeviceInput 后加入到會(huì)話中。然后配置會(huì)話輸出:
- (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];
        }
    }
}
  • 當(dāng)然你也可以合成在一個(gè)方法里面直接設(shè)置捕獲會(huì)話
- (BOOL)setupSession:(NSError **)error {

    
    //創(chuàng)建捕捉會(huì)話。AVCaptureSession 是捕捉場(chǎng)景的中心樞紐
    self.captureSession = [[AVCaptureSession alloc]init];
    
    /*
     AVCaptureSessionPresetHigh
     AVCaptureSessionPresetMedium
     AVCaptureSessionPresetLow
     AVCaptureSessionPreset640x480
     AVCaptureSessionPreset1280x720
     AVCaptureSessionPresetPhoto
     */
    //設(shè)置圖像的分辨率
    self.captureSession.sessionPreset = AVCaptureSessionPresetHigh;
    
    //拿到默認(rèn)視頻捕捉設(shè)備 iOS系統(tǒng)返回后置攝像頭
    AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    
    //將捕捉設(shè)備封裝成AVCaptureDeviceInput
    //注意:為會(huì)話添加捕捉設(shè)備雷绢,必須將設(shè)備封裝成AVCaptureDeviceInput對(duì)象
    AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:error];
    
    //判斷videoInput是否有效
    if (videoInput)
    {
        //canAddInput:測(cè)試是否能被添加到會(huì)話中
        if ([self.captureSession canAddInput:videoInput])
        {
            //將videoInput 添加到 captureSession中
            [self.captureSession addInput:videoInput];
            self.activeVideoInput = videoInput;
        }
    }else
    {
        return NO;
    }
    
    //選擇默認(rèn)音頻捕捉設(shè)備 即返回一個(gè)內(nèi)置麥克風(fēng)
    AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
    
    //為這個(gè)設(shè)備創(chuàng)建一個(gè)捕捉設(shè)備輸入
    AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:error];
   
    //判斷audioInput是否有效
    if (audioInput) {
        
        //canAddInput:測(cè)試是否能被添加到會(huì)話中
        if ([self.captureSession canAddInput:audioInput])
        {
            //將audioInput 添加到 captureSession中
            [self.captureSession addInput:audioInput];
        }
    }else
    {
        return NO;
    }

    //AVCaptureStillImageOutput 實(shí)例 從攝像頭捕捉靜態(tài)圖片
    self.imageOutput = [[AVCaptureStillImageOutput alloc]init];
    
    //配置字典:希望捕捉到JPEG格式的圖片
    self.imageOutput.outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG};
    
    //輸出連接 判斷是否可用泛烙,可用則添加到輸出連接中去
    if ([self.captureSession canAddOutput:self.imageOutput])
    {
        [self.captureSession addOutput:self.imageOutput];
        
    }
    
    
    //創(chuàng)建一個(gè)AVCaptureMovieFileOutput 實(shí)例,用于將Quick Time 電影錄制到文件系統(tǒng)
    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 啟動(dòng), 停止會(huì)話

  • 可以在一個(gè) VC 的生命周期內(nèi)啟動(dòng)和停止會(huì)話,由于這個(gè)操作是比較耗時(shí)的同步操作蔽氨,因此建議在異步線程里執(zhí)行此方法。如下:
- (void)startSession {

    //檢查是否處于運(yùn)行狀態(tài)
    if (![self.captureSession isRunning])
    {
        //使用同步調(diào)用會(huì)損耗一定的時(shí)間,則用異步的方式處理
        dispatch_async(self.videoQueue, ^{
            [self.captureSession startRunning];
        });
    }
}

- (void)stopSession {
    
    //檢查是否處于運(yùn)行狀態(tài)
    if ([self.captureSession isRunning])
    {
        //使用異步方式鹉究,停止運(yùn)行
        dispatch_async(self.videoQueue, ^{
            [self.captureSession stopRunning];
        });
    }
}

2.4 切換攝像頭

  • 大多數(shù) ios 設(shè)備都有前后兩個(gè)攝像頭宇立,標(biāo)識(shí)前后攝像頭需要用到 AVCaptureDevicePosition 枚舉類:
typedef NS_ENUM(NSInteger, AVCaptureDevicePosition) {
    AVCaptureDevicePositionUnspecified = 0, // 未知
    AVCaptureDevicePositionBack        = 1, // 后置攝像頭
    AVCaptureDevicePositionFront       = 2, // 前置攝像頭
}
  • 接下來(lái)獲取當(dāng)前活躍的設(shè)備,沒有激活的設(shè)備:
- (AVCaptureDevice *)activeCamera {
    //返回當(dāng)前捕捉會(huì)話對(duì)應(yīng)的攝像頭的device 屬性
    return self.activeVideoInput.device;
}

//返回當(dāng)前未激活的攝像頭
- (AVCaptureDevice *)inactiveCamera {

    //通過查找當(dāng)前激活攝像頭的反向攝像頭獲得自赔,如果設(shè)備只有1個(gè)攝像頭妈嘹,則返回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個(gè)攝像頭可用
//判斷是否有超過1個(gè)攝像頭可用
- (BOOL)canSwitchCameras {
    return self.cameraCount > 1;
}

  • 可用視頻捕捉設(shè)備的數(shù)量:
//可用視頻捕捉設(shè)備的數(shù)量
- (NSUInteger)cameraCount {
     return [[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count];
}
  • 然后從 AVCaptureDeviceInput 就可以獲取到當(dāng)前活躍的 device,然后找到與其相對(duì)的設(shè)備:
#pragma mark - Device Configuration   配置攝像頭支持的方法

- (AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position {
    
    //獲取可用視頻設(shè)備
    NSArray *devicess = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    
    //遍歷可用的視頻設(shè)備 并返回position 參數(shù)值
    for (AVCaptureDevice *device in devicess)
    {
        if (device.position == position) {
            return device;
        }
    }
    return nil;
}

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

    //判斷是否有多個(gè)攝像頭
    if (![self canSwitchCameras])
    {
        return NO;
    }
    
    //獲取當(dāng)前設(shè)備的反向設(shè)備
    NSError *error;
    AVCaptureDevice *videoDevice = [self inactiveCamera];
    
    //將輸入設(shè)備封裝成AVCaptureDeviceInput
    AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
    
    //判斷videoInput 是否為nil
    if (videoInput)
    {
        //標(biāo)注原配置變化開始
        [self.captureSession beginConfiguration];
        
        //將捕捉會(huì)話中绍妨,原本的捕捉輸入設(shè)備移除
        [self.captureSession removeInput:self.activeVideoInput];
        
        //判斷新的設(shè)備是否能加入
        if ([self.captureSession canAddInput:videoInput])
        {
            //能加入成功润脸,則將videoInput 作為新的視頻捕捉設(shè)備
            [self.captureSession addInput:videoInput];
            
            //將獲得設(shè)備 改為 videoInput
            self.activeVideoInput = videoInput;
        }else
        {
            //如果新設(shè)備,無(wú)法加入他去。則將原本的視頻捕捉設(shè)備重新加入到捕捉會(huì)話中
            [self.captureSession addInput:self.activeVideoInput];
        }
        
        //配置完成后毙驯, AVCaptureSession commitConfiguration 會(huì)分批的將所有變更整合在一起。
        [self.captureSession commitConfiguration];
    }else
    {
        //創(chuàng)建AVCaptureDeviceInput 出現(xiàn)錯(cuò)誤灾测,則通知委托來(lái)處理該錯(cuò)誤
        [self.delegate deviceConfigurationFailedWithError:error];
        return NO;
    }
    
    return YES;
}

注意:

  1. AVCapture Device 定義了很多方法爆价,讓開發(fā)者控制ios設(shè)備上的攝像頭∠碧拢可以獨(dú)立調(diào)整和鎖定攝像頭的焦距铭段、曝光、白平衡蛾号。對(duì)焦和曝光可以基于特定的興趣點(diǎn)進(jìn)行設(shè)置稠项,使其在應(yīng)用中實(shí)現(xiàn)點(diǎn)擊對(duì)焦、點(diǎn)擊曝光的功能鲜结。
    還可以讓你控制設(shè)備的LED作為拍照的閃光燈或手電筒的使用
  2. 每當(dāng)修改攝像頭設(shè)備時(shí)展运,一定要先測(cè)試修改動(dòng)作是否能被設(shè)備支持。并不是所有的攝像頭都支持所有功能精刷,例如牽制攝像頭就不支持對(duì)焦操作拗胜,因?yàn)樗湍繕?biāo)距離一般在一臂之長(zhǎng)的距離。但大部分后置攝像頭是可以支持全尺寸對(duì)焦怒允。嘗試應(yīng)用一個(gè)不被支持的動(dòng)作埂软,會(huì)導(dǎo)致異常崩潰。所以修改攝像頭設(shè)備前纫事,需要判斷是否支持
  • 獲取到對(duì)應(yīng)的 device 后就可以封裝為 AVCaptureInput 對(duì)象勘畔,然后進(jìn)行配置:

//這里 beginConfiguration 和 commitConfiguration 可以使修改操作成為原子性操作,保證設(shè)備運(yùn)行安全丽惶。
            [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 調(diào)整焦距和曝光, 閃光燈和手電筒模式

2.5.1 對(duì)焦

  • 對(duì)焦時(shí),isFocusPointOfInterestSupported 用于判斷設(shè)備是否支持興趣點(diǎn)對(duì)焦钾唬,isFocusModeSupported 判斷是否支持某種對(duì)焦模式万哪,AVCaptureFocusModeAutoFocus 即自動(dòng)對(duì)焦侠驯,然后進(jìn)行對(duì)焦設(shè)置。代碼如下:
#pragma mark - Focus Methods 點(diǎn)擊聚焦方法的實(shí)現(xiàn)

- (BOOL)cameraSupportsTapToFocus {
    
    //詢問激活中的攝像頭是否支持興趣點(diǎn)對(duì)焦
    return [[self activeCamera]isFocusPointOfInterestSupported];
}

- (void)focusAtPoint:(CGPoint)point {
    
    AVCaptureDevice *device = [self activeCamera];
    
    //是否支持興趣點(diǎn)對(duì)焦 & 是否自動(dòng)對(duì)焦模式
    if (device.isFocusPointOfInterestSupported && [device isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {
        
        NSError *error;
        //鎖定設(shè)備準(zhǔn)備配置奕巍,如果獲得了鎖
        if ([device lockForConfiguration:&error]) {
            
            //將focusPointOfInterest屬性設(shè)置CGPoint
            device.focusPointOfInterest = point;
            
            //focusMode 設(shè)置為AVCaptureFocusModeAutoFocus
            device.focusMode = AVCaptureFocusModeAutoFocus;
            
            //釋放該鎖定
            [device unlockForConfiguration];
        }else{
            //錯(cuò)誤時(shí)吟策,則返回給錯(cuò)誤處理代理
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}

2.5.2 曝光

  • 先詢問設(shè)備是否支持對(duì)一個(gè)興趣點(diǎn)進(jìn)行曝光
- (BOOL)cameraSupportsTapToExpose {
    
    //詢問設(shè)備是否支持對(duì)一個(gè)興趣點(diǎn)進(jìn)行曝光
    return [[self activeCamera] isExposurePointOfInterestSupported];
}
  • 曝光與對(duì)焦非常類似,核心方法如下:
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;
        
        //鎖定設(shè)備準(zhǔn)備配置
        if ([device lockForConfiguration:&error])
        {
            //配置期望值
            device.exposurePointOfInterest = point;
            device.exposureMode = exposureMode;
            
            //判斷設(shè)備是否支持鎖定曝光的模式的止。
            if ([device isExposureModeSupported:AVCaptureExposureModeLocked]) {
                
                //支持檩坚,則使用kvo確定設(shè)備的adjustingExposure屬性的狀態(tài)。
                [device addObserver:self forKeyPath:@"adjustingExposure" options:NSKeyValueObservingOptionNew context:&THCameraAdjustingExposureContext];
                
            }
            
            //釋放該鎖定
            [device unlockForConfiguration];
            
        }else
        {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}

2.5.3 閃光燈

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

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

//設(shè)置閃光燈
- (void)setFlashMode:(AVCaptureFlashMode)flashMode {

    //獲取會(huì)話
    AVCaptureDevice *device = [self activeCamera];
    
    //判斷是否支持閃光燈模式
    if ([device isFlashModeSupported:flashMode]) {
    
        //如果支持旺芽,則鎖定設(shè)備
        NSError *error;
        if ([device lockForConfiguration:&error]) {

            //修改閃光燈模式
            device.flashMode = flashMode;
            //修改完成东囚,解鎖釋放設(shè)備
            [device unlockForConfiguration];
            
        }else
        {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
        
    }

}

2.5.4 手電筒

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

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

    return [[self activeCamera]torchMode];
}


//設(shè)置是否打開手電筒
- (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 拍攝靜態(tài)圖片

  • 設(shè)置捕捉會(huì)話時(shí)我們將 AVCaptureStillImageOutput (注意 :AVCaptureStillImageOutput 在IOS10 之后被廢棄了,使用AVCapturePhotoOutput 代替)實(shí)例加入到會(huì)話中,這個(gè)會(huì)話可以用來(lái)拍攝靜態(tài)圖片可免。如下代碼:
    AVCaptureConnection *connection = [self.cameraHelper.imageOutput connectionWithMediaType:AVMediaTypeVideo];
    if ([connection isVideoOrientationSupported]) {
        [connection setVideoOrientation:self.cameraHelper.videoOrientation];
    }
    if (!connection.enabled || !connection.isActive) { // connection 不可用
        // 處理非法情況
        return;
    }
  1. 通過監(jiān)聽重力感應(yīng)器修改 orientation
  2. 通過 UIDevice 獲取
  • 通過監(jiān)聽重力感應(yīng)器修改 orientation:
    // 監(jiān)測(cè)重力感應(yīng)器并調(diào)整 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;
    }
  • 然后我們調(diào)用方法來(lái)獲取 CMSampleBufferRef(CMSampleBufferRef 是一個(gè) Core Media 定義的 Core Foundation 對(duì)象)溯香,可以通過 AVCaptureStillImageOutput 的 jpegStillImageNSDataRepresentation 類方法將其轉(zhuǎn)化為 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 以后已經(jīng)被 PHPhotoLibrary 替代奋救,這里用 PHPhotoLibrary 實(shí)現(xiàn)保存圖片的功能。代碼如下:
    [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
        PHAssetChangeRequest *changeRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:targetImage];
        NSString *imageIdentifier = changeRequest.placeholderForCreatedAsset.localIdentifier;
    } completionHandler:^( BOOL success, NSError * _Nullable error ) {
    }];
  • 我們可以通過保存時(shí)返回的 imageIdentifier 從相冊(cè)里找到這個(gè)圖片反惕。

  • 完整捕獲靜態(tài)圖片的代碼如下:

#pragma mark - Image Capture Methods 拍攝靜態(tài)圖片
/*
    AVCaptureStillImageOutput 是AVCaptureOutput的子類尝艘。用于捕捉圖片
 */
- (void)captureStillImage {
    
    //獲取連接
    AVCaptureConnection *connection = [self.imageOutput connectionWithMediaType:AVMediaTypeVideo];
    
    //程序只支持縱向,但是如果用戶橫向拍照時(shí)姿染,需要調(diào)整結(jié)果照片的方向
    //判斷是否支持設(shè)置視頻方向
    if (connection.isVideoOrientationSupported) {
        
        //獲取方向值
        connection.videoOrientation = [self currentVideoOrientation];
    }
    
    //定義一個(gè)handler 塊背亥,會(huì)返回1個(gè)圖片的NSData數(shù)據(jù)
    id handler = ^(CMSampleBufferRef sampleBuffer,NSError *error)
                {
                    if (sampleBuffer != NULL) {
                        NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:sampleBuffer];
                        UIImage *image = [[UIImage alloc]initWithData:imageData];
                        
                        //重點(diǎn):捕捉圖片成功后,將圖片傳遞出去
                        [self writeImageToAssetsLibrary:image];
                    }else
                    {
                        NSLog(@"NULL sampleBuffer:%@",[error localizedDescription]);
                    }
                        
                };
    
    //捕捉靜態(tài)圖片
    [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 框架 
    用來(lái)讓開發(fā)者通過代碼方式訪問iOS photo
    注意:會(huì)訪問到相冊(cè)悬赏,需要修改plist 權(quán)限狡汉。否則會(huì)導(dǎo)致項(xiàng)目崩潰
 */

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

    //創(chuàng)建ALAssetsLibrary  實(shí)例
    ALAssetsLibrary *library = [[ALAssetsLibrary alloc]init];
    
    //參數(shù)1:圖片(參數(shù)為CGImageRef 所以image.CGImage)
    //參數(shù)2:方向參數(shù) 轉(zhuǎn)為NSUInteger
    //參數(shù)3:寫入成功、失敗處理
    [library writeImageToSavedPhotosAlbum:image.CGImage
                             orientation:(NSUInteger)image.imageOrientation
                         completionBlock:^(NSURL *assetURL, NSError *error) {
                             //成功后闽颇,發(fā)送捕捉圖片通知轴猎。用于繪制程序的左下角的縮略圖
                             if (!error)
                             {
                                 [self postThumbnailNotifification:image];
                             }else
                             {
                                 //失敗打印錯(cuò)誤信息
                                 id message = [error localizedDescription];
                                 NSLog(@"%@",message);
                             }
                         }];
}

//發(fā)送縮略圖通知
- (void)postThumbnailNotifification:(UIImage *)image {
    
    //回到主隊(duì)列
    dispatch_async(dispatch_get_main_queue(), ^{
        //發(fā)送請(qǐng)求
        NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
        [nc postNotificationName:THThumbnailCreatedNotification object:image];
    });
}

2.7 視頻捕捉

  • QuickTime 格式的影片,元數(shù)據(jù)處于影片文件的開頭位置进萄,這樣可以幫助視頻播放器快速讀取頭文件來(lái)確定文件內(nèi)容、結(jié)構(gòu)和樣本位置,但是錄制時(shí)需要等所有樣本捕捉完成才能創(chuàng)建頭數(shù)據(jù)并將其附在文件結(jié)尾處中鼠。這樣一來(lái)可婶,如果錄制時(shí)發(fā)生崩潰或中斷就會(huì)導(dǎo)致無(wú)法創(chuàng)建影片頭,從而在磁盤生成一個(gè)不可讀的文件援雇。

  • 因此 AVFoundationAVCaptureMovieFileOutput 類就提供了分段捕捉能力矛渴,錄制開始時(shí)生成最小化的頭信息,錄制進(jìn)行中惫搏,片段間隔一定周期再次創(chuàng)建頭信息具温,從而逐步完成創(chuàng)建。默認(rèn)狀態(tài)下每 10s 寫入一個(gè)片段筐赔,可以通過 movieFragmentInterval 屬性來(lái)修改铣猩。

  • 首先是開啟視頻拍攝:

    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:@"結(jié)束" forState:UIControlStateNormal];
    } else {
    }
  • 設(shè)置 PreferredVideoStabilizationMode 可以支持視頻拍攝時(shí)的穩(wěn)定性和拍攝質(zhì)量,但是這一穩(wěn)定效果只會(huì)在拍攝的視頻中感受到茴丰,預(yù)覽視頻時(shí)無(wú)法感知达皿。
  • 我們將視頻文件臨時(shí)寫入到臨時(shí)文件中,等待拍攝結(jié)束時(shí)會(huì)調(diào)用 AVCaptureFileOutputRecordingDelegate 的 (void)captureOutput:(AVCaptureFileOutput *)captureOutput didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray *)connections error:(NSError *)error 方法贿肩。此時(shí)可以進(jìn)行保存視頻和生成視頻縮略圖的操作峦椰。
- (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) {
                // 錯(cuò)誤處理
            } 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; //捕捉縮略圖時(shí)考慮視頻 orientation 變化,避免錯(cuò)誤的縮略圖方向
    CMTime snaptime = kCMTimeZero;
    CGImageRef cgImageRef = [generator copyCGImageAtTime:snaptime actualTime:NULL error:nil];
    UIImage *assetImage = [UIImage imageWithCGImage:cgImageRef];
    CGImageRelease(cgImageRef);
}
  • 梳理一下視頻捕獲的流程
  • (1)判斷是否錄制狀態(tài)
//判斷是否錄制狀態(tài)
- (BOOL)isRecording {

    return self.movieOutput.isRecording;
}
  • (2)開始錄制
//開始錄制
- (void)startRecording {

    if (![self isRecording]) {
        
        //獲取當(dāng)前視頻捕捉連接信息汰规,用于捕捉視頻數(shù)據(jù)配置一些核心屬性
        AVCaptureConnection * videoConnection = [self.movieOutput connectionWithMediaType:AVMediaTypeVideo];
        
        //判斷是否支持設(shè)置videoOrientation 屬性汤功。
        if([videoConnection isVideoOrientationSupported])
        {
            //支持則修改當(dāng)前視頻的方向
            videoConnection.videoOrientation = [self currentVideoOrientation];
            
        }
        
        //判斷是否支持視頻穩(wěn)定 可以顯著提高視頻的質(zhì)量。只會(huì)在錄制視頻文件涉及
        if([videoConnection isVideoStabilizationSupported])
        {
            videoConnection.enablesVideoStabilizationWhenAvailable = YES;
        }
        
        AVCaptureDevice *device = [self activeCamera];
        
        //攝像頭可以進(jìn)行平滑對(duì)焦模式操作溜哮。即減慢攝像頭鏡頭對(duì)焦速度滔金。當(dāng)用戶移動(dòng)拍攝時(shí)攝像頭會(huì)嘗試快速自動(dòng)對(duì)焦。
        if (device.isSmoothAutoFocusEnabled) {
            NSError *error;
            if ([device lockForConfiguration:&error]) {
                
                device.smoothAutoFocusEnabled = YES;
                [device unlockForConfiguration];
            }else
            {
                [self.delegate deviceConfigurationFailedWithError:error];
            }
        }
        
        //查找寫入捕捉視頻的唯一文件系統(tǒng)URL.
        self.outputURL = [self uniqueURL];
        
        //在捕捉輸出上調(diào)用方法 參數(shù)1:錄制保存路徑  參數(shù)2:代理
        [self.movieOutput startRecordingToOutputFileURL:self.outputURL recordingDelegate:self];
        
    }
}

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


//寫入視頻唯一文件系統(tǒng)URL
- (NSURL *)uniqueURL {

    NSFileManager *fileManager = [NSFileManager defaultManager];
    
    //temporaryDirectoryWithTemplateString  可以將文件寫入的目的創(chuàng)建一個(gè)唯一命名的目錄茬射;
    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)捕獲視頻回調(diào)函數(shù)AVCaptureFileOutputRecordingDelegate
#pragma mark - AVCaptureFileOutputRecordingDelegate

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

    //錯(cuò)誤
    if (error) {
        [self.delegate mediaCaptureFailedWithError:error];
    }else
    {
        //寫入
        [self writeVideoToAssetsLibrary:[self.outputURL copy]];
        
    }
    
    self.outputURL = nil;
}
  • (5)將得到的視頻數(shù)據(jù)保存寫入視頻文件
//寫入捕捉到的視頻
- (void)writeVideoToAssetsLibrary:(NSURL *)videoURL {
    
    //ALAssetsLibrary 實(shí)例 提供寫入視頻的接口
    ALAssetsLibrary *library = [[ALAssetsLibrary alloc]init];
    
    //寫資源庫(kù)寫入前鹦蠕,檢查視頻是否可被寫入 (寫入前盡量養(yǎng)成判斷的習(xí)慣)
    if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:videoURL]) {
        
        //創(chuàng)建block塊
        ALAssetsLibraryWriteVideoCompletionBlock completionBlock;
        completionBlock = ^(NSURL *assetURL,NSError *error)
        {
            if (error) {
                
                [self.delegate assetLibraryWriteFailedWithError:error];
            }else
            {
                //用于界面展示視頻縮略圖
                [self generateThumbnailForVideoAtURL:videoURL];
            }
        };
        
        //執(zhí)行實(shí)際寫入資源庫(kù)的動(dòng)作
        [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];
        
        //設(shè)置maximumSize 寬為100在抛,高為0 根據(jù)視頻的寬高比來(lái)計(jì)算圖片的高度
        imageGenerator.maximumSize = CGSizeMake(100.0f, 0.0f);
        
        //捕捉視頻縮略圖會(huì)考慮視頻的變化(如視頻的方向變化)钟病,如果不設(shè)置,縮略圖的方向可能出錯(cuò)
        imageGenerator.appliesPreferredTrackTransform = YES;
        
        //獲取CGImageRef圖片 注意需要自己管理它的創(chuàng)建和釋放
        CGImageRef imageRef = [imageGenerator copyCGImageAtTime:kCMTimeZero actualTime:NULL error:nil];
        
        //將圖片轉(zhuǎn)化為UIImage
        UIImage *image = [UIImage imageWithCGImage:imageRef];
        
        //釋放CGImageRef imageRef 防止內(nèi)存泄漏
        CGImageRelease(imageRef);
        
        //回到主線程
        dispatch_async(dispatch_get_main_queue(), ^{
            
            //發(fā)送通知刚梭,傳遞最新的image
            [self postThumbnailNotifification:image];
            
        });
        
    });
    
}

2.8 視頻縮放

  • iOS 7.0 為 AVCaptureDevice 提供了一個(gè) videoZoomFactor 屬性用于對(duì)視頻輸出和捕捉提供縮放效果肠阱,這個(gè)屬性的最小值為 1.0,最大值由下面的方法提供:self.cameraHelper.activeVideoDevice.activeFormat.videoMaxZoomFactor;
  • 因而判斷一個(gè)設(shè)備能否進(jìn)行縮放也可以通過判斷這一屬性來(lái)獲知:
- (BOOL)cameraSupportsZoom
{
    return self.cameraHelper.activeVideoDevice.activeFormat.videoMaxZoomFactor > 1.0f;
}
  • 設(shè)備執(zhí)行縮放效果是通過居中裁剪由攝像頭傳感器捕捉到的圖片實(shí)現(xiàn)的朴读,也可以通過 videoZoomFactorUpscaleThreshold 來(lái)設(shè)置具體的放大中心屹徘。當(dāng) zoom factors 縮放因子比較小的時(shí)候,裁剪的圖片剛好等于或者大于輸出尺寸(考慮與抗邊緣畸變有關(guān))衅金,則無(wú)需放大就可以返回噪伊。但是當(dāng) zoom factors 比較大時(shí)簿煌,設(shè)備必須縮放裁剪圖片以符合輸出尺寸,從而導(dǎo)致圖片質(zhì)量上的丟失鉴吹。具體的臨界點(diǎn)由 videoZoomFactorUpscaleThreshold 值來(lái)確定姨伟。
// 在 iphone6s 和 iphone8plus 上測(cè)試得到此值為 2.0左右
self.cameraHelper.activeVideoDevice.activeFormat.videoZoomFactorUpscaleThreshold;
  • 可以通過一個(gè)變化值從 0.0 到 1.0 的 UISlider 來(lái)實(shí)現(xiàn)對(duì)縮放值的控制。代碼如下:
{
    [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];
        }
    }
}    
  • 首先注意在進(jìn)行配置屬性前需要進(jìn)行設(shè)備的鎖定豆励,否則會(huì)引發(fā)異常夺荒。其次,插值縮放是一個(gè)指數(shù)形式的增長(zhǎng)良蒸,傳入的 slider 值是線性的技扼,需要進(jìn)行一次 pow 運(yùn)算得到需要縮放的值。另外嫩痰,videoMaxZoomFactor 的值可能會(huì)非常大剿吻,在 iphone8p 上這一個(gè)值是 16,縮放到這么大的圖像是沒有太大意義的始赎,因此需要人為設(shè)置一個(gè)最大縮放值和橙,這里選擇 4.0。

  • 當(dāng)然這里進(jìn)行的縮放是立即生效的造垛,下面的方法可以以一個(gè)速度平滑縮放到一個(gè)縮放因子上:

- (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 {
    }
}
  • 當(dāng)然我們還可以監(jiān)聽設(shè)備的 videoZoomFactor 可以獲知當(dāng)前的縮放值:
    [RACObserve(self, activeVideoDevice.videoZoomFactor) subscribeNext:^(id x) {
        NSLog(@"videoZoomFactor: %f", self.activeVideoDevice.videoZoomFactor);
    }];
  • 還可以監(jiān)聽設(shè)備的 rampingVideoZoom 可以獲知設(shè)備是否正在平滑縮放:
    [RACObserve(self, activeVideoDevice.rampingVideoZoom) subscribeNext:^(id x) {
        NSLog(@"rampingVideoZoom : %@", (self.activeVideoDevice.rampingVideoZoom)?@"true":@"false");
    }];

2.9 視頻編輯

  • AVCaptureMovieFileOutput 可以簡(jiǎn)單地捕捉視頻魔招,但是不能進(jìn)行視頻數(shù)據(jù)交互,因此需要使用 AVCaptureVideoDataOutput 類五辽。AVCaptureVideoDataOutput 是一個(gè) AVCaptureOutput 的子類办斑,可以直接訪問攝像頭傳感器捕捉到的視頻幀。與之對(duì)應(yīng)的是處理音頻輸入的 AVCaptureAudioDataOutput 類杆逗。

  • AVCaptureVideoDataOutput 有一個(gè)遵循 AVCaptureVideoDataOutputSampleBufferDelegate 協(xié)議的委托對(duì)象乡翅,它有下面兩個(gè)主要方法:

- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection; // 有新的視頻幀寫入時(shí)調(diào)用,數(shù)據(jù)會(huì)基于 output 的 videoSetting 進(jìn)行解碼或重新編碼
- (void)captureOutput:(AVCaptureOutput *)output didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection; // 有遲到的視頻幀被丟棄時(shí)調(diào)用罪郊,通常是因?yàn)樵谏厦嬉粋€(gè)方法里進(jìn)行了比較耗時(shí)的操作
  • CMSampleBufferRef 是一個(gè)由 Core Media 框架提供的 Core Foundation 風(fēng)格的對(duì)象蠕蚜,用于在媒體管道中傳輸數(shù)字樣本。這樣我們可以對(duì) CMSampleBufferRef 的每一個(gè) Core Video 視頻幀進(jìn)行處理悔橄,如下代碼:
    int BYTES_PER_PIXEL = 4;
    CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); //CVPixelBufferRef 在主內(nèi)存中保存像素?cái)?shù)據(jù)
    CVPixelBufferLockBaseAddress(pixelBuffer, 0); // 獲取相應(yīng)內(nèi)存塊的鎖
    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 ++) { // 遍歷每一個(gè)像素點(diǎn)
            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 生成對(duì)應(yīng)的 CIImage
    CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); // 解除鎖
  • CMSampleBufferRef 還提供了每一幀數(shù)據(jù)的格式信息靶累,CMFormatDescription.h 頭文件定義了大量函數(shù)來(lái)獲取各種信息。
    CMFormatDescriptionRef formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer);
    CMMediaType mediaType = CMFormatDescriptionGetMediaType(formatDescription);
  • 還可以修改時(shí)間信息:
    CMTime presentation = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); // 獲取幀樣本的原始時(shí)間戳
    CMTime decode = CMSampleBufferGetDecodeTimeStamp(sampleBuffer); // 獲取幀樣本的解碼時(shí)間戳
  • 可以附加元數(shù)據(jù):
    CFDictionaryRef exif = (CFDictionaryRef)CMGetAttachment(sampleBuffer, kCGImagePropertyExifDictionary, NULL);
  • AVCaptureVideoDataOutput 的配置與 AVCaptureMovieFileOutput 大致相同癣疟,但要指明它的委托對(duì)象和回調(diào)隊(duì)列挣柬。為了確保視頻幀按順序傳遞,隊(duì)列要求必須是串行隊(duì)列睛挚。
    self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
    self.videoDataOutput.videoSettings = @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)}; // 攝像頭的初始格式為雙平面 420v邪蛔,這是一個(gè) 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 以后加入的勃教,具有更逼真的效果和更好的清晰度,對(duì)于細(xì)節(jié)的加強(qiáng)和動(dòng)作流暢度的提升非常明顯床牧,尤其是錄制快速移動(dòng)的內(nèi)容時(shí)更為明顯,也可以實(shí)現(xiàn)高質(zhì)量的慢動(dòng)作視頻效果戈咳。
  • 實(shí)現(xiàn)高幀率捕捉的基本思路是:首先通過設(shè)備的 formats 屬性獲取所有支持的格式,也就是 AVCaptureDeviceFormat 對(duì)象壕吹;然后根據(jù)對(duì)象的 videoSupportedFrameRateRanges 屬性著蛙,這樣可以獲知其所支持的最小幀率、最大幀率及時(shí)長(zhǎng)信息耳贬;然后手動(dòng)設(shè)置設(shè)備的格式和幀時(shí)長(zhǎng)踏堡。
  • 具體實(shí)現(xiàn)如下:
  • 首先寫一個(gè) AVCaptureDevice 的 category,獲取支持格式的最大幀率的方法如下:
    AVCaptureDeviceFormat *maxFormat = nil;
    AVFrameRateRange *maxFrameRateRange = nil;
    for (AVCaptureDeviceFormat *format in self.formats) {
        FourCharCode codecType = CMVideoFormatDescriptionGetCodecType(format.formatDescription);
        //codecType 是一個(gè)無(wú)符號(hào)32位的數(shù)據(jù)類型咒劲,但是是由四個(gè)字符對(duì)應(yīng)的四個(gè)字節(jié)組成顷蟆,一般可能值為 "420v" 或 "420f",這里選取 420v 格式來(lái)配置腐魂。
        if (codecType == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) {
            NSArray *frameRateRanges = format.videoSupportedFrameRateRanges;
            for (AVFrameRateRange *range in frameRateRanges) {
                if (range.maxFrameRate > maxFrameRateRange.maxFrameRate) {
                    maxFormat = format;
                    maxFrameRateRange = range;
                }
            }
        } else {
        }
    }
  • 我們可以通過判斷最大幀率是否大于 30帐偎,來(lái)判斷設(shè)備是否支持高幀率:
- (BOOL)isHighFrameRate {
    return self.frameRateRange.maxFrameRate > 30.0f;
}
  • 接下來(lái)我們就可以進(jìn)行配置了:
    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];
    }
  • 這里首先鎖定了設(shè)備,然后將最小幀時(shí)長(zhǎng)和最大幀時(shí)長(zhǎng)都設(shè)置成 minFrameDuration蛔屹,幀時(shí)長(zhǎng)與幀率是倒數(shù)關(guān)系削樊,所以最大幀率對(duì)應(yīng)最小幀時(shí)長(zhǎng)。
  • 播放時(shí)可以針對(duì) AVPlayer 設(shè)置不同的 rate 實(shí)現(xiàn)變速播放兔毒,在 iphone8plus 上實(shí)測(cè)漫贞,如果 rate 在 0 到 0.5 之間, 則實(shí)際播放速率仍為 0.5育叁。
  • 另外要注意設(shè)置 AVPlayerItem 的 audioTimePitchAlgorithm 屬性迅脐,這個(gè)屬性允許你指定當(dāng)視頻正在各種幀率下播放的時(shí)候如何播放音頻,通常選擇 AVAudioTimePitchAlgorithmSpectralAVAudioTimePitchAlgoruthmTimeDomain 即可。:
  1. AVAudioTimePitchAlgorithmLowQualityZeroLatency 質(zhì)量低,適合快進(jìn),快退或低質(zhì)量語(yǔ)音
  2. AVAudioTimePitchAlgoruthmTimeDomain 質(zhì)量適中豪嗽,計(jì)算成本較低谴蔑,適合語(yǔ)音
  3. AVAudioTimePitchAlgorithmSpectral 最高質(zhì)量,最昂貴的計(jì)算昵骤,保留了原來(lái)的項(xiàng)目間距
  4. AVAudioTimePitchAlgorithmVarispeed 高品質(zhì)的播放沒有音高校正
  • 此外AVFoundation 提供了人臉識(shí)別树碱,二維碼識(shí)別功能。

2.11 人臉識(shí)別

  • 人臉識(shí)別需要用到 AVCaptureMetadataOutput 作為輸出变秦,首先將其加入到捕捉會(huì)話中:
    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 屬性成榜,將其設(shè)置為 AVMetadataObjectTypeFace 的數(shù)組,它代表著人臉元數(shù)據(jù)對(duì)象蹦玫。然后設(shè)置其遵循 AVCaptureMetadataOutputObjectsDelegate 協(xié)議的委托對(duì)象及回調(diào)線程赎婚,當(dāng)檢測(cè)到人臉時(shí)就會(huì)調(diào)用下面的方法:
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    if (self.detectFaces) {
        self.detectFaces(metadataObjects);
    }
}
  • 其中 metadataObjects 是一個(gè)包含了許多 AVMetadataObject 對(duì)象的數(shù)組刘绣,這里則可以認(rèn)為都是 AVMetadataObject 的子類 AVMetadataFaceObject。對(duì)于 AVMetadataFaceObject 對(duì)象挣输,有四個(gè)重要的屬性:
  1. faceID纬凤,用于標(biāo)識(shí)檢測(cè)到的每一個(gè) face
  2. rollAngle,用于標(biāo)識(shí)人臉斜傾角撩嚼,即人的頭部向肩膀方便的側(cè)傾角度
  3. yawAngle停士,偏轉(zhuǎn)角,即人臉繞 y 軸旋轉(zhuǎn)的角度
  4. bounds完丽,標(biāo)識(shí)檢測(cè)到的人臉區(qū)域
        @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;
}
  • 我們用一個(gè)字典來(lái)管理每一個(gè)展示一個(gè) face 對(duì)象的 layer恋技,它的 key 值即 faceID,回調(diào)時(shí)更新當(dāng)前已存在的 faceLayer逻族,移除不需要的 faceLayer蜻底。其次對(duì)每一個(gè) face,根據(jù)其 rollAngle 和 yawAngle 要通過 transfor 來(lái)變換展示的矩陣聘鳞。

  • 還要注意一點(diǎn)薄辅,transformedMetadataObjectForMetadataObject 方法可以將設(shè)備坐標(biāo)系上的數(shù)據(jù)轉(zhuǎn)換到視圖坐標(biāo)系上,設(shè)備坐標(biāo)系的范圍是 (0, 0) 到 (1抠璃,1)站楚。

2.12 二維碼識(shí)別

  • 機(jī)器可讀代碼包括一維條碼和二維碼等,AVFoundation 支持多種一維碼和三種二維碼鸡典,其中最常見的是 QR 碼源请,也即二維碼。
  • 掃碼仍然需要用到 AVMetadataObject 對(duì)象彻况,首先加入到捕捉會(huì)話中谁尸。
    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;
    }
  • 然后實(shí)現(xiàn)委托方法:
- (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,一個(gè)角點(diǎn)字典表示的數(shù)組悍赢,比 bounds 表示的二維碼區(qū)域更精確
  • 我們可以通過以上屬性决瞳,在 UI 界面上對(duì)二維碼區(qū)域進(jìn)行高亮展示。
  • 首先需要注意左权,一個(gè)從 captureSession 獲得的 AVMetadataMachineReadableCodeObject皮胡,其坐標(biāo)是設(shè)備坐標(biāo)系下的坐標(biāo),需要進(jìn)行坐標(biāo)轉(zhuǎn)換:
- (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];
}
  • 其次赏迟,對(duì)于每一個(gè) AVMetadataMachineReadableCodeObject 對(duì)象屡贺,其 bounds 屬性由于是 CGRect,所以可以直接繪制出一個(gè) UIBezierPath 對(duì)象:
- (UIBezierPath *)bezierPathForBounds:(CGRect)bounds {
    return [UIBezierPath bezierPathWithRect:bounds];
}
  • 而 corners 屬性是一個(gè)字典,需要手動(dòng)生成 CGPoint甩栈,然后進(jìn)行連線操作泻仙,生成 UIBezierPath 對(duì)象:
- (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 字典的形式大致如下所示殴蹄,可以調(diào)用 CGPointMakeWithDictionaryRepresentation 便捷函數(shù)將其轉(zhuǎn)換為 CGPoint 形式妓蛮。一般來(lái)說(shuō)一個(gè) corners 里會(huì)包含 4 個(gè) corner 字典构挤。獲取到每一個(gè) code 對(duì)應(yīng)的兩個(gè) UIBezierPath 對(duì)象后箱歧,就可以在視圖上添加相應(yīng)的 CALayer 來(lái)顯示高亮區(qū)域了洒沦。

3. 實(shí)例

3.1 捕捉照片和錄制視頻Demo Swift版本

  • 此demo 來(lái)自蘋果官方文檔括尸,詳情參考蘋果官方文檔:AVCam: Building a Camera App 章節(jié)肴焊,這個(gè)Demo主要是用深度數(shù)據(jù)捕捉照片届宠,并使用前后的iPhone和iPad攝像頭錄制視頻豌注。這個(gè)Demo使用最新的IOS SDK 要求運(yùn)行在IOS 13.0以上版本轧铁。
  • iOS攝像頭應(yīng)用程序允許你從前后攝像頭捕捉照片和電影。根據(jù)您的設(shè)備齿风,相機(jī)應(yīng)用程序還支持深度數(shù)據(jù)的靜態(tài)捕獲、人像效果和實(shí)時(shí)照片。
  • 這個(gè)示例代碼項(xiàng)目AVCam向您展示了如何在自己的相機(jī)應(yīng)用程序中實(shí)現(xiàn)這些捕獲功能泵额。它利用了內(nèi)置的iPhone和iPad前后攝像頭的基本功能。
  • 要使用AVCam薪寓,你需要一個(gè)運(yùn)行ios13或更高版本的iOS設(shè)備亡资。由于Xcode無(wú)法訪問設(shè)備攝像頭,因此此示例無(wú)法在模擬器中工作向叉。AVCam隱藏了當(dāng)前設(shè)備不支持的模式按鈕锥腻,比如iPhone 7 Plus上的人像效果曝光傳送。
  • 項(xiàng)目代碼結(jié)構(gòu)如下圖:


    Swift項(xiàng)目工程代碼結(jié)構(gòu)

3.1.1 配置捕獲會(huì)話

  • AVCaptureSession接受來(lái)自攝像頭和麥克風(fēng)等捕獲設(shè)備的輸入數(shù)據(jù)母谎。在接收到輸入后瘦黑, AVCaptureSession將數(shù)據(jù)封送到適當(dāng)?shù)妮敵鲞M(jìn)行處理,最終生成一個(gè)電影文件或靜態(tài)照片。配置捕獲會(huì)話的輸入和輸出之后幸斥,您將告訴它開始捕獲匹摇,然后停止捕獲。
 private let session = AVCaptureSession()
  • AVCam默認(rèn)選擇后攝像頭甲葬,并配置攝像頭捕獲會(huì)話以將內(nèi)容流到視頻預(yù)覽視圖廊勃。PreviewView是一個(gè)由AVCaptureVideoPreviewLayer支持的自定義UIView子類。AVFoundation沒有PreviewView類经窖,但是示例代碼創(chuàng)建了一個(gè)類來(lái)促進(jìn)會(huì)話管理坡垫。

  • 下圖顯示了會(huì)話如何管理輸入設(shè)備和捕獲輸出:


    會(huì)話如何管理輸入設(shè)備和捕獲輸出
  • 將與avcapturesessiessie的任何交互(包括它的輸入和輸出)委托給一個(gè)專門的串行調(diào)度隊(duì)列(sessionQueue),這樣交互就不會(huì)阻塞主隊(duì)列画侣。在單獨(dú)的調(diào)度隊(duì)列上執(zhí)行任何涉及更改會(huì)話拓?fù)浠蛑袛嗥湔谶\(yùn)行的視頻流的配置冰悠,因?yàn)闀?huì)話配置總是阻塞其他任務(wù)的執(zhí)行,直到隊(duì)列處理更改為止配乱。類似地溉卓,樣例代碼將其他任務(wù)分派給會(huì)話隊(duì)列,比如恢復(fù)中斷的會(huì)話搬泥、切換捕獲模式的诵、切換攝像機(jī)、將媒體寫入文件佑钾,這樣它們的處理就不會(huì)阻塞或延遲用戶與應(yīng)用程序的交互。

  • 相反烦粒,代碼將影響UI的任務(wù)(比如更新預(yù)覽視圖)分派給主隊(duì)列休溶,因?yàn)锳VCaptureVideoPreviewLayer是CALayer的一個(gè)子類,是示例預(yù)覽視圖的支持層扰她。您必須在主線程上操作UIView子類兽掰,以便它們以及時(shí)的、交互的方式顯示徒役。

  • 在viewDidLoad中孽尽,AVCam創(chuàng)建一個(gè)會(huì)話并將其分配給preview視圖:previewView.session = session

  • 有關(guān)配置圖像捕獲會(huì)話的更多信息,請(qǐng)參見設(shè)置捕獲會(huì)話忧勿。

    配置圖像捕獲會(huì)話

3.1.2 請(qǐng)求訪問輸入設(shè)備的授權(quán)

  • 配置會(huì)話之后杉女,它就可以接受輸入了。每個(gè)avcapturedevice—不管是照相機(jī)還是麥克風(fēng)—都需要用戶授權(quán)訪問鸳吸。AVFoundation使用AVAuthorizationStatus枚舉授權(quán)狀態(tài)熏挎,該狀態(tài)通知應(yīng)用程序用戶是否限制或拒絕訪問捕獲設(shè)備。
  • 有關(guān)準(zhǔn)備應(yīng)用程序信息的更多信息晌砾。有關(guān)自定義授權(quán)請(qǐng)求坎拐,請(qǐng)參閱iOS上的媒體捕獲請(qǐng)求授權(quán)

3.1.3 在前后攝像頭之間切換

  • changeCamera方法在用戶點(diǎn)擊UI中的按鈕時(shí)處理相機(jī)之間的切換。它使用一個(gè)發(fā)現(xiàn)會(huì)話哼勇,該會(huì)話按優(yōu)先順序列出可用的設(shè)備類型都伪,并接受它的設(shè)備數(shù)組中的第一個(gè)設(shè)備。例如积担,AVCam中的videoDeviceDiscoverySession查詢應(yīng)用程序所運(yùn)行的設(shè)備陨晶,查找可用的輸入設(shè)備。此外磅轻,如果用戶的設(shè)備有一個(gè)壞了的攝像頭珍逸,它將不能在設(shè)備陣列中使用。
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方法處理相機(jī)之間的切換聋溜,如果發(fā)現(xiàn)會(huì)話發(fā)現(xiàn)相機(jī)處于適當(dāng)?shù)奈恢米簧牛鼘牟东@會(huì)話中刪除以前的輸入,并將新相機(jī)添加為輸入撮躁。
// 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 處理中斷和錯(cuò)誤

  • 在捕獲會(huì)話期間漱病,可能會(huì)出現(xiàn)諸如電話呼叫、其他應(yīng)用程序通知和音樂播放等中斷把曼。通過添加觀察者來(lái)處理這些干擾杨帽,以偵聽AVCaptureSessionWasInterrupted:
NotificationCenter.default.addObserver(self,
                                       selector: #selector(sessionWasInterrupted),
                                       name: .AVCaptureSessionWasInterrupted,
                                       object: session)
NotificationCenter.default.addObserver(self,
                                       selector: #selector(sessionInterruptionEnded),
                                       name: .AVCaptureSessionInterruptionEnded,
                                       object: session)
  • 當(dāng)AVCam接收到中斷通知時(shí),它可以暫袜途或掛起會(huì)話注盈,并提供一個(gè)在中斷結(jié)束時(shí)恢復(fù)活動(dòng)的選項(xiàng)。AVCam將sessionwas注冊(cè)為接收通知的處理程序叙赚,當(dāng)捕獲會(huì)話出現(xiàn)中斷時(shí)通知用戶:
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老客,當(dāng)錯(cuò)誤發(fā)生時(shí)接收通知:
NotificationCenter.default.addObserver(self,
                                       selector: #selector(sessionRuntimeError),
                                       name: .AVCaptureSessionRuntimeError,
                                       object: session)
  • 當(dāng)運(yùn)行時(shí)錯(cuò)誤發(fā)生時(shí),重新啟動(dòng)捕獲會(huì)話:
// 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
}
  • 如果設(shè)備承受系統(tǒng)壓力震叮,比如過熱荞下,捕獲會(huì)話也可能停止哗伯。相機(jī)本身不會(huì)降低拍攝質(zhì)量或減少幀數(shù);為了避免讓你的用戶感到驚訝铅协,你可以讓你的應(yīng)用手動(dòng)降低幀速率雏亚,關(guān)閉深度,或者根據(jù)AVCaptureDevice.SystemPressureState:的反饋來(lái)調(diào)整性能击罪。
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 捕捉一張照片

  • 在會(huì)話隊(duì)列上拍照哲嘲。該過程首先更新AVCapturePhotoOutput連接以匹配視頻預(yù)覽層的視頻方向。這使得相機(jī)能夠準(zhǔn)確地捕捉到用戶在屏幕上看到的內(nèi)容:
if let photoOutputConnection = self.photoOutput.connection(with: .video) {
    photoOutputConnection.videoOrientation = videoPreviewLayerOrientation!
}
  • 對(duì)齊輸出后媳禁,AVCam繼續(xù)創(chuàng)建AVCapturePhotoSettings來(lái)配置捕獲參數(shù)撤蚊,如焦點(diǎn)、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
  • 該示例使用一個(gè)單獨(dú)的對(duì)象PhotoCaptureProcessor作為照片捕獲委托损话,以隔離每個(gè)捕獲生命周期侦啸。對(duì)于實(shí)時(shí)照片來(lái)說(shuō)槽唾,這種清晰的捕獲周期分離是必要的,因?yàn)閱蝹€(gè)捕獲周期可能涉及多個(gè)幀的捕獲光涂。
  • 每次用戶按下中央快門按鈕時(shí)庞萍,AVCam都會(huì)通過調(diào)用capturePhoto(帶有:delegate:)來(lái)使用之前配置的設(shè)置捕捉照片:
self.photoOutput.capturePhoto(with: photoSettings, delegate: photoCaptureProcessor)
  • capturePhoto方法接受兩個(gè)參數(shù):
  1. 一個(gè)avcapturephotoset對(duì)象,它封裝了用戶通過應(yīng)用配置的設(shè)置忘闻,比如曝光钝计、閃光、對(duì)焦和手電筒齐佳。
  2. 一個(gè)符合AVCapturePhotoCaptureDelegate協(xié)議的委托私恬,以響應(yīng)系統(tǒng)在捕獲照片期間傳遞的后續(xù)回調(diào)。
  • 一旦應(yīng)用程序調(diào)用capturePhoto(帶有:delegate:)炼吴,開始拍照的過程就結(jié)束了本鸣。此后,對(duì)單個(gè)照片捕獲的操作將在委托回調(diào)中發(fā)生硅蹦。

3.1.6 通過照片捕獲委托跟蹤結(jié)果

  • capturePhoto方法只是開始拍照的過程荣德。剩下的過程發(fā)生在應(yīng)用程序?qū)崿F(xiàn)的委托方法中。


    照片捕獲流程
  • 當(dāng)你調(diào)用capturePhoto時(shí)童芹,photoOutput(_:willBeginCaptureFor:)首先到達(dá)涮瞻。解析的設(shè)置表示相機(jī)將為即將到來(lái)的照片應(yīng)用的實(shí)際設(shè)置。AVCam僅將此方法用于特定于活動(dòng)照片的行為假褪。AVCam通過檢查livephotomovieviedimensions尺寸來(lái)判斷照片是否為活動(dòng)照片;如果照片是活動(dòng)照片署咽,AVCam會(huì)增加一個(gè)計(jì)數(shù)來(lái)跟蹤活動(dòng)中的照片:

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:)正好在系統(tǒng)播放快門聲之后到達(dá)。AVCam利用這個(gè)機(jī)會(huì)來(lái)閃爍屏幕生音,提醒用戶照相機(jī)捕獲了一張照片艇抠。示例代碼通過將預(yù)覽視圖層的不透明度從0調(diào)整到1來(lái)實(shí)現(xiàn)此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:)在系統(tǒng)完成深度數(shù)據(jù)處理和人像效果處理后到達(dá)久锥。AVCam檢查肖像效果,曝光和深度元數(shù)據(jù)在這個(gè)階段:
self.sessionQueue.async {
    self.inProgressPhotoCaptureDelegates[photoCaptureProcessor.requestedPhotoSettings.uniqueID] = nil
}
  • 您可以在此委托方法中應(yīng)用其他視覺效果异剥,例如動(dòng)畫化捕獲照片的預(yù)覽縮略圖瑟由。
  • 有關(guān)通過委托回調(diào)跟蹤照片進(jìn)度的更多信息,請(qǐng)參見跟蹤照片捕獲進(jìn)度冤寿。

捕捉攝像頭拍照一個(gè)iOS設(shè)備是一個(gè)復(fù)雜的過程,涉及物理相機(jī)機(jī)制歹苦、圖像信號(hào)處理、操作系統(tǒng)和應(yīng)用程序督怜。雖然你的應(yīng)用有可能忽略許多階段,這個(gè)過程,只是等待最終的結(jié)果,您可以創(chuàng)建一個(gè)更具響應(yīng)性相機(jī)接口通過監(jiān)控每一步殴瘦。
在調(diào)用capturePhoto(帶有:delegate:)之后,您的委派對(duì)象可以遵循該過程中的五個(gè)主要步驟(或者更多号杠,取決于您的照片設(shè)置)蚪腋。根據(jù)您的捕獲工作流和您想要?jiǎng)?chuàng)建的捕獲UI丰歌,您的委托可以處理以下部分或全部步驟:

捕獲照片流程

捕獲系統(tǒng)在這個(gè)過程的每一步都提供一個(gè)avcaptureresolvedphotoset對(duì)象。由于多個(gè)捕獲可以同時(shí)進(jìn)行屉凯,因此每個(gè)解析后的照片設(shè)置對(duì)象都有一個(gè)uniqueID立帖,其值與您用于拍攝照片的avcapturephotos的uniqueID相匹配。

3.1.7 捕捉實(shí)時(shí)的照片

  • 當(dāng)您啟用實(shí)時(shí)照片捕捉功能時(shí)悠砚,相機(jī)會(huì)在捕捉瞬間拍攝一張靜止圖像和一段短視頻晓勇。該應(yīng)用程序以與靜態(tài)照片捕獲相同的方式觸發(fā)實(shí)時(shí)照片捕獲:通過對(duì)capturePhotoWithSettings的單個(gè)調(diào)用,您可以通過livePhotoMovieFileURL屬性傳遞實(shí)時(shí)照片短視頻的URL灌旧。您可以在AVCapturePhotoOutput級(jí)別啟用活動(dòng)照片绑咱,也可以在每次捕獲的基礎(chǔ)上在avcapturephotoset級(jí)別配置活動(dòng)照片。

  • 由于Live Photo capture創(chuàng)建了一個(gè)簡(jiǎn)短的電影文件枢泰,AVCam必須表示將電影文件保存為URL的位置描融。此外,由于實(shí)時(shí)照片捕捉可能會(huì)重疊宗苍,因此代碼必須跟蹤正在進(jìn)行的實(shí)時(shí)照片捕捉的數(shù)量稼稿,以確保實(shí)時(shí)照片標(biāo)簽在這些捕捉期間保持可見。上一節(jié)中的photoOutput(_:willBeginCaptureFor:)委托方法實(shí)現(xiàn)了這個(gè)跟蹤計(jì)數(shù)器讳窟。


    捕捉實(shí)時(shí)的照片流程
  • photoOutput(_:didFinishRecordingLivePhotoMovieForEventualFileAt:resolvedSettings:)在錄制短片結(jié)束時(shí)觸發(fā)让歼。AVCam取消了這里的活動(dòng)標(biāo)志。因?yàn)閿z像機(jī)已經(jīng)完成了短片的錄制丽啡,AVCam執(zhí)行Live Photo處理器遞減完成計(jì)數(shù)器:livePhotoCaptureHandler(false)

  • photoOutput(_:didFinishProcessingLivePhotoToMovieFileAt:duration:photoDisplayTime:resolvedSettings:error:)最后觸發(fā)谋右,表示影片已完全寫入磁盤,可以使用了补箍。AVCam利用這個(gè)機(jī)會(huì)來(lái)顯示任何捕獲錯(cuò)誤改执,并將保存的文件URL重定向到它的最終輸出位置:

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

3.1.8 捕獲深度數(shù)據(jù)和人像效果曝光

  • 使用AVCapturePhotoOutput, AVCam查詢捕獲設(shè)備辈挂,查看其配置是否可以將深度數(shù)據(jù)和人像效果傳送到靜態(tài)圖像。如果輸入設(shè)備支持這兩種模式中的任何一種裹粤,并且您在捕獲設(shè)置中啟用了它們终蒂,則相機(jī)將深度和人像效果作為輔助元數(shù)據(jù)附加到每張照片請(qǐng)求的基礎(chǔ)上。如果設(shè)備支持深度數(shù)據(jù)遥诉、人像效果或?qū)崟r(shí)照片的傳輸拇泣,應(yīng)用程序會(huì)顯示一個(gè)按鈕,用來(lái)切換啟用或禁用該功能的設(shè)置矮锈。
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
           }
  • 相機(jī)存儲(chǔ)深度和人像效果的曝光元數(shù)據(jù)作為輔助圖像霉翔,可通過圖像I/O API發(fā)現(xiàn)和尋址。AVCam通過搜索kCGImageAuxiliaryDataTypePortraitEffectsMatte類型的輔助圖像來(lái)訪問這個(gè)元數(shù)據(jù):
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設(shè)備上子眶,捕獲系統(tǒng)可以記錄深度信息。深度圖就像一個(gè)圖像;但是葱弟,它不是每個(gè)像素提供一個(gè)顏色壹店,而是表示從相機(jī)到圖像的那一部分的距離(以絕對(duì)值表示,或與深度圖中的其他像素相對(duì))芝加。
您可以使用一個(gè)深度地圖和照片一起創(chuàng)建圖像處理效果,對(duì)前景和背景照片不同的元素,像iOS的豎屏模式相機(jī)應(yīng)用硅卢。通過保存顏色和深度數(shù)據(jù)分開,你甚至可以應(yīng)用,改變這些影響長(zhǎng)照片后被抓獲藏杖。

使用深度捕獲照片

3.1.9 捕捉語(yǔ)義分割

  • 使用AVCapturePhotoOutput, AVCam還可以捕獲語(yǔ)義分割圖像蝌麸,將一個(gè)人的頭發(fā)来吩、皮膚和牙齒分割成不同的圖像弟疆。將這些輔助圖像與你的主要照片一起捕捉同廉,可以簡(jiǎn)化照片效果的應(yīng)用柑司,比如改變一個(gè)人的頭發(fā)顏色或讓他們的笑容更燦爛迫肖。
    通過將照片輸出的enabledSemanticSegmentationMatteTypes屬性設(shè)置為首選值(頭發(fā)、皮膚和牙齒)攒驰,可以捕獲這些輔助圖像蟆湖。要捕獲所有受支持的類型,請(qǐng)?jiān)O(shè)置此屬性以匹配照片輸出的availableSemanticSegmentationMatteTypes屬性玻粪。
// Capture all available semantic segmentation matte types.
photoOutput.enabledSemanticSegmentationMatteTypes = 
    photoOutput.availableSemanticSegmentationMatteTypes
  • 當(dāng)照片輸出完成捕獲一張照片時(shí)隅津,您可以通過查詢照片的semanticSegmentationMatte(for:)方法來(lái)檢索相關(guān)的分割matte圖像。此方法返回一個(gè)AVSemanticSegmentationMatte奶段,其中包含matte圖像和處理圖像時(shí)可以使用的其他元數(shù)據(jù)。示例應(yīng)用程序?qū)⒄Z(yǔ)義分割的matte圖像數(shù)據(jù)添加到一個(gè)數(shù)組中剥纷,這樣您就可以將其寫入用戶的照片庫(kù)痹籍。
// 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 保存照片到用戶的照片庫(kù)

  • 在將圖像或電影保存到用戶的照片庫(kù)之前,必須首先請(qǐng)求訪問該庫(kù)晦鞋。請(qǐng)求寫授權(quán)的過程鏡像捕獲設(shè)備授權(quán):使用Info.plist中提供的文本顯示警報(bào)蹲缠。
    AVCam在fileOutput(_:didFinishRecordingTo:from:error:)回調(diào)方法中檢查授權(quán)娜谊,其中AVCaptureOutput提供了要保存為輸出的媒體數(shù)據(jù)。PHPhotoLibrary.requestAuthorization { status in

  • 有關(guān)請(qǐng)求訪問用戶的照片庫(kù)的更多信息,請(qǐng)參見請(qǐng)求訪問照片的授權(quán)

  1. 用戶必須明確授予您的應(yīng)用程序訪問照片的權(quán)限鉴竭。通過提供調(diào)整字符串來(lái)準(zhǔn)備你的應(yīng)用祭埂。調(diào)整字符串是一個(gè)可本地化的消息,你添加到你的應(yīng)用程序的信息泰演。plist文件垃喊,告訴用戶為什么你的應(yīng)用程序需要訪問用戶的照片庫(kù)。然后溜在,當(dāng)照片提示用戶授予訪問權(quán)限時(shí),警報(bào)將以用戶設(shè)備上選擇的語(yǔ)言環(huán)境顯示您提供的調(diào)整字符串。
  2. PHCollection,第一次您的應(yīng)用程序使用PHAsset PHAssetCollection,從圖書館或PHCollectionList方法獲取內(nèi)容,或使用一個(gè)照片庫(kù)中列出的方法應(yīng)用更改請(qǐng)求更改庫(kù)內(nèi)容,照片自動(dòng)和異步提示用戶請(qǐng)求授權(quán)。
    系統(tǒng)用戶授予權(quán)限后,記得將來(lái)使用的選擇在你的應(yīng)用程序,但是用戶可以在任何時(shí)候改變這個(gè)選擇使用設(shè)置應(yīng)用程序。如果用戶否認(rèn)你的應(yīng)用照片庫(kù)訪問,還沒有回復(fù)權(quán)限提示,或不能授予訪問權(quán)限限制,任何試圖獲取照片庫(kù)內(nèi)容將返回空PHFetchResult對(duì)象,和任何試圖更改照片庫(kù)將會(huì)失敗。如果這個(gè)方法返回PHAuthorizationStatus呜袁。您可以調(diào)用requestAuthorization(_:)方法來(lái)提示用戶訪問照片庫(kù)權(quán)限膘融。
  3. 使用與照片庫(kù)交互的類,如PHAsset、PHPhotoLibrary和PHImageManager(應(yīng)用程序的信息)臼疫。plist文件必須包含面向用戶的NSPhotoLibraryUsageDescription鍵文本塔逃,系統(tǒng)在請(qǐng)求用戶訪問權(quán)限時(shí)將顯示該文本。如果沒有這個(gè)鍵,iOS 10或之后的應(yīng)用程序?qū)?huì)崩潰。


    修改權(quán)限plist文件

3.1.11 錄制視頻文件

  • AVCam通過使用.video限定符查詢和添加輸入設(shè)備來(lái)支持視頻捕獲。該應(yīng)用程序默認(rèn)為后雙攝像頭,但如果設(shè)備沒有雙攝像頭,該應(yīng)用程序默認(rèn)為廣角攝像頭。
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
}
  • 不像靜態(tài)照片那樣將設(shè)置傳遞給系統(tǒng)匪蟀,而是像活動(dòng)照片那樣傳遞輸出URL。委托回調(diào)提供相同的URL,因此應(yīng)用程序不需要將其存儲(chǔ)在中間變量中。
  • 一旦用戶點(diǎn)擊記錄開始捕獲鼓鲁,AVCam調(diào)用startRecording(to:recordingDelegate:):
movieFileOutput.startRecording(to: URL(fileURLWithPath: outputFilePath), recordingDelegate: self)

  • 與capturePhoto為still capture觸發(fā)委托回調(diào)一樣歧寺,startRecording為影片錄制觸發(fā)一系列委托回調(diào)。
錄制視頻流程
  • 通過委托回調(diào)鏈跟蹤影片錄制的進(jìn)度嗤练。與其實(shí)現(xiàn)AVCapturePhotoCaptureDelegate此疹,不如實(shí)現(xiàn)AVCaptureFileOutputRecordingDelegate。由于影片錄制委托回調(diào)需要與捕獲會(huì)話進(jìn)行交互蹦骑,因此AVCam將CameraViewController作為委托慈省,而不是創(chuàng)建單獨(dú)的委托對(duì)象。
  • 當(dāng)文件輸出開始向文件寫入數(shù)據(jù)時(shí)觸發(fā)fileOutput(_:didStartRecordingTo:from:)眠菇。AVCam利用這個(gè)機(jī)會(huì)將記錄按鈕更改為停止按鈕:
DispatchQueue.main.async {
    self.recordButton.isEnabled = true
    self.recordButton.setImage(#imageLiteral(resourceName: "CaptureStop"), for: [])
}
  • fileOutput(_:didFinishRecordingTo:from:error:)最后觸發(fā)边败,表示影片已完全寫入磁盤登疗,可以使用了。AVCam利用這個(gè)機(jī)會(huì)將臨時(shí)保存的影片從給定的URL移動(dòng)到用戶的照片庫(kù)或應(yīng)用程序的文檔文件夾:
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進(jìn)入后臺(tái)——例如用戶接受來(lái)電時(shí)——應(yīng)用程序必須獲得用戶的許可才能繼續(xù)錄制差购。AVCam通過后臺(tái)任務(wù)從系統(tǒng)請(qǐng)求時(shí)間來(lái)執(zhí)行此保存陈惰。這個(gè)后臺(tái)任務(wù)確保有足夠的時(shí)間將文件寫入照片庫(kù),即使AVCam退到后臺(tái)奴愉。為了結(jié)束后臺(tái)執(zhí)行,AVCam在保存記錄文件后調(diào)用fileOutput(:didFinishRecordingTo:from:error:)中的endBackgroundTask(:)容为。
self.backgroundRecordingID = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)

3.1.12 錄制視頻時(shí)要抓拍圖片

  • 與iOS攝像頭應(yīng)用程序一樣慷暂,AVCam也可以在拍攝錄像的同時(shí)拍照。AVCam以與視頻相同的分辨率捕捉這些照片。實(shí)現(xiàn)代碼如下:
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)
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市昭娩,隨后出現(xiàn)的幾起案子击困,更是在濱河造成了極大的恐慌由蘑,老刑警劉巖闽寡,帶你破解...
    沈念sama閱讀 222,865評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件代兵,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡爷狈,警方通過查閱死者的電腦和手機(jī)植影,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,296評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)涎永,“玉大人思币,你說(shuō)我怎么就攤上這事∠畚ⅲ” “怎么了谷饿?”我有些...
    開封第一講書人閱讀 169,631評(píng)論 0 364
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)妈倔。 經(jīng)常有香客問我博投,道長(zhǎng),這世上最難降的妖魔是什么盯蝴? 我笑而不...
    開封第一講書人閱讀 60,199評(píng)論 1 300
  • 正文 為了忘掉前任毅哗,我火速辦了婚禮,結(jié)果婚禮上捧挺,老公的妹妹穿的比我還像新娘虑绵。我一直安慰自己,他們只是感情好松忍,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,196評(píng)論 6 398
  • 文/花漫 我一把揭開白布蒸殿。 她就那樣靜靜地躺著筷厘,像睡著了一般鸣峭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上酥艳,一...
    開封第一講書人閱讀 52,793評(píng)論 1 314
  • 那天摊溶,我揣著相機(jī)與錄音,去河邊找鬼充石。 笑死莫换,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的骤铃。 我是一名探鬼主播拉岁,決...
    沈念sama閱讀 41,221評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼惰爬!你這毒婦竟也來(lái)了喊暖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,174評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤撕瞧,失蹤者是張志新(化名)和其女友劉穎陵叽,沒想到半個(gè)月后狞尔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,699評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡巩掺,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,770評(píng)論 3 343
  • 正文 我和宋清朗相戀三年偏序,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胖替。...
    茶點(diǎn)故事閱讀 40,918評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡研儒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出刊殉,到底是詐尸還是另有隱情殉摔,我是刑警寧澤,帶...
    沈念sama閱讀 36,573評(píng)論 5 351
  • 正文 年R本政府宣布记焊,位于F島的核電站逸月,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏遍膜。R本人自食惡果不足惜碗硬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,255評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望瓢颅。 院中可真熱鬧恩尾,春花似錦、人聲如沸挽懦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,749評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)信柿。三九已至冀偶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間渔嚷,已是汗流浹背进鸠。 一陣腳步聲響...
    開封第一講書人閱讀 33,862評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留形病,地道東北人客年。 一個(gè)月前我還...
    沈念sama閱讀 49,364評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像漠吻,于是被迫代替她去往敵國(guó)和親量瓜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,926評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容