====== 深入浅出FFMPEG =========== 数字媒体处理的基本流程 =====
===== 认识FFMPEG =====
FFMPEG堪称自由软件中最完备的一套多媒体支持库,它几乎实现了所有当下常见的数据封装格式、多媒体传输协议以及音视频编解码器。因此,对于从事多媒体技术开发的工程师来说,深入研究FFMPEG成为一门必不可少的工作,可以这样说,FFMPEG之于多媒体开发工程师的重要性正如kernel之于嵌入式系统工程师一般。
几个小知识:
===== 一个简单的测试程序 =====
<code c>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "libavutil/avstring.h"
#include "libavformat/avformat.h"
#include "libavdevice/avdevice.h"
#include "libavcodec/opt.h"
#include "libswscale/swscale.h"
#define DECODED_AUDIO_BUFFER_SIZE
struct options
{
};
int parse_options(struct options *opts, int argc, char** argv)
{
}
void show_help(char* program)
{
}
static void log_callback(void* ptr, int level, const char* fmt, va_list vl)
{
}
#include <sys/ioctl.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/soundcard.h>
#define OSS_DEVICE "/dev/dsp0"
struct audio_dsp
{
};
int map_formats(enum SampleFormat format)
{
}
int set_audio(struct audio_dsp* dsp)
{
}
int play_pcm(struct audio_dsp* dsp, unsigned char *buf, int size)
{
}
#include <linux/fb.h>
#include <sys/mman.h>
#define FB_DEVICE "/dev/fb0"
enum pic_format
{
};
struct video_fb
{
};
int open_video(struct video_fb *fb, int x, int y)
{
}
int show_picture(struct video_fb *fb, AVFrame *frame, int width, int height, enum pic_format format)
{
}
void close_video(struct video_fb *fb)
{
}
int main(int argc, char **argv)
{
#if 0
#endif
fail:
}
</code>
这一小段代码可以实现的功能包括:
这些功能足以支持一个功能强大的多媒体播放器,因为最复杂的解复用、解码、数据分析过程已经在FFMpeg内部实现了,需要关注的仅剩同步问题。
===== 用户接口 =====
==== 数据结构====
=== 基本概念 ===
编解码器、数据帧、媒体流和容器是数字媒体处理系统的四个基本概念,它们的关系如下图所示:
首先需要统一术语:
在FFMPEG中,使用AVFormatContext、AVStream、AVPacket、AVCodecContext及AVCodec来抽象这几个基本要素:
=== AVPacket ===
AVPacket定义在avcodec.h中,如下:
<code c>
typedef struct AVPacket {
} AVPacket;
</code>
FFMPEG使用AVPacket来暂存解复用之后、解码之前的媒体数据(一个音/视频帧、一个字幕包等)及附加信息(解码时间戳、显示时间戳、时长等)。其中:
AVPacket结构本身只是个容器,它使用data成员指向实际的数据缓冲区,这个缓冲区可以通过av_new_packet创建,可以通过av_dup_packet拷贝,也可以由FFMPEG的API产生(如av_read_frame),使用之后需要通过调用av_free_packet释放。av_free_packet调用的是结构体本身的destruct函数,它的值有两种情况:1)av_destruct_packet_nofree或0;2)av_destruct_packet,其中,前者仅仅是将data和size的值清0而已,后者才会真正地释放缓冲区。FFMPEG内部使用AVPacket结构建立缓冲区装载数据,同时提供destruct函数,如果FFMPEG打算自己维护缓冲区,则将destruct设为av_destruct_packet_nofree,用户调用av_free_packet清理缓冲区时并不能够将其释放;如果FFMPEG不会再使用该缓冲区,则将destruct设为av_destruct_packet,表示它能够被释放。对于缓冲区不能够被释放的AVPackt,用户在使用之前最好调用av_dup_packet进行缓冲区的克隆,将其转化为缓冲区能够被释放的AVPacket,以免对缓冲区的不当占用造成异常错误。而av_dup_packet会为destruct指针为av_destruct_packet_nofree的AVPacket新建一个缓冲区,然后将原缓冲区的数据拷贝至新缓冲区,置data的值为新缓冲区的地址,同时设destruct指针为av_destruct_packet。
=== 时间信息 ===
时间信息用于实现多媒体同步。
同步的目的在于展示多媒体信息时,能够保持媒体对象之间固有的时间关系。同步有两类,一类是流内同步,其主要任务是保证单个媒体流内的时间关系,以满足感知要求,如按照规定的帧率播放一段视频;另一类是流间同步,主要任务是保证不同媒体流之间的时间关系,如音频和视频之间的关系(lipsync)。
对于固定速率的媒体,如固定帧率的视频或固定比特率的音频,可以将时间信息(帧率或比特率)置于文件首部(header),如[[tech:multimedia:digital_media#容器|AVI的hdrl List、MP4的moov box]],还有一种相对复杂的方案是将时间信息嵌入媒体流的内部,如MPEG TS和Real video,这种方案可以处理变速率的媒体,亦可有效避免同步过程中的时间漂移。
FFMPEG会为每一个数据包打上时间标签,以更有效地支持上层应用的同步机制。时间标签有两种,一种是DTS,称为解码时间标签,另一种是PTS,称为显示时间标签。对于声音来说 ,这两个时间标签是相同的,但对于某些视频编码格式,由于采用了双向预测技术,会造成DTS和PTS的不一致。
无双向预测帧的情况:
有双向预测帧的情况:
对于存在双向预测帧的情况,通常要求解码器对图像重排序,以保证输出的图像顺序为显示顺序:
**时间信息的获取:**
通过调用av_find_stream_info,多媒体应用可以从AVFormatContext对象中拿到媒体文件的时间信息:主要是总时间长度和开始时间,此外还有与时间信息相关的比特率和文件大小。其中时间信息的单位是AV_TIME_BASE:微秒。
<code c>
typedef struct AVFormatContext {
} AVFormatContext;
</code>
以上4个成员变量都是只读的,基于FFMpeg的中间件需要将其封装到某个接口中,如:
<code c>
LONG GetDuratioin(IntfX*);
LONG GetStartTime(IntfX*);
LONG GetFileSize(IntfX*);
LONG GetBitRate(IntfX*);
</code>
==== APIs ====
=== av_open_input_file ===
<code c>
int av_open_input_file(AVFormatContext **ic_ptr, const char *filename,
</code>
av_open_input_file完成两个任务:
一个多媒体文件或多媒体流与其包含的原始流的关系如下:
关于输入参数:
这个函数通过解析多媒体文件或流的头信息及其他辅助数据,能够获取足够多的关于文件、流和编解码器的信息,但由于任何一种多媒体格式提供的信息都是有限的,而且不同的多媒体内容制作软件对头信息的设置不尽相同,此外这些软件在产生多媒体内容时难免会引入一些错误,因此这个函数并不保证能够获取所有需要的信息,在这种情况下,则需要考虑另一个函数:av_find_stream_info。
=== av_find_stream_info ===
<code c>
int av_find_stream_info(AVFormatContext *ic);
</code>
这个函数主要用于获取必要的编解码器参数,设置到ic->streams[i]->codec中。
首先必须得到各媒体流对应编解码器的类型和id,这是两个定义在avutils.h和avcodec.h中的枚举:
<code>
enum AVMediaType {
};
enum CodecID {
};
</code>
通常,如果某种媒体格式具备完备而正确的头信息,调用av_open_input_file即可以得到这两个参数,但若是因某种原因av_open_input_file无法获取它们,这一任务将由av_find_stream_info完成。
其次还要获取各媒体流对应编解码器的时间基准。
此外,对于音频编解码器,还需要得到:
对于视频编解码器,则是:
=== av_seek_frame ===
<code c>
int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, int flags);
</code>
这个函数用于实现对媒体文件的随机访问,支持以下三种方式:
关于参数:
=== av_read_frame ===
<code c>
int av_read_frame(AVFormatContext *s, AVPacket *pkt);
</code>