VideoToolbox 硬編碼 h.264

前言

VideoToolboxAppleiOS 8 以後推出的用於視頻硬編碼、解碼的工具庫。 平時所說的軟編解碼是指使用 ffmpeg 這個第三方庫去作編碼解碼。bash

1. 原始裸流 CMSampleBuffer 獲取

通常在作音視頻應用開發的時候,咱們都是用 AVFoundation 去作原始數據採集的,使用前置攝像頭或者後置攝像頭採集視頻數據,使用麥克風採集音頻數據。app

AVCaptureVideoDataOutputSampleBufferDelegate這個代理的async

- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
複製代碼

回調方法裏面能夠獲取採集的視頻裸流信息。注意AVCaptureAudioDataOutputSampleBufferDelegate音頻輸出的代理方法也是這個,那麼咱們如何區分究竟是音頻數據仍是視頻數據呢?這裏有兩種方案:ide

  • 判斷 outputAVCaptureAudioDataOutput(音頻) 仍是 AVCaptureVideoDataOutput(視頻)
  • 判斷 connectionaudioConnection(音頻) 仍是 videoConnection(視頻), 我本身在代碼裏使用屬性聲明瞭 audioConnectionvideoConnection

2. H.264 硬編碼

2.1 初始化編碼會話

  • 建立編碼會話函數

    建立編碼會話的時候注意傳入了咱們的編碼回調函數 VideoEncodeCallback,這個函數會屢次調用。工具

    //建立編碼會話
    OSStatus status = VTCompressionSessionCreate(kCFAllocatorDefault, (int32_t)_config.width, (int32_t)_config.height, kCMVideoCodecType_H264, NULL, NULL, NULL, VideoEncodeCallback, (__bridge void * _Nullable)(self), &_encodeSession);
    if (status != noErr) {
        NSLog(@"VTCompressionSession create failed. status=%d", (int)status);
        return self;
    } else {
        NSLog(@"VTCompressionSession create success");
    }
    複製代碼
  • 設置編碼會話屬性學習

    //設置實時編碼
    VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
    //指定編碼比特流的配置文件和級別。直播通常使用baseline,可減小因爲b幀帶來的延時
    VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel); 
    
    //設置碼率均值(比特率能夠高於此。默認比特率爲零,表示視頻編碼器。應該肯定壓縮數據的大小。注意,比特率設置只在定時時有效)
    CFNumberRef bit = (__bridge CFNumberRef)@(_config.bitrate);
    VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_AverageBitRate, bit); 
    
    //設置碼率上限
    CFArrayRef limits = (__bridge CFArrayRef)@[@(_config.bitrate / 4), @(_config.bitrate * 4)];
    VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_DataRateLimits,limits);
    
    //設置關鍵幀間隔(GOPSize)GOP太大圖像會模糊,越小圖像質量越高,固然數據量也會隨之變大
    CFNumberRef maxKeyFrameInterval = (__bridge CFNumberRef)@(_config.fps * 2);
    VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, maxKeyFrameInterval); 
    
    //設置fps(預期)
    CFNumberRef expectedFrameRate = (__bridge CFNumberRef)@(_config.fps);
    VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_ExpectedFrameRate, expectedFrameRate); 
    複製代碼
  • 準備編碼ui

    OSStatus status = VTCompressionSessionPrepareToEncodeFrames(_encodeSession);
    複製代碼

2.2 拿到 CMSampleBuffer 開始編碼

  1. 先從原始裸流 CMSampleBuffer 獲取原始圖像信息 CVImageBuffer
  2. 生成 PTS
    • PTS:Presentation Time Stamp,PTS 主要用於度量解碼後的視頻幀何時被顯示出來
    • DTS:Decode Time Stamp,DTS 主要是標識讀入內存中的比特流在何時開始送入解碼器中進行解碼
  3. 設置持續時間 durationkCMTimeInvalid,表示會一直進行解碼
  4. 聲明 VTEncodeInfoFlags 來記錄編碼信息
  5. 調用 VTCompressionSessionEncodeFrame() 開始解碼
- (void)encodeVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer {
    CFRetain(sampleBuffer);
    dispatch_async(_encodeQueue, ^{
        // 幀數據
        CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
        // 該幀的顯示時間戳 (PTS: 用於視頻顯示的時間戳)
        CMTime presentationTimeStamp = CMTimeMake(frameID++, 1000);
        //持續時間
        CMTime duration = kCMTimeInvalid;
        //編碼
        VTEncodeInfoFlags flags;
        OSStatus status = VTCompressionSessionEncodeFrame(self.encodeSesion, imageBuffer, presentationTimeStamp, duration, NULL, NULL, &flags);
        if (status != noErr) {
            NSLog(@"VTCompression: encode failed: status=%d",(int)status);
        }
        CFRelease(sampleBuffer);
    });
}
複製代碼

這裏的 frameID 是咱們聲明的圖像幀的遞增序標識,每次編碼自增就能夠了。編碼

2.3 編碼

  1. 容錯處理,先判斷當前狀態是否正常,數據是否準備好
  2. 判斷是否爲關鍵幀 keyFrame
  3. 若是是關鍵幀,先獲取圖像源格式 CMFormatDescriptionRef, 處理 SPS 數據和 PPS 數據
    • 先處理 SPS 數據
    • 再處理 PPS 數據
    • 將獲取的 SPS 數據和 PPS 數據寫入 h.264 文件或者交給解碼器去處理
  4. 若是不是關鍵幀,處理正常的 NALU 數據
    • 先從 CMSampleBuffer 獲取 CMBlockBufferRef 數據
    • 讀取數據內容,記錄數據長度和總長度
    • 定義一個數據偏移量 bufferOffset, 而後 while 循環讀取 NALU 數據,注意手動添加起始碼 "\x00\x00\x00\x01"
    • 將獲取的 NALU 數據寫入 h.264 文件或者交給解碼器去處理
// startCode 長度 4
const Byte startCode[] = "\x00\x00\x00\x01";
//編碼成功回調
void VideoEncodeCallback(void * CM_NULLABLE outputCallbackRefCon, void * CM_NULLABLE sourceFrameRefCon,OSStatus status, VTEncodeInfoFlags infoFlags,  CMSampleBufferRef sampleBuffer ) {
    
    if (status != noErr) {
        NSLog(@"VideoEncodeCallback: encode error, status = %d", (int)status);
        return;
    }
    if (!CMSampleBufferDataIsReady(sampleBuffer)) {
        NSLog(@"VideoEncodeCallback: data is not ready");
        return;
    }
    CCVideoEncoder *encoder = (__bridge CCVideoEncoder *)(outputCallbackRefCon);
    
    //判斷是否爲關鍵幀
    CFArrayRef attachArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
    BOOL keyFrame = !CFDictionaryContainsKey(CFArrayGetValueAtIndex(attachArray, 0), kCMSampleAttachmentKey_NotSync);//(注意取反符號)
    
    //獲取 sps & pps 數據 ,只需獲取一次,保存在h264文件開頭便可
    if (keyFrame) {
        //獲取圖像源格式
        CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
        // 聲明 sps 數據大小, 個數 以及 數據內容, 先獲取 sps 數據
        size_t spsSize, spsCount;
        const uint8_t *spsData;
        OSStatus spsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDesc, 0, &spsData, &spsSize, &spsCount, 0);
        if (spsStatus == noErr) {
            // 聲明 pps 數據大小, 個數 以及 數據內容, 後獲取 pps 數據
            size_t ppsSize, ppsCount;
            const uint8_t *ppsData;
            OSStatus ppsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDesc, 1, &ppsData, &ppsSize, &ppsCount, 0);
            if (ppsStatus == noErr) {
                NSLog(@"VideoEncodeCallback:got both sps and pps successfully");
                encoder->hasSpsPps = true;
                //sps data
                NSMutableData *sps = [NSMutableData dataWithCapacity:4 + spsSize];
                [sps appendBytes:startCode length:4];
                [sps appendBytes:spsData length:spsSize];
                //pps data
                NSMutableData *pps = [NSMutableData dataWithCapacity:4 + ppsSize];
                [pps appendBytes:startCode length:4];
                [pps appendBytes:ppsData length:ppsSize];
                
                dispatch_async(encoder.callbackQueue, ^{
                    //回調方法傳遞sps/pps
                    [encoder.delegate videoEncodeCallbacksps:sps pps:pps];
                });
            } else {
                NSLog(@"VideoEncodeCallback: get pps failed ppsStatus=%d", (int)ppsStatus);
            }
        } else {
            NSLog(@"VideoEncodeCallback: get sps failed spsStatus=%d", (int)spsStatus);
        }
    }
    
    //獲取NALU數據
    size_t lengthAtOffset, totalLength;
    char *dataPoint;
    
    // 從 CMSampleBuffer 獲取 DataBuffer, 將數據複製到 dataPoint
    CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    OSStatus dataBufferStatus = CMBlockBufferGetDataPointer(blockBuffer, 0, &lengthAtOffset, &totalLength, &dataPoint);
    if (dataBufferStatus == noErr) {
        // 循環獲取 nalu 數據
        size_t bufferOffset = 0;
        // 這個常量 4 是大端模式的幀長度length, 而不是 nalu 數據前四個字節(0001 的 startcode)
        static const int headerLength = 4;
        
        while (bufferOffset < totalLength - headerLength) {
            uint32_t nalUnitLength = 0;
            // 讀取 nalu 長度的數據
            memcpy(&nalUnitLength, dataPoint + bufferOffset, headerLength);
            // 大端轉系統端(iOS 的系統端是小端模式)
            nalUnitLength = CFSwapInt32BigToHost(nalUnitLength);
            // 獲取到編碼好的視頻數據
            NSMutableData *data = [NSMutableData dataWithCapacity:4 + nalUnitLength];
            // 先添加 startcode
            [data appendBytes:startCode length:4];
            // 再拼接 nalu 數據
            [data appendBytes:dataPoint + bufferOffset + headerLength length:nalUnitLength];
            
            //將NALU數據回調到代理中
            dispatch_async(encoder.callbackQueue, ^{
                [encoder.delegate videoEncodeCallback:data];
            });
            
            // 移動偏移量,繼續讀取下一個 NALU 數據
            bufferOffset += headerLength + nalUnitLength;
        }
    } else {
        NSLog(@"VideoEncodeCallback: get datapoint failed, status = %d", (int)dataBufferStatus);
    }
}
複製代碼

2.4 釋放資源

編碼結束了之後,在合理的地方要釋放資源spa

- (void)releaseEncodeSession
{
    if (_encodeSession) {
        VTCompressionSessionCompleteFrames(_encodeSession, kCMTimeInvalid);
        VTCompressionSessionInvalidate(_encodeSession);
        
        CFRelease(_encodeSession);
        _encodeSession = NULL;
    }
}
複製代碼

備註

此文章是本身學習音視頻的筆記記錄,也參考了網上不少的資料和文章,在這裏推薦一下:

落影loyinglin

CC老師_HelloCoder

小東邪

相關文章
相關標籤/搜索