深入浅出FFMPEG(二) SDK version 0.11

用户接口

数据结构

基本概念

编解码器、数据帧、媒体流和容器是数字媒体处理系统的四个基本概念。

首先需要统一术语:

  • 容器/文件(Conainer/File):即特定格式的多媒体文件。

  • 媒体流(Stream):指时间轴上的一段连续数据,如一段声音数据,一段视频数据或一段字幕数据,可以是压缩的,也可以是非压缩的,压缩的数据需要关联特定的编解码器。

  • 数据帧/数据包(Frame/Packet):通常,一个媒体流由大量的数据帧组成,对于压缩数据,帧对应着编解码器的最小处理单元。通常,分属于不同媒体流的数据帧交错复用于容器之中,参见交错。

  • 编解码器:编解码器以帧为单位实现压缩数据和原始数据之间的相互转换。

在FFMPEG中,使用AVFormatContext、AVStream、AVCodecContext、AVCodec及AVPacket等结构来抽象这些基本要素,它们的关系如下图所示: 

AVCodecContext

这是一个描述编解码器上下文的数据结构,包含了众多编解码器需要的参数信息,如下列出了部分比较重要的域:

typedef struct AVCodecContext {
 
    ......
 
    
    uint8_t *extradata;
    int extradata_size;
    
    AVRational time_base;
 
    
    
    int width, height;
 
    ......
 
    
    int sample_rate; ///< samples per second
    int channels;    ///< number of audio channels
 
    
    enum SampleFormat sample_fmt;  ///< sample format
 
    
    
    int frame_size;
    int frame_number;   ///< audio or video frame number
 
    ......
 
    char codec_name[32];
    enum AVMediaType codec_type; 
    enum CodecID codec_id; 
 
    
    unsigned int codec_tag;            
 
    ......
 
    
    int has_b_frames;
 
    
    int block_align;
 
    ......
 
    
     int bits_per_coded_sample;  
 
     ......
 
} AVCodecContext;

如果是单纯使用libavcodec,这部分信息需要调用者进行初始化;如果是使用整个FFMPEG库,这部分信息在调用avformat_open_input和avformat_find_stream_info的过程中根据文件的头信息及媒体流内的头部信息完成初始化。其中几个主要域的释义如下:

  1. extradata/extradata_size:这个buffer中存放了解码器可能会用到的额外信息,在av_read_frame中填充。一般来说,首先,某种具体格式的demuxer在读取格式头信息的时候会填充extradata,其次,如果demuxer没有做这个事情,比如可能在头部压根儿就没有相关的编解码信息,则相应的parser会继续从已经解复用出来的媒体流中继续寻找。在没有找到任何额外信息的情况下,这个buffer指针为空。

  2. time_base:

  3. width/height:视频的宽和高。

  4. sample_rate/channels:音频的采样率和信道数目。

  5. sample_fmt: 音频的原始采样格式。

  6. codec_name/codec_type/codec_id/codec_tag:编解码器的信息。

AVStream

该结构体描述一个媒体流,定义如下:

typedef struct AVStream {
    int index;    
    int id;       
    AVCodecContext *codec; 
    
    AVRational r_frame_rate;
 
    ......
 
    
    AVRational time_base;
 
    ......
 
    
    int64_t start_time;
    
    int64_t duration;
 
#if LIBAVFORMAT_VERSION_INT < (53<<16)
    char language[4]; 
#endif
 
    
    enum AVStreamParseType need_parsing;
    struct AVCodecParserContext *parser;
 
    ......
 
    
    AVIndexEntry *index_entries; 
    int nb_index_entries;
    unsigned int index_entries_allocated_size;
 
    int64_t nb_frames;                 ///< number of frames in this stream if known or 0
 
    ......
 
    
    AVRational avg_frame_rate;
    ......
} AVStream;

主要域的释义如下,其中大部分域的值可以由avformat_open_input根据文件头的信息确定,缺少的信息需要通过调用avformat_find_stream_info读帧及软解码进一步获取:

  1. index/id:index对应流的索引,这个数字是自动生成的,根据index可以从AVFormatContext::streams表中索引到该流;而id则是流的标识,依赖于具体的容器格式。比如对于MPEG TS格式,id就是pid。

  2. time_base:流的时间基准,是一个实数,该流中媒体数据的pts和dts都将以这个时间基准为粒度。通常,使用av_rescale/av_rescale_q可以实现不同时间基准的转换。

  3. start_time:流的起始时间,以流的时间基准为单位,通常是该流中第一个帧的pts。

  4. duration:流的总时间,以流的时间基准为单位。

  5. need_parsing:对该流parsing过程的控制域。

  6. nb_frames:流内的帧数目。

  7. r_frame_rate/framerate/avg_frame_rate:帧率相关。

  8. codec:指向该流对应的AVCodecContext结构,调用avformat_open_input时生成。

  9. parser:指向该流对应的AVCodecParserContext结构,调用avformat_find_stream_info时生成。。

AVFormatContext

这个结构体描述了一个媒体文件或媒体流的构成和基本信息,定义如下:

typedef struct AVFormatContext {
    const AVClass *av_class; 
    
    struct AVInputFormat *iformat;
    struct AVOutputFormat *oformat;
    void *priv_data;
    ByteIOContext *pb;
    unsigned int nb_streams;
    AVStream *streams[MAX_STREAMS];
    char filename[1024]; 
    
    int64_t timestamp;
#if LIBAVFORMAT_VERSION_INT < (53<<16)
    char title[512];
    char author[512];
    char copyright[512];
    char comment[512];
    char album[512];
    int year;  
    int track; 
    char genre[32]; 
#endif
 
    int ctx_flags; 
    
    
    struct AVPacketList *packet_buffer;
 
    
    int64_t start_time;
    
    int64_t duration;
    
    int64_t file_size;
    
    int bit_rate;
 
    
    AVStream *cur_st;
#if LIBAVFORMAT_VERSION_INT < (53<<16)
    const uint8_t *cur_ptr_deprecated;
    int cur_len_deprecated;
    AVPacket cur_pkt_deprecated;
#endif
 
    
    int64_t data_offset; 
    int index_built;
 
    int mux_rate;
    unsigned int packet_size;
    int preload;
    int max_delay;
 
#define AVFMT_NOOUTPUTLOOP -1
#define AVFMT_INFINITEOUTPUTLOOP 0
    
    int loop_output;
 
    int flags;
#define AVFMT_FLAG_GENPTS       0x0001 ///< Generate missing pts even if it requires parsing future frames.
#define AVFMT_FLAG_IGNIDX       0x0002 ///< Ignore index.
#define AVFMT_FLAG_NONBLOCK     0x0004 ///< Do not block when reading packets from input.
#define AVFMT_FLAG_IGNDTS       0x0008 ///< Ignore DTS on frames that contain both DTS & PTS
#define AVFMT_FLAG_NOFILLIN     0x0010 ///< Do not infer any values from other values, just return what is stored in the container
#define AVFMT_FLAG_NOPARSE      0x0020 ///< Do not use AVParsers, you also must set AVFMT_FLAG_NOFILLIN as the fillin code works on frames and no parsing -> no frames. Also seeking to frames can not work if parsing to find frame boundaries has been disabled
#define AVFMT_FLAG_RTP_HINT     0x0040 ///< Add RTP hinting to the output file
 
    int loop_input;
    
    unsigned int probesize;
 
    
    int max_analyze_duration;
 
    const uint8_t *key;
    int keylen;
 
    unsigned int nb_programs;
    AVProgram **programs;
 
    
    enum CodecID video_codec_id;
    
    enum CodecID audio_codec_id;
    
    enum CodecID subtitle_codec_id;
 
    
    unsigned int max_index_size;
 
    
    unsigned int max_picture_buffer;
 
    unsigned int nb_chapters;
    AVChapter **chapters;
 
    
    int debug;
#define FF_FDEBUG_TS        0x0001
 
    
    struct AVPacketList *raw_packet_buffer;
    struct AVPacketList *raw_packet_buffer_end;
 
    struct AVPacketList *packet_buffer_end;
 
    AVMetadata *metadata;
 
    
#define RAW_PACKET_BUFFER_SIZE 2500000
    int raw_packet_buffer_remaining_size;
 
    
    int64_t start_time_realtime;
} AVFormatContext;

这是FFMpeg中最为基本的一个结构,是其他所有结构的根,是一个多媒体文件或流的根本抽象。其中:

  • nb_streams和streams所表示的AVStream结构指针数组包含了所有内嵌媒体流的描述;

  • iformat和oformat指向对应的demuxer和muxer指针;

  • pb则指向一个控制底层数据读写的ByteIOContext结构。

  • start_time和duration是从streams数组的各个AVStream中推断出的多媒体文件的起始时间和长度,以微妙为单位。

通常,这个结构由avformat_open_input在内部创建并以缺省值初始化部分成员。但是,如果调用者希望自己创建该结构,则需要显式为该结构的一些成员置缺省值——如果没有缺省值的话,会导致之后的动作产生异常。以下成员需要被关注:

  • probesize

  • mux_rate

  • packet_size

  • flags

  • max_analyze_duration

  • key

  • max_index_size

  • max_picture_buffer

  • max_delay

AVPacket

AVPacket定义在avcodec.h中,如下:

typedef struct AVPacket {
    
    int64_t pts;
    
    int64_t dts;
    uint8_t *data;
    int   size;
    int   stream_index;
    int   flags;
    
    int   duration;
    void  (*destruct)(struct AVPacket *);
    void  *priv;
    int64_t pos;                            ///< byte position in stream, -1 if unknown
 
    
    int64_t convergence_duration;
} AVPacket;

FFMPEG使用AVPacket来暂存解复用之后、解码之前的媒体数据(一个音/视频帧、一个字幕包等)及附加信息(解码时间戳、显示时间戳、时长等)。其中:

  • dts表示解码时间戳,pts表示显示时间戳,它们的单位是所属媒体流的时间基准。

  • stream_index给出所属媒体流的索引;

  • data为数据缓冲区指针,size为长度;

  • duration为数据的时长,也是以所属媒体流的时间基准为单位;

  • pos表示该数据在媒体流中的字节偏移量;

  • destruct为用于释放数据缓冲区的函数指针;

  • flags为标志域,其中,最低为置1表示该数据是一个关键帧。

AVPacket结构本身只是个容器,它使用data成员引用实际的数据缓冲区。这个缓冲区通常是由av_new_packet创建的,但也可能由FFMPEG的API创建(如av_read_frame)。当某个AVPacket结构的数据缓冲区不再被使用时,要需要通过调用av_free_packet释放。av_free_packet调用的是结构体本身的destruct函数,它的值有两种情况:1)av_destruct_packet_nofree或0;2)av_destruct_packet,其中,情况1)仅仅是将data和size的值清0而已,情况2)才会真正地释放缓冲区。

FFMPEG内部使用AVPacket结构建立缓冲区装载数据,同时提供destruct函数,如果FFMPEG打算自己维护缓冲区,则将destruct设为av_destruct_packet_nofree,用户调用av_free_packet清理缓冲区时并不能够将其释放;如果FFMPEG打算将该缓冲区彻底交给调用者,则将destruct设为av_destruct_packet,表示它能够被释放。安全起见,如果用户希望自由地使用一个FFMPEG内部创建的AVPacket结构,最好调用av_dup_packet进行缓冲区的克隆,将其转化为缓冲区能够被释放的AVPacket,以免对缓冲区的不当占用造成异常错误。av_dup_packet会为destruct指针为av_destruct_packet_nofree的AVPacket新建一个缓冲区,然后将原缓冲区的数据拷贝至新缓冲区,置data的值为新缓冲区的地址,同时设destruct指针为av_destruct_packet。

时间信息

时间信息用于实现多媒体同步。

同步的目的在于展示多媒体信息时,能够保持媒体对象之间固有的时间关系。同步有两类,一类是流内同步,其主要任务是保证单个媒体流内的时间关系,以满足感知要求,如按照规定的帧率播放一段视频;另一类是流间同步,主要任务是保证不同媒体流之间的时间关系,如音频和视频之间的关系(lipsync)。

对于固定速率的媒体,如固定帧率的视频或固定比特率的音频,可以将时间信息(帧率或比特率)置于文件首部(header),如AVI的hdrl List、MP4的moov box,还有一种相对复杂的方案是将时间信息嵌入媒体流的内部,如MPEG TS和Real video,这种方案可以处理变速率的媒体,亦可有效避免同步过程中的时间漂移。

FFMPEG会为每一个数据包打上时间标签,以更有效地支持上层应用的同步机制。时间标签有两种,一种是DTS,称为解码时间标签,另一种是PTS,称为显示时间标签。对于声音来说 ,这两个时间标签是相同的,但对于某些视频编码格式,由于采用了双向预测技术,会造成DTS和PTS的不一致。

无双向预测帧的情况:

图像类型: I   P   P   P   P   P   P ...  I   P   P
DTS:     0   1   2   3   4   5   6...  100 101 102
PTS:     0   1   2   3   4   5   6...  100 101 102

有双向预测帧的情况:

图像类型: I   P   B   B   P   B   B ...  I   P   B
DTS:     0   1   2   3   4   5   6 ...  100 101 102
PTS:     0   3   1   2   6   4   5 ...  100 104 102

对于存在双向预测帧的情况,通常要求解码器对图像重排序,以保证输出的图像顺序为显示顺序:

解码器输入:I   P   B   B   P   B   B
 (DTS)     0   1   2   3   4   5   6  
 (PTS)     0   3   1   2   6   4   5
解码器输出:X   I   B   B   P   B   B   P
 (PTS)     X   0   1   2   3   4   5   6

时间信息的获取:

通过调用avformat_find_stream_info,多媒体应用可以从AVFormatContext对象中拿到媒体文件的时间信息:主要是总时间长度和开始时间,此外还有与时间信息相关的比特率和文件大小。其中时间信息的单位是AV_TIME_BASE:微秒。

typedef struct AVFormatContext {
 
    ......
 
    
    int64_t start_time;
    
    int64_t duration;
    
    int64_t file_size;
    
    int bit_rate;
 
    ......
 
} AVFormatContext;

以上4个成员变量都是只读的,基于FFMpeg的中间件需要将其封装到某个接口中,如:

LONG GetDuratioin(IntfX*);
LONG GetStartTime(IntfX*);
LONG GetFileSize(IntfX*);
LONG GetBitRate(IntfX*);

APIs

avformat_open_input

int avformat_open_input(AVFormatContext **ic_ptr, const char *filename, AVInputFormat *fmt, AVDictionary **options);

avformat_open_input完成两个任务:

  1. 打开一个文件或URL,基于字节流的底层输入模块得到初始化。

  2. 解析多媒体文件或多媒体流的头信息,创建AVFormatContext结构并填充其中的关键字段,依次为各个原始流建立AVStream结构。

一个多媒体文件或多媒体流与其包含的原始流的关系如下:

多媒体文件/多媒体流 (movie.mkv)
  原始流 1  (h.264 video)
  原始流 2  (aac audio for Chinese)
  原始流 3  (aac audio for english)
  原始流 4  (Chinese Subtitle)
  原始流 5  (English Subtitle)
  ...

关于输入参数:

  • ic_ptr,这是一个指向指针的指针,用于返回avformat_open_input内部构造的一个AVFormatContext结构体。

  • filename,指定文件名。

  • fmt,用于显式指定输入文件的格式,如果设为空则自动判断其输入格式。

  • options

这个函数通过解析多媒体文件或流的头信息及其他辅助数据,能够获取足够多的关于文件、流和编解码器的信息,但由于任何一种多媒体格式提供的信息都是有限的,而且不同的多媒体内容制作软件对头信息的设置不尽相同,此外这些软件在产生多媒体内容时难免会引入一些错误,因此这个函数并不保证能够获取所有需要的信息,在这种情况下,则需要考虑另一个函数:avformat_find_stream_info。

avformat_find_stream_info

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

这个函数主要用于获取必要的编解码器参数,设置到ic→streams[i]→codec中。

首先必须得到各媒体流对应编解码器的类型和id,这是两个定义在avutils.h和avcodec.h中的枚举:

enum AVMediaType {
    AVMEDIA_TYPE_UNKNOWN = -1,
    AVMEDIA_TYPE_VIDEO,
    AVMEDIA_TYPE_AUDIO,
    AVMEDIA_TYPE_DATA,
    AVMEDIA_TYPE_SUBTITLE,
    AVMEDIA_TYPE_ATTACHMENT,
    AVMEDIA_TYPE_NB
};
enum CodecID {
    CODEC_ID_NONE,

    
    CODEC_ID_MPEG1VIDEO,
    CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding
    CODEC_ID_MPEG2VIDEO_XVMC,
    CODEC_ID_H261,
    CODEC_ID_H263,
    ...
};

通常,如果某种媒体格式具备完备而正确的头信息,调用avformat_open_input即可以得到这两个参数,但若是因某种原因avformat_open_input无法获取它们,这一任务将由avformat_find_stream_info完成。

其次还要获取各媒体流对应编解码器的时间基准。

此外,对于音频编解码器,还需要得到:

  1. 采样率,

  2. 声道数,

  3. 位宽,

  4. 帧长度(对于某些编解码器是必要的),

对于视频编解码器,则是:

  1. 图像大小,

  2. 色彩空间及格式,

av_read_frame

int av_read_frame(AVFormatContext *s, AVPacket *pkt);

这个函数用于从多媒体文件或多媒体流中读取媒体数据,获取的数据由AVPacket结构pkt来存放。对于音频数据,如果是固定比特率,则pkt中装载着一个或多个音频帧;如果是可变比特率,则pkt中装载有一个音频帧。对于视频数据,pkt中装载有一个视频帧。需要注意的是:再次调用本函数之前,必须使用av_free_packet释放pkt所占用的资源。

通过pkt→stream_index可以查到获取的媒体数据的类型,从而将数据送交相应的解码器进行后续处理。

av_seek_frame

int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, int flags);

这个函数通过改变媒体文件的读写指针来实现对媒体文件的随机访问,支持以下三种方式:

  • 基于时间的随机访问:具体而言就是将媒体文件读写指针定位到某个给定的时间点上,则之后调用av_read_frame时能够读到时间标签等于给定时间点的媒体数据,通常用于实现媒体播放器的快进、快退等功能。

  • 基于文件偏移的随机访问:相当于普通文件的seek函数,timestamp也成为文件的偏移量。

  • 基于帧号的随机访问:timestamp为要访问的媒体数据的帧号。

关于参数:

  • s:是个AVFormatContext指针,就是avformat_open_input返回的那个结构。

  • stream_index:指定媒体流,如果是基于时间的随机访问,则第三个参数timestamp将以此媒体流的时间基准为单位;如果设为负数,则相当于不指定具体的媒体流,FFMPEG会按照特定的算法寻找缺省的媒体流,此时,timestamp的单位为AV_TIME_BASE(微秒)。

  • timestamp:时间标签,单位取决于其他参数。

  • flags:定位方式,AVSEEK_FLAG_BYTE表示基于字节偏移,AVSEEK_FLAG_FRAME表示基于帧号,其它表示基于时间。

av_close_input_file

void av_close_input_file(AVFormatContext *s);

关闭一个媒体文件:释放资源,关闭物理IO。

avcodec_find_decoder

AVCodec *avcodec_find_decoder(enum CodecID id);
AVCodec *avcodec_find_decoder_by_name(const char *name);

根据给定的codec id或解码器名称从系统中搜寻并返回一个AVCodec结构的指针。

avcodec_open

int avcodec_open(AVCodecContext *avctx, AVCodec *codec);

此函数根据输入的AVCodec指针具体化AVCodecContext结构。在调用该函数之前,需要首先调用avcodec_alloc_context分配一个AVCodecContext结构,或调用avformat_open_input获取媒体文件中对应媒体流的AVCodecContext结构;此外还需要通过avcodec_find_decoder获取AVCodec结构。

这一函数还将初始化对应的解码器。

avcodec_decode_video2

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

解码一个视频帧。got_picture_ptr指示是否有解码数据输出。

输入数据在AVPacket结构中,输出数据在AVFrame结构中。AVFrame是定义在avcodec.h中的一个数据结构:

typedef struct AVFrame {
    FF_COMMON_FRAME
} AVFrame;

FF_COMMON_FRAME定义了诸多数据域,大部分由FFMpeg内部使用,对于用户来说,比较重要的主要包括:

#define FF_COMMON_FRAME \
......
    uint8_t *data[4];\
    int linesize[4];\
    int key_frame;\
    int pict_type;\
    int64_t pts;\        
    int reference;\   
......

FFMpeg内部以planar的方式存储原始图像数据,即将图像像素分为多个平面(R/G/B或Y/U/V),data数组内的指针分别指向四个像素平面的起始位置,linesize数组则存放各个存贮各个平面的缓冲区的行宽:

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+++data[0]->#################################++++++++++++
++++++++++++###########picture data##########++++++++++++
++++++++++++#################################++++++++++++
++++++++++++#################################++++++++++++  
              ........................
++++++++++++#################################++++++++++++
|<-------------------line_size[0]---------------------->|

此外,key_frame标识该图像是否是关键帧;pict_type表示该图像的编码类型:I(1)/P(2)/B(3)……;pts是以time_base为单位的时间标签,对于部分解码器如H.261、H.263和MPEG4,可以从头信息中获取;reference表示该图像是否被用作参考。

avcodec_decode_audio4

int avcodec_decode_audio4(AVCodecContext *avctx, AVFrame *frame, int *got_frame_ptr, AVPacket *avpkt);

解码一个音频帧。输入数据在AVPacket结构中,输出数据在frame中,got_frame_ptr表示是否有数据输出。

avcodec_close

int avcodec_close(AVCodecContext *avctx);

关闭解码器,释放avcodec_open中分配的资源。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值