美文网首页音视频开发
iOS硬解码 videoToolBox h264 hevc

iOS硬解码 videoToolBox h264 hevc

作者: 野码道人 | 来源:发表于2019-12-18 19:33 被阅读0次

解码流程概要

  • 从解码前视频队列里取出视频包
  • 解析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_AutoLevelprofile,都是支持b帧的,所以需要帧排序

4、解码暂停策略

解码器没有暂停功能,进来的帧只能解码或者丢掉,所以解码器管理模块需要设计一个停止给解码器输入数据模块,通过我们的测试,解码后视频队列大于3帧的时候,就不在输入数据给解码器,可以保证内存占用不多并且流畅播放

相关文章

网友评论

    本文标题:iOS硬解码 videoToolBox h264 hevc

    本文链接:https://www.haomeiwen.com/subject/pvaunctx.html