音視頻學習從零到整--(5)實現(xiàn)視頻編碼

音視頻學習從零到整--(1)
音視頻學習從零到整--(2)
音視頻學習從零到整--(3)
音視頻學習從零到整--(4)
音視頻學習從零到整--(6)
音視頻學習從零到整--(7)
音視頻學習從零到整--(8)
音視頻學習從零到整--(9)
音視頻學習從零到整--(10)

一.了解VideoToolBox 硬編碼

VideoToolBox 官方文檔

在iOS4.0,蘋果就已經支持硬編解碼.但是硬編解碼在當時屬于私有API. 不提供給開發(fā)者使用
在2014年的WWDC大會上,iOS 8.0 之后,蘋果開放了硬編解碼的API愤钾。就是VideoToolbox.framework的API。VideoToolbox 是一套純C語言API。其中包含了很多C語言函數.VideoToolbox.framework 是基于Core Foundation庫函數,基于C語言

VideoToolBox實際上屬于低級框架,它是可以直接訪問硬件編碼器和解碼器.它存在于視頻壓縮和解壓縮以及存儲在像素緩存區(qū)中的數據轉換提供服務.

硬編碼的優(yōu)點

  • 提高編碼性能(使用CPU的使用率大大降低,傾向使用GPU)
  • 增加編碼效率(將編碼一幀的時間縮短)
  • 延長電量使用(耗電量大大降低)

這個框架在音視頻項目開發(fā)中,也是會要頻繁使用的.如果大家有想法去從事音視頻的開發(fā).那么這個框架將會是你學習的一個重點.

VideoToolBox框架的流程

  • 創(chuàng)建session
  • 設置編碼相關參數
  • 開始編碼
  • 循環(huán)獲取采集數據
  • 獲取編碼后數據
  • 將數據寫入H264文件

1.1 編碼的輸入和輸出

在我們開始編碼工作之前,需要了解VideoToolBox進行編解碼的輸入輸出分別是什么? 只有了解了這個,我們才能清楚知道如何去向VideoToolBox添加數據,并且如何獲取數據.

如圖所示,左邊的三幀視頻幀是發(fā)送給編碼器之前的數據,開發(fā)者必須將原始圖像數據封裝為CVPixelBuufer的數據結構.該數據結構是使用VideoToolBox的核心.

Apple Developer CVPixelBuffer 官方文檔介紹

1.2 CVPixelBuffer 解析

在這個官方文檔的介紹中,CVPixelBuffer 給的官方解釋,是其主內存存儲所有像素點數據的一個對象.那么什么是主內存了?
其實它并不是我們平常所操作的內存,它指的是存儲區(qū)域存在于緩存之中. 我們在訪問這個塊內存區(qū)域,需要先鎖定這塊內存區(qū)域.


 //1.鎖定內存區(qū)域:
    CVPixelBufferLockBaseAddress(pixel_buffer,0);
 //2.讀取該內存區(qū)域數據到NSData對象中
    Void *data = CVPixelBufferGetBaseAddress(pixel_buffer);
 //3.數據讀取完畢后,需要釋放鎖定區(qū)域
    CVPixelBufferRelease(pixel_buffer); 

單純從它的使用方式,我們就可以知道這一塊內存區(qū)域不是普通內存區(qū)域.它需要加鎖,解鎖等一系列操作.
作為視頻開發(fā),盡量減少進行顯存和內存的交換.所以在iOS開發(fā)過程中也要盡量減少對它的內存區(qū)域訪問.建議使用iOS平臺提供的對應的API來完成相應的一系列操作.

AVFoundation 回調方法中,它有提供我們的數據其實就是CVPixelBuffer.只不過當時使用的是引用類型CVImageBufferRef,其實就是CVPixelBuffer的另外一個定義.

Camera 返回的CVImageBuffer 中存儲的數據是一個CVPixelBuffer,而經過VideoToolBox編碼輸出的CMSampleBuffer中存儲的數據是一個CMBlockBuffer的引用.

image

iOS中,會經常使用到session的方式.比如我們使用任何硬件設備都要使用對應的session,麥克風就要使用AudioSession,使用Camera就要使用AVCaptureSession,使用編碼則需要使用VTCompressionSession.解碼時,要使用VTDecompressionSessionRef.

1.3 視頻編碼步驟分解

第一步: 使用VTCompressionSessionCreate方法,創(chuàng)建編碼會話;


   //1.調用VTCompressionSessionCreate創(chuàng)建編碼session
        //參數1:NULL 分配器,設置NULL為默認分配
        //參數2:width
        //參數3:height
        //參數4:編碼類型,如kCMVideoCodecType_H264
        //參數5:NULL encoderSpecification: 編碼規(guī)范。設置NULL由videoToolbox自己選擇
        //參數6:NULL sourceImageBufferAttributes: 源像素緩沖區(qū)屬性.設置NULL不讓videToolbox創(chuàng)建,而自己創(chuàng)建
        //參數7:NULL compressedDataAllocator: 壓縮數據分配器.設置NULL,默認的分配
        //參數8:回調  當VTCompressionSessionEncodeFrame被調用壓縮一次后會被異步調用.注:當你設置NULL的時候,你需要調用VTCompressionSessionEncodeFrameWithOutputHandler方法進行壓縮幀處理,支持iOS9.0以上
        //參數9:outputCallbackRefCon: 回調客戶定義的參考值
        //參數10:compressionSessionOut: 編碼會話變量
        OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)(self), &cEncodeingSession);

第二步:?設置相關的參數

/*
    session: 會話
    propertyKey: 屬性名稱
    propertyValue: 屬性值
*/
VT_EXPORT OSStatus 
VTSessionSetProperty(
  CM_NONNULL VTSessionRef       session,
  CM_NONNULL CFStringRef        propertyKey,
  CM_NULLABLE CFTypeRef         propertyValue ) API_AVAILABLE(macosx(10.8), ios(8.0), tvos(10.2));

  • ?kVTCompressionPropertyKey_RealTime:設置是否實時編碼
  • kVTProfileLevel_H264_Baseline_AutoLevel:表示使用H264Profile規(guī)格,可以設置HightAutoLevel規(guī)格.
  • kVTCompressionPropertyKey_AllowFrameReordering:表示是否使用產生B幀數據(因為B幀在解碼是非必要數據,所以開發(fā)者可以拋棄B幀數據)
  • kVTCompressionPropertyKey_MaxKeyFrameInterval : 表示關鍵幀的間隔,也就是我們常說的gop size.
  • kVTCompressionPropertyKey_ExpectedFrameRate : 表示設置幀率
  • kVTCompressionPropertyKey_AverageBitRate/kVTCompressionPropertyKey_DataRateLimits 設置編碼輸出的碼率.

第三步: 準備編碼


//開始編碼
VTCompressionSessionPrepareToEncodeFrames(cEncodeingSession);

第四步: 捕獲編碼數據

  • 通過AVFoundation 捕獲的視頻,這個時候我們會走到AVFoundation捕獲結果代理方法:

#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate
//AV Foundation 獲取到視頻流
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
    //開始視頻錄制闻妓,獲取到攝像頭的視頻幀,傳入encode 方法中
    dispatch_sync(cEncodeQueue, ^{
        [self encode:sampleBuffer];
    });

}

第五步:數據編碼

  • 將獲取的視頻數據編碼
- (void) encode:(CMSampleBufferRef )sampleBuffer
{
    //拿到每一幀未編碼數據
    CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);

    //設置幀時間次和,如果不設置會導致時間軸過長一姿。時間戳以ms為單位
    CMTime presentationTimeStamp = CMTimeMake(frameID++, 1000);

    VTEncodeInfoFlags flags;

    //參數1:編碼會話變量
    //參數2:未編碼數據
    //參數3:獲取到的這個sample buffer數據的展示時間戳。每一個傳給這個session的時間戳都要大于前一個展示時間戳.
    //參數4:對于獲取到sample buffer數據,這個幀的展示時間.如果沒有時間信息,可設置kCMTimeInvalid.
    //參數5:frameProperties: 包含這個幀的屬性.幀的改變會影響后邊的編碼幀.
    //參數6:ourceFrameRefCon: 回調函數會引用你設置的這個幀的參考值.
    //參數7:infoFlagsOut: 指向一個VTEncodeInfoFlags來接受一個編碼操作.如果使用異步運行,kVTEncodeInfo_Asynchronous被設置摘仅;同步運行,kVTEncodeInfo_FrameDropped被設置靶庙;設置NULL為不想接受這個信息.

    OSStatus statusCode = VTCompressionSessionEncodeFrame(cEncodeingSession, imageBuffer, presentationTimeStamp, kCMTimeInvalid, NULL, NULL, &flags);

    if (statusCode != noErr) {

        NSLog(@"H.264:VTCompressionSessionEncodeFrame faild with %d",(int)statusCode);

        VTCompressionSessionInvalidate(cEncodeingSession);
        CFRelease(cEncodeingSession);
        cEncodeingSession = NULL;
        return;
    }

    NSLog(@"H264:VTCompressionSessionEncodeFrame Success");

}

第六步: 編碼數據處理-獲取SPS/PPS

當編碼成功后,就會回調到最開始初始化編碼器會話時傳入的回調函數,回調函數的原型如下:

void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer)

  • 判斷status,如果成功則返回0(noErr);成功則繼續(xù)處理,不成功則不處理.
  • 判斷是否關鍵幀
    • 為什么要判斷關鍵幀呢?
    • 因為VideoToolBox編碼器在每一個關鍵幀前面都會輸出SPS/PPS信息.所以如果本幀未關鍵幀,則可以取出對應的SPS/PPS信息.
 //判斷當前幀是否為關鍵幀
    //獲取sps & pps 數據 只獲取1次,保存在h264文件開頭的第一幀中
    //sps(sample per second 采樣次數/s),是衡量模數轉換(ADC)時采樣速率的單位
    //pps()
    if (keyFrame) {

        //圖像存儲方式娃属,編碼器等格式描述
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);

        //sps
        size_t sparameterSetSize,sparameterSetCount;
        const uint8_t *sparameterSet;
        OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0);

        if (statusCode == noErr) {

            //獲取pps
            size_t pparameterSetSize,pparameterSetCount;
            const uint8_t *pparameterSet;

            //從第一個關鍵幀獲取sps & pps
            OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0);

            //獲取H264參數集合中的SPS和PPS
            if (statusCode == noErr)
            {
                //Found pps & sps
                NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
                NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];

                if(encoder)
                {
                    [encoder gotSpsPps:sps pps:pps];
                }
            }
        }

    }

第七步 編碼壓縮數據并寫入H264文件

當我們獲取了SPS/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;
        static const int AVCCHeaderLength = 4;//返回的nalu數據前4個字節(jié)不是001的startcode,而是大端模式的幀長度length

        //循環(huán)獲取nalu數據
        while (bufferOffset < totalLength - AVCCHeaderLength) {

            uint32_t NALUnitLength = 0;

            //讀取 一單元長度的 nalu
            memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);

            //從大端模式轉換為系統(tǒng)端模式
            NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);

            //獲取nalu數據
            NSData *data = [[NSData alloc]initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];

            //將nalu數據寫入到文件
            [encoder gotEncodedData:data isKeyFrame:keyFrame];

            //move to the next NAL unit in the block buffer
            //讀取下一個nalu 一次回調可能包含多個nalu數據
            bufferOffset += AVCCHeaderLength + NALUnitLength;

        }

    }

}

//第一幀寫入 sps & pps
- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps
{
    NSLog(@"gotSpsPp %d %d",(int)[sps length],(int)[pps length]);

    const char bytes[] = "\x00\x00\x00\x01";

    size_t length = (sizeof bytes) - 1;

    NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];

    [fileHandele writeData:ByteHeader];
    [fileHandele writeData:sps];
    [fileHandele writeData:ByteHeader];
    [fileHandele writeData:pps];

}

- (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame
{
    NSLog(@"gotEncodeData %d",(int)[data length]);

    if (fileHandele != NULL) {

        //添加4個字節(jié)的H264 協(xié)議 start code 分割符
        //一般來說編碼器編出的首幀數據為PPS & SPS
        //H264編碼時六荒,在每個NAL前添加起始碼 0x000001,解碼器在碼流中檢測起始碼,當前NAL結束膳犹。
        /*
         為了防止NAL內部出現(xiàn)0x000001的數據恬吕,h.264又提出'防止競爭 emulation prevention"機制,在編碼完一個NAL時须床,如果檢測出有連續(xù)兩個0x00字節(jié)铐料,就在后面插入一個0x03。當解碼器在NAL內部檢測到0x000003的數據豺旬,就把0x03拋棄钠惩,恢復原始數據。

         總的來說H264的碼流的打包方式有兩種,一種為annex-b byte stream format 的格式族阅,這個是絕大部分編碼器的默認輸出格式篓跛,就是每個幀的開頭的3~4個字節(jié)是H264的start_code,0x00000001或者0x000001。
         另一種是原始的NAL打包格式坦刀,就是開始的若干字節(jié)(1愧沟,2蔬咬,4字節(jié))是NAL的長度,而不是start_code,此時必須借助某個全局的數據來獲得編 碼器的profile,level,PPS,SPS等信息才可以解碼沐寺。

         */
        const char bytes[] ="\x00\x00\x00\x01";

        //長度
        size_t length = (sizeof bytes) - 1;

        //頭字節(jié)
        NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];

        //寫入頭字節(jié)
        [fileHandele writeData:ByteHeader];

        //寫入H264數據
        [fileHandele writeData:data];

    }

}


推薦文集

* 抖音效果實現(xiàn)

* BAT—最新iOS面試題總結

* iOS面試題合集

原文作者:集才華美貌于一身的—C姐

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末林艘,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子混坞,更是在濱河造成了極大的恐慌狐援,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件究孕,死亡現(xiàn)場離奇詭異啥酱,居然都是意外死亡,警方通過查閱死者的電腦和手機厨诸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門镶殷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人泳猬,你說我怎么就攤上這事批钠。” “怎么了得封?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵埋心,是天一觀的道長。 經常有香客問我忙上,道長拷呆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任疫粥,我火速辦了婚禮茬斧,結果婚禮上,老公的妹妹穿的比我還像新娘梗逮。我一直安慰自己项秉,他們只是感情好,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布慷彤。 她就那樣靜靜地躺著娄蔼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪底哗。 梳的紋絲不亂的頭發(fā)上岁诉,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天,我揣著相機與錄音跋选,去河邊找鬼涕癣。 笑死,一個胖子當著我的面吹牛前标,可吹牛的內容都是我干的坠韩。 我是一名探鬼主播距潘,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼同眯!你這毒婦竟也來了绽昼?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤须蜗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后目溉,有當地人在樹林里發(fā)現(xiàn)了一具尸體明肮,經...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年缭付,在試婚紗的時候發(fā)現(xiàn)自己被綠了柿估。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡陷猫,死狀恐怖秫舌,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情绣檬,我是刑警寧澤足陨,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站娇未,受9級特大地震影響墨缘,放射性物質發(fā)生泄漏。R本人自食惡果不足惜零抬,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一镊讼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧平夜,春花似錦蝶棋、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至锰扶,卻和暖如春献酗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背坷牛。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工罕偎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人京闰。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓颜及,卻偏偏與公主長得像甩苛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子俏站,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內容