AVFoundation音視頻采集(三)

前言

從本文開始逐漸學(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)流程

image.png

上圖介紹了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中

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市险耀,隨后出現(xiàn)的幾起案子弄喘,更是在濱河造成了極大的恐慌,老刑警劉巖甩牺,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蘑志,死亡現(xiàn)場離奇詭異,居然都是意外死亡贬派,警方通過查閱死者的電腦和手機急但,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來赠群,“玉大人羊始,你說我怎么就攤上這事〔槊瑁” “怎么了突委?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長冬三。 經(jīng)常有香客問我匀油,道長,這世上最難降的妖魔是什么勾笆? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任敌蚜,我火速辦了婚禮,結(jié)果婚禮上窝爪,老公的妹妹穿的比我還像新娘弛车。我一直安慰自己齐媒,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布纷跛。 她就那樣靜靜地躺著喻括,像睡著了一般。 火紅的嫁衣襯著肌膚如雪贫奠。 梳的紋絲不亂的頭發(fā)上唬血,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天,我揣著相機與錄音唤崭,去河邊找鬼拷恨。 笑死,一個胖子當(dāng)著我的面吹牛谢肾,可吹牛的內(nèi)容都是我干的腕侄。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼勒叠,長吁一口氣:“原來是場噩夢啊……” “哼兜挨!你這毒婦竟也來了膏孟?” 一聲冷哼從身側(cè)響起眯分,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎柒桑,沒想到半個月后弊决,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡魁淳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年飘诗,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片界逛。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡昆稿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出息拜,到底是詐尸還是另有隱情溉潭,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布少欺,位于F島的核電站喳瓣,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏赞别。R本人自食惡果不足惜畏陕,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望仿滔。 院中可真熱鬧惠毁,春花似錦犹芹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至洞豁,卻和暖如春盐固,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背丈挟。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工刁卜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人曙咽。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓蛔趴,卻偏偏與公主長得像,于是被迫代替她去往敵國和親例朱。 傳聞我的和親對象是個殘疾皇子孝情,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,914評論 2 355