FFmpeg學習(五)H264結構


一: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;
}
View Code
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;
}
View Code
 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:設置輸出規格(分辨率)

(六)使用案例

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM