VideoToolbox硬編碼YUV為h264(九)

前言

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

h264碼流格式

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

ffplay -f h264 test.h264

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

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

一般只用到1前弯、5蚪缀、7、8這4個類型,類型為5表示這是一個I幀恕出,I幀前面必須有SPS和PPS數據询枚,也就是類型為7和8,類型為1表示這是一個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ū)別就是,同等分辨率前者占用空間少一半弓乙。

視頻采集相關代碼

蘋果官方文檔-AVFoundation

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


captureDetail_2x.png

有如下幾個很重要的對象
1、AVCaptureSession:
管理視頻輸入輸出的會話(輸入:攝像頭丧裁;輸出:輸送數據給app端)
2、AVCaptureDevice:
代表了一個具體的物理設備二庵,比如攝像頭(前置/后置),揚聲器等等杭隙;備注:模擬器無法運行攝像頭相關代碼
3因妙、AVCaptureDeviceInput:
代表具體的視頻輸入兰迫,它要由具體的物理設備創(chuàng)建
4汁果、AVCaptureVideoDataOutput:
它是AVCaptureOutput(它是一個抽象類)的子類,用于輸出原始視頻數據
5鳄乏、AVCaptureConnection:
代表了AVCaptureInputPort和AVCaptureOutput橱野、AVCaptureVideoPreviewLayer之間的連接通道水援,通過它可以將視頻數據輸送給AVCaptureVideoPreviewLayer進行顯示蜗元,設置輸出視頻的輸出視頻的方向系冗,鏡像等等掌敬。
6、AVCaptureVideoPreviewLayer:
是一個可以顯示攝像頭內容的CAlayer的子類

具體采集相關代碼如下:
1楷兽、初始化AVCaptureSession

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

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

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

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

self.mVideoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
// 當回調因為耗時操作還在進行時左腔,系統(tǒng)對新的一幀圖像的處理方式,如果設置為YES振亮,則立馬丟棄該幀坊秸。
// NO褒搔,則緩存起來(如果累積的幀過多喷面,緩存的內存將持續(xù)增長)乖酬;該值默認為YES
self.mVideoDataOutput.alwaysDiscardsLateVideoFrames = NO;
/** 設置采集的視頻數據幀的格式咬像。這里代表生成的圖像數據為YUV數據,顏色范圍是full-range的
 *  并且是bi-planner存儲方式(也就是Y數據占用一個內存塊;UV數據占用另外一個內存塊)
 */
[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方式編碼肮柜,所以這里必須設置為yuv顏色空間
4审洞、配置采集的視頻數據通過AVCaptureVideoPreviewLayer渲染出來(非必須)

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

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

此步驟對于視頻采集來說也是很重要的仰剿,因為可以實時看到自己想要采集的具體內容
5南吮、開始采集

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

6部凑、通過代理獲取到原始的視頻數據

- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
    NSLog(@"采集到的數據 ==>%@",[NSThread currentThread]);
    /** CVImageBufferRef 表示原始視頻數據的對象涂邀;
     *  包含未壓縮的像素數據比勉,包括圖像寬度敷搪、高度等幢哨;
     *  等同于CVPixelBufferRef
     */
    // 獲取CMSampleBufferRef中具體的視頻數據
    CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
    /** 執(zhí)行編碼
     *  參數1:已經創(chuàng)建并且準備好的VTCompressionSessionRef對象
     *  參數2:具體的視頻原始數據;CVImageBufferRef類型
     *  參數3:視頻數據開始編碼的時間;CMTime類型捞镰,一般是CMTimeMake(幀序號, 壓縮單位(比如1000));
     *  參數4:該幀視頻的時長岸售,一般不需要計算(因為沒法算)凸丸,傳kCMTimeInvalid即可
     *  參數5:要編碼的視頻相關屬性;CFDictionaryRef類型
     *  參數6:傳遞給編碼輸出回調的參數;void* 類型
     *  參數7:編碼結果標記瞭稼;通過回調函數獲取
     */
    // 幀序號時間环肘,用于表示幀開始編碼的時間(備注:這個時間是相對時間悔雹,并不是真正時間)
    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;
    }
}

采集到的原始視頻數據將通過該回調函數傳回腌零,原始視頻數據存放在CMSampleBufferRef類型對象中益涧。

CMSampleBufferRef:
1饰躲、包含音視頻描述信息嘹裂,比如包含音頻的格式描述 AudioStreamBasicStreamDescription寄狼、包含視頻的格式描述 CMVideoFormatDescriptionRef
2泊愧、包含音視頻數據删咱,可以是原始數據也可以是壓縮數據;通過CMSampleBufferGetxxx()系列函數提取
CVImageBufferRef:
表示原始視頻數據的對象痰滋;包含未壓縮的像素數據续崖,包括圖像寬度严望、高度等像吻;
等同于CVPixelBufferRef

編碼相關代碼

在進行編碼之前得先初始化編碼器,設置編碼參數等等準備工作奸披,具體使用流程如下:
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)建編碼器對象 VTCompressionSessionRef
VTCompressionSessionCreate(...)
* 參數1:創(chuàng)建對象內存使用的內存分配器样刷,NULL代表使用默認分配器kCFAllocatorDefault
* 參數2/3:要編碼的視頻幀的寬和高置鼻;單位像素
* 參數4:使用的編碼方式 比如H264(kCMVideoCodecType_H264)
* 參數5:設置編碼方式相關的參數箕母,比如H264編碼所需的參數;CFDictionaryRef類型钙勃,NULL辖源,則默認值;也可以
* 通過VTSessionSetProperty()函數設置
* 參數6:設置原始視頻數據緩存的方式克饶,CFDictionaryRef類型矾湃,NULL則代表使用默認值
* 參數7:設置編碼數據的內存分配器及其它保存方式洲尊,CFAllocatorRef類型坞嘀,NULL則使用默認值
* 參數8:設置編碼數據輸出回調函數
* 參數9:設置傳入給該回調函數的參數惊来;void類型
* 參數10:要創(chuàng)建的VTCompressionSessionRef對象
/
/
遇到問題:返回-12902錯誤
* 分析問題:在VTErrors.h中查看錯誤說明裁蚁,意思參數錯誤枉证,經檢查是_width和_height沒有指定具體的值
* 解決問題:給_width和_height賦上具體的值
*/
在創(chuàng)建編碼器時一定要指定要編碼的原始視頻的寬和高室谚,否則會返回錯誤。

2猪瞬、設置編碼器參數
通過VTSessionSetProperty()接口設置編碼器相關參數陈瘦,比如編碼效率級別痊项,GOP,平均碼率遏弱,幀率,碼率上限值等等

/** VTSessionSetProperty()函數既可以設置編碼相關屬性游沿,又可以設置解碼相關屬性
 *  對于H264編碼來說诀黍,以下屬性是必須的
 *  1眯勾、編碼效率級別:kVTCompressionPropertyKey_ProfileLevel
 *      kVTProfileLevel_H264_Baseline_AutoLevel
 *  2吃环、GOP(關鍵幀間隔):
 *      kVTCompressionPropertyKey_MaxKeyFrameInterval
 *  3洋幻、編碼后的幀率:
 *      kVTCompressionPropertyKey_ExpectedFrameRate文留;
 *      改變該值可以加快視頻速度或者減慢視頻速度
 *  4燥翅、編碼后的平均碼率:
 *      kVTCompressionPropertyKey_AverageBitRate
 *      平均碼率決定了壓縮的程度
 *  5森书、編碼后的碼率上限:
 *      kVTCompressionPropertyKey_DataRateLimits
 */
// 設置實時編碼輸出(避免延遲)
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
/** 設置H264編碼的壓縮級別
 *  BP(Baseline Profile):基本畫質谎势。支持I/P 幀它浅,只支持無交錯(Progressive)和CAVLC姐霍;主要應用:可視電話镊折,會議
 *  電視介衔,和無線通訊等實時視頻通訊領域
 *  EP(Extended profile):進階畫質。支持I/P/B/SP/SI 幀赃泡,只支持無交錯(Progressive)和CAVLC升熊;
 *  MP(Main profile):主流畫質级野。提供I/P/B 幀蓖柔,支持無交錯(Progressive)和交錯(Interlaced)况鸣,也支持CAVLC 和CABAC 的支持懒闷;主要應用:數字廣播電視和數字視頻存儲
 *  HP(High profile):高級畫質栈幸。在main Profile 的基礎上增加了8×8內部預測速址、自定義量化芍锚、 無損視頻編碼和更多的YUV 格式蔓榄;
 *  應用于廣電和存儲領域
 *  iPhone上方案如下:
 *  實時直播:
 *      低清Baseline Level 1.3
 *      標清Baseline Level 3
 *      半高清Baseline Level 3.1
 *      全高清Baseline Level 4.1
 *  存儲媒體:
 *  低清 Main Level 1.3
 *  標清 Main Level 3
 *  半高清 Main Level 3.1
 *  全高清 Main Level 4.1
 *  高清存儲:
 *  半高清 High Level 3.1
 *  全高清 High Level 4.1
 *
 *  參考文章:https://blog.csdn.net/sphone89/article/details/17492433
 */
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
// 設置是否開啟B幀編碼;默認開啟甥郑,注意只有EP澜搅,MP勉躺,HP級別才支持B幀饵溅,如果是BP級別蜕企,該設置無效轻掩。
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanTrue);
/** 設置關鍵幀GOP間隔
 *  1轩端、碼率不變的前提下基茵,GOP值越大P拱层、B幀的數量會越多根灯,平均每個I烙肺、P氧卧、B幀所占用的字節(jié)數就越多沙绝,也就更容易獲取較好的圖像質量鼠锈;B幀的數量越多购笆,同
 *  理也更容易獲得較好的圖像質量;
 *  2同欠、需要說明的是行您,通過提高GOP值來提高圖像質量是有限度的娃循,在遇到場景切換的情況時捌斧,H.264編碼器會自動強制插入一個I幀捞蚂,此時實際的GOP值被縮短了姓迅。
 *  另一方面丁存,在一個GOP中解寝,P聋伦、B幀是由I幀預測得到的界睁,當I幀的圖像質量比較差時抑片,會影響到一個GOP中后續(xù)P杨赤、B幀的圖像質量,直到下一個GOP開始才有
 *  可能得以恢復衙解,所以GOP值也不宜設置過大焰枢。
 *  3济锄、同時荐绝,由于P低滩、B幀的復雜度大于I幀恕沫,所以過多的P婶溯、B幀會影響編碼效率迄委,使編碼效率降低跑筝。另外曲梗,過長的GOP還會影響Seek操作的響應速度,由于P妓忍、B幀
 *  是由前面的I或P幀預測得到的虏两,所以Seek操作需要直接定位,解碼某一個P或B幀時世剖,需要先解碼得到本GOP內的I幀及之前的N個預測幀才可以定罢,GOP值越長
 *  需要解碼的預測幀就越多,seek響應的時間也越長旁瘫。
 */
int iFrameInternal = 10;
CFNumberRef iFrameRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &iFrameInternal);
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, iFrameRef);
// 設置期望幀率
int fps = 25;
CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);

/** 設置均值碼率祖凫,單位是bps琼蚯,它不是一個硬性指標,實際的碼率可能會上下浮動;VideoToolBox框架只支持ABR模式遭庶,而對于H264來說权埠,它有四種
 *  碼率控制模式,如下:
 *  CBR:恒定比特率方式進行編碼叔扼,Motion發(fā)生時与柑,由于碼率恒定,只能通過增大QP來減少碼字大小,圖像質量變差嵌屎,當場景靜止時尼夺,圖像質量又變好
 *      因此圖像質量不穩(wěn)定。這種算法優(yōu)先考慮碼率(帶寬)竞端。
 *  VBR:動態(tài)比特率统台,其碼率可以隨著圖像的復雜程度的不同而變化谤逼,因此其編碼效率比較高,Motion發(fā)生時,馬賽克很少球切。碼率控制算法根據圖像
 *      內容確定使用的比特率,圖像內容比較簡單則分配較少的碼率(似乎碼字更合適),圖像內容復雜則分配較多的碼字焚鹊,這樣既保證了質量,又
 *      兼顧帶寬限制嚷炉。這種算法優(yōu)先考慮圖像質量哗讥。
 * CVBR:它是VBR的一種改進方法這種算法對應的Maximum bitRate恒定或者Average BitRate恒定决乎。這種方法的兼顧了以上兩種方法的優(yōu)點,
 *      在圖像內容靜止時唤反,節(jié)省帶寬逆趋,有Motion發(fā)生時魄眉,利用前期節(jié)省的帶寬來盡可能的提高圖像質量,達到同時兼顧帶寬和圖像質量的目的
 *  ABR:在一定的時間范圍內達到設定的碼率,但是局部碼率峰值可以超過設定的碼率,平均碼率恒定告材〔涌剩可以作為VBR和CBR的一種折中選擇。
 *
 *  H264各個分辨率推薦的碼率表: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);
/** 遇到問題:編碼的視頻馬賽克嚴重
 *  原因分析:沒有正確的設置碼率上限值
 *  解決思路:正確設置碼率上限
 *
 *  備注:碼率上限一個數組扫茅,按照@[比特數,時長.....]方式傳值排列,至少一對 比特數,時長怀喉;如果有多個,這些值必須平滑荒辕,內部會有一個算法算出最終值
 *  均值碼率過低,也會造成馬賽克
 */
// 設置碼率上限
int bitRateLimits = avgbitRate; // 一秒鐘的最大碼率
NSArray *limit = @[@(bitRateLimits * 1.5), @(1)];
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);

3茧跋、準備編碼相關上下文

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

這里有一個地方要注意下蝇棉,如果沒有設置碼率上限值或者碼率上限值設置方式不對,平均碼率過小都會引起編碼出現馬賽克瓶您,請看上面具體的注釋
4乡革、開始編碼
開始編碼應該在采集的回調函數中,也就是采集相關代碼的最后一部中的代碼

// 幀序號時間岛啸,用于表示幀開始編碼的時間(備注:這個時間是相對時間,并不是真正時間)
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碼流

調用VTCompressionSessionEncodeFrame()函數后,系統(tǒng)內部會進行編碼从诲,編碼結果通過第一步創(chuàng)建的回調函數返回
編碼的NALU數據格式為:NALU長度(四字節(jié))+編碼類型+編碼數據定页,h264碼流的NALU數據格式為:起始碼+編碼類型+編碼數據,所以要先轉換一下在保存钦购,具體代碼如下

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是否可以進行操作了
    if (!CMSampleBufferDataIsReady(sampleBuffer)) {
        NSLog(@"CMSampleBufferDataIsReady is not ready");
        return;
    }
    
    VideoEnDecodeViewController *mySelf = (__bridge VideoEnDecodeViewController*)outputCallbackRefCon;
    // CMSampleBufferGetSampleAttachmentsArray獲取視頻幀的描述信息节猿,比如是否關鍵幀等等;kCMSampleAttachmentKey_NotSync標記是否關鍵幀
    BOOL keyframe = CFDictionaryContainsKey(CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES), 0), kCMSampleAttachmentKey_NotSync);
    if (keyframe) {
        /** CMFormatDescriptionRef中包含了PPS/SPS/SEI煤墙,寬高劣针、顏色空間、編碼格式等描述信息的結構體契讲,它等同于
         *  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表示一個內存塊,用來存放編碼后的音頻/視頻數據
    CMBlockBufferRef dataBlockRef = CMSampleBufferGetDataBuffer(sampleBuffer);
    size_t lenght,totalLenght;
    char *dataptr;
    // 獲取指向內存塊數據的指針
    OSStatus status1 = CMBlockBufferGetDataPointer(dataBlockRef, 0, &lenght, &totalLenght, &dataptr);
    if (status1 == noErr) {
        size_t bufferOffset = 0;
        static const int AACStartCodeLenght = 4;
        /** 一次編碼可能會包含多個nalu
         *  所以要循環(huán)獲取所有的nalu數據挟伙,并解析出來
         *  每個NALU的格式為
         *  四字節(jié)(NALU總長度)+視頻數據(NALU總長度-4)
         *  和正規(guī)的h264的nalu封裝格式0001開頭的有點不一樣
         */
        while (bufferOffset < totalLenght - AACStartCodeLenght) {
            uint32_t naluUnitLenght = 0;
            // 讀取該NALU的數據總長度,該NALU就是一幀完整的編碼的視頻
            memcpy(&naluUnitLenght, dataptr+bufferOffset, AACStartCodeLenght);
            
            // 返回的nalu數據前四個字節(jié)不是0001的startcode永淌,而是大端模式的幀長度length
            // 從大端轉系統(tǒng)端(必須,否則會造成長度錯誤問題)
            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声诸、編碼的數據都存儲在CMSampleBufferRef對象變量sampleBuffer中,要注意一次編碼可能會包含多個nalu
2退盯、h264碼流文件存儲順序要注意下彼乌,一定要按照sps pps I幀 p幀 p幀/b幀....sps pps I幀 p幀 p幀/b幀....的順序,否則會導致無法播放

導出保存的h264文件渊迁,使用ffplay命令播放

由于只能使用手機進行視頻采集慰照,所以需要將保存在真機中的文件導出來,具體方法為:


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)建編碼器時返回-12902錯誤;主要是因為寬高的參數沒有設置箱叁,正確設置即可
2墅垮、編碼后的視頻出現馬賽克惕医;因為碼率上限值設置不正確導致,正確設置方式為算色,kVTCompressionPropertyKey_DataRateLimits必須對應一個數組
int bitRateLimits = avgbitRate; // 一秒鐘的最大碼率
NSArray *limit = @[@(bitRateLimits * 1.5), @(1)];
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);

項目地址

參考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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末曹锨,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子剃允,更是在濱河造成了極大的恐慌沛简,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件斥废,死亡現場離奇詭異椒楣,居然都是意外死亡,警方通過查閱死者的電腦和手機牡肉,發(fā)現死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門捧灰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人统锤,你說我怎么就攤上這事毛俏。” “怎么了饲窿?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵煌寇,是天一觀的道長。 經常有香客問我逾雄,道長阀溶,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任鸦泳,我火速辦了婚禮银锻,結果婚禮上,老公的妹妹穿的比我還像新娘做鹰。我一直安慰自己击纬,他們只是感情好,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布钾麸。 她就那樣靜靜地躺著更振,像睡著了一般。 火紅的嫁衣襯著肌膚如雪喂走。 梳的紋絲不亂的頭發(fā)上殃饿,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天,我揣著相機與錄音芋肠,去河邊找鬼乎芳。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的奈惑。 我是一名探鬼主播吭净,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼肴甸!你這毒婦竟也來了寂殉?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤原在,失蹤者是張志新(化名)和其女友劉穎友扰,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體庶柿,經...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡村怪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了浮庐。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片甚负。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖审残,靈堂內的尸體忽然破棺而出梭域,到底是詐尸還是另有隱情,我是刑警寧澤搅轿,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布病涨,位于F島的核電站,受9級特大地震影響介时,放射性物質發(fā)生泄漏没宾。R本人自食惡果不足惜凌彬,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一沸柔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧铲敛,春花似錦褐澎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至先鱼,卻和暖如春俭正,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背焙畔。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工掸读, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓儿惫,卻偏偏與公主長得像澡罚,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子肾请,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內容