VideoToolBox 解码H.264

关于VideoToolBox 解码 H264 ,这次我们通过 ffmpeg 提取一个视频流的 的视频流,也就是 h264 编码格式的视频流(没有音频);

命令如下:

ffmpeg -i /Users/pengchao/Downloads/download.mp4 -codec copy  -f h264 output.h264

1. 获取 NALU 单元

demo中,我们首先把h264 文件读到内存中,通过创建定时器,来读取 一个NALU单元;该步骤重点是如何在文件流中找到 NALU 单元,众所周知 ,每个 NALU单元前面都有起始码 0x00 0x00 0x00 0x010x00 0x00 0x01来分割 NALU 单元; 这里 我们画图来解释 如何通过指针移动来找到 一个NALU 单元,并拿到NALU 单元的长度,从而获取到 一个完整NALU

image.png

源码逻辑可参考如下代码:

- (void)tick {
    
    dispatch_sync(_decodeQueue, ^{
        //1.获取packetBuffer和packetSize
        packetSize = 0;
        if (packetBuffer) {
            free(packetBuffer);
            packetBuffer = NULL;
        }
        if (_inputSize < _inputMaxSize && _inputStream.hasBytesAvailable) { //一般情况下只会执行一次,使得inputMaxSize等于inputSize
            _inputSize += [_inputStream read:_inputBuffer + _inputSize maxLength:_inputMaxSize - _inputSize];
        }
        if ((memcmp(_inputBuffer, startCode, 4) == 0) && (_inputSize > 4)) {
            
            uint8_t *pStart = _inputBuffer + 4;         //pStart 表示 NALU 的起始指针
            uint8_t *pEnd = _inputBuffer + _inputSize;  //pEnd 表示 NALU 的末尾指针
            while (pStart != pEnd) {                    //这里使用一种简略的方式来获取这一帧的长度:通过查找下一个0x00000001来确定。
                if(memcmp(pStart - 3, startCode, 4) == 0 ) {
                    packetSize = pStart - _inputBuffer - 3;
                    if (packetBuffer) {
                        free(packetBuffer);
                        packetBuffer = NULL;
                    }
                    packetBuffer = malloc(packetSize);
                    memcpy(packetBuffer, _inputBuffer, packetSize); //复制packet内容到新的缓冲区
                    memmove(_inputBuffer, _inputBuffer + packetSize, _inputSize - packetSize); //把缓冲区前移
                    _inputSize -= packetSize;
                    break;
                }
                else {
                    ++pStart;
                }
            }
        }
        if (packetBuffer == NULL || packetSize == 0) {
            [self endDecode];
            return;
        }
        /// 拿到NALU 的首地址和 长度后,解析该NALU
}

2. 获取SPS 和PPS

在上一篇文章中,我们首先保存的是SPSPPS 数据,所以在文件流的读取中,我们应该晓得第一个和第二个NALU分别是SPSPPS,这正是我们创建VideoToolBox所需要的参数;
在解析NALU的时候,还是要再讲一下 H264 码流的结构。H264码流是由一个个的NAL单元组成,其中SPSPPSIDRSLICENAL单元某一类型的数据。

如下图所示:

image.png

所以在找到 start code后,第一个字节为NALU Header ,通过NALU Header判断这是一个什么类型的NALU
关于 NALU Header 的结构:

  • 第 0位 F
  • 第1-2 位 NRI
  • 第3-7位:TYPE
image.png

关于NALU 类型的定义我们可以参考下图:

image.png

解析NALU 的代码如所示:

        //2.将packet的前4个字节换成大端的长度
        //大端:高字节保存在低地址
        //小端:高字节保存在高地址
        //大小端的转换实际上及时将字节顺序换一下即可
        uint32_t nalSize = (uint32_t)(packetSize - 4);
        uint8_t *pNalSize = (uint8_t*)(&nalSize);
        packetBuffer[0] = pNalSize[3];
        packetBuffer[1] = pNalSize[2];
        packetBuffer[2] = pNalSize[1];
        packetBuffer[3] = pNalSize[0];
        
        //3.判断帧类型(根据码流结构可知,startcode后面紧跟着就是码流的类型)
        int nalType = packetBuffer[4] & 0x1f;
        switch (nalType) {
            case 0x05:
                //IDR frame
                [self initDecodeSession];
                [self decodePacket];
                break;
            case 0x07:
                //sps
                if (_sps) { _sps = nil;}
                size_t spsSize = (size_t) packetSize - 4;
                uint8_t *sps = malloc(spsSize);
                memcpy(sps, packetBuffer+4, spsSize);
                _sps = [NSData dataWithBytes:sps length:spsSize];
                break;
            case 0x08:
                //pps
                if (_pps) { _pps = nil; }
                size_t ppsSize = (size_t) packetSize - 4;
                uint8_t *pps = malloc(ppsSize);
                memcpy(pps, packetBuffer+4, ppsSize);
                _pps = [NSData dataWithBytes:pps length:ppsSize];
                break;
            default:
                // B/P frame
                [self decodePacket];
                break;
        }
    });

3. 创建 VideoToolBox

在拿到 spspps后,创建videoTooBox
如果没有spspps我们 需要 xxx 来创建 videoToolBox;

-(void)initVideoToolBox {
    
    if (_decodeSession) {
        return;
    }
    
    CMFormatDescriptionRef formatDescriptionOut;
    const uint8_t * const param[2] = {_sps.bytes,_pps.bytes};
    const size_t paramSize[2] = {_sps.length,_pps.length};
    OSStatus formateStatus =
    CMVideoFormatDescriptionCreateFromH264ParameterSets(NULL,
                                                        2,
                                                        param,
                                                        paramSize,
                                                        4,
                                                        &formatDescriptionOut);
    _formatDescriptionOut = formatDescriptionOut;
    
    if (formateStatus!=noErr) {
        NSLog(@"FormatDescriptionCreate fail");
        return;
    }
    //2. 创建VTDecompressionSessionRef
    //确定编码格式
    const void *keys[] = {kCVPixelBufferPixelFormatTypeKey};
    
    uint32_t t = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange;
    const void *values[] = {CFNumberCreate(NULL, kCFNumberSInt32Type, &t)};
    
    CFDictionaryRef att = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
    
    VTDecompressionOutputCallbackRecord VTDecompressionOutputCallbackRecord;
    VTDecompressionOutputCallbackRecord.decompressionOutputCallback = decodeCompressionOutputCallback;
    VTDecompressionOutputCallbackRecord.decompressionOutputRefCon = (__bridge void * _Nullable)(self);
    
    OSStatus sessionStatus = VTDecompressionSessionCreate(NULL,
                                 formatDescriptionOut,
                                 NULL,
                                 att,
                                 &VTDecompressionOutputCallbackRecord,
                                 &_decodeSession);
    CFRelease(att);
    if (sessionStatus != noErr) {
        NSLog(@"SessionCreate fail");
        [self endDecode];
    }
}

4.解码NALU 单元

再拿到 关键关键帧后,我们 通过NSData 构造videoToolBox 需要的sampleBuffe ;并送入编码器;

关于解码的源码如下:

- (void)encoderWithData:(NSData *)data{
    if (!_decodeSession) {
        return;
    }
    //1.创建CMBlockBufferRef
    CMBlockBufferRef blockBuffer = NULL;
    OSStatus blockBufferStatus =
    CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault,
                                       data.bytes,
                                       data.length,
                                       NULL,
                                       NULL,
                                       0,
                                       data.length,
                                       0,
                                       &blockBuffer);
    if (blockBufferStatus!=noErr) {
        NSLog(@"BolkBufferCreate fail");
        return;
    }
    //2.创建CMSampleBufferRef
    CMSampleBufferRef sampleBuffer = NULL;
    const size_t sampleSizeArray[] = {data.length};
    OSStatus sampleBufferStatus =
    CMSampleBufferCreateReady(kCFAllocatorDefault,
                              blockBuffer,
                              _formatDescriptionOut,
                              1, //sample 的数量
                              0, //sampleTimingArray 的长度
                              NULL, //sampleTimingArray 对每一个设置一些属性,这些我们并不需要
                              1, //sampleSizeArray 的长度
                              sampleSizeArray,
                              &sampleBuffer);
    
    if (blockBuffer && sampleBufferStatus == kCMBlockBufferNoErr) {
        //3.编码生成
        VTDecodeFrameFlags flags = 0;
        VTDecodeInfoFlags flagOut = 0;
        OSStatus decodeStatus = VTDecompressionSessionDecodeFrame(_decodeSession,
                                          sampleBuffer,flags,
                                          NULL,
                                          &flagOut); //receive information about the decode operation
        if (decodeStatus!= noErr) {
            NSLog(@"DecodeFrame fail %d",(int)decodeStatus);
            return;
        }
    }
    if (sampleBufferStatus != noErr) {
        NSLog(@"SampleBufferCreate fail");
        return;
    }
}

5.获取解码后的pixelBuffer图像信息

解码成功后的回调


static void decodeCompressionOutputCallback(void * CM_NULLABLE decompressionOutputRefCon,
                                      void * CM_NULLABLE sourceFrameRefCon,
                                      OSStatus status,
                                      VTDecodeInfoFlags infoFlags,
                                      CM_NULLABLE CVImageBufferRef imageBuffer,
                                      CMTime presentationTimeStamp,
                                      CMTime presentationDuration ){
    
    VideoDecoder *self = (__bridge VideoDecoder *)(decompressionOutputRefCon);
    dispatch_queue_t callbackQuque = self ->_decodeCallbackQueue;
    
    CIImage *ciimage = [CIImage imageWithCVPixelBuffer:imageBuffer];
    UIImage *image = [UIImage imageWithCIImage:ciimage];
    if (imageBuffer && [self.delegate respondsToSelector:@selector(videoDecoderCallbackPixelBuffer:)]) {
        CIImage *ciimage = [CIImage imageWithCVPixelBuffer:imageBuffer];
        UIImage *image = [UIImage imageWithCIImage:ciimage];
        dispatch_async(callbackQuque, ^{
            [self.delegate videoDecoderCallbackPixelBuffer:image];
        });
    }
}

6. 总结

源码地址: https://github.com/hunter858/OpenGL_Study/AVFoundation/VideoToolBox-decoder

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

推荐阅读更多精彩内容