視訊採集:iOS平臺基於AVCaptureDevice的實現

weixin_34402408發表於2018-12-24

前言

這篇文章簡單介紹下移動端iOS系統下利用AVCaptureDevice進行視訊資料採集的方法。
按照慣例先上一份原始碼:iOSVideo
攝像頭採集相關核心實現在:NTVideoCapture.m
官方文件可以參考:AVFoundation官方文件

PS:採集部分的邏輯會相對比較簡單,後續會在視訊的採集基礎上面介紹怎麼利用OpenGL去繪製採集獲取到的資料。

入門知識

AVCaptureSession
在iOS平臺開發中只要跟硬體相關的都要從會話開始進行配置,如果我們使用攝像頭的話可以利用AVCaptureSession進行視訊採集,其可以對輸入和輸出資料進行管理,負責協調從哪裡採集資料,輸出到哪裡去。

AVCaptureDevice
一個AVCaptureDevice對應的是一個物理採集裝置,我們可以通過該物件來獲取和識別裝置屬性。
例如通過AVCaptureDevice.position檢測其攝像頭的方向。

AVCaptureInput
AVCaptureInput是一個抽象類,AVCaptureSession的輸入端必須是AVCaptureInput的實現類。
例如利用AVCaptureDevice構建AVCaptureDeviceInput作為採集裝置輸入端。

AVCaptureOutput
AVCaptureOutput是一個抽象類,AVCaptureSession的輸出端必須是AVCaptureOutput的實現類。
例如AVCaptureVideoDataOutput可以作為一個原始視訊資料的輸出端。

AVCaptureConnection
AVCaptureConnectionAVCaptureSession用來建立和維護AVCaptureInputAVCaptureOutput之間的連線的,一個AVCaptureSession可能會有多個AVCaptureConnection例項。

採集步驟

  1. 建立AVCaptureSession並初始化。
  2. 通過前後置攝像頭找到對應的AVCaptureDevice
  3. 通過AVCaptureDevice建立輸入端AVCaptureDeviceInput,並將其新增到AVCaptureSession的輸入端。
  4. 建立輸出端AVCaptureVideoDataOutput,並進行Format和Delgate的配置,最後新增到AVCaptureSession的輸出端。
  5. 獲取AVCaptureConnection,並進行相應的引數設定。
  6. 呼叫AVCaptureSessionstartRunningstopRunning設定採集狀態。

配置會話

建立一個AVCaptureSession很簡單:

AVCaptureSession *captureSession;
captureSession = [[AVCaptureSession alloc] init];

我們可以在AVCaptureSession來配置指定所需的影象質量和解析度,可選引數請參考AVCaptureSessionPreset.h
在設定前需要檢測是否支援該Preset是否被支援:

//指定採集1280x720解析度大小格式
AVCaptureSessionPreset preset = AVCaptureSessionPreset1280x720;
//檢查AVCaptureSession是否支援該AVCaptureSessionPreset
if ([captureSession canSetSessionPreset:preset]) {
    captureSession.sessionPreset = preset;
}
else {
    //錯誤處理,不支援該AVCaptureSessionPreset型別值
}

配置輸入端

通過AVCaptureDevicedevicesWithMediaType的方法來獲取攝像頭,由於iOS存在多個攝像頭,所以這裡一般返回一個裝置的陣列。
根據業務需要(例如前後置攝像頭),我們找到其中對應的AVCaptureDevice,並將其構造成AVCaptureDeviceInput例項。

AVCaptureDevice *device;
AVCaptureDeviceInput *captureInput;
//獲取前後置攝像頭的標識
AVCaptureDevicePosition position = _isFront ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack;
//獲取裝置的AVCaptureDevice列表
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *item in devices) {
    //如果找到對應的攝像頭
    if ([item position] == position) {
        device = item;
        break;
    }
}
if (device == nil) {
    //錯誤處理,沒有找到對應的攝像頭
}
//建立AVCaptureDeviceInput輸入端
captureInput = [[AVCaptureDeviceInput alloc] initWithDevice:device error:nil];

配置輸出端

如果我們想要獲取到攝像頭採集到的原始視訊資料的話,需要配置一個AVCaptureVideoDataOutput作為AVCaptureSession的輸出端,我們需要給其設定採集的視訊格式和採集資料回撥佇列。

AVCaptureVideoDataOutput *captureOutput;
//建立一個輸出端AVCaptureVideoDataOutput例項
captureOutput = [[AVCaptureVideoDataOutput new];
//配置輸出的資料格式
[captureOutput setVideoSettings:@{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_420YpCbCr8PlanarFullRange)}];
//設定輸出代理和採集資料的佇列
dispatch_queue_t outputQueue = dispatch_queue_create("ACVideoCaptureOutputQueue", DISPATCH_QUEUE_SERIAL);
[captureOutput setSampleBufferDelegate:self queue:outputQueue];
// 丟棄延遲的幀
captureOutput.alwaysDiscardsLateVideoFrames = YES;

需要注意的幾個點

  • 對於setVideoSettings,雖然AVCaptureVideoDataOutput提供的是一個字典設定,但是現在只支援kCVPixelBufferPixelFormatTypeKey這個key。
  • 畫素格式預設使用的是YUVFullRange型別,表示其YUV取值範圍是0~255,而還有另外一種型別YUVVideoRange型別則是為了防止溢位,將YUV的取值範圍限制為16~235。
  • setSampleBufferDelegate必須指定序列佇列來確保視訊資料獲取委託呼叫的正確順序,當然你也可以修改佇列來設定視訊處理的優先順序別。
  • alwaysDiscardsLateVideoFrames = YES可以在你沒有足夠時間處理視訊幀時丟棄任何延遲的視訊幀而不是等待處理,如果你設定了NO並不能保證幀不會被丟棄,只是他們不會被提前有意識的丟棄而已。

配置會話的輸入和輸出

//新增輸入裝置到會話
if ([captureSession canAddInput:captureInput]) {
    [captureSession addInput:captureInput];
}
//新增輸出裝置到會話
if ([captureSession canAddOutput:captureOutput]) {
    [captureSession addOutput:captureOutput];
}
//獲取連線並設定視訊方向為豎屏方向
AVCaptureConnection *conn = [captureOutput connectionWithMediaType:AVMediaTypeVideo];
conn.videoOrientation = AVCaptureVideoOrientationPortrait;
//前置攝像頭採集到的資料本來就是映象翻轉的,這裡設定為映象把畫面轉回來
if (device.position == AVCaptureDevicePositionFront && conn.supportsVideoMirroring) {
    conn.videoMirrored = YES;
}

如果AVCaptureSession已經開啟了採集,如果這個時候需要修改解析度、輸入輸出等配置。那麼需要用到beginConfigurationcommitConfiguration方法把修改的程式碼包圍起來,也就是先呼叫beginConfiguration啟動事務,然後配置解析度、輸入輸出等資訊,最後呼叫commitConfiguration提交修改;這樣才能確保相應修改作為一個事務組提交,避免狀態的不一致性。

AVCaptureSession管理了採集過程中的狀態,當開始採集、停止採集、出現錯誤等都會發起通知,我們可以監聽通知來獲取AVCaptureSession的狀態,也可以呼叫其屬性來獲取當前AVCaptureSession的狀態,值得注意一點是AVCaptureSession相關的通知都是在主執行緒的。

開始採集資料和資料回撥

當上面的配置搞定後,呼叫startRunning就可以開始資料的採集了。

if (![captureSession isRunning]) {
    [captureSession startRunning];
}

停止採集只需要呼叫stopRunning方法即可。

if ([captureSession isRunning]) {
    [captureSession stopRunning];
}

對於採集回撥的視訊資料,會在[captureOutput setSampleBufferDelegate:self queue:outputQueue]設定的代理方法觸發返回,
其中最重要的是CMSampleBufferRef,其中實際儲存著攝像頭採集到的影象。
方法原型如下:

- (void)captureOutput:(AVCaptureOutput *)output 
        didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer 
        fromConnection:(AVCaptureConnection *)connection 

切換前後攝像頭

在視訊採集的過程中,我們經常需要切換前後攝像頭,這裡我們也就是需要把AVCaptureSession的輸入端改為對應的攝像頭就可以了。
當然我們可以用beginConfigurationcommitConfiguration將修改邏輯包圍起來,也可以先呼叫stopRunning方法停止採集,然後重新配置好輸入和輸出,再呼叫startRunning開啟採集。

//獲取攝像頭列表
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
//獲取當前攝像頭方向
AVCaptureDevicePosition currentPosition = captureInput.device.position;
//轉換攝像頭
if (currentPosition == AVCaptureDevicePositionBack){
    currentPosition = AVCaptureDevicePositionFront;
}
else{
    currentPosition = AVCaptureDevicePositionBack;
}
//獲取到新的AVCaptureDevice
NSArray *captureDeviceArray = [devices filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"position == %d", currentPosition]];
AVCaptureDevice *device = captureDeviceArray.firstObject;
//開始配置
[captureSession beginConfiguration];
//構造一個新的AVCaptureDeviceInput的輸入端
AVCaptureDeviceInput *newInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil];
//移除掉就的AVCaptureDeviceInput
[captureSession removeInput:captureInput];
//將新的AVCaptureDeviceInput新增到AVCaptureSession中
if ([captureSession canAddInput:newInput]){
    [captureSession addInput:newInput];
    captureInput = newInput;
}
//提交配置
[captureSession commitConfiguration];
//重新獲取連線並設定視訊的方向、是否映象
AVCaptureConnection *conn = [captureOutput connectionWithMediaType:AVMediaTypeVideo];
conn.videoOrientation = AVCaptureVideoOrientationPortrait;
if (device.position == AVCaptureDevicePositionFront && conn.supportsVideoMirroring){
    conn.videoMirrored = YES;
}

視訊幀率

iOS預設的幀率設定是30幀,如果我們的業務場景不需要用到30幀,或者我們的處理能力達不到33ms(1000ms/30幀)的話,我們可以通過設定修改視訊的輸出幀率:

NSInteger fps = 15;
//獲取設定支援設定的幀率範圍
AVFrameRateRange *fpsRange = [captureInput.device.activeFormat.videoSupportedFrameRateRanges objectAtIndex:0];
if (fps > fpsRange.maxFrameRate || fps < fpsRange.minFrameRate) {
    //不支援該fps設定
    return;
}
// 設定輸入的幀率
captureInput.device.activeVideoMinFrameDuration = CMTimeMake(1, (int)fps);
captureInput.device.activeVideoMaxFrameDuration = CMTimeMake(1, (int)fps);

簡易預覽

如果不想通過自己實現OpenGL渲染採集到的視訊幀,當然,iOS也提供了一個預覽元件AVCaptureVideoPreviewLayer,其繼承於CALayer
可以將這個layer新增到UIView上面就可以實現採集到的視訊的實時預覽。

//建立一個AVCaptureVideoPreviewLayer,並將AVCaptureSession傳入
AVCaptureVideoPreviewLayer *previewLayer;
previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:captureSession];
previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
previewLayer.frame = self.view.bounds;
//將其載入到UIView上面即可
[self.view.layer addSublayer:previewLayer];

PS:如果採用AVCaptureVideoPreviewLayer進行視訊預覽的話,那麼可以不配置AVCaptureSession的輸出端相關。

結語

這篇文章簡單介紹下移動端iOS系統下利用AVCaptureDevice進行視訊資料採集的方法,並提供了相關程式碼的使用示例。
限於篇幅就不對閃光燈、對焦等展開介紹,詳細請參考官方文件
後續文章將介紹怎麼利用OpenGL來渲染攝像頭採集到的視訊幀。

本文同步釋出於簡書CSDN

End!

相關文章