ffmpeg架构和解码流程分析

系统 1495 0

 

一,ffmpeg架构

1. 简介

FFmpeg 是一个集录制、转换、音 / 视频编码解码功能为一体的完整的开源解决方案。 FFmpeg

开发是基于 Linux 操作系统,但是可以在大多数操作系统中编译和使用。 FFmpeg 支持 MPEG

DivX MPEG4 AC3 DV FLV 40 多种编码, AVI MPEG OGG Matroska ASF 90 多种解码 .

TCPMP, VLC, MPlayer 等开源播放器都用到了 FFmpeg

FFmpeg 主目录下主要有 libavcodec libavformat libavutil 等子目录。其中 libavcodec

于存放各个 encode/decode 模块, libavformat 用于存放 muxer/demuxer 模块, libavutil 用于

存放内存操作等辅助性模块。

flash movie flv 文件格式为例, muxer/demuxer flvenc.c flvdec.c 文件在

libavformat 目录下, encode/decode mpegvideo.c h263de.c libavcodec 目录下。

 

2. muxer/demuxer encoder/decoder 定义与初始化

muxer/demuxer encoder/decoder FFmpeg 中的实现代码里,有许多相同的地方,而二者最

大的差别是 muxer demuxer 分别是不同的结构 AVOutputFormat AVInputFormat ,而 encoder

decoder 都是用的 AVCodec 结构。

 

muxer/demuxer encoder/decoder FFmpeg 中相同的地方有:

    二者都是在 main() 开始的 av_register_all() 函数内初始化的

    二者都是以链表的形式保存在全局变量中的

        muxer/demuxer 是分别保存在全局变量 AVOutputFormat *first_oformat

        AVInputFormat *first_iformat 中的。

        encoder/decoder 都是保存在全局变量 AVCodec *first_avcodec 中的。

    二者都用函数指针的方式作为开放的公共接口

   

demuxer 开放的接口有:

    int (*read_probe)(AVProbeData *);

    int (*read_header)(struct AVFormatContext *, AVFormatParameters *ap);

    int (*read_packet)(struct AVFormatContext *, AVPacket *pkt);

    int (*read_close)(struct AVFormatContext *);

    int (*read_seek)(struct AVFormatContext *, int stream_index, int64_t timestamp, int flags);

   

muxer 开放的接口有:

    int (*write_header)(struct AVFormatContext *);

    int (*write_packet)(struct AVFormatContext *, AVPacket *pkt);

    int (*write_trailer)(struct AVFormatContext *);

 

encoder/decoder 的接口是一样的,只不过二者分别只实现 encoder decoder 函数:

    int (*init)(AVCodecContext *);

    int (*encode)(AVCodecContext *, uint8_t *buf, int buf_size, void *data);

    int (*close)(AVCodecContext *);

    int (*decode)(AVCodecContext *, void *outdata, int *outdata_size, uint8_t *buf, int buf_size);

 

仍以 flv 文件为例来说明 muxer/demuxer 的初始化。

libavformat\allformats.c 文件的 av_register_all(void) 函数中,通过执行

REGISTER_MUXDEMUX(FLV, flv);

将支持 flv 格式的 flv_muxer flv_demuxer 变量分别注册到全局变量 first_oformat first_iformat 链表的最后位置。

其中 flv_muxer libavformat\flvenc.c 中定义如下:

AVOutputFormat flv_muxer = {

    "flv",

    "flv format",

    "video/x-flv",

    "flv",

    sizeof(FLVContext),

#ifdef CONFIG_LIBMP3LAME

    CODEC_ID_MP3,

#else // CONFIG_LIBMP3LAME

    CODEC_ID_NONE,

    CODEC_ID_FLV1,

    flv_write_header,

    flv_write_packet,

    flv_write_trailer,

    .codec_tag= (const AVCodecTag*[]){flv_video_codec_ids, flv_audio_codec_ids, 0},

}

AVOutputFormat 结构的定义如下:

typedef struct AVOutputFormat {

    const char *name;

    const char *long_name;

    const char *mime_type;

    const char *extensions;

   

    int priv_data_size;

   

    enum CodecID audio_codec;

    enum CodecID video_codec;

    int (*write_header)(struct AVFormatContext *);

    int (*write_packet)(struct AVFormatContext *, AVPacket *pkt);

    int (*write_trailer)(struct AVFormatContext *);

   

    int flags;

   

    int (*set_parameters)(struct AVFormatContext *, AVFormatParameters *);

    int (*interleave_packet)(struct AVFormatContext *, AVPacket *out, AVPacket *in, int flush);

 

   

    const struct AVCodecTag **codec_tag;

   

    struct AVOutputFormat *next;

} AVOutputFormat;

AVOutputFormat 结构的定义可知, flv_muxer 变量初始化的第一、二个成员分别为该 muxer

的名称与长名称,第三、第四个成员为所对应 MIMIE Type 和后缀名,第五个成员是所对应的

私有结构的大小,第六、第七个成员为所对应的音频编码和视频编码类型 ID ,接下来就是三

个重要的接口函数,该 muxer 的功能也就是通过调用这三个接口实现的。

 

flv_demuxer libavformat\flvdec.c 中定义如下 , flv_muxer 类似,在这儿主要也是设置

5 个接口函数,其中 flv_probe 接口用途是测试传入的数据段是否是符合当前文件格式,这

个接口在匹配当前 demuxer 时会用到。

AVInputFormat flv_demuxer = {

    "flv",

    "flv format",

    0,

    flv_probe,

    flv_read_header,

    flv_read_packet,

    flv_read_close,

    flv_read_seek,

    .extensions = "flv",

    .value = CODEC_ID_FLV1,

};

 

在上述 av_register_all(void) 函数中通过执行 libavcodec\allcodecs.c 文件里的

avcodec_register_all(void) 函数来初始化全部的 encoder/decoder

 

因为不是每种编码方式都支持 encode decode ,所以有以下三种注册方式:

#define REGISTER_ENCODER(X,x) \

    if(ENABLE_##X##_ENCODER) register_avcodec(&x##_encoder)

#define REGISTER_DECODER(X,x) \

    if(ENABLE_##X##_DECODER) register_avcodec(&x##_decoder)

#define REGISTER_ENCDEC(X,x) REGISTER_ENCODER(X,x); REGISTER_DECODER(X,x)

 

如支持 flv flv_encoder flv_decoder 变量就分别是在 libavcodec\mpegvideo.c libavcodec\h263de.c 中创建的。

3. 当前 muxer/demuxer 的匹配

FFmpeg 的文件转换过程中,首先要做的就是根据传入文件和传出文件的后缀名 [FIXME] 匹配

合适的 demuxer muxer 。匹配上的 demuxer muxer 都保存在如下所示,定义在 ffmpeg.c 里的

全局变量 file_iformat file_oformat 中:

    static AVInputFormat *file_iformat;

    static AVOutputFormat *file_oformat;

3.1 demuxer 匹配

libavformat\utils.c 中的 static AVInputFormat *av_probe_input_format2(

AVProbeData *pd, int is_opened, int *score_max) 函数用途是根据传入的 probe data 数据

,依次调用每个 demuxer read_probe 接口,来进行该 demuxer 是否和传入的文件内容匹配的

判断。其调用顺序如下:

void parse_options(int argc, char **argv, const OptionDef *options,

void (* parse_arg_function)(const char *));

static void opt_input_file(const char *filename)

int av_open_input_file(…… )

AVInputFormat *av_probe_input_format(AVProbeData *pd,

                                int is_opened)

static AVInputFormat *av_probe_input_format2(……)

opt_input_file 函数是在保存在 const OptionDef options[] 数组中,用于

void parse_options(int argc, char **argv, const OptionDef *options) 中解析 argv 里的

“-i” 参数,也就是输入文件名时调用的。

3.2 muxer 匹配

demuxer 的匹配不同, muxer 的匹配是调用 guess_format 函数,根据 main() 函数的 argv 里的

输出文件后缀名来进行的。

void parse_options(int argc, char **argv, const OptionDef *options,

          void (* parse_arg_function)(const char *));

void parse_arg_file(const char *filename)

static void opt_output_file(const char *filename)

AVOutputFormat *guess_format(const char *short_name,

                            const char *filename,

                            const char *mime_type)

3.3 当前 encoder/decoder 的匹配

main() 函数中除了解析传入参数并初始化 demuxer muxer parse_options( ) 函数以外,

其他的功能都是在 av_encode( ) 函数里完成的。

libavcodec\utils.c 中有如下二个函数 :

    AVCodec *avcodec_find_encoder(enum CodecID id)

    AVCodec *avcodec_find_decoder(enum CodecID id)

他们的功能就是根据传入的 CodecID ,找到匹配的 encoder decoder

av_encode( ) 函数的开头,首先初始化各个 AVInputStream AVOutputStream ,然后分别调

用上述二个函数,并将匹配上的 encoder decoder 分别保存在 :

AVInputStream->AVStream *st->AVCodecContext *codec->struct AVCodec *codec

AVOutputStream->AVStream *st->AVCodecContext *codec->struct AVCodec *codec 变量。

4. 其他主要数据结构

4.1 AVFormatContext

AVFormatContext FFMpeg 格式转换过程中实现输入和输出功能、保存相关数据的主要结构。

每一个输入和输出文件,都在如下定义的指针数组全局变量中有对应的实体。

    static AVFormatContext *output_files[MAX_FILES];

    static AVFormatContext *input_files[MAX_FILES];

对于输入和输出,因为共用的是同一个结构体,所以需要分别对该结构中如下定义的 iformat

oformat 成员赋值。

    struct AVInputFormat *iformat;

    struct AVOutputFormat *oformat;

对一个 AVFormatContext 来说,这二个成员不能同时有值,即一个 AVFormatContext 不能同时

含有 demuxer muxer 。在 main( ) 函数开头的 parse_options( ) 函数中找到了匹配的 muxer

demuxer 之后,根据传入的 argv 参数,初始化每个输入和输出的 AVFormatContext 结构,并保

存在相应的 output_files input_files 指针数组中。在 av_encode( ) 函数中, output_files

input_files 是作为函数参数传入后,在其他地方就没有用到了。

4.2 AVCodecContext

保存 AVCodec 指针和与 codec 相关数据,如 video width height audio sample rate 等。

AVCodecContext 中的 codec_type codec_id 二个变量对于 encoder/decoder 的匹配来说,最为

重要。

    enum CodecType codec_type;   

    enum CodecID codec_id;       

如上所示, codec_type 保存的是 CODEC_TYPE_VIDEO CODEC_TYPE_AUDIO 等媒体类型,

codec_id 保存的是 CODEC_ID_FLV1 CODEC_ID_VP6F 等编码方式。

以支持 flv 格式为例,在前述的 av_open_input_file(…… ) 函数中,匹配到正确的

AVInputFormat demuxer 后,通过 av_open_input_stream( ) 函数中调用 AVInputFormat

read_header 接口来执行 flvdec.c 中的 flv_read_header( ) 函数。在 flv_read_header( ) 函数

内,根据文件头中的数据,创建相应的视频或音频 AVStream ,并设置 AVStream

AVCodecContext 的正确的 codec_type 值。 codec_id 值是在解码过程中 flv_read_packet( )

数执行时根据每一个 packet 头中的数据来设置的。

4.3 AVStream

AVStream 结构保存与数据流相关的编解码器,数据段等信息。比较重要的有如下二个成员:

    AVCodecContext *codec;

    void *priv_data;

其中 codec 指针保存的就是上节所述的 encoder decoder 结构。 priv_data 指针保存的是和具

体编解码流相关的数据,如下代码所示,在 ASF 的解码过程中, priv_data 保存的就是

ASFStream 结构的数据。

    AVStream *st;

    ASFStream *asf_st; 

    … …

    st->priv_data = asf_st;

4.4 AVInputStream/ AVOutputStream

根据输入和输出流的不同,前述的 AVStream 结构都是封装在 AVInputStream AVOutputStream

结构中,在 av_encode( ) 函数中使用。 AVInputStream 中还保存的有与时间有关的信息。

AVOutputStream 中还保存有与音视频同步等相关的信息。

4.5 AVPacket

AVPacket 结构定义如下,其是用于保存读取的 packet 数据。

typedef struct AVPacket {

    int64_t pts;            ///< presentation time stamp in time_base units

    int64_t dts;            ///< decompression time stamp in time_base units

    uint8_t *data;

    int  size;

    int  stream_index;

    int  flags;

    int  duration;        ///< presentation duration in time_base units (0 if not available)

    void (*destruct)(struct AVPacket *);

    void *priv;

    int64_t pos;          ///< byte position in stream, -1 if unknown

} AVPacket;

av_encode() 函数中,调用 AVInputFormat

(*read_packet)(struct AVFormatContext *, AVPacket *pkt) 接口,读取输入文件的一帧数

据保存在当前输入 AVFormatContext AVPacket 成员中。

---------------------------------------------------------------------

FFMPEG 是目前被应用最广泛的编解码软件库,支持多种流行的编解码器,它是 C 语言实现的,不仅被集成到各种 PC 软件,也经常被移植到多种嵌入式设备中。使用面向对象的办法来设想这样一个编解码库,首先让人想到的是构造各种编解码器的类,然后对于它们的抽象基类确定运行数据流的规则,根据算法转换输入输出对象。

在实际的代码,将这些编解码器分成 encoder/decoder muxer/demuxer device 三种对象,分别对应于编解码,输入输 出格式和设备。在 main 函数的开始,就是初始化这三类对象。在 avcodec_register_all 中,很多编解码器被注册,包括视频的 H.264 解码器和 X264 编码器等,

REGISTER_DECODER (H264, h264);

REGISTER_ENCODER (LIBX264, libx264);

找到相关的宏代码如下

#define REGISTER_ENCODER(X,x) { \

          extern AVCodec x##_encoder; \

          if(CONFIG_##X##_ENCODER)  avcodec_register(&x##_encoder); }

#define REGISTER_DECODER(X,x) { \

          extern AVCodec x##_decoder; \

          if(CONFIG_##X##_DECODER)  avcodec_register(&x##_decoder); }

这样就实际在代码中根据 CONFIG_##X##_ENCODER 这样的编译选项来注册 libx264_encoder h264_decoder ,注册的过程发生在 avcodec_register(AVCodec *codec) 函数中,实际上就是向全局链表 first_avcodec 中加入 libx264_encoder h264_decoder 特定的编解码 器,输入参数 AVCodec 是一个结构体,可以理解为编解码器的基类,其中不仅包含了名称, id 等属性,而且包含了如下函数指针,让每个具体的编解码器扩展类实现。

    int (*init)(AVCodecContext *);

    int (*encode)(AVCodecContext *, uint8_t *buf, int buf_size, void *data);

    int (*close)(AVCodecContext *);

    int (*decode)(AVCodecContext *, void *outdata, int *outdata_size,

                  const uint8_t *buf, int buf_size);

    void (*flush)(AVCodecContext *);

继续追踪 libx264 ,也就是 X264 的静态编码库,它在 FFMPEG 编译的时候被引入作为 H.264 编码器。在 libx264.c 中有如下代码

AVCodec libx264_encoder = {

    .name = "libx264",

    .type = CODEC_TYPE_VIDEO,

    .id = CODEC_ID_H264,

    .priv_data_size = sizeof(X264Context),

    .init = X264_init,

    .encode = X264_frame,

    .close = X264_close,

    .capabilities = CODEC_CAP_DELAY,

    .pix_fmts = (enum PixelFormat[]) { PIX_FMT_YUV420P, PIX_FMT_NONE },

    .long_name = NULL_IF_CONFIG_SMALL("libx264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10"),

};

这里具体对来自 AVCodec 得属性和方法赋值。其中

    .init = X264_init,

    .encode = X264_frame,

    .close = X264_close,

将函数指针指向了具体函数,这三个函数将使用 libx264 静态库中提供的 API ,也就是 X264 的主要接口函数进行具体实现。 pix_fmts 定义了所支持的输入格式,这里 4 2 0

PIX_FMT_YUV420P,   ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)

上面看到的 X264Context 封装了 X264 所需要的上下文管理数据,

typedef struct X264Context {

    x264_param_t params;

    x264_t *enc;

    x264_picture_t pic;

    AVFrame out_pic;

} X264Context;

它 属于结构体 AVCodecContext void *priv_data 变量,定义了每种编解码器私有的上下文属性, AVCodecContext 也类似上下文基类一样,还提供其他表示屏幕解析率、量化范围等的上下文属性和 rtp_callback 等函数指针供编解码使用。

回到 main 函数,可以看到完成了各类编解码器,输入输出格式和设备注册以后,将进行上下文初始化和编解码参数读入,然后调用 av_encode ()函数进行具体的编解码工作。根据该函数的注释一路查看其过程:

1. 输入输出流初始化。

2. 根据输入输出流确定需要的编解码器,并初始化。

3. 写输出文件的各部分

重点关注一下 step2 3 ,看看怎么利用前面分析的编解码器基类来实现多态。大概查看一下这段代码的关系,发现在 FFMPEG 里,可以用类图来表示大概的编解码器组合。

http://1824.img.pp.sohu.com.cn/images/blog/2009/7/22/15/29/1234e7d516dg215.jpg

可以参考【 3 】来了解这些结构的含义(见附录)。在这里会调用一系列来自 utils.c 的函数,这里的 avcodec_open ()函数,在打开编解码器都会调用到,它将运行如下代码:

    avctx->codec = codec;

    avctx->codec_id = codec->id;

    avctx->frame_number = 0;

    if(avctx->codec->init){

        ret = avctx->codec->init(avctx);

进行具体适配的编解码器初始化,而这里的 avctx->codec->init(avctx) 就是调用 AVCodec 中函数指针定义的具体初始化函数,例如 X264_init

avcodec_encode_video ()和 avcodec_encode_audio ()被 output_packet ()调用进行音视频编码,将 同样利用函数指针 avctx->codec->encode ()调用适配编码器的编码函数,如 X264_frame 进行具体工作。

从上面的分析,我们可以看到 FFMPEG 怎么利用面向对象来抽象编解码器行为,通过组合和继承关系具体化每个编解码器实体。设想要在 FFMPEG 中加入新的解码器 H265 ,要做的事情如下:

1. config 编译配置中加入 CONFIG_H265_DECODER

2. 利用宏注册 H265 解码器

3. 定义 AVCodec 265_decoder 变量,初始化属性和函数指针

4. 利用解码器 API 具体化 265_decoder init 等函数指针

完成以上步骤,就可以把新的解码器放入 FFMPEG ,外部的匹配和运行规则由基类的多态实现了。

4. X264 架构分析

X264 是一款从 2004 年有法国大学生发起的开源 H.264 编码器,对 PC 进行汇编级代码优化,舍弃了片组和多参考帧等性能效率比不高的功能来提高编码效率,它被 FFMPEG 作为引入的 .264 编码库,也被移植到很多 DSP 嵌入平台。前面第三节已经对 FFMPEG 中的 X264 进行举例分析,这里将继续结合 X264 框架加深相关内容的了解。

查看代码前,还是思考一下对于一款具体的编码器,怎么面向对象分析呢?对熵编码部分对不同算法的抽象,还有帧内或帧间编码各种估计算法的抽象,都可以作为类来构建。

X264 中,我们看到的对外 API 和上下文变量都声明在 X264.h 中, API 函数中,关于辅助功能的函数在 common.c 中定义

void x264_picture_alloc( x264_picture_t *pic, int i_csp, int i_width, int i_height );

void x264_picture_clean( x264_picture_t *pic );

int x264_nal_encode( void *, int *, int b_annexeb, x264_nal_t *nal );

而编码功能函数定义在 encoder.c

x264_t *x264_encoder_open   ( x264_param_t * );

int     x264_encoder_reconfig( x264_t *, x264_param_t * );

int     x264_encoder_headers( x264_t *, x264_nal_t **, int * );

int     x264_encoder_encode ( x264_t *, x264_nal_t **, int *, x264_picture_t *, x264_picture_t * );

void    x264_encoder_close  ( x264_t * );

x264.c 文件中,有程序的 main 函数,可以看作做 API 使用的例子,它也是通过调用 X264.h 中的 API 和上下文变量来实现实际功能。

X264 最重要的记录上下文数据的结构体 x264_t 定义在 common.h 中,它包含了从线程控制变量到具体的 SPS PPS 、量化矩阵、 cabac 上下文等所有的 H.264 编码相关变量。其中包含如下的结构体

    x264_predict_t      predict_16x16[4+3];

    x264_predict_t      predict_8x8c[4+3];

    x264_predict8x8_t   predict_8x8[9+3];

    x264_predict_t      predict_4x4[9+3];

    x264_predict_8x8_filter_t predict_8x8_filter;

    x264_pixel_function_t pixf;

    x264_mc_functions_t   mc;

    x264_dct_function_t   dctf;

    x264_zigzag_function_t zigzagf;

    x264_quant_function_t quantf;

    x264_deblock_function_t loopf;

跟踪查看可以看到它们或是一个函数指针,或是由函数指针组成的结构,这样的用法很想面向对象中的 interface 接口声明。这些函数指针将在 x264_encoder_open ()函数中被初始化,这里的初始化首先根据 CPU 的不同提供不同的函数实现代码段,很多与可能是汇编实现,以提高代码运行效率。其次把功能相似的函数集中管理,例如类似 intra16 4 种和 intra4 的九种预测函数都被用函数指针数组管理起来。

x264_encoder_encode ()是负责编码的主要函数,而其内包含的 x264_slice_write ()负责片层一下的具体编码,包括了帧内和帧间宏块编码。在这里, cabac cavlc 的行为是根据 h->param.b_cabac 来区别的,分别运行 x264_macroblock_write_cabac ()和 x264_macroblock_write_cavlc ()来写码流,在这一部分,功能函数按文件定义归类,基本按照编码流程图运行,看起来更像面向过程的写法,在已经初始化了具体的函数指针,程序就一直按编码过程的逻辑实现。如果从整体架构来看, x264 利用这种类似接口的形式实现了弱耦合和可重用, 利用 x264_t 这个贯穿始终的上下文,实现信息封装和多态。

本文大概分析了 FFMPEG/X264 的代码架构,重点探讨用 C 语言来实现面向对象编码,虽不至于强行向 C++ 靠拢,但是也各有实现特色,保证实用性。值得规划 C 语言软件项目所借鉴。  

 

【参考文献】

1.“ 用例子说明面向对象和面向过程的区别

2. liyuming1978 liyuming1978 的专栏

3. “FFMpeg 框架代码阅读

 

Using libavformat and libavcodec

Martin Böhme (boehme@inb.uni-luebeckREMOVETHIS.de)

February 18, 2004

Update (January 23 2009): By now, these articles are quite out of date... unfortunately, I haven't found the time to update them, but thankfully, others have jumped in. Stephen Dranger has a more recent tutorial , ryanfb of cryptosystem.org has an updated version of the code, and David Hoerl has a more recent update .

Update (July 22 2004): I discovered that the code I originally presented contained a memory leak (av_free_packet() wasn't being called). My apologies - I've updated the demo program and the code in the article to eliminate the leak.

Update (July 21 2004): There's a new prerelease of ffmpeg (0.4.9-pre1). I describe the changes to the libavformat / libavcodec API in this article .

The libavformat and libavcodec libraries that come with ffmpeg are a great way of accessing a large variety of video file formats. Unfortunately, there is no real documentation on using these libraries in your own programs (at least I couldn't find any), and the example programs aren't really very helpful either.

This situation meant that, when I used libavformat/libavcodec on a recent project, it took quite a lot of experimentation to find out how to use them. Here's what I learned - hopefully I'll be able to save others from having to go through the same trial-and-error process. There's also a small demo program that you can download. The code I'll present works with libavformat/libavcodec as included in version 0.4.8 of ffmpeg (the most recent version as I'm writing this). If you find that later versions break the code, please let me know.

In this document, I'll only cover how to read video streams from a file; audio streams work pretty much the same way, but I haven't actually used them, so I can't present any example code.

In case you're wondering why there are two libraries, libavformat and libavcodec: Many video file formats (AVI being a prime example) don't actually specify which codec(s) should be used to encode audio and video data; they merely define how an audio and a video stream (or, potentially, several audio/video streams) should be combined into a single file. This is why sometimes, when you open an AVI file, you get only sound, but no picture - because the right video codec isn't installed on your system. Thus, libavformat deals with parsing video files and separating the streams contained in them, and libavcodec deals with decoding raw audio and video streams.

Opening a Video File

First things first - let's look at how to open a video file and get at the streams contained in it. The first thing we need to do is to initialize libavformat/libavcodec:

av_register_all();

This registers all available file formats and codecs with the library so they will be used automatically when a file with the corresponding format/codec is opened. Note that you only need to call av_register_all() once, so it's probably best to do this somewhere in your startup code. If you like, it's possible to register only certain individual file formats and codecs, but there's usually no reason why you would have to do that.

Next off, opening the file:

AVFormatContext *pFormatCtx;

const char       *filename="myvideo.mpg";

// Open video file

if(av_open_input_file(&pFormatCtx, filename, NULL, 0, NULL)!=0)

    handle_error(); // Couldn't open file

The last three parameters specify the file format, buffer size and format parameters; by simply specifying NULL or 0 we ask libavformat to auto-detect the format and use a default buffer size. Replace handle_error() with appropriate error handling code for your application.

Next, we need to retrieve information about the streams contained in the file:

// Retrieve stream information

if(av_find_stream_info(pFormatCtx)<0)

    handle_error(); // Couldn't find stream information

This fills the streams field of the AVFormatContext with valid information. As a debugging aid, we'll dump this information onto standard error, but of course you don't have to do this in a production application:

dump_format(pFormatCtx, 0, filename, false);

As mentioned in the introduction, we'll handle only video streams, not audio streams. To make things nice and easy, we simply use the first video stream we find:

int             i, videoStream;

AVCodecContext *pCodecCtx;

// Find the first video stream

videoStream=-1;

for(i=0; i<pFormatCtx->nb_streams; i++)

    if(pFormatCtx->streams[i]->codec.codec_type==CODEC_TYPE_VIDEO)

    {

        videoStream=i;

        break;

    }

if(videoStream==-1)

    handle_error(); // Didn't find a video stream

// Get a pointer to the codec context for the video stream

pCodecCtx=&pFormatCtx->streams[videoStream]->codec;

OK, so now we've got a pointer to the so-called codec context for our video stream, but we still have to find the actual codec and open it:

AVCodec *pCodec;

// Find the decoder for the video stream

pCodec=avcodec_find_decoder(pCodecCtx->codec_id);

if(pCodec==NULL)

    handle_error(); // Codec not found

// Inform the codec that we can handle truncated bitstreams -- i.e.,

// bitstreams where frame boundaries can fall in the middle of packets

if(pCodec->capabilities & CODEC_CAP_TRUNCATED)

    pCodecCtx->flags|=CODEC_FLAG_TRUNCATED;

// Open codec

if(avcodec_open(pCodecCtx, pCodec)<0)

    handle_error(); // Could not open codec

(So what's up with those "truncated bitstreams"? Well, as we'll see in a moment, the data in a video stream is split up into packets. Since the amount of data per video frame can vary, the boundary between two video frames need not coincide with a packet boundary. Here, we're telling the codec that we can handle this situation.)

One important piece of information that is stored in the AVCodecContext structure is the frame rate of the video. To allow for non-integer frame rates (like NTSC's 29.97 fps), the rate is stored as a fraction, with the numerator in pCodecCtx->frame_rate and the denominator in pCodecCtx->frame_rate_base. While testing the library with different video files, I noticed that some codecs (notably ASF) seem to fill these fields incorrectly (frame_rate_base contains 1 instead of 1000). The following hack fixes this:

// Hack to correct wrong frame rates that seem to be generated by some

// codecs

if(pCodecCtx->frame_rate>1000 && pCodecCtx->frame_rate_base==1)

    pCodecCtx->frame_rate_base=1000;

Note that it shouldn't be a problem to leave this fix in place even if the bug is corrected some day - it's unlikely that a video would have a frame rate of more than 1000 fps.

One more thing left to do: Allocate a video frame to store the decoded images in:

AVFrame *pFrame;

pFrame=avcodec_alloc_frame();

That's it! Now let's start decoding some video.

Decoding Video Frames

As I've already mentioned, a video file can contain several audio and video streams, and each of those streams is split up into packets of a particular size. Our job is to read these packets one by one using libavformat, filter out all those that aren't part of the video stream we're interested in, and hand them on to libavcodec for decoding. In doing this, we'll have to take care of the fact that the boundary between two frames can occur in the middle of a packet.

Sound complicated? Lucikly, we can encapsulate this whole process in a routine that simply returns the next video frame:

bool GetNextFrame(AVFormatContext *pFormatCtx, AVCodecContext *pCodecCtx,

    int videoStream, AVFrame *pFrame)

{

    static AVPacket packet;

    static int       bytesRemaining=0;

    static uint8_t   *rawData;

    static bool      fFirstTime=true;

    int              bytesDecoded;

    int              frameFinished;

    // First time we're called, set packet.data to NULL to indicate it

    // doesn't have to be freed

    if(fFirstTime)

    {

        fFirstTime=false;

        packet.data=NULL;

    }

    // Decode packets until we have decoded a complete frame

    while(true)

    {

        // Work on the current packet until we have decoded all of it

        while(bytesRemaining > 0)

        {

            // Decode the next chunk of data

            bytesDecoded=avcodec_decode_video(pCodecCtx, pFrame,

                &frameFinished, rawData, bytesRemaining);

            // Was there an error?

            if(bytesDecoded < 0)

            {

                fprintf(stderr, "Error while decoding frame\n");

                return false;

            }

            bytesRemaining-=bytesDecoded;

            rawData+=bytesDecoded;

            // Did we finish the current frame? Then we can return

            if(frameFinished)

                return true;

        }

        // Read the next packet, skipping all packets that aren't for this

        // stream

        do

        {

            // Free old packet

            if(packet.data!=NULL)

                av_free_packet(&packet);

            // Read new packet

            if(av_read_packet(pFormatCtx, &packet)<0)

                goto loop_exit;

        } while(packet.stream_index!=videoStream);

        bytesRemaining=packet.size;

        rawData=packet.data;

    }

loop_exit:

    // Decode the rest of the last frame

    bytesDecoded=avcodec_decode_video(pCodecCtx, pFrame, &frameFinished,

        rawData, bytesRemaining);

    // Free last packet

    if(packet.data!=NULL)

        av_free_packet(&packet);

    return frameFinished!=0;

}

Now, all we have to do is sit in a loop, calling GetNextFrame() until it returns false. Just one more thing to take care of: Most codecs return images in YUV 420 format (one luminance and two chrominance channels, with the chrominance channels samples at half the spatial resolution of the luminance channel). Depending on what you want to do with the video data, you may want to convert this to RGB. (Note, though, that this is not necessary if all you want to do is display the video data; take a look at the X11 Xvideo extension, which does YUV-to-RGB and scaling in hardware.) Fortunately, libavcodec provides a conversion routine called img_convert, which does conversion between YUV and RGB as well as a variety of other image formats. The loop that decodes the video thus becomes:

while(GetNextFrame(pFormatCtx, pCodecCtx, videoStream, pFrame))

{

    img_convert((AVPicture *)pFrameRGB, PIX_FMT_RGB24, (AVPicture*)pFrame,

        pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height);

    // Process the video frame (save to disk etc.)

    DoSomethingWithTheImage(pFrameRGB);

}

The RGB image pFrameRGB (of type AVFrame *) is allocated like this:

AVFrame *pFrameRGB;

int      numBytes;

uint8_t *buffer;

// Allocate an AVFrame structure

pFrameRGB=avcodec_alloc_frame();

if(pFrameRGB==NULL)

    handle_error();

// Determine required buffer size and allocate buffer

numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,

    pCodecCtx->height);

buffer=new uint8_t[numBytes];

// Assign appropriate parts of buffer to image planes in pFrameRGB

avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,

    pCodecCtx->width, pCodecCtx->height);

Cleaning up

OK, we've read and processed our video, now all that's left for us to do is clean up after ourselves:

// Free the RGB image

delete [] buffer;

av_free(pFrameRGB);

// Free the YUV frame

av_free(pFrame);

// Close the codec

avcodec_close(pCodecCtx);

// Close the video file

av_close_input_file(pFormatCtx);

Done!

Sample Code

A sample app that wraps all of this code up in compilable form is here . If you have any additional comments, please contact me at boehme@inb.uni-luebeckREMOVETHIS.de. Standard disclaimer: I assume no liability for the correct functioning of the code and techniques presented in this article.

 

 

 

 

 

二,解码流程

FFMpeg的解码流程

1. 从基础谈起
先给出几个概念,以在后面的分析中方便理解
Container:在音视频中的容器,一般指的是一种特定的文件格式,里面指明了所包含的
    音视频,字幕等相关信息
Stream:这个词有些微妙,很多地方都用到,比如TCP,SVR4系统等,其实在音视频,你
    可以理解为单纯的音频数据或者视频数据等
Frames:这个概念不是很好明确的表示,指的是Stream中的一个数据单元,要真正对这
    个概念有所理解,可能需要看一些音视频编码解码的理论知识
Packet:是Stream的raw数据
Codec:Coded + Decoded
其实这些概念在在FFmpeg中都有很好的体现,我们在后续分析中会慢慢看到

2.解码的基本流程
我很懒,于是还是选择了从<An ffmpeg and SDL Tutorial>中的流程概述:

10 OPEN video_stream FROM video.avi
20 READ packet FROM video_stream INTO frame
30 IF frame NOT COMPLETE GOTO 20
40 DO SOMETHING WITH frame
50 GOTO 20

这就是解码的全过程,一眼看去,是不是感觉不过如此:),不过,事情有深有浅,从浅
到深,然后从深回到浅可能才是一个有意思的过程,我们的故事,就从这里开始,展开
来讲。

3.例子代码
在<An ffmpeg and SDL Tutorial 1>中,给出了一个阳春版的解码器,我们来仔细看看
阳春后面的故事,为了方便讲述,我先贴出代码:

#include <ffmpeg/avcodec.h>
#include <ffmpeg/avformat.h>

#include <stdio.h>

void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {
FILE *pFile;
char szFilename[32];
int y;

// Open file
sprintf(szFilename, "frame%d.ppm", iFrame);
pFile=fopen(szFilename, "wb");
if(pFile==NULL)
    return;

// Write header
fprintf(pFile, "P6\n%d %d\n255\n", width, height);

// Write pixel data
for(y=0; y<height; y++)
    fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);

// Close file
fclose(pFile);
}

int main(int argc, char *argv[]) {
AVFormatContext *pFormatCtx;
int             i, videoStream;
AVCodecContext *pCodecCtx;
AVCodec         *pCodec;
AVFrame         *pFrame; 
AVFrame         *pFrameRGB;
AVPacket        packet;
int             frameFinished;
int             numBytes;
uint8_t         *buffer;

if(argc < 2) {
    printf("Please provide a movie file\n");
    return -1;
}
// Register all formats and codecs
########################################
[1]
########################################
av_register_all();

// Open video file
########################################
[2]
########################################
if(av_open_input_file(&pFormatCtx, argv[1], NULL, 0, NULL)!=0)
    return -1; // Couldn't open file

// Retrieve stream information
########################################
[3]
########################################
if(av_find_stream_info(pFormatCtx)<0)
    return -1; // Couldn't find stream information

// Dump information about file onto standard error
dump_format(pFormatCtx, 0, argv[1], 0);

// Find the first video stream
videoStream=-1;
for(i=0; i<pFormatCtx->nb_streams; i++)
    if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_VIDEO) {
      videoStream=i;
      break;
    }
if(videoStream==-1)
    return -1; // Didn't find a video stream

// Get a pointer to the codec context for the video stream
pCodecCtx=pFormatCtx->streams[videoStream]->codec;

// Find the decoder for the video stream
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1; // Codec not found
}
// Open codec
if(avcodec_open(pCodecCtx, pCodec)<0)
    return -1; // Could not open codec

// Allocate video frame
pFrame=avcodec_alloc_frame();

// Allocate an AVFrame structure
pFrameRGB=avcodec_alloc_frame();
if(pFrameRGB==NULL)
    return -1;
    
// Determine required buffer size and allocate buffer
numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,
                  pCodecCtx->height);
buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));

// Assign appropriate parts of buffer to image planes in pFrameRGB
// Note that pFrameRGB is an AVFrame, but AVFrame is a superset
// of AVPicture
avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,
         pCodecCtx->width, pCodecCtx->height);

// Read frames and save first five frames to disk
########################################
[4]
########################################
i=0;
while(av_read_frame(pFormatCtx, &packet)>=0) {
    // Is this a packet from the video stream?
    if(packet.stream_index==videoStream) {
      // Decode video frame
      avcodec_decode_video(pCodecCtx, pFrame, &frameFinished, 
               packet.data, packet.size);
      
      // Did we get a video frame?
      if(frameFinished) {
    // Convert the image from its native format to RGB
    img_convert((AVPicture *)pFrameRGB, PIX_FMT_RGB24, 
                    (AVPicture*)pFrame, pCodecCtx->pix_fmt, 
                    pCodecCtx->width, 
                    pCodecCtx->height);
    
    // Save the frame to disk
    if(++i<=5)
      SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height, 
            i);
      }
    }
    
    // Free the packet that was allocated by av_read_frame
    av_free_packet(&packet);
}

// Free the RGB image
av_free(buffer);
av_free(pFrameRGB);

// Free the YUV frame
av_free(pFrame);

// Close the codec
avcodec_close(pCodecCtx);

// Close the video file
av_close_input_file(pFormatCtx);

return 0;
}

代码注释得很清楚,没什么过多需要讲解的,关于其中的什么YUV420,RGB,PPM等格式
,如果不理解,麻烦还是google一下,也可以参考:http://barrypopy.cublog.cn/里面
的相关文章

其实这部分代码,很好了Demo了怎么样去抓屏功能的实现,但我们得去看看魔术师在后
台的一些手法,而不只是简单的享受其表演。

4.背后的故事
真正的难度,其实就是上面的[1],[2],[3],[4],其他部分,都是数据结构之间的转换,
如果你认真看代码的话,不难理解其他部分。

[1]:没什么太多好说的,如果不明白,看我转载的关于FFmepg框架的文章

[2]:先说说里面的AVFormatContext *pFormatCtx结构,字面意思理解AVFormatContext
就是关于AVFormat(其实就是我们上面说的Container格式)的所处的Context(场景),自
然是保存Container信息的总控结构了,后面你也可以看到,基本上所有的信息,都可
以从它出发而获取到
    
我们来看看av_open_input_file()都做了些什么:
[libavformat/utils.c]
int av_open_input_file(AVFormatContext **ic_ptr, const char *filename,
                       AVInputFormat *fmt,
                       int buf_size,
                       AVFormatParameters *ap)
{
    ......
    if (!fmt) {
       
        fmt = av_probe_input_format(pd, 0);
    }

   ......
    err = av_open_input_stream(ic_ptr, pb, filename, fmt, ap);
   ......
}

这样看来,只是做了两件事情:
1). 侦测容器文件格式
2). 从容器文件获取Stream的信息

这两件事情,实际上就是调用特定文件的demuxer以分离Stream的过程:

具体流程如下:

av_open_input_file
    |
    +---->av_probe_input_format从first_iformat中遍历注册的所有demuxer以 
    |     调用相应的probe函数
    |
    +---->av_open_input_stream调用指定demuxer的read_header函数以获取相关
          流的信息ic->iformat->read_header

如果反过来再参考我转贴的关于ffmpeg框架的文章,是否清楚一些了呢:)

[3]:简单从AVFormatContext获取Stream的信息,没什么好多说的

[4]:先简单说一些ffmpeg方面的东西,从理论角度说过来,Packet可以包含frame的部
分数据,但ffmpeg为了实现上的方便,使得对于视频来说,每个Packet至少包含一
frame,对于音频也是相应处理,这是实现方面的考虑,而非协议要求.
因此,在上面的代码实际上是这样的:
    从文件中读取packet,从Packet中解码相应的frame;
    从帧中解码;
    if(解码帧完成)
        do something();

我们来看看如何获取Packet,又如何从Packet中解码frame的。

av_read_frame
    |
    +---->av_read_frame_internal
        |
        +---->av_parser_parse调用的是指定解码器的s->parser->parser_parse函数以从raw packet中重构frame

avcodec_decode_video
    |
    +---->avctx->codec->decode调用指定Codec的解码函数
    
因此,从上面的过程可以看到,实际上分为了两部分:

一部分是解复用(demuxer),然后是解码(decode)

使用的分别是:
av_open_input_file()            ---->解复用

av_read_frame()            |
                           |    ---->解码    
avcodec_decode_video()     |

5.后面该做些什么
结合这部分和转贴的ffmepg框架的文章,应该可以基本打通解码的流程了,后面的问题则是针对具体容器格式和具体编码解码器的分析

 

 

ffmpeg架构和解码流程分析


更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论