上篇記錄了利用x264編碼的實現(xiàn)幻枉,這里記錄一下FFmpeg+x264編碼為H.264的實現(xiàn)過程,為什么使用FFmpeg+x264弧呐,因為兩種框架互補組成了強大的編解碼器(FFmpeg解碼折晦,x246編碼)教沾。
編譯FFmpeg+x264
- FFmpeg
地址:https://github.com/FFmpeg/FFmpeg
編譯腳本:https://github.com/kewlbear/FFmpeg-iOS-build-script - x264
地址:http://www.videolan.org/developers/x264.html
編譯腳本:https://github.com/kewlbear/x264-ios
具體編譯過程:x264的編譯過程在上篇iOS音視頻開發(fā)-視頻軟編碼(x264編碼H.264文件)
FFmpeg+x264編譯步驟:
- 1、將編譯好的x264文件夾放置在FFmpeg腳本目錄下朱转,并將文件夾改名為fat-x264(因為腳本中定義的引用x264文件夾的名稱為fat-x264);
- 2蟹地、執(zhí)行腳本文件:./build-ffmpeg.sh。
這里需要注意的問題藤为,之前編譯并沒有遇到怪与,此次總結(jié)重新編譯了一次,遇到一些問題記錄一下凉蜂。- 1)最新的FFmpeg版本為:n3.4.2琼梆,腳本中使用版本號為:n3.4。執(zhí)行腳本的時候會出現(xiàn)諸如此類的錯誤:
解決辦法:修改腳本文件中的版本號:libavcodec/libx264.c: In function 'x264_init_static': libavcodec/libx264.c:892.9 error: 'x264_bit_depth' undeclared(first use in this function) if(x264_bit_depth== 8)
FF_VERSION="3.4
->FF_VERSION="3.4.2"
窿吩。- 2)要將x264編譯進FFmpeg中茎杂,需要取消腳本中對該句代碼的注銷:
#X264=`pwd`/fat-x264
->X264=`pwd`/fat-x264
- 3)關(guān)于bitcode,現(xiàn)階段在編譯庫文件的時候纫雁,支持bitcode還是有必要的(畢竟某個工程因為使用了此編譯庫文件放棄bitcode功能煌往,向上層開發(fā)者提供了功能,是不使用是他們的事了)轧邪,腳本中已經(jīng)實現(xiàn)了支持bitcode功能刽脖,不必修改。
- 4)將編譯好的文件拖拽到工程中忌愚,需要添加的依賴庫為:libiconv.dylib/libz.dylib/libbz2.dylib/CoreMedia.framework/AVFoundation.framework/VideoToolbox.framework曲管,不然會報并未支持arm64......架構(gòu)的錯誤。
命令行命令:
//查看是否支持相應(yīng)的架構(gòu):arm64 i386....
lipo -info libxx.a
//查看是夠支持bitcode >=0
otool -l libx264.a | grep __bitcode | wc -l
介紹:不錯的編譯過程文章
視頻捕獲和編碼配置代碼在前面已記錄硕糊,這里就不贅述了院水。FFmpeg+x264編碼實現(xiàn)代碼如下:
#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>
@class BBVideoConfig;
@interface BBH264SoftEncoder : NSObject
/*
* 設(shè)置編碼后文件的保存路徑
*/
- (void)setFilePath:(NSString *)path;
/*
* 初始化編碼配置
*/
- (void)setupEncodeWithConfig:(BBVideoConfig *)config;
/*
* 將CMSampleBufferRef格式的數(shù)據(jù)編碼成h264并寫入文件
*/
- (void)encoderToH264:(CMSampleBufferRef)sampleBuffer;
/*
* 釋放資源
*/
- (void)freeX264Resource;
@end
#import "BBH264SoftEncoder.h"
#import "BBVideoConfig.h"
#ifdef __cplusplus
extern "C" {
#endif
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#ifdef __cplusplus
};
#endif
@implementation BBH264SoftEncoder
{
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)setFilePath:(NSString *)path;
{
out_file = [self nsstring2char:path];
}
/*
* 將路徑轉(zhuǎn)成C語言字符串(傳入路徑為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
*/
- (void)setupEncodeWithConfig:(BBVideoConfig *)config
{
// 1.默認(rèn)從第0幀開始(記錄當(dāng)前的幀數(shù))
framecnt = 0;
// 2.記錄傳入的寬度&高度
encoder_h264_frame_width = config.videoSize.width;
encoder_h264_frame_height = config.videoSize.height;
// 3.注冊FFmpeg所有編解碼器(無論編碼還是解碼都需要該步驟)
av_register_all();
// 4.初始化AVFormatContext: 用作之后寫入視頻幀并編碼成 h264简十,貫穿整個工程當(dāng)中(釋放資源時需要銷毀)
pFormatCtx = avformat_alloc_context();
// 5.設(shè)置輸出文件的路徑檬某,fmt初始化的時候根據(jù)傳入的參數(shù)猜出video_codec、mime_type螟蝙、extensions等等信息恢恼。
fmt = av_guess_format(NULL, out_file, NULL);
pFormatCtx->oformat = fmt;
// 6.打開文件的緩沖區(qū)輸入輸出,flags 標(biāo)識為 AVIO_FLAG_READ_WRITE 胰默,可讀寫
if (avio_open(&pFormatCtx->pb, out_file, AVIO_FLAG_READ_WRITE) < 0){
printf("Failed to open output file! \n");
}
// 7.創(chuàng)建新的輸出流, 用于寫入文件
video_st = avformat_new_stream(pFormatCtx, 0);
if (video_st==NULL){ printf("Failed to setup stream! \n"); return; }
// 8.pCodecCtx 用戶存儲編碼所需的參數(shù)格式等等
// 8.1.從媒體流中獲取到編碼結(jié)構(gòu)體场斑,他們是一一對應(yīng)的關(guān)系漓踢,一個 AVStream 對應(yīng)一個 AVCodecContext
AVCodec *codec = avcodec_find_encoder(pFormatCtx->oformat->video_codec);
pCodecCtx = avcodec_alloc_context3(codec);
// 8.2.設(shè)置編碼器的編碼格式(是一個id),每一個編碼器都對應(yīng)著自己的 id和簸,例如 h264 的編碼 id 就是 AV_CODEC_ID_H264
pCodecCtx->codec_id = fmt->video_codec;
// 8.3.設(shè)置編碼類型為 視頻編碼
pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
// 8.4.設(shè)置像素格式為 yuv 格式
pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
// 8.5.設(shè)置視頻的寬高
pCodecCtx->width = encoder_h264_frame_width;
pCodecCtx->height = encoder_h264_frame_height;
// 8.6.設(shè)置幀率
pCodecCtx->time_base.num = 1;
pCodecCtx->time_base.den = 25;
// 8.7.設(shè)置碼率(比特率)
pCodecCtx->bit_rate = config.bitrate;
// 8.8.視頻質(zhì)量度量標(biāo)準(zhǔn)(常見qmin=10, qmax=51)
pCodecCtx->qmin = 10;
pCodecCtx->qmax = 51;
// 8.9.設(shè)置圖像組層的大小(GOP-->兩個I幀之間的間隔)
pCodecCtx->gop_size = 30;
// 8.10.設(shè)置 B 幀最大的數(shù)量彭雾,B幀為視頻圖片空間的前后預(yù)測幀, B 幀相對于 I锁保、P 幀來說薯酝,壓縮率比較大,也就是說相同碼率的情況下爽柒,
// 越多 B 幀的視頻吴菠,越清晰,現(xiàn)在很多打視頻網(wǎng)站的高清視頻浩村,就是采用多編碼 B 幀去提高清晰度做葵,
// 但同時對于編解碼的復(fù)雜度比較高,比較消耗性能與時間
pCodecCtx->max_b_frames = 5;
// 9.可選設(shè)置
AVDictionary *param = 0;
// H.264
if(pCodecCtx->codec_id == AV_CODEC_ID_H264) {
// 通過--preset的參數(shù)調(diào)節(jié)編碼速度和質(zhì)量的平衡心墅。
av_dict_set(¶m, "preset", "slow", 0);
// 通過--tune的參數(shù)值指定片子的類型酿矢,是和視覺優(yōu)化的參數(shù),或有特別的情況怎燥。
// zerolatency: 零延遲瘫筐,用在需要非常低的延遲的情況下,比如視頻直播的編碼
av_dict_set(¶m, "tune", "zerolatency", 0);
}
// 10.輸出打印信息铐姚,內(nèi)部是通過printf函數(shù)輸出(不需要輸出可以注釋掉)
av_dump_format(pFormatCtx, 0, out_file, 1);
// 11.通過 codec_id 找到對應(yīng)的編碼器
pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
if (!pCodec) {
printf("Can not find encoder! \n");
}
// 12.打開編碼器策肝,并設(shè)置參數(shù) param
if (avcodec_open2(pCodecCtx, pCodec,¶m) < 0) {
printf("Failed to open encoder! \n");
}
// 13.將AVCodecContext的成員復(fù)制到AVCodecParameters結(jié)構(gòu)體
avcodec_parameters_from_context(video_st->codecpar, pCodecCtx);
// 14.真實幀率
AVRational rational = {1, 25};
av_stream_set_r_frame_rate(video_st, rational);
// 15.初始化原始數(shù)據(jù)對象: AVFrame
pFrame = av_frame_alloc();
// 16.通過像素格式(這里為 YUV)獲取圖片的真實大小,例如將 480 * 720 轉(zhuǎn)換成 int 類型
av_image_fill_arrays(pFrame->data, pFrame->linesize, picture_buf, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, 1);
// 17.h264 封裝格式的文件頭部隐绵,基本上每種編碼都有著自己的格式的頭部之众。
if (avformat_write_header(pFormatCtx, NULL) < 0) { printf("Failed to write! \n"); return; }
// 18.創(chuàng)建編碼后的數(shù)據(jù) AVPacket 結(jié)構(gòu)體來存儲 AVFrame 編碼后生成的數(shù)據(jù)
av_new_packet(&pkt, picture_size);
// 19.設(shè)置 yuv 數(shù)據(jù)中 y 圖的寬高
y_size = pCodecCtx->width * pCodecCtx->height;
}
/*
* 將CMSampleBufferRef格式的數(shù)據(jù)編碼成h264并寫入文件
*
*/
- (void)encoderToH264:(CMSampleBufferRef)sampleBuffer
{
// 1.通過CMSampleBufferRef對象獲取CVPixelBufferRef對象
CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
// 2.鎖定imageBuffer內(nèi)存地址開始進行編碼
if (CVPixelBufferLockBaseAddress(imageBuffer, 0) == kCVReturnSuccess) {
// 3.從CVPixelBufferRef讀取YUV的值
// NV12和NV21屬于YUV格式,是一種two-plane模式依许,即Y和UV分為兩個Plane棺禾,但是UV(CbCr)為交錯存儲,而不是分為三個plane
// 3.1.獲取Y分量的地址
UInt8 *bufferPtr = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,0);
// 3.2.獲取UV分量的地址
UInt8 *bufferPtr1 = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,1);
// 3.3.根據(jù)像素獲取圖片的真實寬度&高度
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
// 獲取Y分量長度
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;
// 4.設(shè)置寬度高度以及YUV格式
pFrame->width = encoder_h264_frame_width;
pFrame->height = encoder_h264_frame_height;
pFrame->format = AV_PIX_FMT_YUV420P;
// 5.對編碼前的原始數(shù)據(jù)(AVFormat)利用編碼器進行編碼峭跳,將 pFrame 編碼后的數(shù)據(jù)傳入pkt 中
int ret = avcodec_send_frame(pCodecCtx, pFrame);
if (ret != 0) {
printf("Failed to encode! \n");
return;
}
while (avcodec_receive_packet(pCodecCtx, &pkt) == 0) {
framecnt++;
pkt.stream_index = video_st->index;
//也可以使用C語言函數(shù):fwrite()膘婶、fflush()寫文件和清空文件寫入緩沖區(qū)。
ret = av_write_frame(pFormatCtx, &pkt);
if (ret < 0) {
printf("Failed write to file坦康!\n");
}
//釋放packet
av_packet_unref(&pkt);
}
// 7.釋放yuv數(shù)據(jù)
free(yuv420_data);
}
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
}
/*
* 釋放資源
*/
- (void)freeX264Resource
{
// 1.將還未輸出的AVPacket輸出出來
av_write_trailer(pFormatCtx);
// 2.關(guān)閉資源
avcodec_close(pCodecCtx);
av_free(pFrame);
avio_close(pFormatCtx->pb);
avformat_free_context(pFormatCtx);
}
@end
編碼使用FFmpeg新版API實現(xiàn)竣付,網(wǎng)上很多都是較舊的API代碼诡延,但不影響滞欠,只是個別接口變更而已,整體的實現(xiàn)思路一致肆良。
學(xué)習(xí)過程中筛璧,查閱了大量的資料逸绎,收獲頗豐,非常感謝學(xué)習(xí)路上各位coder的無私分享夭谤,尤其coderWhy先森棺牧、七牛的深愛、雷霄驊(致敬)朗儒。
參考鏈接:
https://depthlove.github.io/2015/09/18/use-ffmpeg-and-x264-encode-iOS-camera-video-to-h264/
http://blog.csdn.net/leixiaohua1020/article/details/25430425