直播二:iOS中硬編碼(VideoToolBox)

  硬編碼相對於軟編碼來講,使用非CPU進行編碼,如顯卡GPU、專用的DSP、FPGA、ASIC芯片等,性能高,對CPU沒有壓力,可是對其餘硬件要求較高(如GPU等)。git

  在iOS8以後,蘋果開放了接口,而且封裝了VideoToolBox&AudioToolbox兩個框架,分別用於對視頻&音頻進行硬編碼,音頻編碼放在後面作總結,此次主要總結VideoToolBox。github

  Demo的Github地址:https://github.com/wzpziyi1/HardCoding-For-iOS網絡

 

  一、相關基礎數據結構:數據結構

    CVPixelBuffer:編碼前和解碼後的圖像數據結構。框架

    CMTime、CMClock和CMTimebase:時間戳相關。時間以64-bit/32-bit的形式出現。ide

    CMBlockBuffer:編碼後,結果圖像的數據結構。函數

    CMVideoFormatDescription:圖像存儲方式,編解碼器等格式描述。性能

    CMSampleBuffer:存放編解碼先後的視頻圖像的容器數據結構。ui

    

 

    

    如圖所示,編解碼先後的視頻圖像均封裝在CMSampleBuffer中,若是是編碼後的圖像,以CMBlockBuffe方式存儲;解碼後的圖像,以CVPixelBuffer存儲。CMSampleBuffer裏面還有另外的時間信息CMTime和視頻描述信息CMVideoFormatDesc。
 
    代碼A:
      
// 編碼完成回調
void finishCompressH264Callback(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer)
{
    if (status != noErr) return;
    
    //根據傳入的參數獲取對象
    ZYVideoEncoder *encoder = (__bridge ZYVideoEncoder *)(outputCallbackRefCon);
    
    //判斷是不是關鍵幀
    bool isKeyFrame = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
    
    //若是是關鍵幀,獲取sps & pps數據
    if (isKeyFrame)
    {
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
        
        //獲取sps信息
        size_t sparameterSetSize, sparameterSetCount;
        const uint8_t *sparameterSet;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0);
        
        // 獲取PPS信息
        size_t pparameterSetSize, pparameterSetCount;
        const uint8_t *pparameterSet;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0 );
        
        // 裝sps/pps轉成NSData,以方便寫入文件
        NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
        NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
        
        // 寫入文件
        [encoder gotSpsPps:sps pps:pps];
    }
    
    //獲取數據塊
    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    size_t length, totalLength;
    char *dataPointer;
    OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer);
    
    if (statusCodeRet == noErr)
    {
        size_t bufferOffset = 0;
        // 返回的nalu數據前四個字節不是0001的startcode,而是大端模式的幀長度length
        static const int AVCCHeaderLength = 4;
        
        //循環獲取nalu數據
        while (bufferOffset < totalLength - AVCCHeaderLength)
        {
            uint32_t NALUnitLength = 0;
            
            //讀取NAL單元長度
            memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
            
            // 從大端轉系統端
            NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
            
            NSData* data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];
            [encoder gotEncodedData:data isKeyFrame:isKeyFrame];
            
            // 移動到寫一個塊,轉成NALU單元
            bufferOffset += AVCCHeaderLength + NALUnitLength;
        }
        
    }
}

    所須要的信息均可以從CMSampleBufferRef中獲得。編碼

    

  二、NAL(網絡提取層)代碼講解

    直播一中提到了NALU概念上的封裝,下面是代碼部分:

    代碼B:

- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps
{
    // 拼接NALU的header
    const char bytes[] = "\x00\x00\x00\x01";
    size_t length = (sizeof bytes) - 1;
    NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
    
    // 將NALU的頭&NALU的體寫入文件
    [self.fileHandle writeData:ByteHeader];
    [self.fileHandle writeData:sps];
    [self.fileHandle writeData:ByteHeader];
    [self.fileHandle writeData:pps];
    
}

- (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame
{
    NSLog(@"gotEncodedData %d", (int)[data length]);
    if (self.fileHandle != NULL)
    {
        const char bytes[] = "\x00\x00\x00\x01";
        size_t length = (sizeof bytes) - 1; //string literals have implicit trailing '\0'
        NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
        [self.fileHandle writeData:ByteHeader];
        [self.fileHandle writeData:data];
    }
}

    結合這張圖片:

 

    一個GOP序列,最前面是sps和pps,它們單獨被封裝成兩個NALU單元,一個NALU單元包含header和具體數據,NALU單元header序列固定爲00 00 00 01。那麼獲得一幀畫面時,須要判斷該幀是否是I幀,若是是,那麼取出sps和pps,再是相關幀的提取寫入。(具體參考代碼A)。

 

  三、VTCompressionSession進行硬編碼

    a、給出width、height

    b、使用VTCompressionSessionCreate建立compressionSession,並設置使用H264進行編碼,相關type是kCMVideoCodecType_H264

    c、b中還須要設置回調函數finishCompressH264Callback,須要在回調函數裏面取出編碼後的GOP、sps、pps等數據。

    d、設置屬性爲實時編碼,直播必然是實時輸出。

    e、設置指望幀數,每秒多少幀,通常都是30幀以上,以避免畫面卡頓

    f、設置碼率(碼率: 編碼效率, 碼率越高,則畫面越清晰, 若是碼率較低會引發馬賽克 --> 碼率高有利於還原原始畫面,可是也不利於傳輸)

    g、設置關鍵幀間隔(也就是GOP間隔)

    h、設置結束,準備編碼

    代碼:

- (void)setupVideoSession
{
    //用於記錄當前是第幾幀數據
    self.frameID = 0;
    
    //錄製視頻的寬高
    int width = [UIScreen mainScreen].bounds.size.width;
    int height = [UIScreen mainScreen].bounds.size.height;
    
    // 建立CompressionSession對象,該對象用於對畫面進行編碼
    // kCMVideoCodecType_H264 : 表示使用h.264進行編碼
    // finishCompressH264Callback : 當一次編碼結束會在該函數進行回調,能夠在該函數中將數據,寫入文件中
    //傳入的self,就是finishCompressH264Callback回調函數裏面的outputCallbackRefCon,經過bridge就能夠取出此self
    VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, finishCompressH264Callback, (__bridge void * _Nullable)(self), &_compressionSession);
    
    //設置實時編碼,直播必然是實時輸出
    VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
    
    //設置指望幀數,每秒多少幀,通常都是30幀以上,以避免畫面卡頓
    int fps = 30;
    CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &fps);
    VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
    
    //設置碼率(碼率: 編碼效率, 碼率越高,則畫面越清晰, 若是碼率較低會引發馬賽克 --> 碼率高有利於還原原始畫面,可是也不利於傳輸)
    int bitRate = 800 * 1024;
    CFNumberRef rateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
    VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_AverageBitRate, rateRef);
    NSArray *limit = @[@(bitRate * 1.5/8), @(1)];
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);
    
    //設置關鍵幀間隔(也就是GOP間隔)
    //這裏設置與上面的fps一致,意味着每間隔30幀開始一個新的GOF序列,也就是每隔間隔1s生成新的GOF序列
    //由於上面設置的是,一秒30幀
    int frameInterval = 30;
    CFNumberRef intervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &frameInterval);
    VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, intervalRef);
    
    //設置結束,準備編碼
    VTCompressionSessionPrepareToEncodeFrames(_compressionSession);
}

    碼率:

      初始化後經過VTSessionSetProperty設置對象屬性

      編碼方式:H.264編碼

      幀率:每秒鐘多少幀畫面

      碼率:單位時間內保存的數據量

      關鍵幀(GOPsize)間隔:多少幀爲一個GOP

      參數參考:

    

相關文章
相關標籤/搜索