一:H264碼流結構
(一)H264碼流分層
1.VCL video coding layer 視頻編碼層,H264編碼/壓縮的核心,主要負責將視頻數據編碼/壓縮。 2.NAL network abstraction layer 網絡抽象層,負責將VCL的數據組織打包。並且用於處理數據在網絡中出現的各種問題
1.VCL結構關系
每一個幀由很多的slice組成;實際中,一個slice對應一整個圖像;在官方文檔中,是說一個圖像中,包含很多slice,如下圖。
slice是由編解碼器將數據分解為很多個slice,方便於在網絡中傳輸,更加靈活。
Slice與宏塊:slice包含多個宏塊MB,而宏塊中包含有宏塊類型mb_type、預測值mb_pred、殘差值codeed residual
(二)碼流基本概念:詳細見https://www.cnblogs.com/ssyfj/p/14624498.html
上圖缺失部分數據(防止競爭碼),可以參考https://www.cnblogs.com/ssyfj/p/14624498.html。
注意:RTP在網絡傳輸中不需要前面的start code起始碼,但是中間的防競爭碼是一直存在的。可以參考https://www.cnblogs.com/ssyfj/p/14624498.html
二:Profile與Level (SPS參數)
(一)Profile(壓縮特性)
由上圖可以看出,產生了兩個分支。
其中最核心部分Constrained Baseline由P幀(幀間)、I幀(幀內)組成。其中無損壓縮方式為CAVLC
在Main profile中,才出現B幀;所以Main profile壓縮率更高。並且在無損壓縮使用CABAC,更加高效
Baseline中、Extend中逐漸增加特性。
相比較兩種分支,前面的Main profile分支更加常用。
(一)Level(支持的視頻特性)
設置不同level,支持的最大分辨率大小是不一樣的。
三:SPS其他重要參數:
https://blog.csdn.net/shaqoneal/category_1914693.html
SPS,全稱Sequence Paramater Set,翻譯為“序列參數集”。SPS中保存了一組編碼視頻序列(Coded Video Sequence)的全局參數。因此該類型保存的是和編碼序列相關的參數。
(一)分辨率相關
默認宏塊大小16×16。
通過前兩個參數,可以獲取圖像像素大小(各個方向宏塊個數×每個宏塊長寬)
幀編碼是逐行掃描。場編碼隔行掃描,奇偶各為1張圖
后5個為裁剪變量,幀編碼正常,場編碼高度有所改變。
(二)幀相關
log2_max_frame_num_minus4 用於計算GOP中MaxFrameNum的值。計算公式為MaxFrameNum = 2^(log2_max_frame_num_minus4 + 4)。MaxFrameNum是frame_num的上限值,frame_num是圖像序號的一種表示方法,在幀間編碼中常用作一種參考幀標記的手段。
max_num_ref_frames 用於表示參考幀的最大數目。(緩沖隊列大小)
pic_order_cnt_type 表示解碼picture order count(POC)的方法。POC是另一種計量圖像序號(計算圖像顯示的順序)的方式,與frame_num有着不同的計算方法。該語法元素的取值為0、1或2。
四:PPS與slice header
(一)PPS參數
PPS,全稱Picture Paramater Set,翻譯為“圖像參數集”。該類型保存了整體圖像相關的參數。
(二)slice header
包含以下幾大類:
幀類型:I/P/B類型記錄在slice header GOP中解碼幀序號:根據序號進行解碼,如果只有I/P幀,就順序解碼;如果包含B幀,則先I/P再進行B幀 預測權重:PPS中控制是否預測 濾波:PPS中控制是否開啟濾波
五:H264分析工具
下載地址:https://pan.baidu.com/s/1k_KpA9JH94RFMVoQcHOc2Q
六:視頻編碼器(同FFmpeg學習(三)音頻基礎)
補充:編解碼信息
我們對數據進行編碼,那么數據應該為原始數據。對於已經編碼過的數據,比如jpeg、mpeg格式的視頻數據,就不能再使用H264進行編碼了,因為H264是有損壓縮,解壓后的數據不可能是原始數據(jpeg、mpeg),沒有辦法對這些錯誤數據進行解碼。
所以對於這些編碼后的數據,我們需要先進行解碼操作,變為YUV數據(公共中間數據格式),然后對這些YUV數據進行編碼壓縮操作!!!
由FFmpeg學習(四)視頻基礎知道,我手中采集的原始數據為yuyv422數據,可以進行直接編碼。但是為了模仿解碼操作。我們先實現yuyv422轉yuv420p,然后對yuv420p數據進行編碼操作。
(一)原數據轉yuv420p格式(libx264只支持這個格式)

#include <stdio.h> #include <libavutil/log.h> #include <libavcodec/avcodec.h> #include <libavdevice/avdevice.h> #include <libavformat/avformat.h> #define V_WIDTH 640 #define V_HEIGHT 480 AVFormatContext* open_dev(){ char* devicename = "/dev/video0"; //設備文件描述符 char errors[1024]; int ret; AVFormatContext* fmt_ctx=NULL; //格式上下文獲取-----av_read_frame獲取packet AVDictionary* options=NULL; AVInputFormat *iformat=NULL; AVPacket packet; //包結構 //獲取輸入(采集)格式 iformat = av_find_input_format("video4linux2"); //驅動,用來錄制視頻 //設置參數 ffmpeg -f video4linux2 -pixel_format yuyv422 -video_size 640*480 -framerate 15 -i /dev/video0 out.yuv av_dict_set(&options,"video_size","640*480",0); av_dict_set(&options,"framerate","30",0); av_dict_set(&options,"pixel_format","yuyv422",0); //打開輸入設備 ret = avformat_open_input(&fmt_ctx,devicename,iformat,&options); //----打開輸入設備,初始化格式上下文和選項 if(ret<0){ av_strerror(ret,errors,1024); av_log(NULL,AV_LOG_ERROR,"Failed to open video device,[%d]%s\n",ret,errors); return NULL; } av_log(NULL,AV_LOG_INFO,"Success to open video device\n"); return fmt_ctx; } static AVFrame* initFrame(int width,int height){ int ret; AVFrame* frame = av_frame_alloc(); //分配frame空間,但是數據真正被存放在buffer中 if(!frame){ av_log(NULL,AV_LOG_ERROR,"Failed to create frame\n"); return NULL; } //主要是設置分辨率,用來分配空間 frame->width = width; frame->height = height; frame->format = AV_PIX_FMT_YUV420P; ret = av_frame_get_buffer(frame,32); //第二個參數是對齊,對於音頻,我們直接設置0,視頻中必須為32位對齊 if(ret<0){ //內存分配出錯 av_log(NULL,AV_LOG_ERROR,"Failed to alloc frame buffer\n"); av_frame_free(&frame); return NULL; } return frame; } void rec_video(){ char errors[1024]; int ret,count=0,len,i,j,y_idx,u_idx,v_idx,base_h; AVFormatContext* fmt_ctx = NULL; AVCodecContext* enc_ctx = NULL; AVFrame* fmt = NULL; AVPacket packet; //打開文件 FILE* fp = fopen("./video.yuv","wb"); if(fp==NULL){ av_log(NULL,AV_LOG_ERROR,"Failed to open out file\n"); goto fail; } //打開攝像頭設備的上下文格式 fmt_ctx = open_dev(); if(!fmt_ctx) goto fail; //創建AVFrame AVFrame* frame = initFrame(V_WIDTH,V_HEIGHT); //開始從設備中讀取數據 while((ret=av_read_frame(fmt_ctx,&packet))==0&&count++<500){ av_log(NULL,AV_LOG_INFO,"Packet size:%d(%p),cout:%d\n",packet.size,packet.data,count); //------先將YUYV422數據轉YUV420數據(重點) //序列為YU YV YU YV,一個yuv422幀的長度 width * height * 2 個字節 //丟棄偶數行 u v //先存放Y數據 memset(frame->data[0],0,V_WIDTH*V_HEIGHT*sizeof(char)); for(i=0,y_idx=0;i<2*V_HEIGHT*V_WIDTH;i+=2){ frame->data[0][y_idx++]=packet.data[i]; } //再獲取U、V數據 memset(frame->data[1],0,V_WIDTH*V_HEIGHT*sizeof(char)/4); memset(frame->data[2],0,V_WIDTH*V_HEIGHT*sizeof(char)/4); for(i=0,u_idx=0,v_idx=0;i<V_HEIGHT;i+=2){ //丟棄偶數行,注意:i<V_HEIGHT*2,總數據量是Y+UV,可以達到V_HEIGHT*2行 base_h = i*2*V_WIDTH; //獲取奇數行開頭數據位置 for(j=0;j<V_WIDTH*2;j+=4){ //遍歷這一行數據,每隔4個為1組 y u y v frame->data[1][u_idx++] = packet.data[base_h+j+1]; //獲取U數據 frame->data[2][v_idx++] = packet.data[base_h+j+3]; //獲取V數據 } } //寫入yuv420數據 fwrite(frame->data[0],1,V_WIDTH*V_HEIGHT,fp); fwrite(frame->data[1],1,V_WIDTH*V_HEIGHT/4,fp); fwrite(frame->data[2],1,V_WIDTH*V_HEIGHT/4,fp); //釋放空間 av_packet_unref(&packet); } fail: if(fp) fclose(fp); //關閉設備、釋放上下文空間 avformat_close_input(&fmt_ctx); return ; } int main(int argc,char* argv) { av_register_all(); av_log_set_level(AV_LOG_DEBUG); //注冊所有的設備,包括我們需要的音頻設備 avdevice_register_all(); rec_video(); return 0; }
gcc -o eh 02EncodeH264.c -I /usr/local/ffmpeg/include/ -L /usr/local/ffmpeg/lib/ -lavutil -lavformat -lavcodec -lavdevice
ffplay video.yuv -video_size 640*480 -pix_fmt yuv420p
ffplay video.yuv -video_size 640*480 -pix_fmt yuv420p -vf extractplanes="u" #通過獲取分量查看是否分量出錯
(二)yuv420p進行H264編碼

#include <stdio.h> #include <libavutil/log.h> #include <libavcodec/avcodec.h> #include <libavdevice/avdevice.h> #include <libavformat/avformat.h> #define V_WIDTH 640 #define V_HEIGHT 480 AVFormatContext* open_dev(){ char* devicename = "/dev/video0"; //設備文件描述符 char errors[1024]; int ret; AVFormatContext* fmt_ctx=NULL; //格式上下文獲取-----av_read_frame獲取packet AVDictionary* options=NULL; AVInputFormat *iformat=NULL; AVPacket packet; //包結構 //獲取輸入(采集)格式 iformat = av_find_input_format("video4linux2"); //驅動,用來錄制視頻 //設置參數 ffmpeg -f video4linux2 -pixel_format yuyv422 -video_size 640*480 -framerate 15 -i /dev/video0 out.yuv av_dict_set(&options,"video_size","640*480",0); av_dict_set(&options,"framerate","30",0); av_dict_set(&options,"pixel_format","yuyv422",0); //打開輸入設備 ret = avformat_open_input(&fmt_ctx,devicename,iformat,&options); //----打開輸入設備,初始化格式上下文和選項 if(ret<0){ av_strerror(ret,errors,1024); av_log(NULL,AV_LOG_ERROR,"Failed to open video device,[%d]%s\n",ret,errors); return NULL; } av_log(NULL,AV_LOG_INFO,"Success to open video device\n"); return fmt_ctx; } //作用:編碼,將yuv420轉H264 AVCodecContext* open_encoder(int width,int height){ //------1.打開編碼器 AVCodec* codec = avcodec_find_encoder_by_name("libx264"); if(!codec){ av_log(NULL,AV_LOG_ERROR,"Failed to open video encoder\n"); return NULL; } //------2.創建上下文 AVCodecContext* codec_ctx = avcodec_alloc_context3(codec); if(!codec_ctx){ av_log(NULL,AV_LOG_ERROR,"Failed to open video encoder context\n"); return NULL; } //------3.設置上下文參數 //SPS/PPS codec_ctx->profile = FF_PROFILE_H264_HIGH_444; //main分支最高級別編碼 codec_ctx->level = 50; //表示level級別是5.0;支持最大分辨率2560×1920 //分辨率 codec_ctx->width = width; //設置分辨率--寬度 codec_ctx->height = height; //設置分辨率--高度 //GOP codec_ctx->gop_size = 250; //設置GOP個數,根據業務處理; codec_ctx->keyint_min = 25; //(option)如果GOP過大,我們就在中間多設置幾個I幀,使得避免卡頓。這里表示在一組GOP中,最小插入I幀的間隔 //B幀(增加壓縮比,降低碼率) codec_ctx->has_b_frames = 1; //(option)標志是否允許存在B幀 codec_ctx->max_b_frames = 3; //(option)設置中間連續B幀的最大個數 //參考幀(越大,還原性越好,但是壓縮慢) codec_ctx->refs = 3; //(option)設置參考幀最大數量,緩沖隊列 //要進行編碼的數據的原始數據格式(輸入的原始數據) codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; //注意:我們如果不進行轉換yuyv422為yuv420的話,這里直接設置為AV_PIX_FMT_YUYV422P即可 //設置碼率 codec_ctx->bit_rate = 600000; //設置平均碼率600kpbs(根據業務) //設置幀率 codec_ctx->time_base = (AVRational){1,25}; //時間基,為幀率的倒數;幀與幀之間的間隔 codec_ctx->framerate = (AVRational){25,1}; //幀率,每秒25幀 //------4.打開編碼器 if(avcodec_open2(codec_ctx,codec,NULL)<0){ av_log(NULL,AV_LOG_ERROR,"Failed to open libx264 context\n"); avcodec_free_context(&codec_ctx); return NULL; } return codec_ctx; } static AVFrame* initFrame(int width,int height){ int ret; AVFrame* frame = av_frame_alloc(); //分配frame空間,但是數據真正被存放在buffer中 if(!frame){ av_log(NULL,AV_LOG_ERROR,"Failed to create frame\n"); return NULL; } //主要是設置分辨率,用來分配空間 frame->width = width; frame->height = height; frame->format = AV_PIX_FMT_YUV420P; ret = av_frame_get_buffer(frame,32); //第二個參數是對齊,對於音頻,我們直接設置0,視頻中必須為32位對齊 if(ret<0){ //內存分配出錯 av_log(NULL,AV_LOG_ERROR,"Failed to alloc frame buffer\n"); av_frame_free(&frame); return NULL; } return frame; } //開始進行編碼操作 static void encode(AVCodecContext* enc_ctx,AVFrame* frame,AVPacket* newpkt,FILE* encfp){ int len=0; int ret = avcodec_send_frame(enc_ctx,frame); //將frame交給編碼器進行編碼;內部會將一幀數據掛到編碼器的緩沖區 while(ret>=0){ //只有當frame被放入緩沖區之后(數據設置成功),並且下面ret表示獲取緩沖區數據完成后才退出循環。因為可能一個frame對應1或者多個packet,或者多個frame對應1個packet ret = avcodec_receive_packet(enc_ctx,newpkt); //從編碼器中獲取編碼后的packet數據,處理多種情況 if(ret<0){ if(ret==AVERROR(EAGAIN)||ret==AVERROR_EOF){ //編碼數據不足或者到達文件尾部 return; }else{ //編碼器出錯 av_log(NULL,AV_LOG_ERROR,"avcodec_receive_packet error! [%d] %s\n",ret,av_err2str(ret)); return; } } len = fwrite(newpkt->data,1,newpkt->size,encfp); fflush(encfp); if(len!=newpkt->size){ av_log(NULL,AV_LOG_WARNING,"Warning,newpkt size:%d not equal writen size:%d\n",len,newpkt->size); }else{ av_log(NULL,AV_LOG_INFO,"Success write newpkt to file\n"); } } } void rec_video(){ char errors[1024]; int ret,count=0,len,i,j,y_idx,u_idx,v_idx,base_h,base=0; AVFormatContext* fmt_ctx = NULL; AVCodecContext* enc_ctx = NULL; AVFrame* fmt = NULL; AVPacket packet; //打開文件,存放轉換為yuv420的數據 FILE* fp = fopen("./video.yuv","wb"); if(fp==NULL){ av_log(NULL,AV_LOG_ERROR,"Failed to open out file\n"); goto fail; } //打開文件,存放編碼后數據(其實上面沒必要存在) FILE* encfp = fopen("./video.h264","wb"); if(encfp==NULL){ av_log(NULL,AV_LOG_ERROR,"Failed to open out H264 file\n"); goto fail; } //打開攝像頭設備的上下文格式 fmt_ctx = open_dev(); if(!fmt_ctx) goto fail; //打開編碼上下文 enc_ctx = open_encoder(V_WIDTH,V_HEIGHT); if(!enc_ctx) goto fail; //創建AVFrame AVFrame* frame = initFrame(V_WIDTH,V_HEIGHT); //創建AVPacket AVPacket* newpkt = av_packet_alloc(); if(!newpkt){ av_log(NULL,AV_LOG_ERROR,"Failed to alloc avpacket\n"); goto fail; } //開始從設備中讀取數據 while((ret=av_read_frame(fmt_ctx,&packet))==0&&count++<500){ av_log(NULL,AV_LOG_INFO,"Packet size:%d(%p),cout:%d\n",packet.size,packet.data,count); //------先將YUYV422數據轉YUV420數據(重點) //序列為YU YV YU YV,一個yuv422幀的長度 width * height * 2 個字節 //丟棄偶數行 u v //先存放Y數據 memset(frame->data[0],0,V_WIDTH*V_HEIGHT*sizeof(char)); for(i=0,y_idx=0;i<2*V_HEIGHT*V_WIDTH;i+=2){ frame->data[0][y_idx++]=packet.data[i]; } //再獲取U、V數據 memset(frame->data[1],0,V_WIDTH*V_HEIGHT*sizeof(char)/4); memset(frame->data[2],0,V_WIDTH*V_HEIGHT*sizeof(char)/4); for(i=0,u_idx=0,v_idx=0;i<V_HEIGHT;i+=2){ //丟棄偶數行,注意:i<V_HEIGHT*2,總數據量是Y+UV,可以達到V_HEIGHT*2行 base_h = i*2*V_WIDTH; //獲取奇數行開頭數據位置 for(j=0;j<V_WIDTH*2;j+=4){ //遍歷這一行數據,每隔4個為1組 y u y v frame->data[1][u_idx++] = packet.data[base_h+j+1]; //獲取U數據 frame->data[2][v_idx++] = packet.data[base_h+j+3]; //獲取V數據 } } //寫入yuv420數據 fwrite(frame->data[0],1,V_WIDTH*V_HEIGHT,fp); fwrite(frame->data[1],1,V_WIDTH*V_HEIGHT/4,fp); fwrite(frame->data[2],1,V_WIDTH*V_HEIGHT/4,fp); //開始編碼 frame->pts = base++; //重點:對幀的pts進行順序累加;不能設置隨機值;H264要求編碼的幀的pts是連續的值 encode(enc_ctx,frame,newpkt,encfp); //釋放空間 av_packet_unref(&packet); } encode(enc_ctx,NULL,newpkt,encfp); //告訴編碼器編碼結束,將后面剩余的數據全部寫入即可 fail: if(fp) fclose(fp); if(encfp) fclose(encfp); if(frame) av_frame_free(&frame); if(newpkt) av_packet_free(&newpkt); //關閉設備、釋放上下文空間 if(enc_ctx) avcodec_free_context(&enc_ctx); avformat_close_input(&fmt_ctx); return ; } int main(int argc,char* argv) { av_register_all(); av_log_set_level(AV_LOG_DEBUG); //注冊所有的設備,包括我們需要的音頻設備 avdevice_register_all(); rec_video(); return 0; }
ffplay video.h264
七:X264參數(libx264庫)
(一)預設值(X264本身按照不同目標為用戶預設了一些值)
preset:與速度相關;參數有很多,比如fast、slow....;主要用於實時通信時可以設置very fast;轉碼應用中使用slow
tune:與質量相關;
兩者不互斥
(二) 幀相關參數(參考幀、B幀數量...)
keyint: 是GOP_size;min-keyint設置最小I幀間隔(如果切換場景,則會插入一個I幀)
scenecut: 場景切換,設置多少比例不同,則進行切換(插入I幀)
bframes: 設置B幀數量
ref: 設置參考幀數量,解碼器緩沖區中存放參考幀的數量
no-deblock/deblock:濾波器進行銳化
no-cabac: 是否使用CABAC進行熵編碼
(三)碼流的控制
Qp: 量化器參數;(偏算法)
Bitrate: 關注碼流;(偏網絡傳輸)
Crf: 關注質量;(質量)
量化器參數
Qmin: 量化器最小值
Qmax: 量化器最大值
Qpstep: 兩幀之間的量化器最大變化
(四)編碼分析(宏塊、編碼相關分析)
Partitions:宏塊划分;p8×8表示可以對P幀進行8×8宏塊划分,b8×8可以對B幀進行宏塊划分...
Me:運動評估算法
(五)輸出
SAR:設置寬高比
fps:設置幀率
leve:設置輸出規格(分辨率)
(六)使用案例