天天看點

視訊采集:iOS平台基于AVCaptureDevice的實作

入門知識

AVCaptureSession

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

AVCaptureDevice

一個AVCaptureDevice對應的是一個實體采集裝置,我們可以通過該對象來擷取和識别裝置屬性。

例如通過AVCaptureDevice.position檢測其攝像頭的方向。

AVCaptureInput

AVCaptureInput是一個抽象類,AVCaptureSession的輸入端必須是AVCaptureInput的實作類。

例如利用AVCaptureDevice建構AVCaptureDeviceInput作為采集裝置輸入端。

AVCaptureOutput

AVCaptureOutput是一個抽象類,AVCaptureSession的輸出端必須是AVCaptureOutput的實作類。

例如AVCaptureVideoDataOutput可以作為一個原始視訊資料的輸出端。

AVCaptureConnection

AVCaptureConnection是AVCaptureSession用來建立和維護AVCaptureInput和AVCaptureOutput之間的連接配接的,一個AVCaptureSession可能會有多個AVCaptureConnection執行個體。

采集步驟

  1. 建立AVCaptureSession并初始化。
  2. 通過前後置攝像頭找到對應的AVCaptureDevice。
  3. 通過AVCaptureDevice建立輸入端AVCaptureDeviceInput,并将其添加到AVCaptureSession的輸入端。
  4. 建立輸出端AVCaptureVideoDataOutput,并進行Format和Delgate的配置,最後添加到AVCaptureSession的輸出端。
  5. 擷取AVCaptureConnection,并進行相應的參數設定。
  6. 調用AVCaptureSession的startRunning和stopRunning設定采集狀态。

    配置會話

建立一個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類型值           

配置輸入端

通過AVCaptureDevice的devicesWithMediaType的方法來擷取攝像頭,由于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已經開啟了采集,如果這個時候需要修改分辨率、輸入輸出等配置。那麼需要用到beginConfiguration和commitConfiguration方法把修改的代碼包圍起來,也就是先調用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的輸入端改為對應的攝像頭就可以了。
當然我們可以用beginConfiguration和commitConfiguration将修改邏輯包圍起來,也可以先調用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的輸出端相關。