ffmpeg源码分析 (一)

    本文属于重写…………早些天码了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操作时候直接退出进程。

猜你喜欢

转载自my.oschina.net/zzxzzg/blog/1806993