VideoToolBox 解碼H.264

關(guān)于VideoToolBox 解碼 H264 ,這次我們通過 ffmpeg 提取一個視頻流的 的視頻流,也就是 h264 編碼格式的視頻流(沒有音頻);

命令如下:

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

1. 獲取 NALU 單元

demo中,我們首先把h264 文件讀到內(nèi)存中,通過創(chuàng)建定時器,來讀取 一個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) { //一般情況下只會執(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) {                    //這里使用一種簡略的方式來獲取這一幀的長度:通過查找下一個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); //復(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 的首地址和 長度后,解析該NALU
}

2. 獲取SPS 和PPS

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

如下圖所示:

image.png

所以在找到 start code后,第一個字節(jié)為NALU Header ,通過NALU Header判斷這是一個什么類型的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個字節(jié)換成大端的長度
        //大端:高字節(jié)保存在低地址
        //小端:高字節(jié)保存在高地址
        //大小端的轉(zhuǎn)換實際上及時將字節(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
如果沒有spspps我們 需要 xxx 來創(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)鍵幀后,我們 通過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 的長度
                              NULL, //sampleTimingArray 對每一個設(shè)置一些屬性,這些我們并不需要
                              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圖像信息

解碼成功后的回調(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)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容