iOS硬編碼實(shí)現(xiàn)

前言

  • 這里我們重點(diǎn)介紹硬編碼的使用方式孔祸,也就是VideoToolBox框架的使用
  • 編碼的流程:采集--> 獲取到視頻幀--> 對(duì)視頻幀進(jìn)行編碼 --> 獲取到視頻幀信息 --> 將編碼后的數(shù)據(jù)以NALU方式寫入到文件

視頻采集

  • 視頻采集我們已經(jīng)在前面進(jìn)行了介紹和學(xué)習(xí),所有這里就直接貼代碼发皿,只是我對(duì)采集過程進(jìn)行了一些簡單的封裝


    image.png

視頻硬件編碼

  • 初始化壓縮編碼會(huì)話(VTCompressionSessionRef)
    • 在VideoToolbox框架的使用過程中崔慧,基本都是C語言函數(shù)
  • 初始化后通過VTSessionSetProperty設(shè)置對(duì)象屬性
    • 編碼方式:H.264編碼
    • 幀率:每秒鐘多少幀畫面
    • 碼率:單位時(shí)間內(nèi)保存的數(shù)據(jù)量
    • 關(guān)鍵幀(GOPsize)間隔:多少幀為一個(gè)GOP
  • 準(zhǔn)備編碼
  • 代碼如下:
- (void)setupVideoSession {
    // 1.用于記錄當(dāng)前是第幾幀數(shù)據(jù)(畫面幀數(shù)非常多)
    self.frameID = 0;

    // 2.錄制視頻的寬度&高度
    int width = [UIScreen mainScreen].bounds.size.width;
    int height = [UIScreen mainScreen].bounds.size.height;

    // 3.創(chuàng)建CompressionSession對(duì)象,該對(duì)象用于對(duì)畫面進(jìn)行編碼
    // kCMVideoCodecType_H264 : 表示使用h.264進(jìn)行編碼
    // didCompressH264 : 當(dāng)一次編碼結(jié)束會(huì)在該函數(shù)進(jìn)行回調(diào),可以在該函數(shù)中將數(shù)據(jù),寫入文件中
    VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)(self),  &_compressionSession);

    // 4.設(shè)置實(shí)時(shí)編碼輸出(直播必然是實(shí)時(shí)輸出,否則會(huì)有延遲)
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);

    // 5.設(shè)置期望幀率(每秒多少幀,如果幀率過低,會(huì)造成畫面卡頓)
    int fps = 30;
    CFNumberRef  fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);


    // 6.設(shè)置碼率(碼率: 編碼效率, 碼率越高,則畫面越清晰, 如果碼率較低會(huì)引起馬賽克 --> 碼率高有利于還原原始畫面,但是也不利于傳輸)
    int bitRate = 800*1024;
    CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);
    NSArray *limit = @[@(bitRate * 1.5/8), @(1)];
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);

    // 7.設(shè)置關(guān)鍵幀(GOPsize)間隔
    int frameInterval = 30;
    CFNumberRef  frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);

    // 8.基本設(shè)置結(jié)束, 準(zhǔn)備進(jìn)行編碼
    VTCompressionSessionPrepareToEncodeFrames(self.compressionSession);
}
  • 將輸入的幀進(jìn)行編碼
    • 將CMSampleBufferRef轉(zhuǎn)成CVImageBufferRef
    • 開始對(duì)CVImageBufferRef進(jìn)行編碼
- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer {
    // 1.將sampleBuffer轉(zhuǎn)成imageBuffer
    CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);

    // 2.根據(jù)當(dāng)前的幀數(shù),創(chuàng)建CMTime的時(shí)間
    CMTime presentationTimeStamp = CMTimeMake(self.frameID++, 1000);
    VTEncodeInfoFlags flags;

    // 3.開始編碼該幀數(shù)據(jù)
    OSStatus statusCode = VTCompressionSessionEncodeFrame(self.compressionSession,
                                                          imageBuffer,
                                                          presentationTimeStamp,
                                                          kCMTimeInvalid,
                                                          NULL, (__bridge void * _Nullable)(self), &flags);
    if (statusCode == noErr) {
        NSLog(@"H264: VTCompressionSessionEncodeFrame Success");
    }
}
  • 當(dāng)編碼成功后,將編碼后的碼流寫入文件
    • 編碼成功后會(huì)回調(diào)之前輸入的函數(shù)
    • 1> 先判斷是否是關(guān)鍵幀:
      • 如果是關(guān)鍵幀穴墅,則需要在寫入關(guān)鍵幀之前惶室,先寫入PPS、SPS的NALU
      • 取出PPS玄货、SPS數(shù)據(jù)皇钞,并且封裝成NALU單元,寫入文件
    • 2> 將I幀誉结、P幀鹅士、B幀分別封裝成NALU單元寫入文件
    • 寫入后,數(shù)據(jù)存儲(chǔ)方式:


      image.png
  • 代碼如下:
// 編碼完成回調(diào)
void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) {

    // 1.判斷狀態(tài)是否等于沒有錯(cuò)誤
    if (status != noErr) {
        return;
    }

    // 2.根據(jù)傳入的參數(shù)獲取對(duì)象
    VideoEncoder* encoder = (__bridge VideoEncoder*)outputCallbackRefCon;

    // 3.判斷是否是關(guān)鍵幀
    bool isKeyframe = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
    // 判斷當(dāng)前幀是否為關(guān)鍵幀
    // 獲取sps & pps數(shù)據(jù)
    if (isKeyframe)
    {
        // 獲取編碼后的信息(存儲(chǔ)于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 );

        // 裝sps/pps轉(zhuǎn)成NSData惩坑,以方便寫入文件
        NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
        NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];

        // 寫入文件
        [encoder gotSpsPps:sps pps:pps];
    }

    // 獲取數(shù)據(jù)塊
    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數(shù)據(jù)前四個(gè)字節(jié)不是0001的startcode掉盅,而是大端模式的幀長度length

        // 循環(huán)獲取nalu數(shù)據(jù)
        while (bufferOffset < totalLength - AVCCHeaderLength) {
            uint32_t NALUnitLength = 0;
            // Read the NAL unit length
            memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);

            // 從大端轉(zhuǎn)系統(tǒng)端
            NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);

            NSData* data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];
            [encoder gotEncodedData:data isKeyFrame:isKeyframe];

            // 移動(dòng)到寫一個(gè)塊,轉(zhuǎn)成NALU單元
            // Move to the next NAL unit in the block buffer
            bufferOffset += AVCCHeaderLength + NALUnitLength;
        }
    }
}

- (void)gotSpsPps:(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 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];
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末以舒,一起剝皮案震驚了整個(gè)濱河市趾痘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蔓钟,老刑警劉巖永票,帶你破解...
    沈念sama閱讀 222,627評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡侣集,警方通過查閱死者的電腦和手機(jī)键俱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來世分,“玉大人编振,你說我怎么就攤上這事〕袈瘢” “怎么了踪央?”我有些...
    開封第一講書人閱讀 169,346評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長瓢阴。 經(jīng)常有香客問我畅蹂,道長,這世上最難降的妖魔是什么荣恐? 我笑而不...
    開封第一講書人閱讀 60,097評(píng)論 1 300
  • 正文 為了忘掉前任液斜,我火速辦了婚禮,結(jié)果婚禮上募胃,老公的妹妹穿的比我還像新娘旗唁。我一直安慰自己,他們只是感情好痹束,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,100評(píng)論 6 398
  • 文/花漫 我一把揭開白布检疫。 她就那樣靜靜地躺著,像睡著了一般祷嘶。 火紅的嫁衣襯著肌膚如雪屎媳。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,696評(píng)論 1 312
  • 那天论巍,我揣著相機(jī)與錄音烛谊,去河邊找鬼。 笑死嘉汰,一個(gè)胖子當(dāng)著我的面吹牛丹禀,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鞋怀,決...
    沈念sama閱讀 41,165評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼双泪,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了密似?” 一聲冷哼從身側(cè)響起焙矛,我...
    開封第一講書人閱讀 40,108評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎残腌,沒想到半個(gè)月后村斟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體贫导,經(jīng)...
    沈念sama閱讀 46,646評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,709評(píng)論 3 342
  • 正文 我和宋清朗相戀三年蟆盹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了孩灯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,861評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡日缨,死狀恐怖钱反,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情匣距,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布哎壳,位于F島的核電站毅待,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏归榕。R本人自食惡果不足惜尸红,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,196評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望刹泄。 院中可真熱鬧外里,春花似錦、人聲如沸特石。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽姆蘸。三九已至墩莫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間逞敷,已是汗流浹背狂秦。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評(píng)論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留推捐,地道東北人裂问。 一個(gè)月前我還...
    沈念sama閱讀 49,287評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像牛柒,于是被迫代替她去往敵國和親堪簿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,860評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容