VideoToolBox H264 硬解码

2,148 阅读5分钟

一. 主要函数说明

  • 创建解码描述器

使用CMVideoFormatDescriptionCreateFromH264ParameterSets()创建解码描述器。

 CMVideoFormatDescriptionCreateFromH264ParameterSets(
     //  分配器
     CFAllocatorRef  _Nullable allocator,
     //  参数个数
     size_t parameterSetCount,
     //  参数集指针
     const uint8_t *const  _Nonnull * _Nonnull parameterSetPointers,
     //  参数集中每个元素大小的集合
     const size_t * _Nonnull parameterSetSizes,
     //  NAL单元头部的长度
     int NALUnitHeaderLength,
     //  接受生成的描述器的地址
     CMFormatDescriptionRef  _Nullable * _Nonnull formatDescriptionOut
 )
  • 创建会话

使用VTDecompressionSessionCreate()创建解码会话。

 VTDecompressionSessionCreate(
     //  分配器
     CFAllocatorRef  _Nullable allocator,
     //  解码描述器
     CMVideoFormatDescriptionRef  _Nonnull videoFormatDescription,
     //  必须使用的特殊的解码器,若传入NULL,表示让VideoToolBox自行选择
     CFDictionaryRef  _Nullable videoDecoderSpecification,
     //  对图像缓冲区的需求,若传入NULL,表示没有需求
     CFDictionaryRef  _Nullable destinationImageBufferAttributes,
     //  包含解码后的回调函数的结构体
     const VTDecompressionOutputCallbackRecord * _Nullable outputCallback,
     //  接受创建的会话的地址
     VTDecompressionSessionRef  _Nullable * _Nonnull decompressionSessionOut
 )
  • 设置解码会话属性

使用VTSessionSetProperty()来完成编码属性的设置。

 VTSessionSetProperty(
	 VTSessionRef  _Nonnull session,    //  要设置的编码会话
	 CFStringRef  _Nonnull propertyKey, //  属性的key
	 CFTypeRef  _Nullable propertyValue //  属性的value
 )
  • 创建CMBlockBuffer

使用CMBlockBufferCreateWithMemoryBlock()创建CMBlockBuffer

CMBlockBufferCreateWithMemoryBlock(
    //  分配器
    CFAllocatorRef  _Nullable structureAllocator,
    //  内存块
    void * _Nullable memoryBlock,
    //  内存块大小
    size_t blockLength,
    //  内存块的分配器,若传NULL,表示使用默认的分配器
    CFAllocatorRef  _Nullable blockAllocator,
    //  自定义的内存块指针,若不传NULL,该指针将会被用来创建和释放内存块
    const CMBlockBufferCustomBlockSource * _Nullable customBlockSource,
    //  数据偏移
    size_t offsetToData,
    //  数据长度
    size_t dataLength,
    //  特征和功能的标识
    CMBlockBufferFlags flags,
    //  用来接受生成的CMBlockBuffer的地址
    CMBlockBufferRef  _Nullable * _Nonnull blockBufferOut
 )

  • 创建CMSampleBuffer

使用CMSampleBufferCreateReady()创建CMSampleBuffer

CMSampleBufferCreateReady(
    //  分配器
    CFAllocatorRef  _Nullable allocator,
    //  blockBuffer
    CMBlockBufferRef  _Nullable dataBuffer,
    //  解码描述器描述器
    CMFormatDescriptionRef  _Nullable formatDescription,
    //  CMSampleBuffer 个数
    CMItemCount numSamples,
    //  sampleTimingArray的入口,必须为0、1或者numSamples
    CMItemCount numSampleTimingEntries,
    //  样本信息的数组,可以传NULL
    const CMSampleTimingInfo * _Nullable sampleTimingArray,
    //  sampleSizeArray入口的个数,默认为1.(必须为0、1或者numSamples)
    CMItemCount numSampleSizeEntries,
    //  存储内存块大小的数组
    const size_t * _Nullable sampleSizeArray,
    //  接受生成的CMSampleBuffer的地址
    CMSampleBufferRef  _Nullable * _Nonnull sampleBufferOut
 )
  • 解码

使用VTDecompressionSessionDecodeFrame()执行解码操作。

VTDecompressionSessionDecodeFrame(
    VTDecompressionSessionRef  _Nonnull session,   //  解码会话
    CMSampleBufferRef  _Nonnull sampleBuffer,      //  要解码的内容CMSampleBuffer
    VTDecodeFrameFlags decodeFlags,                //  指示解码器同步或异步的标识,若没有指定,回调方法会在该函数完成前被调用
    void * _Nullable sourceFrameRefCon,            //  接受解码后数据的地址
    VTDecodeInfoFlags * _Nullable infoFlagsOut     //  接受解码操作是同步/异步的地址,若传NULL,标识不接受此信息
)
  • 结束解码

使用VTDecompressionSessionInvalidate()结束解码。

VTDecompressionSessionInvalidate(
    VTDecompressionSessionRef  _Nonnull session //  解码会话
)
  • 回调函数结构体
typedef void (*VTDecompressionOutputCallback)(

    void * CM_NULLABLE decompressionOutputRefCon,   //  回调函数的引用数据
    void * CM_NULLABLE sourceFrameRefCon,           //  接受解码后数据的地址
    OSStatus status,                                //  解码执行结果
    VTDecodeInfoFlags infoFlags,                    //  解码操作的信息(异步/被丢弃/可安全修改)
    CM_NULLABLE CVImageBufferRef imageBuffer,       //  解码后的结果
    CMTime presentationTimeStamp,                   //  展示的时间戳
    CMTime presentationDuration                     //  展示的持续时间
);

struct VTDecompressionOutputCallbackRecord {
    CM_NULLABLE VTDecompressionOutputCallback  decompressionOutputCallback; //  回调函数
    void * CM_NULLABLE                         decompressionOutputRefCon;   //  回调函数的引用数据
};
typedef struct VTDecompressionOutputCallbackRecord VTDecompressionOutputCallbackRecord;

二. 解码流程

解码流程

三. 具体实现

1. 解析数据

我们拿到的数据一般都是一个NSData,所以需要先转换成解码器可以解码的数据。

  • 首先将NSData转换成二进制字节流,并且拿到字节流的长度。
uint8_t *nalUnit = (uint8_t *)data.bytes;
uint32_t nalUnitLength = (uint32_t)data.length;
  • 获取NAL单元的类型,并做小端到大端的转换。
int nalType = nalUnit[4] & 0x1F;
       
uint32_t nalSize = nalUnitLength - 4;
uint8_t *nalSizePointer = (uint8_t *)(&nalSize);
	
nalUnit[0] = *(nalSizePointer + 3);
nalUnit[1] = *(nalSizePointer + 2);
nalUnit[2] = *(nalSizePointer + 1);
nalUnit[3] = *(nalSizePointer + 0);

关于类型,可以参考这个下面的表。

nal_unit_type 类型
0 未定义
1 非IDA图像中不采用数据划分片段
2 非IDA图像中A类数据划分片段
3 非IDA图像中B类数据划分片段
4 非IDA图像中C类数据划分片段
5 IDA图像的片(I帧/关键帧)
6 补充增强信息单元(SEI)
7 序列餐数据(SPS)
8 图像参数集(PPS)
9 分界符
10 序列结束
11 码流结束
12 填充
13-23 保留
24-31 不保留(RTP打包时会用到)

其中,nal_unit_type = 6时,类型为补充增强信息单元(SEI),是没有图像数据信息的,单独处理没有意义。

根据上表,我们可以拿到SPS和PPS保存下来,SEI不做处理,其余都进行解码操作。

2. 创建解码描述器

我们拿到了SPS和PPS,就可以使用它们创建解码描述器了。

CMVideoFormatDescriptionRef decodeDesc;
const uint8_t * parameterSetPointers[] = {self.sps, self.pps};
const size_t parameterSetSizes[] = {self.spsSize, self.ppsSize};
int  NALUnitLength = 4;

OSStatus status  = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2, parameterSetPointers, parameterSetSizes, NALUnitLength, &decodeDesc);

if (status != noErr) {
    NSLog(@"format description create error [ status : %d ]", status);
}

3. 创建会话

VTDecompressionSessionRef decodeSession;

NSDictionary *destinationImageBufferAttributes = @{
    //  摄像头的输出数据格式
    (id)kCVPixelBufferPixelFormatTypeKey : [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange],
    //  图像数据的宽
    (id)kCVPixelBufferWidthKey : [NSNumber numberWithInteger:self.width],
    //  图像数据的高
    (id)kCVPixelBufferHeightKey : [NSNumber numberWithInteger:self.height],
    //  是否允许OpenGL直接绘制解码后的图像
    (id)kCVPixelBufferOpenGLCompatibilityKey : [NSNumber numberWithBool:YES]
};

VTDecompressionOutputCallbackRecord record;
record.decompressionOutputCallback = decodeComplete;
record.decompressionOutputRefCon = (__bridge void * _Nullable)self;

OSStatus status = VTDecompressionSessionCreate(kCFAllocatorDefault, self.decodeDesc, NULL, (__bridge CFDictionaryRef _Nullable)(destinationImageBufferAttributes), &record, &decodeSession);

if (status != noErr) {
    NSLog(@"decode session create error! [ ststua : %d ]", status);
}

4. 设置解码会话属性

OSStatus status = VTSessionSetProperty(decodeSesion, kVTDecompressionPropertyKey_RealTime,kCFBooleanTrue);

NSLog(@"decode session set property error! [ status : %d ]", status);

5. 创建CMSampleBuffer

  • 创建CMBlockBuffer
CMBlockBufferRef blockBuffer = NULL;
CMBlockBufferFlags blockBufferFlags = 0;

OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, nalUnit, nalUnitLength, kCFAllocatorNull, NULL, 0, nalUnitLength, blockBufferFlags, &blockBuffer);

if (status != noErr) {
    NSLog(@"blockBuffer create error! [ status : %d ]", status);
}
  • 创建CMSampleBuffer
CMSampleBufferRef sampleBuffer = NULL;
const size_t sampleSizeArray[] = {nalUnitLength};

OSStatus status = CMSampleBufferCreateReady(kCFAllocatorDefault, blockBuffer, self.decodeDesc, 1, 0, NULL, 1, sampleSizeArray, &sampleBuffer);

if (status != noErr) {
    NSLog(@"sampleBuffer create error! [ status : %d ]", status);
}

6. 解码

拿到了CMSampleBuffer后,我们就可以解码了。

VTDecodeFrameFlags frameFlag = kVTDecodeFrame_1xRealTimePlayback;
VTDecodeInfoFlags infoFlag = kVTDecodeInfo_Asynchronous;

status = VTDecompressionSessionDecodeFrame(decodeSession, sampleBuffer, frameFlag, NULL, &infoFlag);

if (status == kVTInvalidSessionErr) {
    NSLog(@"decode invalid iession error! [ status : %d ]", status);
} else if (status == kVTVideoDecoderBadDataErr) {
    NSLog(@"decode  bad data error! [ status : %d ]", status);
} else if (status != noErr) {
    NSLog(@"decode frame error! [ status : %d ]", status);
}

CFRelease(blockBuffer);
CFRelease(sampleBuffer);

7. 解码后处理

解码后的数据就会输出到我们的回调函数中,我们可以在会调函数中根据需求进行Open GL渲染或者直接返回。

void decodeComplete(void * CM_NULLABLE decompressionOutputRefCon, void * CM_NULLABLE sourceFrameRefCon, OSStatus status, VTDecodeInfoFlags infoFlags, CM_NULLABLE CVImageBufferRef imageBuffer, CMTime presentationTimeStamp, CMTime presentationDuration) {
    
    if (status != noErr) {
        NSLog(@"decode callback error! [ status : %d ]", status);
    }
    
    KKKVideoDecoder *decoder = (__bridge KKKVideoDecoder *)decompressionOutputRefCon;
    
    dispatch_async(decoder.callBackQueue, ^{
        
        [decoder.delegate decoderGetImageBuffer:imageBuffer];
    });
}

8. 结束解码

dealloc方法中,我们就可以结束编码,并且释放编码会话。

- (void)dealloc {
    
    if (decodeSession) {
        VTDecompressionSessionInvalidate(decodeSession);
        CFRelease(decodeSession);
        decodeSession = nil;
    }
}