硬編碼相對於軟編碼來講,使用非CPU進行編碼,如顯卡GPU、專用的DSP、FPGA、ASIC芯片等,性能高,對CPU沒有壓力,可是對其餘硬件要求較高(如GPU等)。git
在iOS8以後,蘋果開放了接口,而且封裝了VideoToolBox&AudioToolbox兩個框架,分別用於對視頻&音頻進行硬編碼,音頻編碼放在後面作總結,此次主要總結VideoToolBox。github
Demo的Github地址:https://github.com/wzpziyi1/HardCoding-For-iOS網絡
一、相關基礎數據結構:數據結構
CVPixelBuffer:編碼前和解碼後的圖像數據結構。框架
CMTime、CMClock和CMTimebase:時間戳相關。時間以64-bit/32-bit的形式出現。ide
CMBlockBuffer:編碼後,結果圖像的數據結構。函數
CMVideoFormatDescription:圖像存儲方式,編解碼器等格式描述。性能
CMSampleBuffer:存放編解碼先後的視頻圖像的容器數據結構。ui
// 編碼完成回調 void finishCompressH264Callback(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) { if (status != noErr) return; //根據傳入的參數獲取對象 ZYVideoEncoder *encoder = (__bridge ZYVideoEncoder *)(outputCallbackRefCon); //判斷是不是關鍵幀 bool isKeyFrame = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync); //若是是關鍵幀,獲取sps & pps數據 if (isKeyFrame) { CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer); //獲取sps信息 size_t sparameterSetSize, sparameterSetCount; const uint8_t *sparameterSet; CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0); // 獲取PPS信息 size_t pparameterSetSize, pparameterSetCount; const uint8_t *pparameterSet; CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0 ); // 裝sps/pps轉成NSData,以方便寫入文件 NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize]; NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize]; // 寫入文件 [encoder gotSpsPps:sps pps:pps]; } //獲取數據塊 CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); size_t length, totalLength; char *dataPointer; OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer); if (statusCodeRet == noErr) { size_t bufferOffset = 0; // 返回的nalu數據前四個字節不是0001的startcode,而是大端模式的幀長度length static const int AVCCHeaderLength = 4; //循環獲取nalu數據 while (bufferOffset < totalLength - AVCCHeaderLength) { uint32_t NALUnitLength = 0; //讀取NAL單元長度 memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength); // 從大端轉系統端 NALUnitLength = CFSwapInt32BigToHost(NALUnitLength); NSData* data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength]; [encoder gotEncodedData:data isKeyFrame:isKeyFrame]; // 移動到寫一個塊,轉成NALU單元 bufferOffset += AVCCHeaderLength + NALUnitLength; } } }
所須要的信息均可以從CMSampleBufferRef中獲得。編碼
二、NAL(網絡提取層)代碼講解
直播一中提到了NALU概念上的封裝,下面是代碼部分:
代碼B:
- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps { // 拼接NALU的header const char bytes[] = "\x00\x00\x00\x01"; size_t length = (sizeof bytes) - 1; NSData *ByteHeader = [NSData dataWithBytes:bytes length:length]; // 將NALU的頭&NALU的體寫入文件 [self.fileHandle writeData:ByteHeader]; [self.fileHandle writeData:sps]; [self.fileHandle writeData:ByteHeader]; [self.fileHandle writeData:pps]; } - (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame { NSLog(@"gotEncodedData %d", (int)[data length]); if (self.fileHandle != NULL) { const char bytes[] = "\x00\x00\x00\x01"; size_t length = (sizeof bytes) - 1; //string literals have implicit trailing '\0' NSData *ByteHeader = [NSData dataWithBytes:bytes length:length]; [self.fileHandle writeData:ByteHeader]; [self.fileHandle writeData:data]; } }
結合這張圖片:
一個GOP序列,最前面是sps和pps,它們單獨被封裝成兩個NALU單元,一個NALU單元包含header和具體數據,NALU單元header序列固定爲00 00 00 01。那麼獲得一幀畫面時,須要判斷該幀是否是I幀,若是是,那麼取出sps和pps,再是相關幀的提取寫入。(具體參考代碼A)。
三、VTCompressionSession進行硬編碼
a、給出width、height
b、使用VTCompressionSessionCreate建立compressionSession,並設置使用H264進行編碼,相關type是kCMVideoCodecType_H264
c、b中還須要設置回調函數finishCompressH264Callback,須要在回調函數裏面取出編碼後的GOP、sps、pps等數據。
d、設置屬性爲實時編碼,直播必然是實時輸出。
e、設置指望幀數,每秒多少幀,通常都是30幀以上,以避免畫面卡頓
f、設置碼率(碼率: 編碼效率, 碼率越高,則畫面越清晰, 若是碼率較低會引發馬賽克 --> 碼率高有利於還原原始畫面,可是也不利於傳輸)
g、設置關鍵幀間隔(也就是GOP間隔)
h、設置結束,準備編碼
代碼:
- (void)setupVideoSession { //用於記錄當前是第幾幀數據 self.frameID = 0; //錄製視頻的寬高 int width = [UIScreen mainScreen].bounds.size.width; int height = [UIScreen mainScreen].bounds.size.height; // 建立CompressionSession對象,該對象用於對畫面進行編碼 // kCMVideoCodecType_H264 : 表示使用h.264進行編碼 // finishCompressH264Callback : 當一次編碼結束會在該函數進行回調,能夠在該函數中將數據,寫入文件中 //傳入的self,就是finishCompressH264Callback回調函數裏面的outputCallbackRefCon,經過bridge就能夠取出此self VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, finishCompressH264Callback, (__bridge void * _Nullable)(self), &_compressionSession); //設置實時編碼,直播必然是實時輸出 VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue); //設置指望幀數,每秒多少幀,通常都是30幀以上,以避免畫面卡頓 int fps = 30; CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &fps); VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef); //設置碼率(碼率: 編碼效率, 碼率越高,則畫面越清晰, 若是碼率較低會引發馬賽克 --> 碼率高有利於還原原始畫面,可是也不利於傳輸) int bitRate = 800 * 1024; CFNumberRef rateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate); VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_AverageBitRate, rateRef); NSArray *limit = @[@(bitRate * 1.5/8), @(1)]; VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit); //設置關鍵幀間隔(也就是GOP間隔) //這裏設置與上面的fps一致,意味着每間隔30幀開始一個新的GOF序列,也就是每隔間隔1s生成新的GOF序列 //由於上面設置的是,一秒30幀 int frameInterval = 30; CFNumberRef intervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &frameInterval); VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, intervalRef); //設置結束,準備編碼 VTCompressionSessionPrepareToEncodeFrames(_compressionSession); }
碼率:
初始化後經過VTSessionSetProperty
設置對象屬性
編碼方式:H.264編碼
幀率:每秒鐘多少幀畫面
碼率:單位時間內保存的數據量
關鍵幀(GOPsize)間隔:多少幀爲一個GOP
參數參考: