前言
從本文開始逐漸學(xué)習(xí)iOS自帶的多媒體處理框架,例如AVFoundation贺氓,VideoToolbox湿颅,CoreMedia载绿,CoreVideo實現(xiàn)多媒體的處理,并且將實現(xiàn)方式以及效果和ffmpeg的方式做對比
APP會有這樣的需求油航,錄制一段音頻或者一段視頻卢鹦,或者拍攝一張照片等等,AVFoundation提供了為我們提供了實現(xiàn)這些需求的接口劝堪。通過這些接口我們可以從設(shè)備獲取指定格式的未壓縮的音視頻數(shù)據(jù)冀自,然后又可以壓縮之后保存到文件里面存儲在本地或者在網(wǎng)絡(luò)上傳輸
本文的目的:
1、熟悉AVFoundation中關(guān)于音頻采集視頻采集接口的使用
2秒啦、將采集到的音頻或者視頻壓縮后保存在MP4文件中
采集相關(guān)流程
上圖介紹了AVFoundation框架中關(guān)于采集相關(guān)的對象關(guān)系圖熬粗,如下為具體對象的解釋
要開啟音視頻采集,必須在項目配置文件Info.plist中添加NSMicrophoneUsageDescription音頻使用權(quán)限和NSCameraUsageDescription相機使用權(quán)限
相關(guān)對象及函數(shù)介紹
- 1余境、AVCaptureSession
采集會話對象驻呐,用于管理采集的開始灌诅,結(jié)束等等操作,它一端連接著麥克風(fēng)和攝像頭等等輸入設(shè)備對象接受他們提供的音視頻數(shù)據(jù)含末,另一端連接著音頻輸出對象和視頻輸出對象通過他們向外界提供指定格式的音視頻數(shù)據(jù)
2猜拾、AVCaptureDevice
代表著具體的采集音頻或者視頻的物理對象,例如麥克風(fēng)佣盒,前后置攝像頭等等3挎袜、AVCaptureDeviceInput
采集輸入對象,它是AVCaptureInput的實現(xiàn)子類肥惭,該對象被連接到AVCaptureSession之后就可以對其提供音頻或者視頻數(shù)據(jù)了盯仪,通過如下方法添加采集輸入對象
-(void)addInput:(AVCaptureInput *)input;4、AVCaptureVideoDataOutput
視頻輸出對象蜜葱,它被添加到AVCaptureSession之后就可以向外界提供采集好的視頻數(shù)據(jù)了全景,同時通過該對象設(shè)置采集到的視頻數(shù)據(jù)的格式(包括像素的格式,比如RGB的還是YUV的等等)5牵囤、AVCaptureAudioDataOutput
音頻輸出對象爸黄,它被添加到AVCaptureSession之后就可以向外界提供采集好的音頻數(shù)據(jù)了,該對象對于音頻數(shù)據(jù)的格式設(shè)置比較少揭鳞,如果想要更加詳細(xì)的音頻格式采集可以采用AudioUnit框架進(jìn)行 參考我前面寫的文章 AudioUnit錄制音頻+耳返(四)6馆纳、AVCaptureStillImageOutput
原始照片輸出對象,同樣它也需要通過被添加到AVCaptureSession之后向外界提供采集好的原始照片
AVCaptureVideoDataOutput汹桦、AVCaptureAudioDataOutput鲁驶、AVCaptureStillImageOutput三個對象都是AVCaptureOutput的具體實現(xiàn)子類,
通過-(void)addOutput:(AVCaptureOutput *)output方法被添加AVCaptureSession中去舞骆,然后由他們向外界提供數(shù)據(jù)
- 7钥弯、CMSampleBufferRef
此對象代表了采集到的音視頻數(shù)據(jù)的一個結(jié)構(gòu)體,它包含了音頻或者視頻相關(guān)的參數(shù)督禽,這些參數(shù)包括音頻參數(shù)(編碼方式脆霎,采樣率,采樣格式狈惫,聲道類型睛蛛,聲道數(shù)等等),視頻參數(shù)(編碼方式胧谈,寬高忆肾,顏色標(biāo)準(zhǔn)bt601/bt709,像素格式y(tǒng)uv還是RGB)
8 、- (void)startRunning;和-(void)stopRunning;
分別對應(yīng)著采集開始和結(jié)束采集菱肖,他們一般都成對調(diào)用客冈。備注:startRunning方法可能會花費1秒左右時間,它會阻塞當(dāng)前線程稳强,所以使用是要注意不能阻塞主線程
實現(xiàn)代碼
主要實現(xiàn)的功能就是采集音頻和1280x720的視頻场仲,視頻采用h264方式編碼和悦,音頻采用aac方式編碼,最后保存到mp4中(當(dāng)然也可以保存到mov中)
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#import <CoreAudio/CoreAudioTypes.h>
@interface AVCapture : NSObject
// 錄制一段時間的音視頻然后保存到文件中渠缕,蘋果只支持MOV mp4 m4v等少數(shù)格式
- (void)startCaptureToURL:(NSURL*)dstURL duration:(float)time fileType:(AVFileType)fileType;
@end
#import "AVCapture.h"
/** 要開啟音視頻采集鸽素,必須在項目配置文件Info.plist中添加NSMicrophoneUsageDescription音頻使用權(quán)限
* 和NSCameraUsageDescription相機使用權(quán)限
*/
@interface AVCapture()<AVCaptureVideoDataOutputSampleBufferDelegate,AVCaptureAudioDataOutputSampleBufferDelegate>
{
// 采集相關(guān)對象
AVCaptureSession *session;
AVCaptureVideoDataOutput *videoOutput;
AVCaptureAudioDataOutput *audioOutput;
// 封裝相關(guān)對象
AVAssetWriter *writer;
AVAssetWriterInput *writerAudioInput;
AVAssetWriterInput *WriterVideoInput;
dispatch_queue_t acaptureQueue;
dispatch_queue_t vcaptureQueue;
dispatch_semaphore_t samaphore;
BOOL firstsample;
BOOL isWrite;
AVFileType filetype;
}
@end
@implementation AVCapture
- (void)startCaptureToURL:(NSURL*)dstURL duration:(float)time fileType:(AVFileType)type
{
acaptureQueue = dispatch_queue_create("acaptureQueue.com", DISPATCH_QUEUE_SERIAL);
vcaptureQueue = dispatch_queue_create("vcaptureQueue.com", DISPATCH_QUEUE_SERIAL);
samaphore = dispatch_semaphore_create(0);
firstsample = YES;
isWrite = YES;
filetype = type;
// 創(chuàng)建采集會話
if (![self createCaptureSession]) {
return;
}
// 開啟音視頻封裝器的工作
[self startAssetWriter:dstURL];
// 備注,此方法會阻塞當(dāng)前線程亦鳞,所以最好放在子線程中調(diào)用馍忽,不要阻塞組現(xiàn)場
[session startRunning];
// time 秒后停止錄制
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, time*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self->writerAudioInput markAsFinished];
[self->WriterVideoInput markAsFinished];
[self->writer finishWritingWithCompletionHandler:^{
}];
self->isWrite = NO;
});
dispatch_semaphore_wait(samaphore, DISPATCH_TIME_FOREVER);
}
- (BOOL)createCaptureSession
{
/** AVCaptureSession 采集會話對象,它一頭連接著輸入對象(比如麥克風(fēng)采集音頻蚜迅,攝像頭采集視頻)
* 一頭連接著輸出對象向app提供采集好的原始音視頻數(shù)據(jù)
* 通過它管理采集的開始與結(jié)束
*/
session = [[AVCaptureSession alloc] init];
// 設(shè)置視頻采集的寬高參數(shù)
[session setSessionPreset:AVCaptureSessionPreset1280x720];
// 實際的音頻采集物理設(shè)備對象舵匾,通過此對象來創(chuàng)建音頻輸入對象俊抵;備注postion一定要是AVCaptureDevicePositionUnspecified
// 否則會返回nil
AVCaptureDevice *micro = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInMicrophone mediaType:AVMediaTypeAudio position:AVCaptureDevicePositionUnspecified];
// 通過音頻物理設(shè)備對象來創(chuàng)建音頻輸入對象,如果前面micro為nil這里也不會崩潰谁不,audioInput會返回nil
AVCaptureDeviceInput *audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:micro error:nil];
if (![session canAddInput:audioInput]) { // audioInput為nil這里也不會崩潰,會放回NO
NSLog(@"can not add audioInput");
return NO;
}
[session addInput:audioInput];
// 實際的視頻采集物理設(shè)備對象,這里選擇后置攝像頭
AVCaptureDevice *camera = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionBack];
// 通過視頻物理設(shè)備對象創(chuàng)建視頻輸入對象
AVCaptureDeviceInput *videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:camera error:nil];
if (![session canAddInput:videoInput]) {
NSLog(@"can not add video input");
return NO;
}
[session addInput:videoInput];
// 如果調(diào)用了[session startRunning]之后要想改變音視頻輸出對象配置參數(shù)徽诲,則必須調(diào)用[session beginConfiguration];和
// [session commitConfiguration];才能生效刹帕。如果沒有調(diào)用[session startRunning]則這兩句代碼可以不寫
[session beginConfiguration];
// AVCaptureVideoDataOutput 創(chuàng)建視頻輸出對象,對象用于向外部輸出視頻數(shù)據(jù)谎替,通過該對象設(shè)置向外部輸入的視頻數(shù)據(jù)格式
// 比如像素格式(iOS只支持kCVPixelFormatType_32BGRA/kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
// kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange三種格式)
videoOutput = [[AVCaptureVideoDataOutput alloc] init];
NSDictionary *videoSettings = @{
(id)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)
};
[videoOutput setVideoSettings:videoSettings];
// 當(dāng)采集速度過快而處理速度跟不上時的丟棄策略偷溺,默認(rèn)丟棄最新采集的視頻。這里設(shè)置為NO钱贯,表示不丟棄緩存起來
videoOutput.alwaysDiscardsLateVideoFrames = NO;
[videoOutput setSampleBufferDelegate:self queue:vcaptureQueue];
[session addOutput:videoOutput];
// AVCaptureAudioDataOutput 創(chuàng)建音頻輸出對象
audioOutput = [[AVCaptureAudioDataOutput alloc] init];
[audioOutput setSampleBufferDelegate:self queue:acaptureQueue];
[session addOutput:audioOutput];
[session commitConfiguration];
return YES;
}
- (void)startAssetWriter:(NSURL*)dstURL
{
NSError *error = nil;
// AVAssetWriter 音視頻寫入對象管理器挫掏,通過該對象來控制寫入的開始和結(jié)束以及管理音視頻輸入對象
/** AVAssetWriter對象
* 1、封裝器對象秩命,用于將音視頻數(shù)據(jù)(壓縮或未壓縮數(shù)據(jù))寫入文件
* 2尉共、可以單獨寫入音頻或視頻,如果寫入未壓縮的音視頻數(shù)據(jù)弃锐,AVAssetWriter內(nèi)部會自動調(diào)用編碼器進(jìn)行編碼
*/
/** 遇到問題:調(diào)用startWriting提示Error Domain=AVFoundationErrorDomain Code=-11823 "Cannot Save"
* 分析原因:如果封裝器對應(yīng)的文件已經(jīng)存在袄友,調(diào)用此方法時會提示這樣的錯誤
* 解決方案:調(diào)用此方法之前先刪除已經(jīng)存在的文件
*/
unlink([dstURL.path UTF8String]);
writer = [AVAssetWriter assetWriterWithURL:dstURL fileType:AVFileTypeMPEG4 error:&error];
if (error) {
NSLog(@"create writer failer");
return;
}
// 往封裝器中添加音視頻輸入對象,每添加一個輸入對象代表要往容器中添加一路流霹菊,一般添加一路視頻流
/** AVAssetWriterInput 對象
* 用于將數(shù)據(jù)寫入容器剧蚣,可以寫入壓縮數(shù)據(jù)也可以寫入未壓縮數(shù)據(jù),如果outputSettings為nil則代表對數(shù)據(jù)不做壓縮處理直接寫入容器旋廷,
* 不為nil則代表對對數(shù)據(jù)按照指定格式壓縮后寫入容器
*
*/
// 方式一鸠按、手動創(chuàng)建參數(shù),不推薦饶碘。設(shè)置不對容易出錯待诅,其它參數(shù)采用默認(rèn)值
// NSDictionary *videoSettings = @{
// AVVideoCodecKey:AVVideoCodecH264,
// AVVideoWidthKey:@(640),
// AVVideoHeightKey:@(480)
// };
// 方式二、由蘋果系統(tǒng)返回
NSDictionary *videoSettings = [videoOutput recommendedVideoSettingsForAssetWriterWithOutputFileType:filetype];
WriterVideoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
WriterVideoInput.expectsMediaDataInRealTime = YES;
// 將寫入對象添加到封裝器中
[writer addInput:WriterVideoInput];
/** 遇到問題:調(diào)用appendSampleBuffer寫入音視頻數(shù)據(jù)時出錯
* 分析原因:剛開始設(shè)置的AVEncoderBitRateKey碼率為6400熊镣,太小卑雁,壓縮可能要比較長時間募书,導(dǎo)致時間比較久就出錯了
* 解決方案:將碼率設(shè)置為合適的值96000
*/
// 方式一、手動創(chuàng)建參數(shù)测蹲,不推薦莹捡。設(shè)置不對容易出錯,其它參數(shù)采用默認(rèn)值
// 編碼方式,采樣格式扣甲,采樣率
AudioChannelLayout layout;
bzero(&layout, sizeof(layout));
layout.mChannelLayoutTag = kAudioChannelLayoutTag_Mono;
NSDictionary *audioSettings = @{
AVFormatIDKey:@(kAudioFormatMPEG4AAC),
AVChannelLayoutKey:[NSData dataWithBytes:&layout length:sizeof(layout)],
AVNumberOfChannelsKey:@(1),
AVSampleRateKey:@(44100),
AVEncoderBitRateKey:@(96000)
};
// 方式二篮赢、由蘋果系統(tǒng)返回
// NSDictionary *audioSettings = [audioOutput recommendedAudioSettingsForAssetWriterWithOutputFileType:filetype];
writerAudioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:audioSettings];
writerAudioInput.expectsMediaDataInRealTime = YES;
[writer addInput:writerAudioInput];
BOOL reuslt = [writer startWriting];
if (!reuslt) {
NSLog(@"start writer %@",writer.error);
}
}
- (void)processSample:(CMSampleBufferRef)samplebuffer
{
CMFormatDescriptionRef sampleFormat = CMSampleBufferGetFormatDescription(samplebuffer);
CMMediaType mediatype = CMFormatDescriptionGetMediaType(sampleFormat);
CMTime pts = CMSampleBufferGetPresentationTimeStamp(samplebuffer);
if (mediatype == kCMMediaType_Video) { // 視頻
if (firstsample) {
firstsample = NO;
[writer startSessionAtSourceTime:pts];
}
if (WriterVideoInput.readyForMoreMediaData) {
BOOL reulst = [WriterVideoInput appendSampleBuffer:samplebuffer];
NSLog(@"writer video %d",reulst);
}
} else if(mediatype == kCMMediaType_Audio) { // 音頻
if (firstsample) {
firstsample = NO;
[writer startSessionAtSourceTime:pts];
}
if (writerAudioInput.readyForMoreMediaData) {
BOOL reulst = [writerAudioInput appendSampleBuffer:samplebuffer];
NSLog(@"writer audio %d",reulst);
}
} else {
NSLog(@"其他數(shù)據(jù)");
}
}
- (void)captureOutput:(AVCaptureOutput *)output didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
NSLog(@"drop sample");
}
/** 前面雖然定義了音視頻采集的工作隊列(且是一個串行隊列),但是這個代理的線程是不固定的琉挖,隨機的
*/
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
if (!isWrite) {
NSLog(@"結(jié)束錄制...");
dispatch_semaphore_signal(samaphore);
return;
}
if (output == audioOutput) {
// 獲取音頻數(shù)據(jù)的大小
size_t size = CMSampleBufferGetTotalSampleSize(sampleBuffer);
NSLog(@"audio thread %@ size %ld",[NSThread currentThread],size);
} else if(output == videoOutput){
// 獲取視頻數(shù)據(jù)的大小启泣,視頻數(shù)據(jù)大小不能通過上面CMSampleBufferGetTotalSampleSize獲取
CVImageBufferRef imagebuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
size_t size = CVPixelBufferGetDataSize(imagebuffer);
NSLog(@"video thread %@ size %ld",[NSThread currentThread],size);
} else {
NSLog(@"unknown output");
}
[self processSample:sampleBuffer];
}
@end
上述代碼運行起來還是比較流暢的,CPU消耗也不高示辈,因為AVFoundation編碼時采用的硬編碼
遇到問題
- 1寥茫、調(diào)用appendSampleBuffer寫入音視頻數(shù)據(jù)時出錯
分析原因:剛開始設(shè)置的AVEncoderBitRateKey碼率為6400,太小矾麻,壓縮可能要比較長時間纱耻,導(dǎo)致時間比較久就出錯了
解決方案:將碼率設(shè)置為合適的值96000
項目地址
https://github.com/nldzsz/ffmpeg-demo
位于AVFoundation目錄下文件AVCapture.h/AVCapture.m中