前言
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框架完成,如下圖所示

有如下幾個(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)出來,具體方法為:

然后使用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