本文属于重写…………早些天码了2000来个字,想等着系列一起发出来,于是存了草稿。今天偶尔翻看,发现草稿上只有几个字……所有内容都丢失了。当时是心情是崩溃的。
咬着牙重写本篇,也算是为解读ffmpeg打下更加牢固的基础。
简介
ffmpeg在近几个版本的变化还是很大的,这里有两张网上流传的图,算是学习ffmpeg的初识宝典。
ffmpeg decoder流程图
ffmpeg encoder流程图
图片比较大,请下载之后查看。
粉红色背景函数:FFmpeg的API函数。
白色背景的函数:FFmpeg的内部函数。
黄色背景的函数:URLProtocol结构体中的函数,包含了读写各种协议的功能。
绿色背景的函数:AVOutputFormat结构体中的函数,包含了读写各种封装格式的功能(AVI,MKV,FLV.)。
蓝色背景的函数:AVCodec结构体中的函数,包含了编解码的功能。
我觉得ffmpeg结构的核心点在于 模板模式 ,而且是用c语言实现了模板模式(核心技巧就是函数指针)。
最明显的体现在于虚线框左侧白色背景框的方法。
举几个例子: URLProtocol结构体包含如下协议处理函数指针:
url_open():打开
url_read():读取
url_write():写入
url_seek():调整进度
url_close():关闭
不同的协议对应着上述接口有不同的实现函数,
File协议(即文件)对应的URLProtocol结构体ff_file_protocol:
url_open() -> file_open() -> open()
url_read() -> file_read() -> read()
url_write() -> file_write() -> write()
url_seek() -> file_seek() -> lseek()
url_close() -> file_close() -> close()
更加详细的我们会在后篇一点点积累。
再前言
笔者使用的系统是os x,假设我们使用了linux系列的系统。这将会影响到一些环境配置。在编译ffmpeg的时候,使用config进行配置,不同的环境编译之后生成的config.h中的宏会有不同的配置。
thread
由于c语言没有对thread的统一实现,所以不同的平台就会有不同的实现。另外,ffmpeg自己也实现了一份线程。
在thread.h头文件中,ffmpeg对线程进行了一些配置。
在Linux环境下
#if HAVE_PTHREADS
#include <pthread.h>
#define AVMutex pthread_mutex_t
#define AVOnce pthread_once_t
#define ff_thread_once(control, routine) pthread_once(control, routine)
这是非常常用到的配置项。
比如ff_thread_once 在linux环境下,实际上就是pthread_once。简单介绍下pthread_once
int pthread_once(pthread_once_t *once_control, void (*init_routine) (void));
功能:本函数使用初值为PTHREAD_ONCE_INIT的once_control变量保证init_routine()函数在本进程执行序列中仅执行一次。在多线程编程环境下,尽管pthread_once()调用会出现在多个线程中,init_routine()函数仅执行一次,究竟在哪个线程中执行是不定的,是由内核调度来决定。
实际上,这个函数并不关心运行的函数指针到底是谁,这个函数是否被第一次调用,他只关系pthread_once_t这个变量当前的状态值是执行过还是未执行过。
avcodec_register_all
ffmpeg中带有很多register_all字样的方法,一般都是用来注册各种组件的,比如注册解*码*器,注册编码器,注册混合器等。
而avcodec_register_all就是用来注册所有的解*码*器和编码器的函数。
void avcodec_register_all(void)
{
ff_thread_once(&av_codec_next_init, av_codec_init_next);
}
static void av_codec_init_next(void)
{
AVCodec *prev = NULL, *p;
void *i = 0;
while ((p = (AVCodec*)av_codec_iterate(&i))) {
if (prev)
prev->next = p;
prev = p;
}
}
const AVCodec *av_codec_iterate(void **opaque)
{
// i 最初的值是0, 表示指向0地址的指针
uintptr_t i = (uintptr_t)*opaque;
//使用地址值 0 来当做index。
const AVCodec *c = codec_list[i];
ff_thread_once(&av_codec_static_init, av_codec_init_static);
if (c)//地址值+1
*opaque = (void*)(i + 1);
return c;
}
static AVOnce av_codec_static_init = AV_ONCE_INIT;
static void av_codec_init_static(void)
{
for (int i = 0; codec_list[i]; i++) {
if (codec_list[i]->init_static_data)
codec_list[i]->init_static_data((AVCodec*)codec_list[i]);
}
}
为了方面阅读,我将函数定义的顺序反了过来,在c中从上而下,如果没有定义或者声明,直接使用函数是会报错的。
首先avcodec_register_all实际上使用ff_pthread_once来保证只会注册一次(这是近期版本才改良的方法,早的ffmpeg并没有使用ff_pthread_once)。实际上定义的方法是av_codec_init_next。
一个AVCodec结构体代表了一个解*码*器或者编码器,我们在后面会陆续讲解这个结构体,秩至于现在,这个程度的认知以及足够理解代码了。 AVCodec结构体中拥有一个next指针,是的,这就是典型的链表结构。而av_codec_init_next方法就是初始化这个链表,将所有的AVCodec结构体变量串成一个链表。
av_codec_iterate的作用是依次返回一个AVCodec对象。初次阅读这段代码可能有些困惑,比如使用到了指针的指针什么的。这里的核心思想就是,使用指针的地址值替代一个int类型的值来遍历codec_list这个列表。可能理解的不是很准确,但是这样做确确实实省下了一个迭代器的内存地址……我们不需要真的去开辟一个int类型的地址,存放index,然后使用一个指针指向这个index,保证在不同的方法中也能使这个值+1.
不过这都不重要,总之av_codec_iterate就是遍历了codec_list,依次返回所有的AVCodec。另外还调用了av_codec_init_static(只会运行依次)
av_codec_init_static的作用是依次调用所有codec_list中的init_static_data指针指向的方法。用于初始化这个解*码*器或者编码。
虽然大多数解*码*器和编码器都没有实现这个方法。但是作为设计者这段代码是否有改进的余地?比如在首次使用这个结构体时去调用初始化方法,而不是一次调用所有解*码*器的方法,因为实际使用中并不是所有类型的解*码*器都会用到。求解释这样设计的原因。
av_register_all
曾经的王者!在以前,只要使用到ffmpeg都会首先调用av_register_all,就像名字一样,他用来注册所有东西。不过在ffmpeg的演进过程中,功能组件弱化了。
muxer:封装器,用来将音视频封装成avi mp4等格式
demuxer:解封装器,将avi mp4等视频还原出视频和音频源
void av_register_all(void)
{
ff_thread_once(&av_format_next_init, av_format_init_next);
}
static void av_format_init_next(void)
{
AVOutputFormat *prevout = NULL, *out;
AVInputFormat *previn = NULL, *in;
ff_mutex_lock(&avpriv_register_devices_mutex);
//将所有 muxer(封装器,用来将音视频封装成avi mp4等格式)组成链表
for (int i = 0; (out = (AVOutputFormat*)muxer_list[i]); i++) {
if (prevout)
prevout->next = out;
prevout = out;
}
if (outdev_list) {
for (int i = 0; (out = (AVOutputFormat*)outdev_list[i]); i++) {
if (prevout)
prevout->next = out;
prevout = out;
}
}
//将所有demuxer组成链表
for (int i = 0; (in = (AVInputFormat*)demuxer_list[i]); i++) {
if (previn)
previn->next = in;
previn = in;
}
if (indev_list) {
for (int i = 0; (in = (AVInputFormat*)indev_list[i]); i++) {
if (previn)
previn->next = in;
previn = in;
}
}
ff_mutex_unlock(&avpriv_register_devices_mutex);
}
实际代码已经只是用来注册muxer和demuxer了。这里有两个量,一个是outdev_list 还有一个是indev_list。这两个对象是开发者可以自己设置(avpriv_register_devices),用来自定义需要哪些muxer和demuxer。
avfilter_register_all
用于将所有filter串联成链表。filter是ffmpeg给出的一种食品处理组件,可以对视频进行缩放,裁剪,加水印等各种操作。
static void av_filter_init_next(void)
{
AVFilter *prev = NULL, *p;
void *i = 0;
while ((p = (AVFilter*)av_filter_iterate(&i))) {
if (prev)
prev->next = p;
prev = p;
}
}
void avfilter_register_all(void)
{
ff_thread_once(&av_filter_next_init, av_filter_init_next);
}
avformat_network_init
用于初始化网络。
int avformat_network_init(void)
{
#if CONFIG_NETWORK
int ret;
//初始化网络(windows平台下生效)
if ((ret = ff_network_init()) < 0)
return ret;
//初始化tls
if ((ret = ff_tls_init()) < 0)
return ret;
#endif
return 0;
}
ffurl_register_protocol
原来用于注册URLProtocol协议列表的,但是在当前的ffmpeg中已经找不到该方法,不需要再注册URLProtocol协议列表了。
register_exit
这是一个挺鸡肋的方法。定义在cmdutils.c中
static void (*program_exit)(int ret);
void register_exit(void (*cb)(int ret))
{
program_exit = cb;
}
void exit_program(int ret)
{
if (program_exit)
program_exit(ret);
exit(ret);
}
它注册一个函数指针,相当于回调,在exit_program是被处理,用来做一些善后工作,比如释放资源等。
但是问题在于exit_program这个方法,他在调用完成清理方法之后,会继续调用exit来退出当前进程。这就是在实际项目中很难使用它的原因。大多数时候你并不打算在结束ffmpeg操作时候直接退出进程。