iOS視頻開發(三):視頻H264硬解碼

前言

系列文章:
《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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容