直接在上一篇音视频学习之 - H264编码代码基础上微调进行解码,即在生成sps/pps和视频流二进制的地方不去存储而是直接进行解码,修改上一篇源码中 函数didCompressH264 的代码:数组
获取H264参数集合中的SPS和PPS:bash
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数据:session
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:app
- (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获得解码后的CVPixelBufferRefasync
- (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;
}
复制代码
实际上就是显示纹理,将生成的CVPixelBufferRef转换为纹理对象,而后使用CAEAGLLayer进行显示,具体显示代码下一篇文章来实现。ide