VideoToolBox H264 硬解码

一. 主要函数说明

  • 创建解码描述器

使用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;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容