解码流程概要
- 从解码前视频队列里取出视频包
- 解析NALU,获取vps、sps、pps、sei等信息
- 根据获取到的参数信息初始化解码器参数CMVideoFormatDescriptionRef
- 创建解码器
- 解码
详情
一、从解码前视频队列里取出视频包
通常取出的是AVPacket或者封装的视频包结构体对象,无论是h264还是hevc一个视频包里都只有一个视频帧(音频包可能有多个音频帧),必要信息:数据帧、数据长度、显示时间戳、解码时间戳、数据的时长
二、解析NALU,获取vps、sps、pps、sei等信息
根据codecType获取naluType,如下codecType为h264或者hevc,data是码流的nalu头,转成十进制即是我们要的naluId
int naluId = -1;
if(codecType == VT_CODEC_TYPE_H264)
{
naluId = data & 0x1F;
}
if(codecType == VT_CODEC_TYPE_H265)
{
naluId = (data & 0x7E) >> 1;
}
naluId对应的naluType如下:
int naluType = VT_NALU_TYPE_UNK;
if(codecType == VT_CODEC_TYPE_H264)
{
switch (val)
{
case 7:
naluType = VT_NALU_TYPE_SPS;
break;
case 8:
naluType = VT_NALU_TYPE_PPS;
break;
case 5:
naluType = VT_NALU_TYPE_IDR;
break;
case 1:
naluType = VT_NALU_TYPE_NONIDR;
default:
break;
}
}
else if(codecType == VT_CODEC_TYPE_H265)
{
if(val <= 9)
naluType = VT_NALU_TYPE_NONIDR;
else if(val >= 16 && val <= 23)
naluType = VT_NALU_TYPE_IDR;
else if(val == 33)
naluType = VT_NALU_TYPE_SPS;
else if(val == 34)
naluType = VT_NALU_TYPE_PPS;
else if(val == 32)
naluType = VT_NALU_TYPE_VPS;
}
如果是hevc,解析到参数的顺序是vps、sps、pps,保存起来用于创建解码器参数配置
三、根据获取到的参数信息初始化解码器参数CMVideoFormatDescriptionRef
CMVideoFormatDescriptionRef input_format = nullptr;
OSStatus status = -1;
if(codecType == VT_CODEC_TYPE_H264)
{
int count = 1 + _ppsNums;
uint8_t **parameterSetPointers =(uint8_t**) malloc(count * sizeof(uint8_t *));
size_t *parameterSetSizes = (size_t *)malloc(count * sizeof(size_t));
int i = 1;
parameterSetPointers[0] = _sps;
parameterSetSizes[0] = _spsSize;
for(i ; i < count ; i++)
{
parameterSetSizes[i] = _ppsSize[i-1];
parameterSetPointers[i] = _pps[i-1];
}
status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
count,
parameterSetPointers,
parameterSetSizes,
4,
&input_format);
free(parameterSetPointers);
free(parameterSetSizes);
}
else if(codecType == VT_CODEC_TYPE_H265)
{
int count = 2 + _ppsNums;
uint8_t **parameterSetPointers =(uint8_t**) malloc(count * sizeof(uint8_t *));
size_t *parameterSetSizes = (size_t *)malloc(count * sizeof(size_t));
int i = 2;
parameterSetPointers[0] = _vps;
parameterSetPointers[1] = _sps;
parameterSetSizes[0] = _vpsSize;
parameterSetSizes[1] = _spsSize;
for(i ; i < count ; i++)
{
parameterSetSizes[i] = _ppsSize[i-2];
parameterSetPointers[i] = _pps[i-2];
}
status = CMVideoFormatDescriptionCreateFromHEVCParameterSets(kCFAllocatorDefault,
count,
parameterSetPointers,
parameterSetSizes,
4,
NULL,
&input_format);
free(parameterSetPointers);
free(parameterSetSizes);
}
四、创建解码器
苹果建议用kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange(nv12)类型解码,根据上面的input_format创建解码器session并且设置解码回调didDecompress
CFDictionaryRef attrs = NULL;
const void *keys[] = { kCVPixelBufferPixelFormatTypeKey };
uint32_t v = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
const void *values[] = { CFNumberCreate(NULL, kCFNumberSInt32Type, &v) };
attrs = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
VTDecompressionOutputCallbackRecord callBackRecord;
callBackRecord.decompressionOutputCallback = didDecompress;
callBackRecord.decompressionOutputRefCon = this;
OSStatus status = VTDecompressionSessionCreate(kCFAllocatorDefault,
input_format,
NULL,
attrs,
&callBackRecord,
&_deocderSession);
CFRelease(attrs);
五、解码
解码过程中可以动态配置解码参数,苹果官方文档定义如下
//解码参数
typedef CF_OPTIONS(uint32_t, VTDecodeFrameFlags) {
kVTDecodeFrame_EnableAsynchronousDecompression = 1<<0,
kVTDecodeFrame_DoNotOutputFrame = 1<<1,
kVTDecodeFrame_1xRealTimePlayback = 1<<2,
kVTDecodeFrame_EnableTemporalProcessing = 1<<3,
};
解释一下上面的参数
kVTDecodeFrame_EnableAsynchronousDecompression
允许异步解码,解码器默认是同步解码,即解码之后同步回调,设置允许异步解码之后,解码器会同时解码几帧数据,带来的后果是,解码总体时间更短,但是前面几帧回调的时间可能长一些
kVTDecodeFrame_DoNotOutputFrame
通知解码器不要输出帧,设置之后解码回调会返回NULL,某些情况我们不需要解码器输出帧,比如发生解码器状态错误的时候
kVTDecodeFrame_1xRealTimePlayback
通知解码器使用低功耗模式,设置之后处理器消耗会变少,解码速度会变慢,通常我们不会设置这个参数,因为硬解码使用的是专用处理器,不消耗cpu,所以越快越好
kVTDecodeFrame_EnableTemporalProcessing
通知解码器需要处理帧序,设置之后解码回调会变慢,因为无论是异步解码还是pts、dts不等的时候都需要进行帧排序,会耗时,苹果官方文档不建议我们使用这个参数
解码流程如下
void *sourceRef = NULL;
CMBlockBufferRef blockBuffer = NULL;
CMSampleTimingInfo timingInfo;
timingInfo.presentationTimeStamp = CMTimeMake(pts, 1000);
timingInfo.duration = CMTimeMake(duration, 1000);
timingInfo.decodeTimeStamp = CMTimeMake(dts, 1000);
OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault,
(void*)pBuffer,
size,
kCFAllocatorNull,
NULL,
0,
size,
0,
&blockBuffer);
if(status == kCMBlockBufferNoErr)
{
CMSampleBufferRef sampleBuffer = NULL;
const size_t sampleSizeArray[] = {size};
status = CMSampleBufferCreateReady(kCFAllocatorDefault,
blockBuffer,
_decoderFormatDescription ,
1,
1,
&timingInfo,
1,
sampleSizeArray,
&sampleBuffer);
if (status == kCMBlockBufferNoErr && sampleBuffer)
{
VTDecodeFrameFlags flags = 0;
VTDecodeInfoFlags flagOut = 0;
if(m_useAsync)
flags |= kVTDecodeFrame_EnableAsynchronousDecompression;
if(m_bNeedRefresh || m_bNeedRecovery)
{
flags |= kVTDecodeFrame_DoNotOutputFrame;
}
OSStatus decodeStatus = VTDecompressionSessionDecodeFrame(_deocderSession,
sampleBuffer,
flags,
&sourceRef,
&flagOut);
if(decodeStatus == kVTInvalidSessionErr)
{
m_bNeedRefresh = 1;
}
else if(decodeStatus == kVTVideoDecoderBadDataErr)
{
NSLog(@"VT decoder failed kVTVideoDecoderBadDataErr \n");
}
CFRelease(sampleBuffer);
}
CFRelease(blockBuffer);
}
关键流程处理
1、解码器参数更新策略
对于直播场景,观众端看播可能是任意时间节点的,这意味着,客户端播放器需要支持任意节点解码直播流,这就需要推流端每个关键帧前面都要加上vps、sps、pps信息,客户端存储这些参数信息,每次解析到相关参数时候用于对比,如果参数有更新,重新初始化解码器
对于点播而言,通常vps、sps、pps信息指在视频头有一份,解析一遍即可,但是也不完全如此,因为这个点播可能是直播生成的回放,这中情况就必须与直播的策略相同,否侧会解码失败或者花屏
2、app进入后台策略
iOS硬解码是支持后台解码的,当程序进入后台之后解码器会返kVTInvalidSessionErr
错误,
注意:这与程序是否有后台权限无关,这时候需要保留数据包,重新创建解码器,再行解码
3、帧排序策略
通常直播场景我们是不会编b帧进来的,延时会变大,h264的一般用baseline,iOS平台参数kVTProfileLevel_H264_Baseline_AutoLevel
,除了baseline,其他规格的都需要解码后帧排序,hevc在iOS平台只开放两种kVTProfileLevel_HEVC_Main_AutoLevel``kVTProfileLevel_HEVC_Main10_AutoLevel
profile,都是支持b帧的,所以需要帧排序
4、解码暂停策略
解码器没有暂停功能,进来的帧只能解码或者丢掉,所以解码器管理模块需要设计一个停止给解码器输入数据模块,通过我们的测试,解码后视频队列大于3帧的时候,就不在输入数据给解码器,可以保证内存占用不多并且流畅播放
网友评论