初識AV Foundation-視訊、影象採集

weixin_34146805發表於2018-09-13
2718213-f0a63852ab33f110
image

最近上手一個藍芽控制手機視訊錄製的app專案,藍芽這一塊改天再做分享,今天先說說如何呼叫AV Foundation去控制視訊錄製的。AV Foundation 視訊和影象採集主要應用到以上幾個介面類。

AVCaptureDeviceInput 【音視訊輸入口】

作為視訊和音訊的輸入口,而輸入源則是鏡頭(前置和後置)、麥克風(手機自帶麥克風和耳麥),它的初始化如下:

初始化鏡頭輸入


AVCaptureDevice * device =[self getCameraDeviceWithPosition:position];

NSError *initError =nil;

_mVideoInput =[[AVCaptureDeviceInput alloc] initWithDevice:device error:&initError];

if (initError) {

NSLog(@"初始化攝像頭輸入錯誤:%@",initError.localizedDescription);

}

初始化音訊輸入


NSArray *devices =[AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];

if (!devices) {

NSLog(@"無法獲取音訊裝置");

return;

}

AVCaptureDevice *mdevice =devices.firstObject;

NSError *initError =nil;

_mAudioInput =[[AVCaptureDeviceInput alloc] initWithDevice:mdevice error:&initError];

if (initError) {

NSLog(@"初始化音訊輸入錯誤:%@",initError.localizedDescription);

}

補充說明,獲取前置或後置攝像頭相關裝置的程式碼如下:


-(AVCaptureDevice *)getCameraDeviceWithPosition:(AVCaptureDevicePosition )position{

NSArray *cameras= [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];

for (AVCaptureDevice *camera in cameras) {

     if ([camera position]==position) {

             [[camera formats] enumerateObjectsUsingBlock:^(AVCaptureDeviceFormat* obj,    NSUInteger idx, BOOL * _Nonnull stop) {

        }];

             return camera;

     }

}

return nil;

}

前置攝像頭對應的AVCaptureDevicePosition的值為AVCaptureDevicePositionFront,而後置對應的為AVCaptureDevicePositionBack。

AVCaptureVideoDataOutput【音視訊流輸出口】

該類主要作為音視訊的資料輸出口,可以對取樣得到的音訊流和視訊流進行裁剪,拼接等操作。比如app想達到暫停錄製的話,這個是一個很好地切入點。那麼他的初始化及應用程式碼如下:

初始化視訊輸出


//先初始化輸出佇列

if (!mRecordOutputQueue) {

mRecordOutputQueue =dispatch_queue_create(OutputQueueKey, NULL);

}

_mVideoOutput =[[AVCaptureVideoDataOutput alloc] init];

//設定視訊

_mVideoConnection =[_mVideoOutput connectionWithMediaType:AVMediaTypeVideo];

//設定視訊拍攝方向為正方向

_mVideoConnection.videoOrientation =AVCaptureVideoOrientationPortrait;

//設定取樣輸出回撥代理

[_mVideoOutput setSampleBufferDelegate:self queue:mRecordOutputQueue];

初始化音訊輸出


if (!mRecordOutputQueue) {

mRecordOutputQueue =dispatch_queue_create(OutputQueueKey, NULL);

}

_mAudioOutput =[[AVCaptureAudioDataOutput alloc] init];

_mAudioConnection =[_mAudioOutput connectionWithMediaType:AVMediaTypeAudio];

//設定取樣輸出回撥代理

[_mAudioOutput setSampleBufferDelegate:self queue:mRecordOutputQueue];

而取樣得到的資料將在**- (void)captureOutput:(AVCaptureOutput )captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection )connection代理函式中得以獲取。下面是一段簡單的視訊資料儲存的過程對應的程式碼:


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

BOOL isVideo =YES;

if (captureOutput!=_mVideoOutput) {

isVideo =NO;//音訊

}

//初步建立音訊

if (!mVideoEncodeOp&&!isVideo) {

//獲取音訊相關資訊

CMFormatDescriptionRef fmt = CMSampleBufferGetFormatDescription(sampleBuffer);

const AudioStreamBasicDescription *asbd = CMAudioFormatDescriptionGetStreamBasicDescription(fmt);

audio_sample_rate = asbd->mSampleRate;

audio_channel = asbd->mChannelsPerFrame;

//建立檔案路徑

NSString *fileFullPath =[[mFileManger getVideoCachePath] stringByAppendingString:[mFileManger createDefaultVideoFileName:@"mp4"]];

//建立編碼器

mVideoEncodeOp =[[VideoEncodeOperation alloc] initWithPath:fileFullPath audioChannel:audio_channel audioSampleRate:audio_sample_rate videoDimensionWidth:video_dimension_width videoHeight:video_dimension_height];

}

// 計算當前視訊流的時長

CMTime dur = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); 

if (startTime.value == 0) {

    startTime = dur;

}

CMTime sub = CMTimeSubtract(dur, startTime);

currentRecordTime = CMTimeGetSeconds(sub);

// 進行資料編碼

[mVideoEncodeOp encodeFrame:sampleBuffer isVideo:isVideo];

//錄製程式時間回撥

if ([self.delegate respondsToSelector:@selector(recordProgress:)]) {

      dispatch_async(dispatch_get_main_queue(), ^{

           [self.delegate recordProgress:currentRecordTime];

     });      

}

CFRelease(sampleBuffer);//釋放記憶體

}

除了使用AVCaptureVideoDataOutput取輸出資料流以外,還可以直接使用AVCaptureMovieFileOutput直接輸出到檔案,但很明顯的是,你無法使用AVCaptureMovieFileOutput對資料流進行諸如裁剪,拼接等操作。根據需求不同可以自主選擇合適的輸出方式。

AVCaptureStillImageOutput【靜態影象輸出】

如果專案需要進行拍攝等操作的話,就要考慮如何接入AVCaptureStillImageOutput來進行操作了。靜態影象捕捉過程可以沿用當前攝像頭的解析度,也可以採用最高質量的解析度,具體過程請參考AVCaptureStillImageOutput的highResolutionStillImageOutputEnabled屬性。以下是初始化和使用的程式碼過程:

初始化


_mStillImageOuput =[[AVCaptureStillImageOutput alloc] init];

NSDictionary *outputSettings = [[NSDictionary alloc] initWithObjectsAndKeys:AVVideoCodecJPEG, AVVideoCodecKey, nil];

_mStillImageOuput.outputSettings =outputSettings;

拍照


for (AVCaptureConnection *connection in _mStillImageOuput.connections) {

      for (AVCaptureInputPort *port in [connection inputPorts]) {

             if ([[port mediaType] isEqual:AVMediaTypeVideo]) {

                   _mStillImageConnection = connection;

                  break;

             }

       }

       if (_mStillImageConnection) {

             break;

       }

}

if (!_mStillImageConnection) {

       NSLog(@"無法獲取影象輸出connection");

       return;

}

//設定高質量的取樣影象解析度

if (_mStillImageOuput.isHighResolutionStillImageOutputEnabled) {

      _mStillImageOuput.highResolutionStillImageOutputEnabled =YES;

}

[_mStillImageOuput captureStillImageAsynchronouslyFromConnection:_mStillImageConnection completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) {

if (imageDataSampleBuffer == NULL) {

      return;

}

NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];

UIImage *image0 = [UIImage imageWithData:imageData];

}];

AVCaptureConnection【輸入輸出的橋接】

輸入、輸出都已經設定好了,但尚未不清楚他們之間的聯絡,所以我們得給顯式或隱式給他構造一個橋接。程式碼如下:


//視訊輸出與視訊輸入之間的連線[顯式構造]

_mVideoConnection =[_mVideoOutput connectionWithMediaType:AVMediaTypeVideo];

//音訊輸出和音訊輸入之間的連線[顯式構造]

_mAudioConnection =[_mAudioOutput connectionWithMediaType:AVMediaTypeAudio];

//靜態捕捉影象的連線[隱實構造]

for (AVCaptureConnection *connection in _mStillImageOuput.connections) {

          for (AVCaptureInputPort *port in [connection inputPorts]) {

               if ([[port mediaType] isEqual:AVMediaTypeVideo]) {

                       _mStillImageConnection = connection;

                       break;

              }

          }

}

AVCaptureSession【音視訊捕捉總控制】

無論是輸入輸出,都需要受到管理排程,而AVCaptureSession就很好扮演了該角色。你只需要將AVCaptureDeviceInput、AVCaptureVideoDataOutput、AVCaptureAudioDataOutput、AVCaptureStillImageOutput等往裡放,即可將從捕捉到輸出的過程疏通,AVCaptureSession僅僅負責開始捕捉和停止捕捉。執行程式碼如下:


_mRecorderSession =[[AVCaptureSession alloc] init];

if ([_mRecorderSession canAddInput:_mVideoInput]) {

       [_mRecorderSession addInput:_mVideoInput];

}

if ([_mRecorderSession canAddInput:_mAudioInput]) {

       [_mRecorderSession addInput:_mAudioInput];

}

if ([_mRecorderSession canAddOutput:_mVideoOutput]) {

       [_mRecorderSession addOutput:_mVideoOutput];

}

if ([_mRecorderSession canAddOutput:_mAudioOutput]) {

      [_mRecorderSession addOutput:_mAudioOutput];

}

if ([_mRecorderSession canAddOutput:_mStillImageOuput]) {

      [_mRecorderSession addOutput:_mStillImageOuput];

}

//開始捕捉

[_mRecorderSession startRunning];

//結束捕捉

[_mRecorderSession stopRunning];

到此你就可以進行錄製視訊、拍照等操作了。但還有一個問題,如何顯示實時鏡頭畫面?這個就更簡單了,看下面。

AVCaptureVideoPreviewLayer【實時捕捉畫面】

很顯然這是一個layer,你需要做的是初始化AVCaptureVideoPreviewLayer,然後將其新增到某個view的layer上,即可看到當前的畫面了,程式碼如下:


_mRecoderPreviewLayer =[[AVCaptureVideoPreviewLayer alloc] initWithSession:_mRecorderSession];

//設定比例為鋪滿全屏

_mRecoderPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;

CGRect rect =self.view.frame;

_mRecoderPreviewLayer.frame =CGRectMake(0, 0, rect.size.width, rect.size.height);

[self.view.layer insertSublayer:_mRecoderPreviewLayer atIndex:0];

至此,簡單的捕捉過程已經實現。但這都不是重點,重點還在下面:

不知道你常使用iPhone裝置內建相機是否考慮裡面的鏡頭拉近拉遠、慢動作錄製等等功能是怎麼實現的,又或者專業的錄影app FiLMic Pro是如何實現調整錄製的解析度和取樣幀率的?當然上面所介紹的內容是無法解決的,你要是急著去翻文件的話,那先看看下面這篇介紹吧!

New AV Foundation Camera Features for the iPhone 6 and iPhone 6 Plus

或者看看翻譯

iPhone 6和iPhone 6 plus的AV Foundation框架特性

不知道你是否注意到文中常提及的AVCaptureDeviceFormat?這就是今天基本的重點了。。。

首先說說很早之前蘋果在鏡頭使用過程中常用到的一個方法:封裝好幾個合適的Preset,然後讓你去挑。當然這種方法至今還是保留著的,它的好處在於簡便高效。鏡頭本身有很多可調節的相關引數,諸如解析度、取樣幀率、曝光率、白平衡等等。假設每次使用鏡頭都需要你設定各種引數,而你作為普通的開發者,只是希望能夠簡單的呼叫一下鏡頭,錄製和拍照,並不對影象質量有太多額外追求,那麼設定各種鏡頭引數過程對來說是相當崩潰的,那麼蘋果採用一種策略,提供幾種可行的引數設定方案,並將其封裝起來,你只需要使用這些引數方案即可將鏡頭自動設定到相應的引數,這工作量至少成倍減少。那麼它的peset有幾種呢?看看蘋果官網提供的:Video Input Preset。很顯然預設情況下蘋果給AVCaptureSession設定的preset為AVCaptureSessionPresetHigh,而AVCaptureSessionPresetHigh下的iPhone5s後置攝像頭對應的取樣視訊解析度為1080p,既不是最高,也不是最低,而取樣頻率則達到了最高的30FPS,要注意的是不同裝置不同解析度的最高取樣幀率是不同的。而採用preset的程式碼如下:


[_mRecorderSession beginConfiguration];

if (![_mRecorderSession canSetSessionPreset:AVCaptureSessionPresetHigh]) {

[_mRecorderSession commitConfiguration];

return NO;

}

_mRecorderSession.sessionPreset =AVCaptureSessionPresetHigh;

[_mRecorderSession commitConfiguration];

那麼現在我們想自己手動調節相應的引數,該如何去設定?此時AVCaptureDeviceFormat就派上用場了。

設定解析度

New AV Foundation Camera Features for the iPhone 6 and iPhone 6 Plus文中可以看出iPhone6鏡頭解析度能達到4k,也就是3264X2448。那麼如何獲取當前裝置支援的所有的解析度?程式碼如下:

AVCaptureDevice * device =[self getCameraDeviceWithPosition:position];//獲取鏡頭裝置

_mVideoDeviceFormats =[NSArray arrayWithArray:device.formats];//獲取所有裝置格式

2718213-8321847517823182
image

那麼如何找到你想要的解析度對應的AVCaptureDeviceFormat?遍歷formats,然後解釋解析度,程式碼如下:

CMVideoDimensions dims = CMVideoFormatDescriptionGetDimensions(format.formatDescription);

然後比對dims.width或dims.height即可知道那個AVCaptureDeviceFormat是你想要的了,下面下網上一個朋友寫的format資訊列印的函式,看程式碼:


+ (void) dumpCaptureDeviceFormat:(AVCaptureDeviceFormat*)format

{

#if DEBUG

CMVideoDimensions dims = CMVideoFormatDescriptionGetDimensions(format.formatDescription);

FourCharCode fourCC = CMFormatDescriptionGetMediaSubType(format.formatDescription);

unichar c[4];

c[0] = (fourCC >> 24) & 0xFF;

c[1] = (fourCC >> 16) & 0xFF;

c[2] = (fourCC >> 8) & 0xFF;

c[3] = (fourCC >> 0) & 0xFF;

NSString* fourCCStr = [NSString stringWithCharacters:c length:4];

NSLog(@"解析度(%d x %d) %@", dims.width, dims.height, fourCCStr);

NSLog(@"曝光範圍: %f - %f", CMTimeGetSeconds(format.minExposureDuration), CMTimeGetSeconds(format.maxExposureDuration));

NSLog(@"ISO範圍: %f - %f", format.minISO, format.maxISO);

NSString* ratesStr = @"";

NSArray* supportedFrameRateRanges = [format videoSupportedFrameRateRanges];

int count = 0;

for (AVFrameRateRange* range in supportedFrameRateRanges)

{

if (count > 0)

{

ratesStr = [ratesStr stringByAppendingString:@", "];

}

NSString* rate = [NSString stringWithFormat:@"%f - %f", range.minFrameRate, range.maxFrameRate];

ratesStr = [ratesStr stringByAppendingString:rate];

count++;

}

NSLog(@"取樣幀率範圍: %@", ratesStr);

#endif

}

具體列印出來的資訊如下:

2718213-5a7f15da35daaf05
image

拿到想要的format之後,你可以將該format直接傳給鏡頭device的activeFormat,即可設定當前鏡頭的指定取樣解析度了,程式碼如下:


if ([_mVideoInput.device lockForConfiguration:NULL]) {

[_mVideoInput.device setActiveFormat:format];

[_mVideoInput.device unlockForConfiguration];

}

[_mRecorderSession commitConfiguration];

設定取樣幀率

翻看api文件,沒有發現直接設定鏡頭的取樣頻率,但發現activeVideoMinFrameDuration,activeVideoMaxFrameDuration,然後文件說可以這麼處理來設定取樣的幀率:


-(void)setCaptureVideFrameRate:(NSInteger)rate{

AVCaptureDeviceFormat *format =_mVideoInput.device.activeFormat;

if (!format) {

return ;

}

if ([_mVideoInput.device lockForConfiguration:NULL]) {

        //獲取format允許的最大采樣幀率

       float maxrate=((AVFrameRateRange*)[format.videoSupportedFrameRateRanges objectAtIndex:0]).maxFrameRate;

      //將最小,最大的取樣幀率設為相同的值,即可達到指定取樣幀率的效果

     if (rate<=maxrate) {

           [_mVideoInput.device setActiveVideoMinFrameDuration:CMTimeMake(10, 10*rate)];

           [_mVideoInput.device setActiveVideoMaxFrameDuration:CMTimeMake(10, 10*rate)];

     }

      [_mVideoInput.device unlockForConfiguration];

}

}

設定鏡頭伸縮

網上有文章說通過設定AVCaptureConnection的videoScaleAndCropFactor來達到鏡頭伸縮效果,但我嘗試過,無法成功,後來還是翻了文件找到了,使用到了AVCaptureDevice的rampToVideoZoomFactor函式來實現,程式碼如下:


-(BOOL)captureZoomIn_Out:(CGFloat)zoomRate velocity:(CGFloat)velocity{

[_mVideoInput.device lockForConfiguration:nil];

AVCaptureDeviceFormat *format =_mVideoInput.device.activeFormat;

//最大的放大因子,最小為1.0

CGFloat maxZoom =format.videoMaxZoomFactor;

CGFloat curZoom =_mVideoInput.device.videoZoomFactor;

curZoom +=(maxZoom-1.0)*zoomRate;

if (curZoom<1.0) {

curZoom =1.0;

}

if (curZoom>maxZoom) {

curZoom =maxZoom;

}

[_mVideoInput.device rampToVideoZoomFactor:curZoom withRate:velocity];

[_mVideoInput.device unlockForConfiguration];

return YES;

}

至此文章基本介紹完畢,希望對你有幫助,程式碼暫時無法網上公開,需要程式碼的朋友可以發郵件到我郵箱1475134837@qq.com.有什麼問題也可留言提問,工作繁忙,繁文絮節就多說了。

相關文章