• 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
    ",ret,errors);
            return NULL;
        }
        av_log(NULL,AV_LOG_INFO,"Success to open video device
    ");
    
        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
    ");
            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
    ");
            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
    ");
            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
    ",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
    ",ret,errors);
            return NULL;
        }
        av_log(NULL,AV_LOG_INFO,"Success to open video device
    ");
    
        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
    ");
            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
    ");
            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
    ");
            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
    ");
            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
    ");
            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
    ",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
    ",len,newpkt->size);
            }else{
                av_log(NULL,AV_LOG_INFO,"Success write newpkt to file
    ");
            }
        }
    }
    
    
    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
    ");
            goto fail;
        }
    
        //打开文件,存放编码后数据(其实上面没必要存在)
        FILE* encfp = fopen("./video.h264","wb");
        if(encfp==NULL){
            av_log(NULL,AV_LOG_ERROR,"Failed to open out H264 file
    ");
            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
    ");
            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
    ",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:设置输出规格(分辨率)

    (六)使用案例

  • 相关阅读:
    一起谈.NET技术,WPF 自定义快捷键命令(Command) 狼人:
    一起谈.NET技术,WPF 基础到企业应用系列5——WPF千年轮回2 狼人:
    一起谈.NET技术,asp.net页面中输出变量、Eval数据绑定等总结 狼人:
    单片机沉思录——再谈static
    Java平台对脚本语言支持之ScriptEngine创建方式
    [置顶] [Html] Jquery那些事
    codeforces 165E Compatible Numbers
    2013第六周上机任务【项目2 程序填空(1)】
    腾讯再否认微信收费 三大运营商态度分化
    电子钟程序
  • 原文地址:https://www.cnblogs.com/ssyfj/p/14676355.html
Copyright © 2020-2023  润新知