音視頻學習之 - H264解碼
摘要:/** 根據sps pps設置解碼參數 param kCFAllocatorDefault 分配器 param 2 參數個數 param parameterSetPointers 參數集指針 param parameterSetSizes 參數集大小 param naluHeaderLen nalu nalu start code 的長度 4 param _decodeDesc 解碼器描述 return 狀態 */ OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2, parameterSetPointers, parameterSetSizes, naluHeaderLen, &_decodeDesc)。//增強信息 break。
- 解析數據 (SPS PPS NALU Unit)
- 初始化解碼器
- 將解析後的H264 NALU Unit輸入到解碼器
- 解碼完成後回調,輸出解碼數據
- 解碼數據顯示(OpenGL ES)
解析數據
直接在上一篇 音視頻學習之 - H264編碼 代碼基礎上微調進行解碼,即在生成sps/pps和視頻流二進制的地方不去存儲而是直接進行解碼,修改上一篇源碼中 函數 didCompressH264 的代碼:
獲取H264參數集合中的SPS和PPS:
const Byte startCode[] = "\x00\x00\x00\x01"; if (statusCode == noErr) { NSData *spsData = [NSData dataWithBytes:sparameterSet length:sparameterSetSize]; NSData *ppsData = [NSData dataWithBytes:pparameterSet length:pparameterSetSize]; NSMutableData *sps = [NSMutableData dataWithCapacity:4 + sparameterSetSize]; [sps appendBytes:startCode length:4]; [sps appendBytes:spsData length:sparameterSetSize]; NSMutableData *pps = [NSMutableData dataWithCapacity:4 + pparameterSetSize]; [pps appendBytes:startCode length:4]; [pps appendBytes:ppsData length:pparameterSetSize]; } 複製代碼
獲取NALU數據:
const int lengthInfoSize = 4; //循環獲取nalu數據 while (bufferOffset < totalLength - AVCCHeaderLength) { uint32_t NALUnitLength = 0; //讀取 一單元長度的 nalu memcpy(&NALUnitLength, dataPointer + bufferOffset, lengthInfoSize); //從大端模式轉換爲系統端模式 NALUnitLength = CFSwapInt32BigToHost(NALUnitLength); NSMutableData *data = [NSMutableData dataWithCapacity:lengthInfoSize + NALUnitLength]; [data appendBytes:startCode length:lengthInfoSize]; [data dataPointer + bufferOffset + lengthInfoSize length:NALUnitLength]; bufferOffset += lengthInfoSize + NALUnitLength; } 複製代碼
初始化解碼器
使用 VTDecompressionSessionCreate 創建一個解碼器,它的參數中需要一個 CMVideoFormatDescriptionRef 類型的變量來描述視頻的基本信息,所以我們要先準備一些創建session需要的數據,然後才能完成視頻的解碼。
/*初始化解碼器**/ - (BOOL)initDecoder { if (_decodeSesion) return true; const uint8_t * const parameterSetPointers[2] = {_sps, _pps}; const size_t parameterSetSizes[2] = {_spsSize, _ppsSize}; int naluHeaderLen = 4; /** 根據sps pps設置解碼參數 param kCFAllocatorDefault 分配器 param 2 參數個數 param parameterSetPointers 參數集指針 param parameterSetSizes 參數集大小 param naluHeaderLen nalu nalu start code 的長度 4 param _decodeDesc 解碼器描述 return 狀態 */ OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2, parameterSetPointers, parameterSetSizes, naluHeaderLen, &_decodeDesc); if (status != noErr) { NSLog(@"Video hard DecodeSession create H264ParameterSets(sps, pps) failed status= %d", (int)status); return false; } /* 解碼參數: * kCVPixelBufferPixelFormatTypeKey:攝像頭的輸出數據格式 kCVPixelBufferPixelFormatTypeKey,已測可用值爲 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,即420v kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,即420f kCVPixelFormatType_32BGRA,iOS在內部進行YUV至BGRA格式轉換 YUV420一般用於標清視頻,YUV422用於高清視頻,這裏的限制讓人感到意外。但是,在相同條件下,YUV420計算耗時和傳輸壓力比YUV422都小。 * kCVPixelBufferWidthKey/kCVPixelBufferHeightKey: 視頻源的分辨率 width*height * kCVPixelBufferOpenGLCompatibilityKey : 它允許在 OpenGL 的上下文中直接繪製解碼後的圖像,而不是從總線和 CPU 之間複製數據。這有時候被稱爲零拷貝通道,因爲在繪製過程中沒有解碼的圖像被拷貝. */ NSDictionary *destinationPixBufferAttrs = @{ (id)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange], //iOS上 nv12(uvuv排布) 而不是nv21(vuvu排布) (id)kCVPixelBufferWidthKey: [NSNumber numberWithInteger:_config.width], (id)kCVPixelBufferHeightKey: [NSNumber numberWithInteger:_config.height], (id)kCVPixelBufferOpenGLCompatibilityKey: [NSNumber numberWithBool:true] }; //解碼回調設置 /* VTDecompressionOutputCallbackRecord 是一個簡單的結構體,它帶有一個指針 (decompressionOutputCallback),指向幀解壓完成後的回調方法。你需要提供可以找到這個回調方法的實例 (decompressionOutputRefCon)。VTDecompressionOutputCallback 回調方法包括七個參數: 參數1: 回調的引用 參數2: 幀的引用 參數3: 一個狀態標識 (包含未定義的代碼) 參數4: 指示同步/異步解碼,或者解碼器是否打算丟幀的標識 參數5: 實際圖像的緩衝 參數6: 出現的時間戳 參數7: 出現的持續時間 */ VTDecompressionOutputCallbackRecord callbackRecord; callbackRecord.decompressionOutputCallback = videoDecompressionOutputCallback; callbackRecord.decompressionOutputRefCon = (__bridge void * _Nullable)(self); //創建session /*! @function VTDecompressionSessionCreate @abstract 創建用於解壓縮視頻幀的會話。 @discussion 解壓後的幀將通過調用OutputCallback發出 @param allocator 內存的會話。通過使用默認的kCFAllocatorDefault的分配器。 @param videoFormatDescription 描述源視頻幀 @param videoDecoderSpecification 指定必須使用的特定視頻解碼器.NULL @param destinationImageBufferAttributes 描述源像素緩衝區的要求 NULL @param outputCallback 使用已解壓縮的幀調用的回調 @param decompressionSessionOut 指向一個變量以接收新的解壓會話 */ status = VTDecompressionSessionCreate(kCFAllocatorDefault, _decodeDesc, NULL, (__bridge CFDictionaryRef _Nullable)(destinationPixBufferAttrs), &callbackRecord, &_decodeSesion); //判斷一下status if (status != noErr) { NSLog(@"Video hard DecodeSession create failed status= %d", (int)status); return false; } //設置解碼會話屬性(實時編碼) status = VTSessionSetProperty(_decodeSesion, kVTDecompressionPropertyKey_RealTime,kCFBooleanTrue); NSLog(@"Vidoe hard decodeSession set property RealTime status = %d", (int)status); return true; } 複製代碼
數據輸入
獲取到數據之後,開始進行數據處理,前四位代表的是大端模式下的長度信息,第5個字節表示數據類型,轉換爲10進制後,5代表關鍵幀,7代表sps,8代表pps:
- (void)decodeNaluData:(NSData *)frame { //將解碼放在異步隊列. dispatch_async(_decodeQueue, ^{ //獲取frame 二進制數據 uint8_t *nalu = (uint8_t *)frame.bytes; //調用解碼Nalu數據方法,參數1:數據 參數2:數據長度 [self decodeNaluData:nalu size:(uint32_t)frame.length]; }); } - (void)decodeNaluData:(uint8_t *)frame size:(uint32_t)size { int type = (frame[4] & 0x1F); // 將NALU的開始碼轉爲4字節大端NALU的長度信息 uint32_t naluSize = size - 4; uint8_t *pNaluSize = (uint8_t *)(&naluSize); CVPixelBufferRef pixelBuffer = NULL; frame[0] = *(pNaluSize + 3); frame[1] = *(pNaluSize + 2); frame[2] = *(pNaluSize + 1); frame[3] = *(pNaluSize); //第一次解析時: 初始化解碼器initDecoder switch (type) { case 0x05: //關鍵幀 if ([self initDecoder]) { pixelBuffer= [self decode:frame withSize:size]; } break; case 0x06: //NSLog(@"SEI");//增強信息 break; case 0x07: //sps _spsSize = naluSize; _sps = malloc(_spsSize); memcpy(_sps, &frame[4], _spsSize); break; case 0x08: //pps _ppsSize = naluSize; _pps = malloc(_ppsSize); memcpy(_pps, &frame[4], _ppsSize); break; default: //其他幀(1-5) if ([self initDecoder]) { pixelBuffer = [self decode:frame withSize:size]; } break; } } 複製代碼
解碼函數
先來看一下CMSampleBuffer的數據結構
最終要解碼成一個CVPixelBufferRef類型的對象,先創建一個BlockBuffer,然後根據BlockBuffer創建SampleBuffer,最後將SampleBuffer傳入函數 VTDecompressionSessionDecodeFrame 得到解碼後的CVPixelBufferRef
- (CVPixelBufferRef)decode:(uint8_t *)frame withSize:(uint32_t)frameSize { CVPixelBufferRef outputPixelBuffer = NULL; CMBlockBufferRef blockBuffer = NULL; CMBlockBufferFlags flag0 = 0; //創建blockBuffer /*! 參數1: structureAllocator kCFAllocatorDefault 參數2: memoryBlock frame 參數3: frame size 參數4: blockAllocator: Pass NULL 參數5: customBlockSource Pass NULL 參數6: offsetToData 數據偏移 參數7: dataLength 數據長度 參數8: flags 功能和控制標誌 參數9: newBBufOut blockBuffer地址,不能爲空 */ OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, frame, frameSize, kCFAllocatorNull, NULL, 0, frameSize, flag0, &blockBuffer); if (status != kCMBlockBufferNoErr) { NSLog(@"Video hard decode create blockBuffer error code=%d", (int)status); return outputPixelBuffer; } CMSampleBufferRef sampleBuffer = NULL; const size_t sampleSizeArray[] = {frameSize}; //創建sampleBuffer /* 參數1: allocator 分配器,使用默認內存分配, kCFAllocatorDefault 參數2: blockBuffer.需要編碼的數據blockBuffer.不能爲NULL 參數3: formatDescription,視頻輸出格式 參數4: numSamples.CMSampleBuffer 個數. 參數5: numSampleTimingEntries 必須爲0,1,numSamples 參數6: sampleTimingArray. 數組.爲空 參數7: numSampleSizeEntries 默認爲1 參數8: sampleSizeArray 參數9: sampleBuffer對象 */ status = CMSampleBufferCreateReady(kCFAllocatorDefault, blockBuffer, _decodeDesc, 1, 0, NULL, 1, sampleSizeArray, &sampleBuffer); if (status != noErr || !sampleBuffer) { NSLog(@"Video hard decode create sampleBuffer failed status=%d", (int)status); CFRelease(blockBuffer); return outputPixelBuffer; } //解碼 //向視頻解碼器提示使用低功耗模式是可以的 VTDecodeFrameFlags flag1 = kVTDecodeFrame_1xRealTimePlayback; //異步解碼 VTDecodeInfoFlags infoFlag = kVTDecodeInfo_Asynchronous; //解碼數據 /* 參數1: 解碼session 參數2: 源數據 包含一個或多個視頻幀的CMsampleBuffer 參數3: 解碼標誌 參數4: 解碼後數據outputPixelBuffer 參數5: 同步/異步解碼標識 */ status = VTDecompressionSessionDecodeFrame(_decodeSesion, sampleBuffer, flag1, &outputPixelBuffer, &infoFlag); if (status == kVTInvalidSessionErr) { NSLog(@"Video hard decode InvalidSessionErr status =%d", (int)status); } else if (status == kVTVideoDecoderBadDataErr) { NSLog(@"Video hard decode BadData status =%d", (int)status); } else if (status != noErr) { NSLog(@"Video hard decode failed status =%d", (int)status); } CFRelease(sampleBuffer); CFRelease(blockBuffer); return outputPixelBuffer; } 複製代碼
OpenGL ES進行顯示
實際上就是顯示紋理,將生成的CVPixelBufferRef轉換爲紋理對象,然後使用 CAEAGLLayer 進行顯示,具體顯示代碼下一篇文章來實現。