VideoToolBox 解碼H.264

關(guān)于VideoToolBox 解碼 H264 用押,這次我們通過(guò) ffmpeg 提取一個(gè)視頻流的 的視頻流汞舱,也就是 h264 編碼格式的視頻流(沒(méi)有音頻);

命令如下:

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

1. 獲取 NALU 單元

demo中,我們首先把h264 文件讀到內(nèi)存中柠偶,通過(guò)創(chuàng)建定時(shí)器,來(lái)讀取 一個(gè)NALU單元睬关;該步驟重點(diǎn)是如何在文件流中找到 NALU 單元诱担,眾所周知 ,每個(gè) NALU單元前面都有起始碼 0x00 0x00 0x00 0x010x00 0x00 0x01來(lái)分割 NALU 單元共螺; 這里 我們畫(huà)圖來(lái)解釋 如何通過(guò)指針移動(dòng)來(lái)找到 一個(gè)NALU 單元该肴,并拿到NALU 單元的長(zhǎng)度情竹,從而獲取到 一個(gè)完整NALU藐不;

image.png

源碼邏輯可參考如下代碼:

- (void)tick {
    
    dispatch_sync(_decodeQueue, ^{
        //1.獲取packetBuffer和packetSize
        packetSize = 0;
        if (packetBuffer) {
            free(packetBuffer);
            packetBuffer = NULL;
        }
        if (_inputSize < _inputMaxSize && _inputStream.hasBytesAvailable) { //一般情況下只會(huì)執(zhí)行一次,使得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) {                    //這里使用一種簡(jiǎn)略的方式來(lái)獲取這一幀的長(zhǎng)度:通過(guò)查找下一個(gè)0x00000001來(lái)確定。
                if(memcmp(pStart - 3, startCode, 4) == 0 ) {
                    packetSize = pStart - _inputBuffer - 3;
                    if (packetBuffer) {
                        free(packetBuffer);
                        packetBuffer = NULL;
                    }
                    packetBuffer = malloc(packetSize);
                    memcpy(packetBuffer, _inputBuffer, packetSize); //復(fù)制packet內(nèi)容到新的緩沖區(qū)
                    memmove(_inputBuffer, _inputBuffer + packetSize, _inputSize - packetSize); //把緩沖區(qū)前移
                    _inputSize -= packetSize;
                    break;
                }
                else {
                    ++pStart;
                }
            }
        }
        if (packetBuffer == NULL || packetSize == 0) {
            [self endDecode];
            return;
        }
        /// 拿到NALU 的首地址和 長(zhǎng)度后秦效,解析該NALU
}

2. 獲取SPS 和PPS

在上一篇文章中雏蛮,我們首先保存的是SPSPPS 數(shù)據(jù),所以在文件流的讀取中阱州,我們應(yīng)該曉得第一個(gè)和第二個(gè)NALU分別是SPSPPS挑秉,這正是我們創(chuàng)建VideoToolBox所需要的參數(shù);
在解析NALU的時(shí)候苔货,還是要再講一下 H264 碼流的結(jié)構(gòu)犀概。H264碼流是由一個(gè)個(gè)的NAL單元組成,其中SPS夜惭、PPS姻灶、IDRSLICENAL單元某一類型的數(shù)據(jù)。

如下圖所示:

image.png

所以在找到 start code后诈茧,第一個(gè)字節(jié)為NALU Header 产喉,通過(guò)NALU Header判斷這是一個(gè)什么類型的NALU
關(guān)于 NALU Header 的結(jié)構(gòu):

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

關(guān)于NALU 類型的定義我們可以參考下圖:

image.png

解析NALU 的代碼如所示:

        //2.將packet的前4個(gè)字節(jié)換成大端的長(zhǎng)度
        //大端:高字節(jié)保存在低地址
        //小端:高字節(jié)保存在高地址
        //大小端的轉(zhuǎn)換實(shí)際上及時(shí)將字節(jié)順序換一下即可
        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.判斷幀類型(根據(jù)碼流結(jié)構(gòu)可知,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. 創(chuàng)建 VideoToolBox

在拿到 spspps后曾沈,創(chuàng)建videoTooBox这嚣;
如果沒(méi)有spspps我們 需要 xxx 來(lái)創(chuàng)建 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. 創(chuàng)建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 單元

再拿到 關(guān)鍵關(guān)鍵幀后塞俱,我們 通過(guò)NSData 構(gòu)造videoToolBox 需要的sampleBuffe 姐帚;并送入編碼器;

關(guān)于解碼的源碼如下:

- (void)encoderWithData:(NSData *)data{
    if (!_decodeSession) {
        return;
    }
    //1.創(chuàng)建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.創(chuàng)建CMSampleBufferRef
    CMSampleBufferRef sampleBuffer = NULL;
    const size_t sampleSizeArray[] = {data.length};
    OSStatus sampleBufferStatus =
    CMSampleBufferCreateReady(kCFAllocatorDefault,
                              blockBuffer,
                              _formatDescriptionOut,
                              1, //sample 的數(shù)量
                              0, //sampleTimingArray 的長(zhǎng)度
                              NULL, //sampleTimingArray 對(duì)每一個(gè)設(shè)置一些屬性障涯,這些我們并不需要
                              1, //sampleSizeArray 的長(zhǎng)度
                              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圖像信息

解碼成功后的回調(diào)


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. 總結(jié)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末卧土,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子像樊,更是在濱河造成了極大的恐慌尤莺,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,589評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件生棍,死亡現(xiàn)場(chǎng)離奇詭異颤霎,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)涂滴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門(mén)友酱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人柔纵,你說(shuō)我怎么就攤上這事缔杉。” “怎么了搁料?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,933評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵或详,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我郭计,道長(zhǎng)霸琴,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,976評(píng)論 1 295
  • 正文 為了忘掉前任昭伸,我火速辦了婚禮梧乘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘庐杨。我一直安慰自己选调,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布灵份。 她就那樣靜靜地躺著仁堪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪各吨。 梳的紋絲不亂的頭發(fā)上枝笨,一...
    開(kāi)封第一講書(shū)人閱讀 51,775評(píng)論 1 307
  • 那天袁铐,我揣著相機(jī)與錄音,去河邊找鬼横浑。 笑死剔桨,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的徙融。 我是一名探鬼主播洒缀,決...
    沈念sama閱讀 40,474評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼欺冀!你這毒婦竟也來(lái)了树绩?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,359評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤隐轩,失蹤者是張志新(化名)和其女友劉穎饺饭,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體职车,經(jīng)...
    沈念sama閱讀 45,854評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瘫俊,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了悴灵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片扛芽。...
    茶點(diǎn)故事閱讀 40,146評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖积瞒,靈堂內(nèi)的尸體忽然破棺而出川尖,到底是詐尸還是另有隱情,我是刑警寧澤茫孔,帶...
    沈念sama閱讀 35,826評(píng)論 5 346
  • 正文 年R本政府宣布叮喳,位于F島的核電站,受9級(jí)特大地震影響银酬,放射性物質(zhì)發(fā)生泄漏嘲更。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評(píng)論 3 331
  • 文/蒙蒙 一揩瞪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧篓冲,春花似錦李破、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,029評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至诽俯,卻和暖如春妇菱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,153評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工闯团, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留辛臊,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,420評(píng)論 3 373
  • 正文 我出身青樓房交,卻偏偏與公主長(zhǎng)得像彻舰,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子候味,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評(píng)論 2 356

推薦閱讀更多精彩內(nèi)容