音视频学习之 - 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 进行显示,具体显示代码下一篇文章来实现。