iOS視頻開發(fā)(一):視頻采集

前言

系列文章:
《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管理多個輸入輸出

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ù)覽了。


AVCaptureConnection表示輸入和輸出之間的連接

視頻采集的步驟

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容