VideoToolbox硬編碼YUV為h264(九)

前言

IOS 8.0系統(tǒng)之后,蘋果提供了VideoToolbox框架,它可以將攝像頭采集的原始視頻數(shù)據(jù)編碼為指定的格式,如常見的h264/h265。攝像頭采集的原始視頻數(shù)據(jù)是很大的,以YUV顏色空間為例,1280x720p 30fps分辨率的視頻,1秒的大小 = 1280x720x1.5x30 = 41.472mbps,所以原始視頻數(shù)據(jù)不利于存儲(chǔ)和在網(wǎng)絡(luò)上進(jìn)行傳輸,一般在采集到原始視頻數(shù)據(jù)后都會(huì)進(jìn)行一次有損壓縮,然后進(jìn)行存儲(chǔ)或者傳輸。本文將記錄如何采集視頻然后編碼為h264碼流

h264碼流格式

1、H264裸碼流是由一個(gè)接一個(gè)的NALU(Nal Unit)組成的,NALU = 開始碼 + NALU類型 + 視頻數(shù)據(jù),h264裸碼流文件ffplay播放命令:

ffplay -f h264 test.h264

2、開始碼:必須是"00 00 00 01" 或"00 00 01"
3、NALU類型:

類型 說明
0 未規(guī)定
1 非IDR圖像中不采用數(shù)據(jù)劃分的片段(P幀/B幀)
2 非IDR圖像中A類數(shù)據(jù)劃分片段
3 非IDR圖像中B類數(shù)據(jù)劃分片段
4 非IDR圖像中C類數(shù)據(jù)劃分片段
5 IDR圖像的片段(I幀/Idr幀)
6 補(bǔ)充增強(qiáng)信息(SEI)
7 序列參數(shù)集(SPS)
8 圖像參數(shù)集(PPS)
9 分割符
10 序列結(jié)束符
1 1 流結(jié)束符
1 2 填充數(shù)據(jù)
1 3 序列參數(shù)集擴(kuò)展
14 帶前綴的NAL單元
15 子序列參數(shù)集
16 -18 保留
19 不采用數(shù)據(jù)劃分的輔助編碼圖像片段
20 編碼片段擴(kuò)展
21-23 保留
24-31 未規(guī)定

一般只用到1、5、7、8這4個(gè)類型,類型為5表示這是一個(gè)I幀,I幀前面必須有SPS和PPS數(shù)據(jù),也就是類型為7和8,類型為1表示這是一個(gè)P幀或B幀。

h264原始碼流一般按照如下順序:NALU(SPS)+NALU(PPS)+NALU(Idr幀)+NALU(P幀)+NALU(P/B幀)+..+NALU(SPS)+NALU(PPS)+NALU(I幀)+.....

tips:
h264編碼只支持yuv顏色空間;YUV顏色空間與RGB顏色空間表示視頻的區(qū)別就是,同等分辨率前者占用空間少一半。

視頻采集相關(guān)代碼

蘋果官方文檔-AVFoundation

視頻采集使用AVFoundation框架完成,如下圖所示


captureDetail_2x.png

有如下幾個(gè)很重要的對(duì)象
1、AVCaptureSession:
管理視頻輸入輸出的會(huì)話(輸入:攝像頭;輸出:輸送數(shù)據(jù)給app端)
2、AVCaptureDevice:
代表了一個(gè)具體的物理設(shè)備,比如攝像頭(前置/后置),揚(yáng)聲器等等;備注:模擬器無法運(yùn)行攝像頭相關(guān)代碼
3、AVCaptureDeviceInput:
代表具體的視頻輸入,它要由具體的物理設(shè)備創(chuàng)建
4、AVCaptureVideoDataOutput:
它是AVCaptureOutput(它是一個(gè)抽象類)的子類,用于輸出原始視頻數(shù)據(jù)
5、AVCaptureConnection:
代表了AVCaptureInputPort和AVCaptureOutput、AVCaptureVideoPreviewLayer之間的連接通道,通過它可以將視頻數(shù)據(jù)輸送給AVCaptureVideoPreviewLayer進(jìn)行顯示,設(shè)置輸出視頻的輸出視頻的方向,鏡像等等。
6、AVCaptureVideoPreviewLayer:
是一個(gè)可以顯示攝像頭內(nèi)容的CAlayer的子類

具體采集相關(guān)代碼如下:
1、初始化AVCaptureSession

self.mCaptureSession = [[AVCaptureSession alloc] init];
self.mCaptureSession.sessionPreset = AVCaptureSessionPreset640x480;   // 配置輸出圖像的分辨率
_width = 640;
_height = 480;

sessionPreset屬性用來配置最終輸出的原始視頻的分辨率
2、創(chuàng)建視頻輸入對(duì)象,并添加到AVCaptureSession中

AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionFront];
// 根據(jù)物理設(shè)備創(chuàng)建輸入對(duì)象
self.mCaptureInput = [[AVCaptureDeviceInput alloc] initWithDevice:videoDevice error:nil];
if ([self.mCaptureSession canAddInput:self.mCaptureInput]) {
    [self.mCaptureSession addInput:self.mCaptureInput];
}

AVCaptureDevicePositionFront代表前置攝像頭
3、創(chuàng)建視頻輸出對(duì)象,設(shè)置輸出代理,并添加到AVCaptureSession中

self.mVideoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
// 當(dāng)回調(diào)因?yàn)楹臅r(shí)操作還在進(jìn)行時(shí),系統(tǒng)對(duì)新的一幀圖像的處理方式,如果設(shè)置為YES,則立馬丟棄該幀。
// NO,則緩存起來(如果累積的幀過多,緩存的內(nèi)存將持續(xù)增長);該值默認(rèn)為YES
self.mVideoDataOutput.alwaysDiscardsLateVideoFrames = NO;
/** 設(shè)置采集的視頻數(shù)據(jù)幀的格式。這里代表生成的圖像數(shù)據(jù)為YUV數(shù)據(jù),顏色范圍是full-range的
 *  并且是bi-planner存儲(chǔ)方式(也就是Y數(shù)據(jù)占用一個(gè)內(nèi)存塊;UV數(shù)據(jù)占用另外一個(gè)內(nèi)存塊)
 */
[self.mVideoDataOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
[self.mVideoDataOutput setSampleBufferDelegate:self queue:captureQueue];
if ([self.mCaptureSession canAddOutput:self.mVideoDataOutput]) {
    [self.mCaptureSession addOutput:self.mVideoDataOutput];
}

由于使用h264方式編碼,所以這里必須設(shè)置為yuv顏色空間
4、配置采集的視頻數(shù)據(jù)通過AVCaptureVideoPreviewLayer渲染出來(非必須)

AVCaptureConnection *connection = [self.mVideoDataOutput connectionWithMediaType:AVMediaTypeVideo];
[connection setVideoOrientation:AVCaptureVideoOrientationPortrait];

/** AVCaptureVideoPreviewLayer是一個(gè)可以顯示攝像頭內(nèi)容的CAlayer的子類
 *  以下代碼直接將攝像頭的內(nèi)容渲染到AVCaptureVideoPreviewLayer上面
 */
self.mVideoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.mCaptureSession];
[self.mVideoPreviewLayer setVideoGravity:AVLayerVideoGravityResizeAspect];
[self.mVideoPreviewLayer setFrame:self.view.bounds];
[self.view.layer addSublayer:self.mVideoPreviewLayer];

此步驟對(duì)于視頻采集來說也是很重要的,因?yàn)榭梢詫?shí)時(shí)看到自己想要采集的具體內(nèi)容
5、開始采集

- (void)startRunCapSession
{
    if (!self.mCaptureSession.isRunning) {
        [self.mCaptureSession startRunning];
    }
}

6、通過代理獲取到原始的視頻數(shù)據(jù)

- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
    NSLog(@"采集到的數(shù)據(jù) ==>%@",[NSThread currentThread]);
    /** CVImageBufferRef 表示原始視頻數(shù)據(jù)的對(duì)象;
     *  包含未壓縮的像素?cái)?shù)據(jù),包括圖像寬度、高度等;
     *  等同于CVPixelBufferRef
     */
    // 獲取CMSampleBufferRef中具體的視頻數(shù)據(jù)
    CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
    /** 執(zhí)行編碼
     *  參數(shù)1:已經(jīng)創(chuàng)建并且準(zhǔn)備好的VTCompressionSessionRef對(duì)象
     *  參數(shù)2:具體的視頻原始數(shù)據(jù);CVImageBufferRef類型
     *  參數(shù)3:視頻數(shù)據(jù)開始編碼的時(shí)間;CMTime類型,一般是CMTimeMake(幀序號(hào), 壓縮單位(比如1000));
     *  參數(shù)4:該幀視頻的時(shí)長,一般不需要計(jì)算(因?yàn)闆]法算),傳kCMTimeInvalid即可
     *  參數(shù)5:要編碼的視頻相關(guān)屬性;CFDictionaryRef類型
     *  參數(shù)6:傳遞給編碼輸出回調(diào)的參數(shù);void* 類型
     *  參數(shù)7:編碼結(jié)果標(biāo)記;通過回調(diào)函數(shù)獲取
     */
    // 幀序號(hào)時(shí)間,用于表示幀開始編碼的時(shí)間(備注:這個(gè)時(shí)間是相對(duì)時(shí)間,并不是真正時(shí)間)
    CMTime presentationTime = CMTimeMake(_frameId++, 1000);
    VTEncodeInfoFlags encodeflags;
    OSStatus status = VTCompressionSessionEncodeFrame(_encodeSession, imageBuffer, presentationTime, kCMTimeInvalid, NULL, NULL, &encodeflags);
    if (status != noErr) {
        NSLog(@"VTCompressionSessionEncodeFrame fail %d",status);
        
        // 釋放資源
        VTCompressionSessionInvalidate(_encodeSession);
        CFRelease(_encodeSession);
        _encodeSession = NULL;
    }
}

采集到的原始視頻數(shù)據(jù)將通過該回調(diào)函數(shù)傳回,原始視頻數(shù)據(jù)存放在CMSampleBufferRef類型對(duì)象中。

CMSampleBufferRef:
1、包含音視頻描述信息,比如包含音頻的格式描述 AudioStreamBasicStreamDescription、包含視頻的格式描述 CMVideoFormatDescriptionRef
2、包含音視頻數(shù)據(jù),可以是原始數(shù)據(jù)也可以是壓縮數(shù)據(jù);通過CMSampleBufferGetxxx()系列函數(shù)提取
CVImageBufferRef:
表示原始視頻數(shù)據(jù)的對(duì)象;包含未壓縮的像素?cái)?shù)據(jù),包括圖像寬度、高度等;
等同于CVPixelBufferRef

編碼相關(guān)代碼

在進(jìn)行編碼之前得先初始化編碼器,設(shè)置編碼參數(shù)等等準(zhǔn)備工作,具體使用流程如下:
1、初始化編碼器

OSStatus status = VTCompressionSessionCreate(NULL, _width, _height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)self, &_encodeSession);
if (status != noErr) {
    NSLog(@"VTCompressionSessionCreate fail %d",status);
    return;
}

/** 創(chuàng)建編碼器對(duì)象 VTCompressionSessionRef
VTCompressionSessionCreate(...)
* 參數(shù)1:創(chuàng)建對(duì)象內(nèi)存使用的內(nèi)存分配器,NULL代表使用默認(rèn)分配器kCFAllocatorDefault
* 參數(shù)2/3:要編碼的視頻幀的寬和高;單位像素
* 參數(shù)4:使用的編碼方式 比如H264(kCMVideoCodecType_H264)
* 參數(shù)5:設(shè)置編碼方式相關(guān)的參數(shù),比如H264編碼所需的參數(shù);CFDictionaryRef類型,NULL,則默認(rèn)值;也可以
* 通過VTSessionSetProperty()函數(shù)設(shè)置
* 參數(shù)6:設(shè)置原始視頻數(shù)據(jù)緩存的方式,CFDictionaryRef類型,NULL則代表使用默認(rèn)值
* 參數(shù)7:設(shè)置編碼數(shù)據(jù)的內(nèi)存分配器及其它保存方式,CFAllocatorRef類型,NULL則使用默認(rèn)值
* 參數(shù)8:設(shè)置編碼數(shù)據(jù)輸出回調(diào)函數(shù)
* 參數(shù)9:設(shè)置傳入給該回調(diào)函數(shù)的參數(shù);void類型
* 參數(shù)10:要?jiǎng)?chuàng)建的VTCompressionSessionRef對(duì)象
/
/
遇到問題:返回-12902錯(cuò)誤
* 分析問題:在VTErrors.h中查看錯(cuò)誤說明,意思參數(shù)錯(cuò)誤,經(jīng)檢查是_width和_height沒有指定具體的值
* 解決問題:給_width和_height賦上具體的值
*/
在創(chuàng)建編碼器時(shí)一定要指定要編碼的原始視頻的寬和高,否則會(huì)返回錯(cuò)誤。

2、設(shè)置編碼器參數(shù)
通過VTSessionSetProperty()接口設(shè)置編碼器相關(guān)參數(shù),比如編碼效率級(jí)別,GOP,平均碼率,幀率,碼率上限值等等

/** VTSessionSetProperty()函數(shù)既可以設(shè)置編碼相關(guān)屬性,又可以設(shè)置解碼相關(guān)屬性
 *  對(duì)于H264編碼來說,以下屬性是必須的
 *  1、編碼效率級(jí)別:kVTCompressionPropertyKey_ProfileLevel
 *      kVTProfileLevel_H264_Baseline_AutoLevel
 *  2、GOP(關(guān)鍵幀間隔):
 *      kVTCompressionPropertyKey_MaxKeyFrameInterval
 *  3、編碼后的幀率:
 *      kVTCompressionPropertyKey_ExpectedFrameRate;
 *      改變?cè)撝悼梢约涌煲曨l速度或者減慢視頻速度
 *  4、編碼后的平均碼率:
 *      kVTCompressionPropertyKey_AverageBitRate
 *      平均碼率決定了壓縮的程度
 *  5、編碼后的碼率上限:
 *      kVTCompressionPropertyKey_DataRateLimits
 */
// 設(shè)置實(shí)時(shí)編碼輸出(避免延遲)
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
/** 設(shè)置H264編碼的壓縮級(jí)別
 *  BP(Baseline Profile):基本畫質(zhì)。支持I/P 幀,只支持無交錯(cuò)(Progressive)和CAVLC;主要應(yīng)用:可視電話,會(huì)議
 *  電視,和無線通訊等實(shí)時(shí)視頻通訊領(lǐng)域
 *  EP(Extended profile):進(jìn)階畫質(zhì)。支持I/P/B/SP/SI 幀,只支持無交錯(cuò)(Progressive)和CAVLC;
 *  MP(Main profile):主流畫質(zhì)。提供I/P/B 幀,支持無交錯(cuò)(Progressive)和交錯(cuò)(Interlaced),也支持CAVLC 和CABAC 的支持;主要應(yīng)用:數(shù)字廣播電視和數(shù)字視頻存儲(chǔ)
 *  HP(High profile):高級(jí)畫質(zhì)。在main Profile 的基礎(chǔ)上增加了8×8內(nèi)部預(yù)測(cè)、自定義量化、 無損視頻編碼和更多的YUV 格式;
 *  應(yīng)用于廣電和存儲(chǔ)領(lǐng)域
 *  iPhone上方案如下:
 *  實(shí)時(shí)直播:
 *      低清Baseline Level 1.3
 *      標(biāo)清Baseline Level 3
 *      半高清Baseline Level 3.1
 *      全高清Baseline Level 4.1
 *  存儲(chǔ)媒體:
 *  低清 Main Level 1.3
 *  標(biāo)清 Main Level 3
 *  半高清 Main Level 3.1
 *  全高清 Main Level 4.1
 *  高清存儲(chǔ):
 *  半高清 High Level 3.1
 *  全高清 High Level 4.1
 *
 *  參考文章:https://blog.csdn.net/sphone89/article/details/17492433
 */
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
// 設(shè)置是否開啟B幀編碼;默認(rèn)開啟,注意只有EP,MP,HP級(jí)別才支持B幀,如果是BP級(jí)別,該設(shè)置無效。
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanTrue);
/** 設(shè)置關(guān)鍵幀GOP間隔
 *  1、碼率不變的前提下,GOP值越大P、B幀的數(shù)量會(huì)越多,平均每個(gè)I、P、B幀所占用的字節(jié)數(shù)就越多,也就更容易獲取較好的圖像質(zhì)量;B幀的數(shù)量越多,同
 *  理也更容易獲得較好的圖像質(zhì)量;
 *  2、需要說明的是,通過提高GOP值來提高圖像質(zhì)量是有限度的,在遇到場(chǎng)景切換的情況時(shí),H.264編碼器會(huì)自動(dòng)強(qiáng)制插入一個(gè)I幀,此時(shí)實(shí)際的GOP值被縮短了。
 *  另一方面,在一個(gè)GOP中,P、B幀是由I幀預(yù)測(cè)得到的,當(dāng)I幀的圖像質(zhì)量比較差時(shí),會(huì)影響到一個(gè)GOP中后續(xù)P、B幀的圖像質(zhì)量,直到下一個(gè)GOP開始才有
 *  可能得以恢復(fù),所以GOP值也不宜設(shè)置過大。
 *  3、同時(shí),由于P、B幀的復(fù)雜度大于I幀,所以過多的P、B幀會(huì)影響編碼效率,使編碼效率降低。另外,過長的GOP還會(huì)影響Seek操作的響應(yīng)速度,由于P、B幀
 *  是由前面的I或P幀預(yù)測(cè)得到的,所以Seek操作需要直接定位,解碼某一個(gè)P或B幀時(shí),需要先解碼得到本GOP內(nèi)的I幀及之前的N個(gè)預(yù)測(cè)幀才可以,GOP值越長
 *  需要解碼的預(yù)測(cè)幀就越多,seek響應(yīng)的時(shí)間也越長。
 */
int iFrameInternal = 10;
CFNumberRef iFrameRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &iFrameInternal);
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, iFrameRef);
// 設(shè)置期望幀率
int fps = 25;
CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);

/** 設(shè)置均值碼率,單位是bps,它不是一個(gè)硬性指標(biāo),實(shí)際的碼率可能會(huì)上下浮動(dòng);VideoToolBox框架只支持ABR模式,而對(duì)于H264來說,它有四種
 *  碼率控制模式,如下:
 *  CBR:恒定比特率方式進(jìn)行編碼,Motion發(fā)生時(shí),由于碼率恒定,只能通過增大QP來減少碼字大小,圖像質(zhì)量變差,當(dāng)場(chǎng)景靜止時(shí),圖像質(zhì)量又變好
 *      因此圖像質(zhì)量不穩(wěn)定。這種算法優(yōu)先考慮碼率(帶寬)。
 *  VBR:動(dòng)態(tài)比特率,其碼率可以隨著圖像的復(fù)雜程度的不同而變化,因此其編碼效率比較高,Motion發(fā)生時(shí),馬賽克很少。碼率控制算法根據(jù)圖像
 *      內(nèi)容確定使用的比特率,圖像內(nèi)容比較簡單則分配較少的碼率(似乎碼字更合適),圖像內(nèi)容復(fù)雜則分配較多的碼字,這樣既保證了質(zhì)量,又
 *      兼顧帶寬限制。這種算法優(yōu)先考慮圖像質(zhì)量。
 * CVBR:它是VBR的一種改進(jìn)方法這種算法對(duì)應(yīng)的Maximum bitRate恒定或者Average BitRate恒定。這種方法的兼顧了以上兩種方法的優(yōu)點(diǎn),
 *      在圖像內(nèi)容靜止時(shí),節(jié)省帶寬,有Motion發(fā)生時(shí),利用前期節(jié)省的帶寬來盡可能的提高圖像質(zhì)量,達(dá)到同時(shí)兼顧帶寬和圖像質(zhì)量的目的
 *  ABR:在一定的時(shí)間范圍內(nèi)達(dá)到設(shè)定的碼率,但是局部碼率峰值可以超過設(shè)定的碼率,平均碼率恒定??梢宰鳛閂BR和CBR的一種折中選擇。
 *
 *  H264各個(gè)分辨率推薦的碼率表:http://www.lighterra.com/papers/videoencodingh264/
 */
SInt32 avgbitRate = 0.96*1000000;   // 注意單位是bit/s 這里是640x480的 為0.96Mbps
CFNumberRef avgRateLimitRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &avgbitRate);
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_AverageBitRate, avgRateLimitRef);
/** 遇到問題:編碼的視頻馬賽克嚴(yán)重
 *  原因分析:沒有正確的設(shè)置碼率上限值
 *  解決思路:正確設(shè)置碼率上限
 *
 *  備注:碼率上限一個(gè)數(shù)組,按照@[比特?cái)?shù),時(shí)長.....]方式傳值排列,至少一對(duì) 比特?cái)?shù),時(shí)長;如果有多個(gè),這些值必須平滑,內(nèi)部會(huì)有一個(gè)算法算出最終值
 *  均值碼率過低,也會(huì)造成馬賽克
 */
// 設(shè)置碼率上限
int bitRateLimits = avgbitRate; // 一秒鐘的最大碼率
NSArray *limit = @[@(bitRateLimits * 1.5), @(1)];
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);

3、準(zhǔn)備編碼相關(guān)上下文

status = VTCompressionSessionPrepareToEncodeFrames(_encodeSession);
if (status == noErr) {
    NSLog(@"CompressionSession 初始化成功 可以開始解碼了");
}

這里有一個(gè)地方要注意下,如果沒有設(shè)置碼率上限值或者碼率上限值設(shè)置方式不對(duì),平均碼率過小都會(huì)引起編碼出現(xiàn)馬賽克,請(qǐng)看上面具體的注釋
4、開始編碼
開始編碼應(yīng)該在采集的回調(diào)函數(shù)中,也就是采集相關(guān)代碼的最后一部中的代碼

// 幀序號(hào)時(shí)間,用于表示幀開始編碼的時(shí)間(備注:這個(gè)時(shí)間是相對(duì)時(shí)間,并不是真正時(shí)間)
CMTime presentationTime = CMTimeMake(_frameId++, 1000);
VTEncodeInfoFlags encodeflags;
OSStatus status = VTCompressionSessionEncodeFrame(_encodeSession, imageBuffer, presentationTime, kCMTimeInvalid, NULL, NULL, &encodeflags);
if (status != noErr) {
    NSLog(@"VTCompressionSessionEncodeFrame fail %d",status);
    
    // 釋放資源
    VTCompressionSessionInvalidate(_encodeSession);
    CFRelease(_encodeSession);
    _encodeSession = NULL;
}

組裝為h264碼流

調(diào)用VTCompressionSessionEncodeFrame()函數(shù)后,系統(tǒng)內(nèi)部會(huì)進(jìn)行編碼,編碼結(jié)果通過第一步創(chuàng)建的回調(diào)函數(shù)返回
編碼的NALU數(shù)據(jù)格式為:NALU長度(四字節(jié))+編碼類型+編碼數(shù)據(jù),h264碼流的NALU數(shù)據(jù)格式為:起始碼+編碼類型+編碼數(shù)據(jù),所以要先轉(zhuǎn)換一下在保存,具體代碼如下

void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer)
{
    NSLog(@"didCompressH264 called with status %d infoFlags %d", (int)status, (int)infoFlags);
    if (status != noErr) {
        NSLog(@"compress fail %d",status);
        return;
    }
    
    // 返回該sampleBuffer是否可以進(jìn)行操作了
    if (!CMSampleBufferDataIsReady(sampleBuffer)) {
        NSLog(@"CMSampleBufferDataIsReady is not ready");
        return;
    }
    
    VideoEnDecodeViewController *mySelf = (__bridge VideoEnDecodeViewController*)outputCallbackRefCon;
    // CMSampleBufferGetSampleAttachmentsArray獲取視頻幀的描述信息,比如是否關(guān)鍵幀等等;kCMSampleAttachmentKey_NotSync標(biāo)記是否關(guān)鍵幀
    BOOL keyframe = CFDictionaryContainsKey(CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES), 0), kCMSampleAttachmentKey_NotSync);
    if (keyframe) {
        /** CMFormatDescriptionRef中包含了PPS/SPS/SEI,寬高、顏色空間、編碼格式等描述信息的結(jié)構(gòu)體,它等同于
         *  CMVideoFormatDescriptionRef
         *  SPS在索引0處;PPS在索引1處
         */
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
        size_t SPSSize, SPSCount;
        const uint8_t *sps;
        OSStatus retStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sps, &SPSSize, &SPSCount, 0);
        if (retStatus == noErr) {
            size_t PPSSize, PPSCount;
            const uint8_t *pps;
            retStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pps, &PPSSize, &PPSCount, 0);
            if (retStatus == noErr) {
                NSData *spsData = [NSData dataWithBytes:sps length:SPSSize];
                NSData *ppsData = [NSData dataWithBytes:pps length:PPSSize];
                
                // 保存sps和pps
                [mySelf saveSPS:spsData pps:ppsData];
            }
        }
    }
    
    // CMBlockBufferRef表示一個(gè)內(nèi)存塊,用來存放編碼后的音頻/視頻數(shù)據(jù)
    CMBlockBufferRef dataBlockRef = CMSampleBufferGetDataBuffer(sampleBuffer);
    size_t lenght,totalLenght;
    char *dataptr;
    // 獲取指向內(nèi)存塊數(shù)據(jù)的指針
    OSStatus status1 = CMBlockBufferGetDataPointer(dataBlockRef, 0, &lenght, &totalLenght, &dataptr);
    if (status1 == noErr) {
        size_t bufferOffset = 0;
        static const int AACStartCodeLenght = 4;
        /** 一次編碼可能會(huì)包含多個(gè)nalu
         *  所以要循環(huán)獲取所有的nalu數(shù)據(jù),并解析出來
         *  每個(gè)NALU的格式為
         *  四字節(jié)(NALU總長度)+視頻數(shù)據(jù)(NALU總長度-4)
         *  和正規(guī)的h264的nalu封裝格式0001開頭的有點(diǎn)不一樣
         */
        while (bufferOffset < totalLenght - AACStartCodeLenght) {
            uint32_t naluUnitLenght = 0;
            // 讀取該NALU的數(shù)據(jù)總長度,該NALU就是一幀完整的編碼的視頻
            memcpy(&naluUnitLenght, dataptr+bufferOffset, AACStartCodeLenght);
            
            // 返回的nalu數(shù)據(jù)前四個(gè)字節(jié)不是0001的startcode,而是大端模式的幀長度length
            // 從大端轉(zhuǎn)系統(tǒng)端(必須,否則會(huì)造成長度錯(cuò)誤問題)
            naluUnitLenght = CFSwapInt32BigToHost(naluUnitLenght);
            // 將真正的編碼后的視頻幀提取出來
            NSData *data = [[NSData alloc] initWithBytes:(dataptr + bufferOffset + AACStartCodeLenght) length:naluUnitLenght];
            
            // 然后添加0001開頭碼組成正規(guī)的h264封裝格式
            [mySelf saveEncodedData:data isKeyFrame:keyframe];
            
            // 循環(huán)讀取
            bufferOffset += AACStartCodeLenght + naluUnitLenght;
        }
    }
}

備注:
1、編碼的數(shù)據(jù)都存儲(chǔ)在CMSampleBufferRef對(duì)象變量sampleBuffer中,要注意一次編碼可能會(huì)包含多個(gè)nalu
2、h264碼流文件存儲(chǔ)順序要注意下,一定要按照sps pps I幀 p幀 p幀/b幀....sps pps I幀 p幀 p幀/b幀....的順序,否則會(huì)導(dǎo)致無法播放

導(dǎo)出保存的h264文件,使用ffplay命令播放

由于只能使用手機(jī)進(jìn)行視頻采集,所以需要將保存在真機(jī)中的文件導(dǎo)出來,具體方法為:


1564319045580.jpg

然后使用ffplay 播放,命令如下
ffplay -f h264 /Users/feipai1/Desktop/qwe.media\ 2019-07-28\ 14:14.36.613.xcappdata/AppData/Documents/abc.h264

遇到問題

1、創(chuàng)建編碼器時(shí)返回-12902錯(cuò)誤;主要是因?yàn)閷捀叩膮?shù)沒有設(shè)置,正確設(shè)置即可
2、編碼后的視頻出現(xiàn)馬賽克;因?yàn)榇a率上限值設(shè)置不正確導(dǎo)致,正確設(shè)置方式為,kVTCompressionPropertyKey_DataRateLimits必須對(duì)應(yīng)一個(gè)數(shù)組
int bitRateLimits = avgbitRate; // 一秒鐘的最大碼率
NSArray *limit = @[@(bitRateLimits * 1.5), @(1)];
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);

項(xiàng)目地址

參考VideoEnDecodeViewController.h/.m文件中代碼
Demo

參考文章

http://www.enkichen.com/2017/11/26/image-h264-encode/
http://www.enkichen.com/2018/03/24/videotoolbox/
https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/AVFoundationPG/Articles/04_MediaCapture.html#//apple_ref/doc/uid/TP40010188-CH5-SW2

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

相關(guān)閱讀更多精彩內(nèi)容

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