iOS 视频硬解码

公司的项目里有拉取H.264视频流进行解码播放的功能,本来是采用FFMpeg多媒体库,用CPU做视频的编码和解码,就是大家常说的软编软解。但是软解存在太占用CPU,解码效率低等缺点,所以我们一合计干脆用硬解码代替原来的方案。当然硬件解码使用的当然就是苹果大名鼎鼎的Video ToolBox框架,众所周知,苹果在iOS8开始才可以在iOS系统中调用该框架中的API

Video ToolBox H.264解码

iOS媒体接口结构.png
AVFoundation:
  • 解压视频后直接播放
  • 直接将视频压缩成文件
Video Toolbox:
  • 将视频解压成 CVPixelBuffer
  • 直接将视频压缩成CMSampleBuffer
Video ToolBox 数据结构
  • CVPixelBuffer:typealias CVPixelBuffer = CVImageBuffer,CVImageBuffer是一种保存图像数据的抽象类型,表示未经编码或解码后的图像数据结构。
  • CVPixelBufferPool:存放和管理CVPixelBuffer的数据结构(具有回收循环利用的妙处)。
  • pixelBufferAttributes - CFDictionary对象,一般包含了视频的宽高,像素格式类型(32RGBA, YCbCr420),是否兼容OpenGL ESCore Animation等相关信息
  • CTime:分子是64-bit的时间值,分母是32-bit的时标(time scale)。
  • CMVideoFormatDescription:视频宽高,格式(kCMPixelFormat_32RGBA, kCMVideoCodecType_H264), 其他诸如颜色空间等信息的扩展。

  • CMBlockBuffer:CMBlockBuffer是一个CFType对象,表示数据偏移量的连续范围。用来存放编码后的数据。

  • CMSampleBuffer:对于编码后的数据,包含了CMTimeCMVideoFormatDescCMBlockBuffer;对于解码后的数据,则包含了CMTimeCMVideoFormatDescCMPixelBuffer

图1.1
  • CMClock - 封装了时间源,其中CMClockGetHostTimeClock()封装了mach_absolute_time()
  • CMTimebase - CMClock上的控制视图。提供了时间的映射:CMTimebaseSetTime(timebase, kCMTimeZero);速率控制:CMTimebaseSetRate(timebase, 1.0)

图2.1展示的是通过AVSampleBufferDisplaylayer播放网络上获取的H.264码流。

图2.1.png

但并不是说AVSampleBufferDisplaylayer能直接播放H.264码流,需要将H.264码流包装成SampleBuffer传给给AVSampleBufferDisplaylayer解码播放。

图2.2.png

再来看一下H.264码流的构成,H.264码流由一系列的NAL单元组成。
NAL单元一般包含:

  • 视频帧(或视频帧片)
  • H.264参数集
    -序列参数集(Sequence Parameter Set(SPS)
    -图像参数集(Picture Parameter Set(PPS)

所以如果要将H.264解码播放就需要将H.264码流包装成CMSampleBuffer。由图1.1可得CMSampleBuffer = CMTime + CMVideoFormatDesc + CMBlockBuffer
解码步骤:
1.从网络获取的码流中获取SPS和PPS生成CMVideoFormatDesc。

(1)H.264 NALU单元的Start Code 是"0x 00 00 01" 或"0x 00 01",按照Start Code定位NALU。
(2)通过类型信息找到SPSPPS并提取,开始码后第一个byte的第5位,7代表SPS,8代表PPS

(3)使用CMVideoFormatDescriptionCreateFromH264ParameterSets函数来构建CMVideoFormatDescription。

// 设置H264Parameter
    uint8_t*  parameterSetPointers[2] = {sps, pps};
    size_t parameterSetSizes[2] = {_spsSize-4, _ppsSize-4};
    status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2,
                                                                 (const uint8_t *const*)parameterSetPointers,
                                                                 parameterSetSizes, 4,
                                                                 &_formatDesc);

2.提取视频图像数据生成CMBlockBuffer。
(1)按照Start Code定位NALU。
(2)CMBlockBuffer数据需要的头部码为4个字节的长度,为:0x 00 80 00,所以需要将H.264的header给替换掉。

//找到偏移量,或者SPS和PPS NALUs结束IDR帧NALU开始
        int offset = (int)(_spsSize + _ppsSize);
        blockLength = frameSize - offset;
        data = malloc(blockLength);
        data = memcpy(data, &frame[offset], blockLength);
        
        //替换该NALU相应长度start code头(AVCC format需要这样)
        // htonl 将数据类型转换为unsigned int
        uint32_t dataLength32 = htonl (blockLength - 4);
        memcpy (data, &dataLength32, sizeof (uint32_t));

(3)CMBlockBufferCreateWithMemoryBlock接口构造CMBlockBufferRef

status = CMBlockBufferCreateWithMemoryBlock(NULL, data,
                                                    blockLength,
                                                    kCFAllocatorNull, NULL,
                                                    0,
                                                    blockLength,
                                                    0, &blockBuffer);

3.根据自己的需要设置CMTime
我的项目中的拉取的实时流需要实时播放,不需要设置时间间隔,所以不用设置CMTime。

4.根据上述得到CMVideoFormatDescriptionRefCMBlockBufferRef和可选的时间信息,使用CMSampleBufferCreate接口得到CMSampleBuffer数据这个待解码的原始的数据。

5.用AVSampleBufferDisplayLayer处理得到sampleBuffer来显示图像。

[_displayLayer enqueueSampleBuffer:sampleBuffer];

至此成功用Video Toolbox硬件解码H.264码流,并在设备上播放视频。
可是,如果我们要拿到每一帧图像进行处理呢,那该怎么得到?
那么我们还需要用VTDecompressionSession解码成CVPixelBuffer,通过UIImageView或者OpenGL ES上显示。
(1)创建VTDecompressionSession,需要以下参数:

  • CMVideoFormatDescription(见上面的第(3)步)
  • 对所输出数据的需求——pixelBufferAttributes
  • 解码结果回调函数VTDecompressionSessionOutputCallback
-(void) createDecompressionSession
{
//创建VTDecompressionSession
    _decompressionSession = NULL;
    VTDecompressionOutputCallbackRecord callBackRecord;
    callBackRecord.decompressionOutputCallback = decompressionSessionDecodeFrameCallback;
    
    callBackRecord.decompressionOutputRefCon = (__bridge void *)self;
    
    NSDictionary* destinationPixelBufferAttributes = @{
                                                       (id)kCVPixelBufferPixelFormatTypeKey : [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange],
                                                       //硬解必须是 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
                                                       //                                                           或者是kCVPixelFormatType_420YpCbCr8Planar
                                                       //因为iOS是  nv12  其他是nv21
                                                       (id)kCVPixelBufferWidthKey : [NSNumber numberWithInt:h264outputHeight*2],
                                                       (id)kCVPixelBufferHeightKey : [NSNumber numberWithInt:h264outputWidth*2],
                                                       //这里宽高和编码反的
                                                       (id)kCVPixelBufferOpenGLCompatibilityKey : [NSNumber numberWithBool:YES]
                                                       };
    OSStatus status = VTDecompressionSessionCreate(kCFAllocatorDefault,
                                          _formatDesc,
                                          NULL,
                                          (__bridge CFDictionaryRef)destinationPixelBufferAttributes,
                                          &callBackRecord,
                                          &_decompressionSession);
    
    if (status == noErr) {
        NSLog(@"Video Decompression Session 创建成功!");
    }else{
        NSLog(@"Video Decompression Session 创建失败,错误码: %d",(int)status);
    }
}

(2)调用VTDecompresSessionDecodeFrame接口进行解码。

 VTDecodeFrameFlags flags = kVTDecodeFrame_EnableAsynchronousDecompression;
    VTDecodeInfoFlags flagOut;
    NSDate* currentTime = [NSDate date];
    VTDecompressionSessionDecodeFrame(_decompressionSession, sampleBuffer, flags,
                                      (void*)CFBridgingRetain(currentTime), &flagOut);
    
    CFRelease(sampleBuffer);

(3)VTDecompressionSessionOutputCallback回调函数中可以得到解码后的结果CVPixelBuffer,可以将CVPixelBuffer转换成UIImage图像显示在ImageView上或者用OpenGL ES渲染图像。

void decompressionSessionDecodeFrameCallback(void *decompressionOutputRefCon,
                                             void *sourceFrameRefCon,
                                             OSStatus status,
                                             VTDecodeInfoFlags infoFlags,
                                             CVImageBufferRef imageBuffer,
                                             CMTime presentationTimeStamp,
                                             CMTime presentationDuration)
{

    if (status != noErr)
    {
        NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
        NSLog(@"解码错误: %@", error);
    }
    else
    {
        NSLog(@"解码成功");
        
        CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon;
        H264HWDecoder *decoder = (__bridge H264HWDecoder *)decompressionOutputRefCon;
        if (decoder.delegate!=nil)
        {
            [decoder.delegate displayImageBuffer:imageBuffer];
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容