VideoToolbox硬解码H264流播放

VideoToolBox硬解码H264

对于H264和VideoToolBox还不熟悉的童鞋一定下先看下
H264基础简介
iOS-VideoToolbox硬编码H264

我们先看下demo效果
demo效果

整体的流程是:

从摄像头获取视频裸数据->使用VideoToolBox编码成H264->使用VideoToolBox解码成image->使用openGLES绘制展示

demo中上面的是实时摄像头的预览图,下方的是解码后渲染图,gif不能显示完整,可以下载demo代码看详细效果。

VideoToolbox解码主要流程

编码部分可以看上一篇的文章。这里重点讲一下解码流程

//解码nalu裸数据
-(void) decodeNalu:(uint8_t *)frame size:(uint32_t)frameSize

传入nalu的裸数据和数据frameSize

我们知道

一个原始的NALU单元结构如下
[StartCode][NALU Header][NALU Payload]三部分。

StartCode,是一个NALU单元开始,必须是00 00 00 01 或者00 00 01。

对于VideoToolBox的NALU前四个字节并不是StartCode,而是FrameSize,所以这里我们写入frameSize到前四个字节中

//填充nalu size 去掉start code 替换成nalu size
    uint32_t nalSize = (uint32_t)(frameSize - 4);
    uint8_t *pNalSize = (uint8_t*)(&nalSize);
    frame[0] = *(pNalSize + 3);
    frame[1] = *(pNalSize + 2);
    frame[2] = *(pNalSize + 1);
    frame[3] = *(pNalSize);

再次之前我们先读取NALU Header判断类型

    //获取nalu type
    int nalu_type = (frame[4] & 0x1F);

区分关键帧还是sps和pps,已经B,P其他帧

switch (nalu_type)
    {
        case 0x05:
            //关键帧
            if([self initH264Decoder])
            {
                pixelBuffer = [self decode:frame size:frameSize];
            }
            break;
        case 0x07:
            //sps
            _spsSize = frameSize - 4;
            _sps = malloc(_spsSize);
            memcpy(_sps, &frame[4], _spsSize);
            break;
        case 0x08:
        {
            //pps
            _ppsSize = frameSize - 4;
            _pps = malloc(_ppsSize);
            memcpy(_pps, &frame[4], _ppsSize);
            break;
        }
        default:
        {
            // B/P frame
            if([self initH264Decoder])
            {
                pixelBuffer = [self decode:frame size:frameSize];
            }
            break;
        }

这里我们可以看到读到关键帧或者B/P其他视频帧的时候我们才去initH264Decoder,初始化VideoToolBox解码,这是因为sps和pps里面包含了视频宽高,以及解码相关参数,必须先获取到sps和pps构建CMVideoFormatDescriptionRef,才能初始化VideoToolBox解码session

封装CMVideoFormatDescriptionRef

@interface H264DecodeTool(){
    
    //解码session
    VTDecompressionSessionRef _decoderSession;
    
    //解码format 封装了sps和pps
    CMVideoFormatDescriptionRef _decoderFormatDescription;
    
    //sps & pps
    uint8_t *_sps;
    NSInteger _spsSize;
    uint8_t *_pps;
    NSInteger _ppsSize;
    
}

 const uint8_t* const parameterSetPointers[2] = { _sps, _pps };
    const size_t parameterSetSizes[2] = { _spsSize, _ppsSize };
    
    //用sps 和pps 实例化_decoderFormatDescription
    OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
                                                                          2, //参数个数
                                                                          parameterSetPointers,
                                                                          parameterSetSizes,
                                                                          4, //nal startcode开始的size
                                                                          &_decoderFormatDescription);

初始化VideoToolBox Session

NSDictionary* destinationPixelBufferAttributes = @{
                                                           (id)kCVPixelBufferPixelFormatTypeKey : [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange],
                                                           //硬解必须是 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
                                                           //                                                           或者是kCVPixelFormatType_420YpCbCr8Planar
                                                           //因为iOS是  nv12  其他是nv21
                                                           (id)kCVPixelBufferWidthKey : [NSNumber numberWithInt:1280],
                                                           (id)kCVPixelBufferHeightKey : [NSNumber numberWithInt:960],
                                                           //这里宽高和编码反的 两倍关系
                                                           (id)kCVPixelBufferOpenGLCompatibilityKey : [NSNumber numberWithBool:YES]
                                                           };

        
        
        VTDecompressionOutputCallbackRecord callBackRecord;
        callBackRecord.decompressionOutputCallback = didDecompress;
        callBackRecord.decompressionOutputRefCon = (__bridge void *)self;
        status = VTDecompressionSessionCreate(kCFAllocatorDefault,
                                              _decoderFormatDescription,
                                              NULL,
                                              (__bridge CFDictionaryRef)destinationPixelBufferAttributes,
                                              &callBackRecord,
                                              &_decoderSession);
        VTSessionSetProperty(_decoderSession, kVTDecompressionPropertyKey_ThreadCount, (__bridge CFTypeRef)[NSNumber numberWithInt:1]);
        VTSessionSetProperty(_decoderSession, kVTDecompressionPropertyKey_RealTime, kCFBooleanTrue);

iOS下硬解码只可以使用:
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:YUV420P
kCVPixelFormatType_420YpCbCr8Planar:NV12

YUV420P和NV12是两种不同的图像数据格式,有兴趣的童鞋可以自行查阅下资料。

需要注意的是kCVPixelBufferWidthKey,kCVPixelBufferHeightKey这里指定的宽和高,和实际视频的宽高是反的,两倍关系。
我们录制的视频是640 * 480,所以这里传入1280和960

解码回调

//解码回调
static void didDecompress( void *decompressionOutputRefCon, void *sourceFrameRefCon, OSStatus status, VTDecodeInfoFlags infoFlags, CVImageBufferRef pixelBuffer, CMTime presentationTimeStamp, CMTime presentationDuration ){
    CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon;
    
    //持有pixelBuffer数据,否则会被释放
    *outputPixelBuffer = CVPixelBufferRetain(pixelBuffer);
    H264DecodeTool *decoder = (__bridge H264DecodeTool *)decompressionOutputRefCon;
    if (decoder.delegate)
    {
        [decoder.delegate gotDecodedFrame:pixelBuffer];
    }
}

这里retain一次回调的pixelBuffer,也就是图像裸数据。然后回调。

渲染

渲染部分使用了APPLE的一个demo Layer,渲染CVImageBufferRef,原理是使用opengl。这块后面在OpenGL专题再做详解,这里不再累述。

总结

H264编码是很复杂的,但是由于框架的封装,事实上平时我们项目中使用的现有API硬件编解码也还是很方便的。理解了流程和原理是最重要的。当然demo仅仅是实现了基本编解码,很多异常处理,例如退到后台,session报错异常,前台恢复等在实际商业项目中是必然需要考虑的。

demo下载地址:iOS-VideoToolBox-demo
也来练习下吧。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 119,988评论 1 241
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 52,451评论 1 202
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 75,316评论 0 168
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 36,762评论 0 128
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 43,589评论 1 207
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 36,153评论 1 127
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 28,125评论 2 209
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 27,241评论 0 120
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 26,193评论 5 173
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 30,305评论 0 179
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 27,513评论 1 170
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 28,746评论 1 179
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 23,040评论 0 25
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 25,664评论 2 166
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 29,545评论 3 174
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 24,525评论 0 4
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 24,506评论 0 113
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 30,764评论 2 191
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 31,153评论 2 189