VideoToolBox之視頻編碼

為什么要進行編碼

因為未經過編碼(壓縮)的視頻,具有極大的數據量,不利于存儲,傳輸,實時應用.

視頻編碼的原理

  1. 空間冗(rong)余 :同一幅圖像的相鄰像素點具有連貫性
  2. 時間冗余: 一組連續的畫面之間存在著關聯性
  3. 結構冗余: 某些場景存在著明顯的圖像分布模式,有規律可循.比如:方格地板,蜂窩
  4. 視覺冗余: 人眼對圖像細節的分辨率有限,對亮度比對色度更敏感.
  5. 知識冗余: 許多圖像的理解和某些知識有關聯.比如人類面部的固定結構.

視頻編碼的規則

在保證視覺效果的前提下盡可能減少視頻的數據量.

視頻編碼(壓縮)方式

有損壓縮與無損壓縮

  • 有損壓縮:解壓后與壓縮前相比數據有丟失,丟失不可恢復,壓縮比高
  • 無損壓縮:解壓后與壓縮前數據完全一致,壓縮比低.
    注: 高效壓縮算法往往使用有損壓縮來減少數據量.

幀內壓縮與幀間壓縮

  • 幀內:一幅幀的相鄰像素點具有空間連續性,不考慮相鄰幀,減少本幀冗余信息,也稱空間壓縮.
  • 幀間: 視頻具有時間連續性,只記錄相鄰幀之間的差異來減少數據量,也成時間壓縮.

對稱編/解碼與不對稱編/解碼

  • 對稱: 編碼和解碼占用相同的計算處理能力和時間,實時性好.
  • 不對稱: 與上相反,一般壓縮慢,解壓快.

視頻編碼標準

ITU-T與ISO/IEC是制定視頻編碼標準的兩大組織
ITU-T的標準包括:H.261,H.263,H.264 主要應用于實時視頻通信領域,如會議電視.
ISO/IEC:MPEG系列標準,主要應用于視頻存儲(DVD),廣播電視,因特網或無線網上的流媒體等
而兩大組織也共同制定了一些標準,H.262標準等同于MPEG-2的視頻編碼標準,H.264標準則被納入MPEG-4的第10部分.

H.264

H264是新一代的編碼標準,以高壓縮高質量和支持多種網絡的流媒體傳輸著稱.
編碼理論依據是:參照一段時間內圖像的統計結果表明,在相鄰幾幅圖像畫面中,一般有差別的像素只有10%以內的點,亮度差值變化不超過2%,而色度差值的變化只有1%以內。所以對于一段變化不大圖像畫面,我們可以先編碼出一個完整的圖像幀X,隨后的Y幀就不編碼全部圖像,只寫入與X幀的差別,這樣Y幀的大小就只有完整幀的1/10或更小!Y幀之后的Z幀如果變化不大,我們可以繼續以參考Y的方式編碼Z幀,這樣循環下去。這段圖像我們稱為一個GOP圖像組(GOP圖像組就是有相同特點的一段數據),也就是對這個圖像生成一個完整幀X1,隨后的圖像就參考X1生成,只寫入與X1的差別內容。當某個圖像與之前的圖像變化很大,無法參考前面的幀來生成,那我們就結束上一個GOP圖像組,開始下一段GOP圖像組。
在H264協議里定義了三種幀,完整編碼的幀叫I幀,參考之前的I幀生成的只包含差異部分編碼的幀叫P幀,還有一種參考前后的幀編碼的幀叫B幀。

H264采用的核心算法是幀內壓縮和幀間壓縮,幀內壓縮是生成I幀的算法,幀間壓縮是生成B幀和P幀的算法。

一些名詞解析

GOP

GOP(Group of Pictures):(畫面組,一個GOP就是一組連續的畫面,每個畫面都是一幀,一個GOP就是很多幀的集合,兩個I幀之間的間隔,在H264中圖像以GOP圖像組為單位進行組織,一個GOP圖像組是一段圖像編碼后的數據流,以I幀開始,到下一個I幀結束。

一個序列的第一個圖像叫做 IDR 圖像(立即刷新圖像),IDR 圖像都是 I 幀圖像。H.264 引入 IDR 圖像是為了解碼的重同步,當解碼器解碼到 IDR 圖像時,立即將參考幀隊列清空,將已解碼的數據全部輸出或拋棄,重新查找參數集,開始一個新的序列。這樣,如果前一個序列出現重大錯誤,在這里可以獲得重新同步的機會。IDR圖像之后的圖像永遠不會使用IDR之前的圖像的數據來解碼。

一個序列就是一段內容差異不太大的圖像編碼后生成的一串數據流。當運動變化比較少時,一個序列可以很長,因為運動變化少就代表圖像畫面的內容變動很小,所以就可以編一個I幀,然后一直P幀、B幀了。當運動變化多時,可能一個序列就比較短了,比如就包含一個I幀和3、4個P幀。

I幀

I幀:

幀內編碼幀,I幀表示關鍵幀,你可以理解為這一幀畫面的完整保留;解碼時只需要本幀數據就可以完成(因為包含完整畫面),一個GOP中,I幀是編解碼的起點,有效防止幀間預測誤差累計擴散。
P幀:(差別幀)保留這一幀跟之前幀的差別,解碼時需要用之前緩存的畫面疊加上本幀定義的差別,生成最終畫面。(P幀沒有完整畫面數據,只有與前一幀的畫面差別的數據)
B幀:(雙向差別幀)保留的是本幀與前后幀的差別,解碼B幀,不僅要取得之前的緩存畫面,還要解碼之后的畫面,通過前后畫面的與本幀數據的疊加取得最終的畫面。B幀壓縮率高,但是解碼時CPU會比較累.

I幀特點:

1.它是一個全幀壓縮編碼幀。它將全幀圖像信息進行H.26L壓縮編碼及傳輸(效果全面超越JPEG,逼近甚至超過JPEG2000);

2.解碼時僅用I幀的數據就可重構完整圖像;

3.I幀描述了圖像背景和運動主體的詳情;

4.I幀不需要參考其他畫面而生成;

5.I幀是P幀和B幀的參考幀(其質量直接影響到同組中以后各幀的質量);

6.I幀是幀組GOP的基礎幀(第一幀),在一組中只有一個I幀;

7.I幀不需要考慮運動矢量;

8.I幀所占數據的信息量比較大

P幀

前向預測編碼幀,使用視頻序列一個時間方向上的相關性進行壓縮。P幀表示的是這一幀跟之前的幀的差別,P幀可以作為后續圖像編碼時的參考幀。解碼時需要用之前緩存的畫面疊加上本幀定義的差別,生成最終畫面。(也就是差別幀,P幀沒有完整畫面數據,只有與前一幀的畫面差別的數據,因此解碼要使用參考圖像的像素值)。

P幀是以I幀為參考幀,在I幀中找出P幀“某點”的預測值和運動矢量,取預測差值和運動矢量一起傳送。在接收端根據運動矢量從I幀中找出P幀“某點”的預測值并與差值相加以得到P幀“某點”樣值,從而可得到完整的P幀。

P幀特點:

1.P幀是I幀后面相隔1~2幀的編碼幀;

2.P幀采用運動補償的方法傳送它與前面的I或P幀的差值及運動矢量(預測誤差);

3.解碼時必須將I幀中的預測值與預測誤差求和后才能重構完整的P幀圖像;

4.P幀屬于前向預測的幀間編碼。它只參考前面最靠近它的I幀或P幀;

5.P幀可以是其后面P幀的參考幀,也可以是其前后的B幀的參考幀;

6.由于P幀是參考幀,它可能造成解碼錯誤的擴散;

7.由于是差值傳送,P幀的壓縮比較高。

B幀

雙向預測內插編碼幀,利用視頻序列兩個時間方向上的相關性進行壓縮,由于B幀的編解碼順序打亂了視頻圖像的自然順序,因此B幀不用作參考幀。B幀是雙向差別幀,也就是B幀記錄的是本幀與前后幀的差別,換言之,要解碼B幀,不僅要取得之前的緩存畫面,還要解碼之后的畫面,通過前后畫面的與本幀數據的疊加取得最終的畫面。B幀壓縮率高,但是解碼時CPU會比較累。

B幀的預測與重構

B幀以前面的I或P幀和后面的P幀為參考幀,“找出”B幀“某點”的預測值和兩個運動矢量,并取預測差值和運動矢量傳送。接收端根據運動矢量在兩個參考幀中“找出(算出)”預測值并與差值求和,得到B幀“某點”樣值,從而可得到完整的B幀。

B幀特點

1.B幀是由前面的I或P幀和后面的P幀來進行預測的;

2.B幀傳送的是它與前面的I或P幀和后面的P幀之間的預測誤差及運動矢量;

3.B幀是雙向預測編碼幀;

4.B幀壓縮比最高,因為它只反映丙參考幀間運動主體的變化情況,預測比較準確;

5.B幀不是參考幀,不會造成解碼錯誤的擴散。

NALU,SPS與PPS

NALU

SPS:序列參數集 作用于一系列連續的編碼圖像;
SPS 對于H264而言,就是編碼后的第一幀,如果是讀取的H264文件,就是第一個幀界定符和第二個幀界定符之間的數據的長度是4
PPS:圖像參數集 作用于編碼視頻序列中一個或多個獨立的圖像;

PPS 就是編碼后的第二幀,如果是讀取的H264文件,就是第二幀界定符和第三幀界定符中間的數據長度不固定。

碼流結構

VideoToolBox之VTCCompressionSessionRef

VideoToolbox是iOS8以后開放的硬編碼與硬解碼的API,一組用C語言寫的函數.VTCCompressionSessionRef是針對一連串的視頻幀進行壓縮,CF層的一個引用計數對象.
步驟:

  1. VTCompressionSessionCreate 創建編碼會話
  2. VTSessionSetProperty 配置相關屬性
  3. VTCompressionSessionPrepareToEncodeFrames
  4. 拿到數據VTCompressionSessionEncodeFrame
  5. 執行編碼回調函數VTCompressionOutputCallback
  6. VTCompressionSessionCompleteFrames
  7. VTCompressionSessionInvalidate
  8. CFRelease

關于視頻捕捉主要使用AVFoundatio框架中的AVCaptureSession.不再敘述

1. VTCompressionSessionCreate 創建編碼會話

//幀ID
    self.frameID = 0;
    int width = [UIScreen mainScreen].bounds.size.width;
    int height = [UIScreen mainScreen].bounds.size.height;
    //參數1:C語言函數一般第一個都有,分配器,傳NULL或kCFAllocatorDefault
    //參數2,參數3:寬度,高度
    //參數4:編碼類型 kCMVideoCodecType_H264
    //參數5:編碼規范 傳NULL VideoToolBox自行選擇
    //參數6:源像素緩沖區
    //參數7:壓縮數據分配器
    //參數8:回調函數
    //參數9:回調函數的引用
    //參數10:編碼會話對象指針
    OSStatus status = VTCompressionSessionCreate(kCFAllocatorDefault, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressionOutputCallback, (__bridge void *)self, &_encodeSesssion);
    if (status != noErr) {
        NSLog(@"create compressionSession failed!");
        return;
    }

2. VTSessionSetProperty 配置相關屬性

實時編碼輸出:kVTCompressionPropertyKey_RealTime
碼率:圖片進行壓縮后每秒顯示的數據量。
幀率:每秒顯示的圖片數。影響畫面流暢度,與畫面流暢度成正比:幀率越大,畫面越流暢;幀率越小,畫面越有跳動感。
由于人類眼睛的特殊生理結構,如果所看畫面之幀率高于16的時候,就會認為是連貫的,此現象稱之為視覺暫留。并且當幀速達到一定數值后,再增長的話,人眼也不容易察覺到有明顯的流暢度提升了。
分辨率:(矩形)圖片的長度和寬度,即圖片的尺寸
壓縮前的每秒數據量:幀率X分辨率(單位應該是若干個字節)
壓縮比:壓縮前的每秒數據量/碼率 (對于同一個視頻源并采用同一種視頻編碼算法,則:壓縮比越高,畫面質量越差。)

    //設置實時編碼輸出(否則會造成延遲)
    VTSessionSetProperty(self.encodeSesssion, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
    
    //設置碼率(碼率:編碼效率,碼率越高,則畫面越清晰,如果碼率較低則會引起馬賽克 -->碼率高有利于還原原始畫面,但是不利于傳輸)
    int bitRate = 800 * 1024;
    CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
    VTSessionSetProperty(self.encodeSesssion, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);
    
    NSArray *limit = @[@(bitRate * 1.5/8),@(1)];
    VTSessionSetProperty(self.encodeSesssion, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);
    
    
//    VTSessionSetProperty(self.encodeSesssion, kVTCompressionPropertyKey_ProfileLevel, );
    
    //設置期望幀率(每秒多少幀,如果幀率過低,會造成畫面卡頓)
    int fps = 30;
    CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
    VTSessionSetProperty(self.encodeSesssion, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
    
    //設置關鍵幀(GOPsize)間隔
    int frameInterval = 30;
    CFNumberRef frameRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
    VTSessionSetProperty(self.encodeSesssion, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameRef);
    

3. VTCompressionSessionPrepareToEncodeFrames

準備編碼

VTCompressionSessionPrepareToEncodeFrames(self.encodeSesssion);

4. 拿到數據VTCompressionSessionEncodeFrame

使用AVFoundation執行輸出的代理方法,得到數據,一般是CMSampleBufferRef類型,編碼前后的數據分別為:

CMSampleBufferRef

而編碼完成會執行創建的時候傳入的回調函數.

- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer{
    
    //1.獲取CVImageBuffer作為編碼的參數之一
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    
    //2.CMTime 創建時間,更新當前的幀數
    CMTime presenttationTimeStamp = CMTimeMake(self.frameID++, 1000);
    
    //3.VTEncodeInfoFlags
    VTEncodeInfoFlags flags;
    
    //編碼
    OSStatus status = VTCompressionSessionEncodeFrame(_encodeSesssion, imageBuffer, presenttationTimeStamp, kCMTimeInvalid, NULL, (__bridge void *)(self), &flags);
    if (status != noErr) {
        NSLog(@"VTCompressionSessionEncodeFrame Failed!");
    }
}

5. 執行編碼回調函數VTCompressionOutputCallback

void didCompressionOutputCallback(void *outputCallbackRefCon,void *sourceFrameRefCon,OSStatus status,VTEncodeInfoFlags infoFlags,CMSampleBufferRef sampleBuffer ){
    
    //1.判斷狀態
    if (status != noErr) {
        NSLog(@"callBack failed!");
        return;
    }
    
    //2.獲取傳入的參數
    VideoEncode *encode = (__bridge VideoEncode *)outputCallbackRefCon;
    
    //3.判斷是否是關鍵幀
    CFArrayRef arrayRef = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
    bool isKeyFrame = !(CFDictionaryContainsKey(CFArrayGetValueAtIndex(arrayRef, 0), kCMSampleAttachmentKey_NotSync));
    
    //4.獲取SPS PPS數據
    if (isKeyFrame) {
        //獲取編碼后的信息(存儲于CMFormatDescriptionRef中)
        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);
        
        //轉成NSData數據,寫入文件
        NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
        NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
        
        //寫入文件
        [encode gotSps:sps pps:pps];
    }
    
    //5.獲取數據塊
    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 unit length
            memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
            
            //從大端轉系統端
            NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
            
            NSData *data = [[NSData alloc]initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];
            [encode gotEncodedData:data];
            
            // 移動到寫一個塊,轉成NALU單元
            // Move to the next NAL unit in the block buffer
            bufferOffset += AVCCHeaderLength + NALUnitLength;
        }
    }
    
}

寫入SPS,PPS

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

寫入主數據

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

6.結束編碼

調用編碼完成函數,將編碼會話銷毀,釋放資源

- (void)endEncode{
    
    VTCompressionSessionCompleteFrames(_encodeSesssion, kCMTimeInvalid);
    VTCompressionSessionInvalidate(_encodeSesssion);
    CFRelease(_encodeSesssion);
    _encodeSesssion = NULL;
    _frameID = 0;
    
}

參考:http://www.voidcn.com/blog/onion2007/article/p-4428513.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容