前言
系列文章:
《iOS視頻開發(fā)(一):視頻采集》
《iOS視頻開發(fā)(二):視頻H264硬編碼》
《iOS視頻開發(fā)(三):視頻H264硬解碼》
《iOS視頻開發(fā)(四):通俗理解YUV數(shù)據(jù)》
作為iOS音視頻開發(fā)之視頻開發(fā)的第一篇,本文介紹iOS視頻采集的相關(guān)概念及視頻采集的工作原理,后續(xù)將對采集后的視頻數(shù)據(jù)進(jìn)行硬編碼、硬解碼、播放等流程進(jìn)行分析講解。
基本概念
AVCaptureSession
蘋果為了管理從攝像頭、麥克風(fēng)等設(shè)備捕獲到的信息,整了一個叫做AVCaptureSession的東西來對輸入和輸出數(shù)據(jù)流進(jìn)行管理。AVFoundation官方文檔

AVCaptureSession對象是用來管理采集數(shù)據(jù)和輸出數(shù)據(jù)的,它負(fù)責(zé)協(xié)調(diào)從哪里采集數(shù)據(jù),輸出到哪里。
AVCaptureDevice
一個AVCaptureDevice對象代表一個物理采集設(shè)備,我們可以通過該對象來設(shè)置物理設(shè)備的屬性。
AVCaptureInput
AVCaptureInput是一個抽象類,AVCaptureSession的輸入端必須是AVCaptureInput的實現(xiàn)類。
這里我們用到的是AVCaptureDeviceInput,作為采集設(shè)備輸入端。
AVCaptureOutput
AVCaptureOutput是一個抽象類,AVCaptureSession的輸出端必須是AVCaptureOutput的實現(xiàn)類。
這里我們用到的是AVCaptureVideoDataOutput,作為視頻數(shù)據(jù)的輸出端。
AVCaptureConnection
AVCaptureConnection是AVCaptureSession用來建立和維護(hù)AVCaptureInput和AVCaptureOutput之間的連接的。
AVCaptureVideoPreviewLayer
這是AVCaptureSession的一個屬性,集成自CALayer,通過類名我們可以知道這個layer是用來預(yù)覽采集到的視頻圖像的,直接把這個layer加到UIView上面就可以實現(xiàn)采集到的視頻實時預(yù)覽了。

視頻采集的步驟
1、創(chuàng)建并初始化輸入(AVCaptureInput)和輸出(AVCaptureOutput)
2、創(chuàng)建并初始化AVCaptureSession,把AVCaptureInput和AVCaptureOutput添加到AVCaptureSession中
3、調(diào)用AVCaptureSession的startRunning開啟采集
初始化輸入(攝像頭)
通過AVCaptureDevice的devicesWithMediaType:方法獲取攝像頭,iPhone都是有前后攝像頭的,這里獲取到的是一個設(shè)備的數(shù)組,要從數(shù)組里面拿到我們想要的前攝像頭或后攝像頭,然后將AVCaptureDevice轉(zhuǎn)化為AVCaptureDeviceInput,一會兒添加到AVCaptureSession中。
// 獲取所有攝像頭
NSArray *cameras= [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
// 獲取前置攝像頭
NSArray *captureDeviceArray = [cameras filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"position == %d", AVCaptureDevicePositionFront]];
if (!captureDeviceArray.count)
{
NSLog(@"獲取前置攝像頭失敗");
return;
}
// 轉(zhuǎn)化為輸入設(shè)備
AVCaptureDevice *camera = captureDeviceArray.firstObject;
NSError *errorMessage = nil;
self.captureDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:camera error:&errorMessage];
if (errorMessage)
{
NSLog(@"AVCaptureDevice轉(zhuǎn)AVCaptureDeviceInput失敗");
return;
}
初始化輸出
初始化視頻輸出并設(shè)置視頻數(shù)據(jù)格式,設(shè)置采集數(shù)據(jù)回調(diào)線程。
這里視頻輸出格式選的是kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,YUV數(shù)據(jù)格式,不理解YUV數(shù)據(jù)的概念的話可以先這么寫,后面編解碼再深入了解YUV數(shù)據(jù)格式
// 設(shè)置視頻輸出
self.captureVideoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
// 設(shè)置視頻數(shù)據(jù)格式
NSDictionary *videoSetting = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange], kCVPixelBufferPixelFormatTypeKey, nil];
[self.captureVideoDataOutput setVideoSettings:videoSetting];
// 設(shè)置輸出代理、串行隊列和數(shù)據(jù)回調(diào)
dispatch_queue_t outputQueue = dispatch_queue_create("ACVideoCaptureOutputQueue", DISPATCH_QUEUE_SERIAL);
[self.captureVideoDataOutput setSampleBufferDelegate:self queue:outputQueue];
// 丟棄延遲的幀
self.captureVideoDataOutput.alwaysDiscardsLateVideoFrames = YES;
初始化AVCaptureSession并設(shè)置輸入輸出
1、初始化AVCaptureSession,把上面的輸入和輸出加進(jìn)來,在添加輸入和輸出到AVCaptureSession先查詢一下AVCaptureSession是否支持添加該輸入或輸出端口。
2、設(shè)置視頻分辨率及圖像質(zhì)量(AVCaptureSessionPreset),設(shè)置之前同樣需要先查詢一下AVCaptureSession是否支持這個分辨率。
3、如果在已經(jīng)開啟采集的情況下需要修改分辨率或輸入輸出,需要用beginConfiguration和commitConfiguration把修改的代碼包圍起來。在調(diào)用beginConfiguration后,可以配置分辨率、輸入輸出等,直到調(diào)用commitConfiguration了才會被應(yīng)用。
4、AVCaptureSession管理了采集過程中的狀態(tài),當(dāng)開始采集、停止采集、出現(xiàn)錯誤等都會發(fā)起通知,我們可以監(jiān)聽通知來獲取AVCaptureSession的狀態(tài),也可以調(diào)用其屬性來獲取當(dāng)前AVCaptureSession的狀態(tài)。AVCaptureSession相關(guān)的通知都是在主線程的。
前置攝像頭采集到的畫面是翻轉(zhuǎn)的,若要解決畫面翻轉(zhuǎn)問題,需要設(shè)置AVCaptureConnection的videoMirrored為YES。
self.captureSession = [[AVCaptureSession alloc] init];
// 不使用應(yīng)用的實例,避免被異常掛斷
self.captureSession.usesApplicationAudioSession = NO;
// 添加輸入設(shè)備到會話
if ([self.captureSession canAddInput:self.captureDeviceInput])
{
[self.captureSession addInput:self.captureDeviceInput];
}
// 添加輸出設(shè)備到會話
if ([self.captureSession canAddOutput:self.captureVideoDataOutput])
{
[self.captureSession addOutput:self.captureVideoDataOutput];
}
// 設(shè)置分辨率
if ([self.captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720])
{
self.captureSession.sessionPreset = AVCaptureSessionPreset1280x720;
}
// 獲取連接并設(shè)置視頻方向為豎屏方向
self.captureConnection = [self.captureVideoDataOutput connectionWithMediaType:AVMediaTypeVideo];
self.captureConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
// 設(shè)置是否為鏡像,前置攝像頭采集到的數(shù)據(jù)本來就是翻轉(zhuǎn)的,這里設(shè)置為鏡像把畫面轉(zhuǎn)回來
if (camera.position == AVCaptureDevicePositionFront && self.captureConnection.supportsVideoMirroring)
{
self.captureConnection.videoMirrored = YES;
}
// 獲取預(yù)覽Layer并設(shè)置視頻方向,注意self.videoPreviewLayer.connection跟self.captureConnection不是同一個對象,要分開設(shè)置
self.videoPreviewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.captureSession];
self.videoPreviewLayer.connection.videoOrientation = self.captureParam.videoOrientation;
self.videoPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
開始視頻數(shù)據(jù)采集
伺候好AVCaptureSession之后,直接調(diào)用startRunning就可以開始采集了,開啟采集前最好判斷一下攝像頭權(quán)限有沒有開啟。采集到的數(shù)據(jù)會通過上面設(shè)置的代理方法captureOutput:didOutputSampleBuffer:fromConnection回調(diào)出來。若要停止采集,只需調(diào)用stopRunning即可。
AVCaptureSession的startRunning方法是個耗時操作,如果在主線程調(diào)用的話會卡UI。
/** 開始采集 */
- (BOOL)startCapture
{
if (self.isCapturing)
{
return NO;
}
// 攝像頭權(quán)限判斷
AVAuthorizationStatus videoAuthStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
if (videoAuthStatus != AVAuthorizationStatusAuthorized)
{
return NO;
}
[self.captureSession startRunning];
self.isCapturing = YES;
return YES;
}
/**
攝像頭采集的數(shù)據(jù)回調(diào)
@param output 輸出設(shè)備
@param sampleBuffer 幀緩存數(shù)據(jù),描述當(dāng)前幀信息
@param connection 連接
*/
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
if ([self.delegate respondsToSelector:@selector(videoCaptureDataCallback:)])
{
[self.delegate videoCaptureDataCallback:sampleBuffer];
}
}
切換前后攝像頭
采集視頻數(shù)據(jù)的過程中,我們可能需要切換前后攝像頭,這時候我們只需要把AVCaptureSession中的輸入端改成前置攝像頭就可以了。這里是修改了輸入端口,也就是需要調(diào)用beginConfiguration和commitConfiguration包起來。當(dāng)然也可以選擇先調(diào)用stopRunning方法停止采集,然后重新配置好輸入和輸出,再調(diào)用startRunning開啟采集。
// 獲取所有攝像頭
NSArray *cameras= [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
// 獲取當(dāng)前攝像頭方向
AVCaptureDevicePosition currentPosition = self.captureDeviceInput.device.position;
AVCaptureDevicePosition toPosition = AVCaptureDevicePositionUnspecified;
if (currentPosition == AVCaptureDevicePositionBack || currentPosition == AVCaptureDevicePositionUnspecified)
{
toPosition = AVCaptureDevicePositionFront;
}
else
{
toPosition = AVCaptureDevicePositionBack;
}
NSArray *captureDeviceArray = [cameras filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"position == %d", toPosition]];
if (captureDeviceArray.count == 0)
{
return;
}
NSError *error = nil;
AVCaptureDevice *camera = captureDeviceArray.firstObject;
// 開始配置
[self.captureSession beginConfiguration];
AVCaptureDeviceInput *newInput = [AVCaptureDeviceInput deviceInputWithDevice:camera error:&error];
[self.captureSession removeInput:self.captureDeviceInput];
if ([_captureSession canAddInput:newInput])
{
[_captureSession addInput:newInput];
self.captureDeviceInput = newInput;
}
// 提交配置
[self.captureSession commitConfiguration];
// 重新獲取連接并設(shè)置視頻的方向、是否鏡像
self.captureConnection = [self.captureVideoDataOutput connectionWithMediaType:AVMediaTypeVideo];
self.captureConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
if (camera.position == AVCaptureDevicePositionFront && self.captureConnection.supportsVideoMirroring)
{
self.captureConnection.videoMirrored = YES;
}
設(shè)置視頻的幀率
iOS默認(rèn)輸出的視頻幀率為30幀/秒,在某些應(yīng)用場景下我們可能用不到30幀,我們也可以設(shè)置或修改視頻的輸出幀率
// 獲取設(shè)置支持設(shè)置的幀率范圍
NSInteger frameRate = 15;
AVFrameRateRange *frameRateRange = [self.captureDeviceInput.device.activeFormat.videoSupportedFrameRateRanges objectAtIndex:0];
if (frameRate > frameRateRange.maxFrameRate || frameRate < frameRateRange.minFrameRate)
{
return;
}
// 設(shè)置輸入的幀率
self.captureDeviceInput.device.activeVideoMinFrameDuration = CMTimeMake(1, (int)frameRate);
self.captureDeviceInput.device.activeVideoMaxFrameDuration = CMTimeMake(1, (int)frameRate);
踩坑及總結(jié)
用的時候我們是有設(shè)置AVCaptureVideoPreviewLayer的connection的videoOrientation屬性來確定畫面的方向的。剛開始的時候出現(xiàn)了很奇怪的現(xiàn)象,AVCaptureVideoPreviewLayer的畫面是正常的豎屏方向,但拿采集出來的視頻流去播放的時候發(fā)現(xiàn)畫面是橫屏方向。后來發(fā)現(xiàn),這里AVCaptureVideoPreviewLayer的connection屬性跟我們通過connectionWithMediaType方法獲取到的connection不是同一個對象,需要分別設(shè)置。像我們上面分析看到,connection是維護(hù)一個輸入設(shè)備和一個輸出的,也就是說AVCaptureSession有多個connection。找到對應(yīng)需要的connection設(shè)置videoOrientation,問題解決。
iOS視頻采集這一塊內(nèi)容不多,難度也比較低,應(yīng)該還是比較好上手的。本文限于篇幅沒有詳細(xì)說明設(shè)置攝像頭的相關(guān)內(nèi)容,例如閃光燈、對焦等。建議閱讀AVFoundation官方文檔,少走彎路。
下一篇將介紹H264硬編碼的實現(xiàn),最后附上Demo地址:https://github.com/GenoChen/MediaService