从学龄前开始解读FFMPEG代码 之 av_register_all函数

开始FFMPEG学龄前知识学习之前想说的一些话

自从接触到视频编解码这个领域开始,就想着花一些时间去好好解读一下FFMPEG的源码。网上固然已经有很多已存在的解读文章,更有雷小花等前辈的文章珠玉在前,但有些东西往往不自己去解读并写下一些记录的时候,是学不好的。就像Q哥说的“光看代码没用,你要写编码器你就得亲自动手写起来”。那就从学龄前水平开始学习一下FFMPEG吧。再多一句嘴,由于一些网上的解读文章是在是有点老,所以这些文章打算基于FFMPEG 4.2.x的版本(也是我用的比较多的版本)去解答。源码是我在github上直接clone并checkout到了4.2的版本。

从第一个函数开始之av_register_all()

该函数是在libavformat/allformates.c中定义,用于初始化libavformat并且注册所有的封装器,解封装器以及协议。在旧版(3.x早期)的ffmpeg中该函数几乎是所有应用的开始。使用版本4.x的FFMPEG很多时候其实并不会直接调用这个函数对封装和解封装器进行注册,该函数也被声明为deprecated了。但这并不妨碍学龄前去阅读学习这个函数。因为确实有很多值得研究的地方,也有很多其他ffmpeg函数中会用到的接口和思路。
多提一嘴,何为解封装:
如果想要将常见的视频源数据转化到电视屏幕或者电脑上可以让肉眼看见的图片、视频帧,需要经过以下的步骤
视频源(RTSP,RTMP,HTTP,普通文件等) -> [经过解协议] ->视频文件或视频流(FLV,MKV,AVI等)-> [经过解封装] ->视频数据(H264、HEVC视频数据)和音频数据(MP3,WMA等)-> [视频数据经过解码] ->YUV视频帧,最后YUV视频帧就可以进行展示和播放并被人眼所捕捉到了。

回到av_register_all(),函数的代码内容很简单,只有如下的几行

void av_register_all(void) {
    ff_thread_once(&av_format_next_init, av_format_init_next);
}

很短的函数包含三个内容,ff_thread_once(一个函数),av_format_next_init(一个宏定义的变量) 以及 av_format_init_next(一个函数)。
这三行代码的实际含义是使用了函数ff_thread_once函数来执行函数av_format_init_next()函数,且第一个参数为宏定义变量av_format_next_init。
值得注意的是,在该函数下面有两个函数:

void av_register_input_format(AVInputFormat *format)
{
    ff_thread_once(&av_format_next_init, av_format_init_next);
}

void av_register_output_format(AVOutputFormat *format)
{
    ff_thread_once(&av_format_next_init, av_format_init_next);
}

函数内容,也就是他们所做的事情是一模一样的,只不过名字不同,也就是注册inputformat和outputformat函数跟register_all所做的事情是完全一致的。

ff_thread_once()

ff_thread_once()是定义在libavutil/thread.h的用于线程安全的函数。在thread.h文件里,在拥有os2threads.h库时,会通过以下define指向pthread_once函数:

#define ff_thread_once(control, routine) pthread_once(control, routine)

在所有情况以外,它创建了一个自己的函数:

static inline int ff_thread_once(char *control, void (*routine)(void)) {
    if (!*control) {
        routine();
        *control = 1;
    }
    return 0;
} //这个函数也很简单就能看出是通过一个control来保证routine函数只会执行一次的。

所以实际上的pthread_once():thread.h中根据平台不同引入不同的线程库,删减掉不必要的代码,引入库的顺序如下代码所示:发现优先使用pthread.h库->其次os2threads.h库->再次w32pthreads.h。pthread_once在不同的平台下映射到不同线程库中的pthread_once函数。

所以总体来说,这个函数的主要功能就是保证传入的函数在多线程的环境下也只会被执行一次。根据平台的不同使用了不同的进程函数。

av_format_next_init

该变量的定义在allformat.c文件内部:

static AVOnce av_format_next_init = AV_ONCE_INIT;

AVOnce也是定义在thread.h内的

#define AVOnce pthread_once_t

在thread.h内,不同的环境下,AVOnce也拥有着不同的内容

#define AVOnce pthread_once_t
#define AVOnce char

该变量也是用于保证传入的函数是在多线程情况下只会执行一次的,对于不同的环境内,AVOnce采取了不同的变量来实现这个功能。

av_format_init_next()

对ff_thread_once()传入的函数为av_formate_init_next(),该函数内容如下:

static void av_format_init_next(void)
{
    AVOutputFormat *prevout = NULL, *out;
    AVInputFormat *previn = NULL, *in;

    ff_mutex_lock(&avpriv_register_devices_mutex);


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

    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_list中的输出封装结构AVOutputFormat串起来形成单向链表,当outdev_list不为空的时候,将outdev_list中的AVOutputFormat也一并串在后头。同理将demuxer_list中的输入封装结构体AVInputFormat挨个穿起来形成单向链表,同时在indev_list不为空的时候,将indev_list中的AVInputFormat也一并串起来挂在后头。

在代码进行之前使用了ff_mutex_lock()函数,是为了保证函数并发安全,上并发锁。传入参数avpriv_register_devices_mutex的定义是

static AVMutex avpriv_register_devices_mutex = AV_MUTEX_INITIALIZER;

可以追查其定义为 #define AVMutex pthread_mutex_t,定义在libavutil/thread.h中,与ptread_once函数一样,对于不同的平台使用不同的线程库,映射到相应的互斥锁变量中。

关于muxer_list.c和demuxer_list.c

函数中使用的两个list,即mutex_list和demutex_list两个静态数组的定义在libavformat/muxer_list.c源文件和libavformat/demuxer_list.c源文件中。具体内容如下所示:

static const AVOutputFormat *muxer_list[] = {
&ff_a64_muxer,
......
&ff_yuv4mpegpipe_muxer,
NULL };

static const AVInputFormat *demuxer_list[] = {
&ff_aa_demuxer,
......
&ff_libmodplug_demuxer,
NULL };

值得注意的是,这里的两个list源文件,libavformat/muxer_list.c和libavformat/demuxer_list.c是在下载下来的源代码中看不到的,只有执行了FFMPEG根目录的configure文件才能够生成这两个list文件。这样做的目的是为了在用户自行往ffmpeg中加入了封装和解封装器的时候,这样做可以让用户的封装/解封装器一并被写入到list中

outdev_list 和 indev_list以及跟avdevice_register_all()的关系

outdev_list && indev_list的定义同样在allformat.c文件当中,如下所示:

static const AVInputFormat * const *indev_list = NULL;
static const AVOutputFormat * const *outdev_list = NULL;

这两个指针初始都为NULL,怎么能够挂载其他的list在上面的呢?
答案其实在allformat.c文件中。该代码中有一个函数avpriv_register_devices(),是供ffmpeg的其他注册函数进行调用的。查找其声明的头文件,为libavformat/internal.h

void avpriv_register_devices(const AVOutputFormat * const o[], const AVInputFormat * const i[]) {
    ff_mutex_lock(&avpriv_register_devices_mutex);
    outdev_list = o;
    indev_list = i;
    ff_mutex_unlock(&avpriv_register_devices_mutex);
#if FF_API_NEXT
    av_format_init_next();
#endif
}

所以这两个表实际上是由其他的函数来调用进行初始化的,调用该方法的函数为libavdevice/alldevice.c的avdevice_register_all()函数,也是经常会使用到的初始注册函数。

void avdevice_register_all(void)
{
    avpriv_register_devices(outdev_list, indev_list);
}

传入的两个list参数,outdev_list定义在libavdevice/outdev_list.c,indev_list定义在libavdevice/indev_list.c中,list中也都是AVInputFormat和AVOutputFormat对象

static const AVInputFormat * const indev_list[] = {
    &ff_alsa_demuxer,
    &ff_fbdev_demuxer,
    &ff_lavfi_demuxer,
    &ff_oss_demuxer,
    &ff_sndio_demuxer,
    &ff_v4l2_demuxer,
    &ff_xcbgrab_demuxer,
    NULL };

static const AVOutputFormat * const outdev_list[] = {
    &ff_alsa_muxer,
    &ff_fbdev_muxer,
    &ff_oss_muxer,
    &ff_sdl2_muxer,
    &ff_sndio_muxer,
    &ff_v4l2_muxer,
    &ff_xv_muxer,
    NULL };

至此实际上av_register_all()函数的初步入口我们也得到了了解,很神奇的是,在阅读源码追根溯源的过程中,连avdevice_register_all()所做的一些事情也顺带着被学龄前吸收了。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值