前言
系列文章:
《iOS視頻開發(一):視頻采集》
《iOS視頻開發(二):視頻H264硬編碼》
《iOS視頻開發(三):視頻H264硬解碼》
《iOS視頻開發(四):通俗理解YUV數據》
上一篇《iOS視頻開發(二):視頻H264硬編碼》我們已經學會了如何對視頻數據進行H264編碼并且了解了H264碼流的基本結構。通常我們將視頻進行H264編碼是為了進行網絡傳輸,如網絡直播、視頻會議等應用。網絡傳輸相關的知識點較多且雜,這里我們且先不進行深入研究。我們接著講對于H264數據,我們如何對其進行解碼,本文就來講一下如何使用VideoToolBox
對H264數據進行硬解碼。
解碼過程
硬解碼流程很簡單:
1、解析H264數據
2、初始化解碼器(VTDecompressionSessionCreate
)
3、將解析后的H264數據送入解碼器(VTDecompressionSessionDecodeFrame
)
4、解碼器回調輸出解碼后的數據(CVImageBufferRef
)
從上一篇文章我們知道H264原始碼流是由一個接一個的NALU(Nal Unit)組成的,I幀是一個完整編碼的幀,P幀和B幀都需要根據I幀進行生成。這也就是說,若H264碼流中沒有I幀,即P幀和B幀失去參考,那么將無法對該碼流進行解碼。VideoToolBox
的硬編碼器編碼出來的H264數據第一幀為I幀,我們也可以手動告訴編碼器編一個I幀給我們。按照H264的數據格式,I幀前面必須有sps和pps數據,解碼的第一步初始化解碼器正是需要sps和pps數據來對編碼器進行初始化。
1、解析并處理H264數據
既然H264數據是一個接一個的NALU組成,要對數據進行解碼我們需要先對NALU數據進行解析。
NALU數據的前4個字節是開始碼,用于標示這是一個NALU 單元的開始,第5字節是NAL類型,我們取出第5個字節轉為十進制,看看什么類型,對其進行處理后送入解碼器進行解碼。
uint8_t *frame = (uint8_t *)naluData.bytes;
uint32_t frameSize = (uint32_t)naluData.length;
// frame的前4個字節是NALU數據的開始碼,也就是00 00 00 01,
// 第5個字節是表示數據類型,轉為10進制后,7是sps, 8是pps, 5是IDR(I幀)信息
int nalu_type = (frame[4] & 0x1F);
// 將NALU的開始碼轉為4字節大端NALU的長度信息
uint32_t nalSize = (uint32_t)(frameSize - 4);
uint8_t *pNalSize = (uint8_t*)(&nalSize);
frame[0] = *(pNalSize + 3);
frame[1] = *(pNalSize + 2);
frame[2] = *(pNalSize + 1);
frame[3] = *(pNalSize);
switch (nalu_type)
{
case 0x05: // I幀
NSLog(@"NALU type is IDR frame");
if([self initH264Decoder])
{
[self decode:frame withSize:frameSize];
}
break;
case 0x07: // SPS
NSLog(@"NALU type is SPS frame");
_spsSize = frameSize - 4;
_sps = malloc(_spsSize);
memcpy(_sps, &frame[4], _spsSize);
break;
case 0x08: // PPS
NSLog(@"NALU type is PPS frame");
_ppsSize = frameSize - 4;
_pps = malloc(_ppsSize);
memcpy(_pps, &frame[4], _ppsSize);
break;
default: // B幀或P幀
NSLog(@"NALU type is B/P frame");
if([self initH264Decoder])
{
[self decode:frame withSize:frameSize];
}
break;
}
這里我們需要把前4個字節的開始碼轉為4字節大端的NALU長度(不包含開始碼)
2、初始化解碼器
①根據sps pps創建解碼視頻參數描述器
const uint8_t* const parameterSetPointers[2] = {_sps, _pps};
const size_t parameterSetSizes[2] = {_spsSize, _ppsSize};
OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2, parameterSetPointers, parameterSetSizes, 4, &_decoderFormatDescription);
②創建解碼器、設置解碼回調
// 從sps pps中獲取解碼視頻的寬高信息
CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(_decoderFormatDescription);
// kCVPixelBufferPixelFormatTypeKey 解碼圖像的采樣格式
// kCVPixelBufferWidthKey、kCVPixelBufferHeightKey 解碼圖像的寬高
// kCVPixelBufferOpenGLCompatibilityKey制定支持OpenGL渲染,經測試有沒有這個參數好像沒什么差別
NSDictionary* destinationPixelBufferAttributes = @{(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange), (id)kCVPixelBufferWidthKey : @(dimensions.width), (id)kCVPixelBufferHeightKey : @(dimensions.height),
(id)kCVPixelBufferOpenGLCompatibilityKey : @(YES)};
// 設置解碼輸出數據回調
VTDecompressionOutputCallbackRecord callBackRecord;
callBackRecord.decompressionOutputCallback = decodeOutputDataCallback;
callBackRecord.decompressionOutputRefCon = (__bridge void *)self;
// 創建解碼器
status = VTDecompressionSessionCreate(kCFAllocatorDefault, _decoderFormatDescription, NULL, (__bridge CFDictionaryRef)destinationPixelBufferAttributes, &callBackRecord, &_deocderSession);
// 解碼線程數量
VTSessionSetProperty(_deocderSession, kVTDecompressionPropertyKey_ThreadCount, (__bridge CFTypeRef)@(1));
// 是否實時解碼
VTSessionSetProperty(_deocderSession, kVTDecompressionPropertyKey_RealTime, kCFBooleanTrue);
3、將解析后的H264數據送入解碼器
比較簡單,直接看代碼
CMBlockBufferRef blockBuffer = NULL;
// 創建 CMBlockBufferRef
OSStatus status = CMBlockBufferCreateWithMemoryBlock(NULL, (void *)frame, frameSize, kCFAllocatorNull, NULL, 0, frameSize, FALSE, &blockBuffer);
if(status != kCMBlockBufferNoErr)
{
return;
}
CMSampleBufferRef sampleBuffer = NULL;
const size_t sampleSizeArray[] = {frameSize};
// 創建 CMSampleBufferRef
status = CMSampleBufferCreateReady(kCFAllocatorDefault, blockBuffer, _decoderFormatDescription , 1, 0, NULL, 1, sampleSizeArray, &sampleBuffer);
if (status != kCMBlockBufferNoErr || sampleBuffer == NULL)
{
return;
}
// VTDecodeFrameFlags 0為允許多線程解碼
VTDecodeFrameFlags flags = 0;
VTDecodeInfoFlags flagOut = 0;
// 解碼 這里第四個參數會傳到解碼的callback里的sourceFrameRefCon,可為空
OSStatus decodeStatus = VTDecompressionSessionDecodeFrame(_deocderSession, sampleBuffer, flags, NULL, &flagOut);
// Create了就得Release
CFRelease(sampleBuffer);
CFRelease(blockBuffer);
踩坑及總結
解碼還沒遇到什么問題,這一塊還是比較簡單的,如果疑惑請留言或私信。
下一篇我們來捋一下YUV數據是個什么玩意兒吧。
本文Demo地址:https://github.com/GenoChen/MediaService