VideoToolbox硬解码H264流播放

转载自:简书-VideoToolbox硬解码H264流播放

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

我们先看下demo效果


iPhone.gif

整体的流程是:

从摄像头获取视频裸数据->使用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
也来练习下吧。

作者:安东_Ace
链接:https://www.jianshu.com/p/72f02136eaf8
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

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

推荐阅读更多精彩内容