龙空技术网

ffmpeg原理和架构

Linux特训营 2706

前言:

此刻同学们对“ffmpegnginxogg流”可能比较讲究,各位老铁们都需要分析一些“ffmpegnginxogg流”的相关内容。那么小编同时在网上收集了一些有关“ffmpegnginxogg流””的相关资讯,希望同学们能喜欢,兄弟们一起来学习一下吧!

流媒体提供解决方案的跨平台的C++开源项目,它实现了对标准流媒体传输是一个为流媒体提供解决方案的跨平台的C++开源项目,它实现了对标准流媒体传输协议如RTP/RTCP、RTSP、SIP等的支持。Live555实现了对多种音视频编码格式的音视频数据的流化、接收和处理等支持,包括MPEG、H.263+、DV、JPEG视频和多种音频编码。

FFmpeg基本原理 文章最后由福利


FFmpeg是相当强大的多媒体编解码框架,在深入分析其源代码之前必须要有基本的多媒体基础知识,否则其源代码会非常晦涩难懂。本文将从介绍一些基本的多媒体只是,主要是为研读ffmpeg源代码做准备,比如一些编解码部分,只有真正了解了多媒体处理的基本流程,研读ffmpeg源代码才能事半功倍。

下面分析一下多媒体中最基本最核心的视频解码过程,平常我们从网上下载一部电影或者一首歌曲,那么相应的多媒体播放器为我们做好了一切工作,我们只用欣赏就ok了。目前几乎所有的主流多媒体播放器都是基于开源多媒体框架ffmpeg来做的,可见ffmpeg的强大。下面是对一个媒体文件进行解码的主要流程:

1. 解复用(Demux)

当我们打开一个多媒体文件之后,第一步就是解复用,称之为Demux。为什么需要这一步,这一步究竟是做什么的?我们知道在一个多媒体文件中,既包括音频也包括视频,而且音频和视频都是分开进行压缩的,因为音频和视频的压缩算法不一样,既然压缩算法不一样,那么肯定解码也不一样,所以需要对音频和视频分别进行解码。虽然音频和视频是分开进行压缩的,但是为了传输过程的方便,将压缩过的音频和视频捆绑在一起进行传输。所以我们解码的第一步就是将这些绑在一起的音频和视频流分开来,也就是传说中的解复用,所以一句话,解复用这一步就是将文件中捆绑在一起的音频流和视频流分开来以方便后面分别对它们进行解码,下面是Demux之后的效果。



2. 解码(Decode)

这一步不用多说,一个多媒体文件肯定是经过某种或几种格式的压缩的,也就是通常所说的视频和音频编码,编码是为了减少数据量,否则的话对我们的存储设备是一个挑战,如果是流媒体的话对网络带宽也是一个几乎不可能完成的任务。所以我们必须对媒体信息进行尽可能的压缩。

3. FFmpeg中解码流程对应的API函数

了解了上面的一个媒体文件从打开到解码的流程,就可以很轻松的阅读ffmpeg代码,ffmpeg的框架也基本是按照这个流程来的,但不是每个流程对应一个API,下面这副图是我分析ffmpeg并根据自己的理解得到的ffmpeg解码流程对应的API,我想这幅图应该对理解ffmpeg和编解码有一些帮助。


Ffmpeg中Demux这一步是通过avformat_open_input()这个api来做的,这个api读出文件的头部信息,并做demux,在此之后我们就可以读取媒体文件中的音频和视频流,然后通过av_read_frame()从音频和视频流中读取出基本数据流packet,然后将packet送到avcodec_decode_video2()和相对应的api进行解码。

ffmpeg架构和解码流程一,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开放的接口有:





仍以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;


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。


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里的

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


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);

找到相关的宏代码如下


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


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


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

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

1. 输入输出流初始化。

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

3. 写输出文件的各部分

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

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


进行具体适配的编解码器初始化,而这里的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语言软件项目所借鉴。

资料内容包括:C/C++,Linux,golang,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,嵌入式 等。。。后台私信;资料;两个字可以免费领取

标签: #ffmpegnginxogg流