GPUImage-3-2、對(duì)視頻編碼

在上一節(jié)中,我們使用GPUImage錄制美顏視頻,但是錄制完成后,1、視頻的體積很大,不利于傳輸;2、并且當(dāng)我們點(diǎn)擊第二次錄制的時(shí)候,會(huì)拋出異常。 對(duì)于問(wèn)題2,我們可以采用單獨(dú)寫(xiě)一個(gè)vc來(lái)處理視頻錄制功能,就可以解決。

1、為什么要對(duì)視頻進(jìn)行壓縮編碼?

  • 要解釋這個(gè)問(wèn)題,我們首先要明白,視頻是由一幀一幀的畫(huà)面組成,當(dāng)視頻中1秒能播放16幀的畫(huà)面時(shí)候,人眼就不會(huì)感覺(jué)到卡頓(一般影院中的電影是1秒30幀),假設(shè)我們不對(duì)視頻進(jìn)行壓縮編碼,舉例來(lái)說(shuō),播放一個(gè)1280720分辨率的視頻,1s的內(nèi)容需要占據(jù)的存儲(chǔ)空間是:12807203060 = 1620M,所以不經(jīng)過(guò)編碼的視頻根本沒(méi)有辦法保存,更何況是網(wǎng)絡(luò)傳輸。

2、為什么視頻可以壓縮編碼

視頻中存在許多冗余的信息,比如說(shuō):空間冗余、時(shí)間冗余、時(shí)間冗余

3、視頻編碼遵守的標(biāo)準(zhǔn)

如果每個(gè)人按照自己的方式去編碼,那么當(dāng)我們需要還原原始數(shù)據(jù)時(shí),很難知道對(duì)方是如何編碼的,所以視頻編碼肯定有套大家都遵守的標(biāo)準(zhǔn)

現(xiàn)在H264標(biāo)準(zhǔn)是被大多數(shù)人遵守的標(biāo)準(zhǔn)之一,h264是由ITU(國(guó)際電傳視訊聯(lián)盟)主導(dǎo)制定的一套標(biāo)準(zhǔn),它以高壓縮高質(zhì)量和支持多種網(wǎng)絡(luò)的流媒體傳輸著稱。除了H264標(biāo)準(zhǔn)外,H265、VP8、VP9也是比較熱門(mén)的。

  • H264
    H264中定義三種幀:
  • I幀:完整編碼的幀叫I幀
  • P幀:參考之前的I幀生成的只包含差異部分編碼的幀叫P幀
  • B幀:參考前后的幀編碼的幀叫B幀
    H264采用的核心算法是幀內(nèi)壓縮和幀間壓縮
    H264的壓縮方法:
  • 分組:把幾幀圖像分為一組(GOP)也稱一個(gè)序列,為防止運(yùn)動(dòng)變化,幀數(shù)不宜取多
  • 定義幀:將每組內(nèi)各幀圖像定位為三種類型,即I幀、B幀、P幀
  • 預(yù)測(cè)幀:以I幀作為基礎(chǔ),以I幀預(yù)測(cè)P幀,再由I幀和P幀預(yù)測(cè)B幀
  • 數(shù)據(jù)傳輸:最后將I幀數(shù)據(jù)與預(yù)測(cè)的差值進(jìn)行存儲(chǔ)和傳輸

序列(GOP)group of picture

  • 在H264中圖像以序列為單位進(jìn)行組織,一個(gè)序列是一段圖像編碼后的數(shù)據(jù)流。
  • 一個(gè)序列的第一個(gè)圖像叫做 IDR 圖像(立即刷新圖像),IDR 圖像都是 I 幀圖像。
    • H.264 引入 IDR 圖像是為了解碼的重同步,當(dāng)解碼器解碼到 IDR 圖像時(shí),立即將參考幀隊(duì)列清空,將已解碼的數(shù)據(jù)全部輸出或拋棄,重新查找參數(shù)集,開(kāi)始一個(gè)新的序列
    • 這樣,如果前一個(gè)序列出現(xiàn)重大錯(cuò)誤,在這里可以獲得重新同步的機(jī)會(huì)。
  • 一個(gè)序列就是一段內(nèi)容差異不太大的圖像編碼后生成的一串?dāng)?shù)據(jù)流
  • 當(dāng)運(yùn)動(dòng)變化比較少時(shí),一個(gè)序列可以很長(zhǎng),因?yàn)檫\(yùn)動(dòng)變化少就代表圖像畫(huà)面的內(nèi)容變動(dòng)很小,所以就可以編一個(gè)I幀,然后一直P(pán)幀、B幀了。

編碼方式

  • 硬編碼:使用非CPU進(jìn)行編碼,如顯卡GPU、專用的DSP、FPGA、ASIC芯片等;在iOS8之后,蘋(píng)果開(kāi)放了接口,并且封裝了VideoToolBox&AudioToolbox兩個(gè)框架,分別用于對(duì)視頻&音頻進(jìn)行硬編碼
  • 軟編碼: 使用CPU進(jìn)行編碼,軟編碼通常使用:ffmpeg+x264

首先是對(duì)視頻進(jìn)行硬編碼

使用的系統(tǒng)的VideoToolBox

//
//  LYVideoEncoder.h
//  視頻處理-硬編碼
//
//  Created by liyang on 16/12/29.
//  Copyright ? 2016年 liyang. All rights reserved.
//

#import <UIKit/UIKit.h>
#import <VideoToolbox/VideoToolbox.h>

@interface LYVideoEncoder : NSObject

- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer;
- (void)endEncode;

@end
//
//  LYVideoEncoder.m
//  視頻處理-硬編碼
//
//  Created by liyang on 16/12/29.
//  Copyright ? 2016年 liyang. All rights reserved.
//

#import "LYVideoEncoder.h"

@interface LYVideoEncoder ()

/** 記錄當(dāng)前的幀數(shù) */
@property (nonatomic, assign) NSInteger frameID;

/** 編碼會(huì)話 */
@property (nonatomic, assign) VTCompressionSessionRef compressionSession;

/** 文件寫(xiě)入對(duì)象 */
@property (nonatomic, strong) NSFileHandle *fileHandle;

@end

@implementation LYVideoEncoder

- (instancetype)init {
    if (self = [super init]) {
        // 1.初始化寫(xiě)入文件的對(duì)象(NSFileHandle用于寫(xiě)入二進(jìn)制文件)
        [self setupFileHandle];
        
        // 2.初始化壓縮編碼的會(huì)話
        [self setupVideoSession];
    }
    
    return self;
}

- (void)setupFileHandle {
    // 1.獲取沙盒路徑
    NSString *file = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"liyang_demo.h264"];
    
    // 2.如果原來(lái)有文件,則刪除
    [[NSFileManager defaultManager] removeItemAtPath:file error:nil];
    [[NSFileManager defaultManager] createFileAtPath:file contents:nil attributes:nil];
    
    // 3.創(chuàng)建對(duì)象
    self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:file];
}

- (void)setupVideoSession {
    // 1.用于記錄當(dāng)前是第幾幀數(shù)據(jù)(畫(huà)面幀數(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ì)畫(huà)面進(jìn)行編碼
    // kCMVideoCodecType_H264 : 表示使用h.264進(jìn)行編碼
    // didCompressH264 : 當(dāng)一次編碼結(jié)束會(huì)在該函數(shù)進(jìn)行回調(diào),可以在該函數(shù)中將數(shù)據(jù),寫(xiě)入文件中
    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è)置期望幀率(每秒多少幀,如果幀率過(guò)低,會(huì)造成畫(huà)面卡頓)
    int fps = 30;
    CFNumberRef  fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
    
    
    // 6.設(shè)置碼率(碼率: 編碼效率, 碼率越高,則畫(huà)面越清晰, 如果碼率較低會(huì)引起馬賽克 --> 碼率高有利于還原原始畫(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);
}

// 編碼完成回調(diào)
void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) {
    
    // 1.判斷狀態(tài)是否等于沒(méi)有錯(cuò)誤
    if (status != noErr) {
        return;
    }
    
    // 2.根據(jù)傳入的參數(shù)獲取對(duì)象
    LYVideoEncoder* encoder = (__bridge LYVideoEncoder*)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,以方便寫(xiě)入文件
        NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
        NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
        
        // 寫(xiě)入文件
        [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,而是大端模式的幀長(zhǎng)度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)到寫(xiě)一個(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的體寫(xiě)入文件
    [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];
    }
}

- (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.開(kāi)始編碼該幀數(shù)據(jù)
    OSStatus statusCode = VTCompressionSessionEncodeFrame(self.compressionSession,
                                                          imageBuffer,
                                                          presentationTimeStamp,
                                                          kCMTimeInvalid,
                                                          NULL, (__bridge void * _Nullable)(self), &flags);
    if (statusCode == noErr) {
        NSLog(@"H264: VTCompressionSessionEncodeFrame Success");
    }
}

- (void)endEncode {
    VTCompressionSessionCompleteFrames(self.compressionSession, kCMTimeInvalid);
    VTCompressionSessionInvalidate(self.compressionSession);
    CFRelease(self.compressionSession);
    self.compressionSession = NULL;
}

@end

對(duì)視頻進(jìn)行軟編碼

使用的ffmpeg和x264

1、編譯FFmpeg所需要的腳本文件gas-preprocessor.pl

  • 首先下載編譯FFmpeg所需要的腳本文件gas-preprocessor.pl
    下載地址
  • 復(fù)制gas-preprocessor.pl文件到 /usr/local/bin 目錄下
  • 修改文件權(quán)限:chmod 777 /usr/local/bin/gas-preprocessor.pl
命名是: 
cp 文件絕對(duì)路徑 空格 目標(biāo)目錄下
cp /Users/liyang/Downloads/gas-preprocessor-master/gas-preprocessor.pl /usr/local/bin

chmod 777 /usr/local/bin/gas-preprocessor.pl
Paste_Image.png

2、編譯FFmpeg腳本

下載腳本ffmpeg
解壓,執(zhí)行腳本命名

 ./build-ffmpeg.sh
 ./build-ffmpeg.sh arm64
 ./build-ffmpeg.sh armv7 x86_64
 ./build-ffmpeg.sh lipo

可能需要消耗點(diǎn)時(shí)間,完成后這樣的,紅色框中的文件就是我們需要的

Paste_Image.png

3、編譯X264

  • 下載X264,x264官網(wǎng) 下載x264源碼,將其文件夾名稱改為x264
  • 下載x264 build shell
  • 下載gas-preprocessor(FFmpeg編譯時(shí)已經(jīng)下載過(guò))
  • 下載build-x264.sh 將文件build-x264.sh放在x264同一級(jí)目錄里面,注意不是放在x264文件夾里面。
  • 修改權(quán)限/執(zhí)行腳本
    sudo chmod u+x build-x264.sh
    sudo ./build-x264.sh

4、iOS項(xiàng)目中集成FFmpeg
libiconv.dylib/libz.dylib/libbz2.dylib/CoreMedia.framework/AVFoundation.framework

準(zhǔn)備編碼
//
//  X264Manager.h
//  02-Encode(FFmpeg+x264)
//
//  Created by liyang on 16/12/15.
//  Copyright ? 2016年 liyang. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>

@interface X264Manager : NSObject

/*
 * 設(shè)置編碼后文件的保存路徑
 */
- (void)setFileSavedPath:(NSString *)path;

/*
 * 設(shè)置X264
 * 0: 成功; -1: 失敗
 * width: 視頻寬度
 * height: 視頻高度
 * bitrate: 視頻碼率,碼率直接影響編碼后視頻畫(huà)面的清晰度, 越大越清晰,但是為了便于保證編碼后的數(shù)據(jù)量不至于過(guò)大,以及適應(yīng)網(wǎng)絡(luò)帶寬傳輸,就需要合適的選擇該值
 */
- (int)setX264ResourceWithVideoWidth:(int)width height:(int)height bitrate:(int)bitrate;

/*
 * 將CMSampleBufferRef格式的數(shù)據(jù)編碼成h264并寫(xiě)入文件
 */
- (void)encoderToH264:(CMSampleBufferRef)sampleBuffer;

/*
 * 釋放資源
 */
- (void)freeX264Resource;

@end
//
//  X264Manager.m
//  02-Encode(FFmpeg+x264)
//
//  Created by liyang on 16/12/15.
//  Copyright ? 2016年 liyang  All rights reserved.
//

#import "X264Manager.h"

#ifdef __cplusplus
extern "C" {
#endif
    
#include <libavutil/opt.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

#ifdef __cplusplus
};
#endif

/*
 編碼之后&解碼之前的畫(huà)面: AVFrame --> 內(nèi)容寫(xiě)入文件
 編碼之前&解碼之后的畫(huà)面: AVPackage --> 解碼之后, 使用OpenGLES渲染
 */

@implementation X264Manager
{
    AVFormatContext                     *pFormatCtx;
    AVOutputFormat                      *fmt;
    AVStream                            *video_st;
    AVCodecContext                      *pCodecCtx;
    AVCodec                             *pCodec;
    AVPacket                             pkt;
    uint8_t                             *picture_buf;
    AVFrame                             *pFrame;
    int                                  picture_size;
    int                                  y_size;
    int                                  framecnt;
    char                                *out_file;
    
    int                                  encoder_h264_frame_width; // 編碼的圖像寬度
    int                                  encoder_h264_frame_height; // 編碼的圖像高度
}

/*
 * 設(shè)置編碼后文件的文件名,保存路徑
 */
- (void)setFileSavedPath:(NSString *)path;
{
    out_file = [self nsstring2char:path];
}

/*
 * 將路徑轉(zhuǎn)成C語(yǔ)言字符串(傳入路徑為C字符串)
 */
- (char*)nsstring2char:(NSString *)path
{

    NSUInteger len = [path length];
    char *filepath = (char*)malloc(sizeof(char) * (len + 1));
    
    [path getCString:filepath maxLength:len + 1 encoding:[NSString defaultCStringEncoding]];
    
    return filepath;
}

/*
 *  設(shè)置X264
 */
- (int)setX264ResourceWithVideoWidth:(int)width height:(int)height bitrate:(int)bitrate
{
    // 1.默認(rèn)從第0幀開(kāi)始(記錄當(dāng)前的幀數(shù))
    framecnt = 0;
    
    // 2.記錄傳入的寬度&高度
    encoder_h264_frame_width = width;
    encoder_h264_frame_height = height;
    
    // 3.注冊(cè)FFmpeg所有編解碼器(無(wú)論編碼還是解碼都需要該步驟)
    av_register_all();
    
    // 4.初始化AVFormatContext: 用作之后寫(xiě)入視頻幀并編碼成 h264,貫穿整個(gè)工程當(dāng)中(釋放資源時(shí)需要銷毀)
    pFormatCtx = avformat_alloc_context();
    
    // 5.設(shè)置輸出文件的路徑
    fmt = av_guess_format(NULL, out_file, NULL);
    pFormatCtx->oformat = fmt;
    
    // 6.打開(kāi)文件的緩沖區(qū)輸入輸出,flags 標(biāo)識(shí)為  AVIO_FLAG_READ_WRITE ,可讀寫(xiě)
    if (avio_open(&pFormatCtx->pb, out_file, AVIO_FLAG_READ_WRITE) < 0){
        printf("Failed to open output file! \n");
        return -1;
    }
    
    // 7.創(chuàng)建新的輸出流, 用于寫(xiě)入文件
    video_st = avformat_new_stream(pFormatCtx, 0);
    
    // 8.設(shè)置 20 幀每秒 ,也就是 fps 為 20
    video_st->time_base.num = 1;
    video_st->time_base.den = 25;
    
    if (video_st==NULL){
        return -1;
    }
    
    // 9.pCodecCtx 用戶存儲(chǔ)編碼所需的參數(shù)格式等等
    // 9.1.從媒體流中獲取到編碼結(jié)構(gòu)體,他們是一一對(duì)應(yīng)的關(guān)系,一個(gè) AVStream 對(duì)應(yīng)一個(gè)  AVCodecContext
    pCodecCtx = video_st->codec;
    
    // 9.2.設(shè)置編碼器的編碼格式(是一個(gè)id),每一個(gè)編碼器都對(duì)應(yīng)著自己的 id,例如 h264 的編碼 id 就是 AV_CODEC_ID_H264
    pCodecCtx->codec_id = fmt->video_codec;
    
    // 9.3.設(shè)置編碼類型為 視頻編碼
    pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
    
    // 9.4.設(shè)置像素格式為 yuv 格式
    pCodecCtx->pix_fmt = PIX_FMT_YUV420P;
    
    // 9.5.設(shè)置視頻的寬高
    pCodecCtx->width = encoder_h264_frame_width;
    pCodecCtx->height = encoder_h264_frame_height;
    
    // 9.6.設(shè)置幀率
    pCodecCtx->time_base.num = 1;
    pCodecCtx->time_base.den = 25;
    
    // 9.7.設(shè)置碼率(比特率)
    pCodecCtx->bit_rate = bitrate;
    
    // 9.8.視頻質(zhì)量度量標(biāo)準(zhǔn)(常見(jiàn)qmin=10, qmax=51)
    pCodecCtx->qmin = 10;
    pCodecCtx->qmax = 51;
    
    // 9.9.設(shè)置圖像組層的大小(GOP-->兩個(gè)I幀之間的間隔)
    pCodecCtx->gop_size = 30;
    
    // 9.10.設(shè)置 B 幀最大的數(shù)量,B幀為視頻圖片空間的前后預(yù)測(cè)幀, B 幀相對(duì)于 I、P 幀來(lái)說(shuō),壓縮率比較大,也就是說(shuō)相同碼率的情況下,
    // 越多 B 幀的視頻,越清晰,現(xiàn)在很多打視頻網(wǎng)站的高清視頻,就是采用多編碼 B 幀去提高清晰度,
    // 但同時(shí)對(duì)于編解碼的復(fù)雜度比較高,比較消耗性能與時(shí)間
    pCodecCtx->max_b_frames = 5;
    
    // 10.可選設(shè)置
    AVDictionary *param = 0;
    // H.264
    if(pCodecCtx->codec_id == AV_CODEC_ID_H264) {
        // 通過(guò)--preset的參數(shù)調(diào)節(jié)編碼速度和質(zhì)量的平衡。
        av_dict_set(&param, "preset", "slow", 0);
        
        // 通過(guò)--tune的參數(shù)值指定片子的類型,是和視覺(jué)優(yōu)化的參數(shù),或有特別的情況。
        // zerolatency: 零延遲,用在需要非常低的延遲的情況下,比如視頻直播的編碼
        av_dict_set(&param, "tune", "zerolatency", 0);
    }
    
    // 11.輸出打印信息,內(nèi)部是通過(guò)printf函數(shù)輸出(不需要輸出可以注釋掉該局)
    av_dump_format(pFormatCtx, 0, out_file, 1);
    
    // 12.通過(guò) codec_id 找到對(duì)應(yīng)的編碼器
    pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
    if (!pCodec) {
        printf("Can not find encoder! \n");
        return -1;
    }
    
    // 13.打開(kāi)編碼器,并設(shè)置參數(shù) param
    if (avcodec_open2(pCodecCtx, pCodec,&param) < 0) {
        printf("Failed to open encoder! \n");
        return -1;
    }
    
    // 13.初始化原始數(shù)據(jù)對(duì)象: AVFrame
    pFrame = av_frame_alloc();
    
    // 14.通過(guò)像素格式(這里為 YUV)獲取圖片的真實(shí)大小,例如將 480 * 720 轉(zhuǎn)換成 int 類型
    avpicture_fill((AVPicture *)pFrame, picture_buf, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height);
    
    // 15.h264 封裝格式的文件頭部,基本上每種編碼都有著自己的格式的頭部,想看具體實(shí)現(xiàn)的同學(xué)可以看看 h264 的具體實(shí)現(xiàn)
    avformat_write_header(pFormatCtx, NULL);
    
    // 16.創(chuàng)建編碼后的數(shù)據(jù) AVPacket 結(jié)構(gòu)體來(lái)存儲(chǔ) AVFrame 編碼后生成的數(shù)據(jù)
    av_new_packet(&pkt, picture_size);
    
    // 17.設(shè)置 yuv 數(shù)據(jù)中 y 圖的寬高
    y_size = pCodecCtx->width * pCodecCtx->height;
    
    return 0;
}

/*
 * 將CMSampleBufferRef格式的數(shù)據(jù)編碼成h264并寫(xiě)入文件
 * 
 */
- (void)encoderToH264:(CMSampleBufferRef)sampleBuffer
{
    // 1.通過(guò)CMSampleBufferRef對(duì)象獲取CVPixelBufferRef對(duì)象
    CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    
    // 2.鎖定imageBuffer內(nèi)存地址開(kāi)始進(jìn)行編碼
    if (CVPixelBufferLockBaseAddress(imageBuffer, 0) == kCVReturnSuccess) {
        // 3.從CVPixelBufferRef讀取YUV的值
        // NV12和NV21屬于YUV格式,是一種two-plane模式,即Y和UV分為兩個(gè)Plane,但是UV(CbCr)為交錯(cuò)存儲(chǔ),而不是分為三個(gè)plane
        // 3.1.獲取Y分量的地址
        UInt8 *bufferPtr = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,0);
        // 3.2.獲取UV分量的地址
        UInt8 *bufferPtr1 = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,1);
        
        // 3.3.根據(jù)像素獲取圖片的真實(shí)寬度&高度
        size_t width = CVPixelBufferGetWidth(imageBuffer);
        size_t height = CVPixelBufferGetHeight(imageBuffer);
        // 獲取Y分量長(zhǎng)度
        size_t bytesrow0 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,0);
        size_t bytesrow1  = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,1);
        UInt8 *yuv420_data = (UInt8 *)malloc(width * height *3/2);
        
        // 3.4.將NV12數(shù)據(jù)轉(zhuǎn)成YUV420P(I420)數(shù)據(jù)
        UInt8 *pY = bufferPtr ;
        UInt8 *pUV = bufferPtr1;
        UInt8 *pU = yuv420_data + width*height;
        UInt8 *pV = pU + width*height/4;
        for(int i =0;i<height;i++)
        {
            memcpy(yuv420_data+i*width,pY+i*bytesrow0,width);
        }
        for(int j = 0;j<height/2;j++)
        {
            for(int i =0;i<width/2;i++)
            {
                *(pU++) = pUV[i<<1];
                *(pV++) = pUV[(i<<1) + 1];
            }
            pUV+=bytesrow1;
        }
        
        // 3.5.分別讀取YUV的數(shù)據(jù)
        picture_buf = yuv420_data;
        pFrame->data[0] = picture_buf;              // Y
        pFrame->data[1] = picture_buf+ y_size;      // U
        pFrame->data[2] = picture_buf+ y_size*5/4;  // V
        
        // 4.設(shè)置當(dāng)前幀
        pFrame->pts = framecnt;
        int got_picture = 0;
        
        // 4.設(shè)置寬度高度以及YUV各式
        pFrame->width = encoder_h264_frame_width;
        pFrame->height = encoder_h264_frame_height;
        pFrame->format = PIX_FMT_YUV420P;
        
        // 5.對(duì)編碼前的原始數(shù)據(jù)(AVFormat)利用編碼器進(jìn)行編碼,將 pFrame 編碼后的數(shù)據(jù)傳入pkt 中
        int ret = avcodec_encode_video2(pCodecCtx, &pkt, pFrame, &got_picture);
        if(ret < 0) {
            printf("Failed to encode! \n");
            
        }
        
        // 6.編碼成功后寫(xiě)入 AVPacket 到 輸入輸出數(shù)據(jù)操作著 pFormatCtx 中,當(dāng)然,記得釋放內(nèi)存
        if (got_picture==1) {
            framecnt++;
            pkt.stream_index = video_st->index;
            ret = av_write_frame(pFormatCtx, &pkt);
            av_free_packet(&pkt);
        }
        
        // 7.釋放yuv數(shù)據(jù)
        free(yuv420_data);
    }
    
    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
}

/*
 * 釋放資源
 */
- (void)freeX264Resource
{
    // 1.釋放AVFormatContext
    int ret = flush_encoder(pFormatCtx,0);
    if (ret < 0) {
        printf("Flushing encoder failed\n");
    }
    
    // 2.將還未輸出的AVPacket輸出出來(lái)
    av_write_trailer(pFormatCtx);
    
    // 3.關(guān)閉資源
    if (video_st){
        avcodec_close(video_st->codec);
        av_free(pFrame);
    }
    avio_close(pFormatCtx->pb);
    avformat_free_context(pFormatCtx);
}

int flush_encoder(AVFormatContext *fmt_ctx,unsigned int stream_index)
{
    int ret;
    int got_frame;
    AVPacket enc_pkt;
    if (!(fmt_ctx->streams[stream_index]->codec->codec->capabilities &
          CODEC_CAP_DELAY))
        return 0;
    
    while (1) {
        enc_pkt.data = NULL;
        enc_pkt.size = 0;
        av_init_packet(&enc_pkt);
        ret = avcodec_encode_video2 (fmt_ctx->streams[stream_index]->codec, &enc_pkt,
                                     NULL, &got_frame);
        av_frame_free(NULL);
        if (ret < 0)
            break;
        if (!got_frame){
            ret=0;
            break;
        }
        ret = av_write_frame(fmt_ctx, &enc_pkt);
        if (ret < 0)
            break;
    }
    return ret;
}

@end

編碼出來(lái)的視頻是 x264 格式的,encoder.h264視頻體積小方便傳輸,下面一章會(huì)介紹如何利用FFmpeg解碼視頻,并且播放。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容