FFmpeg框架解析及核心数据结构

本文详细介绍了FFmpeg中关键的数据结构及其对应的协议层,并深入探讨了协议层、封装层、编码层和解码层的主要结构体及初始化步骤。

ffmpeg关键数据结构及对应的协议层

FFMPEG中最关键的结构体之间的关系 - 雷霄骅(leixiaohua1020)的专栏 - 博客频道 - CSDN.NET

功能结构体
整个音视频文件的抽象(包含下面所有结构体)AVFormatContext
协议层AVIOContext
封装格式层AVInputFormat/AVOutputFormat
解码之前的数据AVPacket
编码层AVStream[0]->AVCodecContext->AVCodec
 AVStream[1]->AVCodecContext->AVCodec
解码之后的数据AVFrame
  

协议层(http,rtsp,rtmp,mms)

AVIOContext,URLProtocol,URLContext主要存储视音频使用的协议的类型以及状态。
URLProtocol存储输入视音频使用的封装格式。
每种协议都对应一个URLProtocol结构。(注意:FFMPEG中文件也被当做一种协议“file”)

封装层(flv,avi,rmvb,mp4)

解封装

结果: 产生压缩的码流数据(解码前数据)——AVPacket
视频的话,每个结构一般是存一帧;音频可能有好几帧

封装

将h.264等裸流,封装为文件

主要结构体及初始化

AVFormatContext主要存储视音频封装格式中包含的信息(非常重要,包含封装层、编码层)

  1. AVFormatContext结构体简介

    FFMPEG结构体分析:AVFormatContext - 雷霄骅(leixiaohua1020)的专栏 - 博客频道 - CSDN.NET
    AVFormatContext是一个贯穿始终的数据结构,很多函数都要用到它作为参数。它是FFMPEG解封装(flv,mp4,rmvb,avi)功能的结构体

    struct AVInputFormat * iformat:输入数据的封装格式
    
    AVIOContext * pb:输入数据的缓存
    
    unsigned int nb_streams:视音频流的个数
    
    AVStream ** streams:视音频流
    
    char filename[1024]:文件名
    
    int64_t duration:时长(单位:微秒us,转换为秒需要除以1000000)
    
    int bit_rate:比特率(单位bps,转换为kbps需要除以1000)
    
    AVDictionary * metadata:元数据
    
  2. 创建 AVFormatContext的方法

    1. 方法一: 通过打开文件获取 AVFormatContext(解封装输入文件格式) (解封装,自顶向下初始化)

      1. 第一步: FFMpeg进行其他操作一样,首先需注册FFMpeg组件

        av_register_all();

      2. 第二步: 打开待处理音视频文件

        然而在此我们不使用打开文件的fopen函数,而是使用avformat_open_input函数。该函数不但会打开输入文件,而且可以根据输入文件读取相应的格式信息。该函数的声明如下:

        int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);

        该函数的各个参数的作用为:

        ps:根据输入文件接收与格式相关的句柄信息;可以指向NULL,那么AVFormatContext类型的实例将由该函数进行分配。
        url:视频url或者文件路径;
        fmt:强制输入格式,可设置为NULL以自动检测;
        options:保存文件格式无法识别的信息;
        返回值:成功返回0,失败则返回负的错误码;

        该函数的调用方式为:

        if (avformat_open_input(&(va_ctx.fmt_ctx), files.src_filename, NULL, NULL) < 0)
        {
        fprintf(stderr, “Could not open source file %s\n”, files.src_filename);
        return -1;
        }

      3. 第三步: 获取文件中的流信息

        该函数的声明为:

        int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

        该函数的第一个参数即前面的文件句柄,第二个参数也是用于保存无法识别的信息的AVDictionary的结构,通常可设为NULL。调用方式如:

        * retrieve stream information *
        if (avformat_find_stream_info(va_ctx.fmt_ctx, NULL) < 0)
        {
        fprintf(stderr, “Could not find stream information\n”);
        return -1;
        }

    2. 方法二: 手动创造 AVFormatContext(封装输出文件格式)(封装,自底向上初始化)

      1. 第一步: 根据输出文件的格式获取AVFormatContext结构

        根据输出文件的格式获取AVFormatContext结构,获取AVFormatContext结构使用函数avformat_alloc_output_context2实现。该函数的声明为:

        int avformat_alloc_output_context2(AVFormatContext **ctx, AVOutputFormat *oformat, const char *format_name, const char *filename);

        其中:

        ctx:输出到AVFormatContext结构的指针,如果函数失败则返回给该指针为NULL;
        oformat:指定输出的AVOutputFormat类型,如果设为NULL则使用format_name和filename生成;
        format_name:输出格式的名称,如果设为NULL则使用filename默认格式;
        filename:目标文件名,如果不使用,可以设为NULL;

      2. 第二步: 判断 AVFormatContext中是否存在音频流或者视频流

        if (fmt->video_codec != AV_CODEC_ID_NONE)
        {
        add_stream(video_st, oc, &video_codec, fmt->video_codec);
        video_st->st->codec->width = io.frame_width;
        video_st->st->codec->height = io.frame_height;
        ret |= HAVE_VIDEO;
        ret |= ENCODE_VIDEO;
        }
        if (fmt->audio_codec != AV_CODEC_ID_NONE)
        {
        add_stream(audio_st, oc, &audio_codec, fmt->audio_codec);
        ret |= HAVE_AUDIO;
        ret |= ENCODE_AUDIO;
        }

      3. 第三步: 向AVFormatContext结构中所代表的媒体文件中添加数据流

        添加流首先需要查找流所包含的媒体的编码器,这需要传入codec_id后使用avcodec_find_encoder函数实现,将查找到的编码器保存在AVCodec指针中。

        调用avformat_new_stream函数向AVFormatContext结构中所代表的媒体文件中添加数据流。该函数的声明如下:

        AVStream *avformat_new_stream(AVFormatContext *s, const AVCodec *c);

        其中各个参数的含义:

        s:AVFormatContext结构,表示要封装生成的视频文件;
        c:上一步根据codec_id产生的编码器指针;
        返回值:指向生成的stream对象的指针;如果失败则返回NULL指针。

        此时,一个新的AVStream便已经加入到输出文件中。

AVStream是视频文件中某一音频流或者视频流的抽象。

存储一个视频/音频流(一系列包的集合)的结构。
AVStream可以表示封装格式中的音频或者视频数据。通过AVStream可以得到音频流或者视频流对应的AVCodecContext,存储该视频/音频流使用解码方式的相关数据。
主要有下面两种获取AVStream的方法:

  1. 方法一: 通过打开文件的 AVFormatContext,获取音频和视频AVStream

    1. 第一步: 通过打开文件获取 AVFormatContext(解封装输入文件格式

    2. 第二步: 获取文件中的音频和视频AVStream

      获取文件中的流使用av_find_best_stream函数,其声明如:

      int av_find_best_stream(AVFormatContext *ic,
      enum AVMediaType type,
      int wanted_stream_nb,
      int related_stream,
      AVCodec **decoder_ret,
      int flags);

      其中各个参数的意义:

      ic:视频文件句柄;
      type:表示数据的类型,常用的有AVMEDIA_TYPE_VIDEO表示视频,AVMEDIA_TYPE_AUDIO表示音频等;
      wanted_stream_nb:我们期望获取到的数据流的数量,设置为-1使用自动获取;
      related_stream:获取相关的音视频流,如果没有则设为-1;
      decoder_ret:返回这一路数据流的解码器;
      flags:未定义;
      返回值:函数执行成功返回 type流的序号 ,失败则返回负的错误码; (解封装)

  2. 方法二: 手动构造 AVStream (封装)

其他结构体

AVInputFormat存储输入视音频使用的封装格式,每种视音频封装格式都对应一个AVInputFormat 结构。

编码或者解码

主要结构体及初始化

编解码器结构体 AVCodec

每个AVCodecContext中对应一个AVCodec,包含该视频/音频对应的解码器。每种解码器都对应一个AVCodec结构。

AVCodec保存一个编解码器的实例,实现时实际的编码功能。通常我们在程序中定义一个指向AVCodec结构的指针指向该实例。

根据编解码器ID,获取编解码器指针的方法有以下两种:

  1. 第一步: 所有涉及到编解码的的功能,都必须要注册音视频编解码器之后才能使用。

    avcodec_register_all();

  2. 第二步: 通过编解码器ID获取编解码器指针

    CODEC_ID通常指定了编解码器的格式,在这里我们使用当前应用最为广泛的H.264格式为例。查找codec调用的函数为avcodec_find_encoder,其声明格式为:

    AVCodec *avcodec_find_encoder(enum AVCodecID id);

    该函数的输入参数为一个AVCodecID的枚举类型,返回值为一个指向AVCodec结构的指针,用于接收找到的编解码器实例。如果没有找到,那么该函数会返回一个空指针。调用方法如下:

    * find the mpeg1 video encoder *
    ctx.codec = avcodec_find_encoder(AV_CODEC_ID_H264); //根据CODEC_ID查找编解码器对象实例的指针
    if (!ctx.codec)
    {
    fprintf(stderr, “Codec not found\n”);
    return false;
    }

编解码器参数配置结构体 AVCodecContext

AVCodecContext表示AVCodec所需要的上下文信息,保存AVCodec所需要的一些参数。实现编码功能时,通过AVCodecContext设置编码器参数。

  1. 方法一: 解封装解码过程,通过AVStream获取编解码器上下文指针

    AVCodecContext *c;
    c = AVFormatContext->AVStream->codec;

  2. 方法二: 通过编解码器AVCodec指针,获取编解码器上下文

    分配AVCodecContext实例需要我们前面查找到的AVCodec作为参数,调用的是avcodec_alloc_context3函数。其声明方式为:

    AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);

    其特点同avcodec_find_encoder类似,返回一个指向AVCodecContext实例的指针。如果分配失败,会返回一个空指针。调用方式为:

    ctx.c = avcodec_alloc_context3(ctx.codec); //分配AVCodecContext实例
    if (!ctx.c)
    {
    fprintf(stderr, “Could not allocate video codec context\n”);
    return false;
    }

编码

成功将原始的YUV像素值保存到了AVframe结构中之后,便可以调用avcodec_encode_video2函数进行实际的编码操作。该函数可谓是整个工程的核心所在,其声明方式为:

int avcodec_encode_video2(AVCodecContext *avctx, AVPacket *avpkt, const AVFrame *frame, int *got_packet_ptr);

其参数和返回值的意义:

avctx: AVCodecContext结构,指定了编码的一些参数;
avpkt: AVPacket对象的指针,用于保存输出码流;
frame:AVframe结构,用于传入原始的像素数据;
got_packet_ptr:输出参数,用于标识AVPacket中是否已经有了完整的一帧;
返回值:编码是否成功。成功返回0,失败则返回负的错误码

通过输出参数*got_packet_ptr,我们可以判断是否应有一帧完整的码流数据包输出,如果是,那么可以将AVpacket中的码流数据输出出来,其地址为AVPacket::data,大小为AVPacket::size。具体调用方式如下:

* encode the image *
ret = avcodec_encode_video2(ctx.c, &(ctx.pkt), ctx.frame, &got_output); //将AVFrame中的像素信息编码为AVPacket中的码流
if (ret < 0)
{
fprintf(stderr, “Error encoding frame\n”);
exit(1);
}

if (got_output)
{
//获得一个完整的编码帧
printf(“Write frame %3d (size=%5d)\n”, frameIdx, ctx.pkt.size);
fwrite(ctx.pkt.data, 1, ctx.pkt.size, io_param.pFout);
av_packet_unref(&(ctx.pkt));
}

解码

在最终解析出一个完整的包之后,我们就可以调用解码API进行实际的解码过程了。解码过程调用的函数为avcodec_decode_video2,该函数的声明为:

int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture,
int *got_picture_ptr,
const AVPacket *avpkt);

这个函数与前篇所遇到的编码函数avcodec_encode_video2有些类似,只是参数的顺序略有不同,解码函数的输入输出参数与编码函数相比交换了位置。该函数各个参数的意义:

AVCodecContext *avctx:编解码器上下文对象,在打开编解码器时生成;
AVFrame *picture: 保存解码完成后的像素数据;我们只需要分配对象的空间,像素的空间codec会为我们分配好;
int *got_picture_ptr: 标识位,如果为1,那么说明已经有一帧完整的像素帧可以输出了
const AVPacket *avpkt: 前面解析好的码流包;

实际调用的方法为:

int ret = avcodec_decode_video2(ctx.pCodecContext, ctx.frame, &got_picture, &(ctx.pkt));
if (ret < 0)
{
printf(“Decode Error.\n”);
return ret;
}

if (got_picture)
{
//获得一帧完整的图像,写出到输出文件
write_out_yuv_frame(ctx, inputoutput);
printf(“Succeed to decode 1 frame!\n”);
}
结果: 产生解压缩后的数据(解码后数据)——AVFrame

编码和解码的数据输入和输出

编码的输入,解码的输出。AVFrame结构体

  1. AVFrame结构体

    即非压缩数据,例如对视频来说是YUV,RGB,对音频来说是PCM
    FFMPEG结构体分析:AVFrame - 雷霄骅(leixiaohua1020)的专栏 - 博客频道 - CSDN.NET
    uint8_t *data[AV_NUM_DATA_POINTERS]:解码后原始数据(对视频来说是YUV,RGB,对音频来说是PCM)
    对于packed格式的数据(例如RGB24),会存到data[0]里面。
    对于planar格式的数据(例如YUV420P),则会分开成data[0],data[1],data[2]…(YUV420P中data[0]存Y,data[1]存U,data[2]存V)

    int linesize[AV_NUM_DATA_POINTERS]:data中“一行”数据的大小。注意:未必等于图像的宽,一般大于图像的宽。

    int width, height:视频帧宽和高(1920x1080,1280x720…)

    int nb_samples:音频的一个AVFrame中可能包含多个音频帧,在此标记包含了几个

    int format:解码后原始数据类型(YUV420,YUV422,RGB24…)

    int key_frame:是否是关键帧

    enum AVPictureType pict_type:帧类型(I,B,P…)

    AVRational sample_aspect_ratio:宽高比(16:9,4:3…)

    int64_t pts:显示时间戳

    int coded_picture_number:编码帧序号

    int display_picture_number:显示帧序号

    int8_t *qscale_table:QP表

    uint8_t *mbskip_table:跳过宏块表

    int16_t (*motion_val[2])[2]:运动矢量表

    uint32_t *mb_type:宏块类型表

    short *dct_coeff:DCT系数,这个没有提取过

    int8_t *ref_index[2]:运动估计参考帧列表(貌似H.264这种比较新的标准才会涉及到多参考帧)

    int interlaced_frame:是否是隔行扫描

    uint8_t motion_subsample_log2:一个宏块中的运动矢量采样个数,取log的

    其他的变量不再一一列举,源代码中都有详细的说明。在这里重点分析一下几个需要一定的理解的变量:

  2. 方法一: 配置编码的输入数据

    1. 第一步: 分配AVFrame对象。

      分配对象空间类似于new操作符一样,只是需要调用函数av_frame_alloc。如果失败,那么函数返回一个空指针。

      ctx.frame = av_frame_alloc(); //分配AVFrame对象
      if (!ctx.frame)
      {
      fprintf(stderr, “Could not allocate video frame\n”);
      return false;
      }

    2. 第二步: AVFrame对象分配成功后,需要设置图像的分辨率和像素格式等

      ctx.frame->format = ctx.c->pix_fmt;
      ctx.frame->width = ctx.c->width;
      ctx.frame->height = ctx.c->height;

    3. 第三步: 分配实际的像素数据的存储空间。

      分配像素的存储空间需要调用av_image_alloc函数,其声明方式为:

      int av_image_alloc(uint8_t *pointers[4], int linesizes[4], int w, int h, enum AVPixelFormat pix_fmt, int align);

      该函数的四个参数分别表示AVFrame结构中的缓存指针、各个颜色分量的宽度、图像分辨率(宽、高)、像素格式和内存对其的大小。该函数会返回分配的内存的大小,如果失败则返回一个负值。具体调用方式如:

      ret = av_image_alloc(ctx.frame->data, ctx.frame->linesize, ctx.c->width, ctx.c->height, ctx.c->pix_fmt, 32);
      if (ret < 0)
      {
      fprintf(stderr, “Could not allocate raw picture buffer\n”);
      return false;
      }

      需要注意的是,linesize中的值通常指的是stride而不是width,也就是说,像素保存区可能是带有一定宽度的无效边区的,在读取数据时需注意。

  3. 方法二: 为frame分配空间

    static AVFrame *alloc_picture(enum AVPixelFormat pix_fmt, int width, int height)
    {
    AVFrame *picture;
    int ret;

    picture = av_frame_alloc();
    if (!picture)
    {
    return NULL;
    }

    picture->format = pix_fmt;
    picture->width = width;
    picture->height = height;

    * allocate the buffers for the frame data *
    ret = av_frame_get_buffer(picture, 32);
    if (ret < 0)
    {
    fprintf(stderr, “Could not allocate frame data.\n”);
    exit(1);
    }

    return picture;  
    

    }

编码的输出,解码的输入。AVPacket结构体

各层结构体初始化步骤

解封装->解码(自顶向下初始化)

AVFormatContext->AVStream->AVCodecContext->AVCodec

封装(自底向上初始化)

AVCodec->AVStream->AVFormatContext
->AVCodecContext

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值