(一)H264码流分层

FFmpeg学习(五)H264结构

1.VCL   video coding layer          视频编码层,H264编码/压缩的核心,主要负责将视频数据编码/压缩。
2.NAL   network abstraction layer   网络抽象层,负责将VCL的数据组织打包。并且用于处理数据在网络中出现的各种问题

1.VCL结构关系

FFmpeg学习(五)H264结构

每一个帧由很多的slice组成;实际中,一个slice对应一整个图像;在官方文档中,是说一个图像中,包含很多slice,如下图。

slice是由编解码器将数据分解为很多个slice,方便于在网络中传输,更加灵活。

FFmpeg学习(五)H264结构

Slice与宏块:slice包含多个宏块MB,而宏块中包含有宏块类型mb_type、预测值mb_pred、残差值codeed residual

FFmpeg学习(五)H264结构

(二)码流基本概念:详细见https://www.cnblogs.com/ssyfj/p/14624498.html

FFmpeg学习(五)H264结构

上图缺失部分数据(防止竞争码),可以参考https://www.cnblogs.com/ssyfj/p/14624498.html

FFmpeg学习(五)H264结构

注意:RTP在网络传输中不需要前面的start code起始码,但是中间的防竞争码是一直存在的。可以参考https://www.cnblogs.com/ssyfj/p/14624498.html

二:Profile与Level (SPS参数)

FFmpeg学习(五)H264结构 

(一)Profile(压缩特性)

FFmpeg学习(五)H264结构

由上图可以看出,产生了两个分支。

其中最核心部分Constrained Baseline由P帧(帧间)、I帧(帧内)组成。其中无损压缩方式为CAVLC

Main profile中,才出现B帧;所以Main profile压缩率更高。并且在无损压缩使用CABAC,更加高效
Baseline中、Extend中逐渐增加特性。

相比较两种分支,前面的Main profile分支更加常用。

FFmpeg学习(五)H264结构

(一)Level(支持的视频特性)

FFmpeg学习(五)H264结构 

设置不同level,支持的最大分辨率大小是不一样的。

三:SPS其他重要参数:

https://blog.csdn.net/shaqoneal/category_1914693.html

SPS,全称Sequence Paramater Set,翻译为“序列参数集”。SPS中保存了一组编码视频序列(Coded Video Sequence)的全局参数。因此该类型保存的是和编码序列相关的参数。

(一)分辨率相关

FFmpeg学习(五)H264结构 

默认宏块大小16×16。

通过前两个参数,可以获取图像像素大小(各个方向宏块个数×每个宏块长宽)

帧编码是逐行扫描。场编码隔行扫描,奇偶各为1张图

后5个为裁剪变量,帧编码正常,场编码高度有所改变。

(二)帧相关

FFmpeg学习(五)H264结构

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

FFmpeg学习(五)H264结构

四:PPS与slice header

(一)PPS参数

PPS,全称Picture Paramater Set,翻译为“图像参数集”。该类型保存了整体图像相关的参数。

FFmpeg学习(五)H264结构

(二)slice header

包含以下几大类:

FFmpeg学习(五)H264结构

帧类型:I/P/B类型记录在slice header
GOP中解码帧序号:根据序号进行解码,如果只有I/P帧,就顺序解码;如果包含B帧,则先I/P再进行B帧
预测权重:PPS中控制是否预测
滤波:PPS中控制是否开启滤波

五:H264分析工具

FFmpeg学习(五)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

相关文章: