視頻採集: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的輸出端相關。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章