文章

剖析 FFmpeg 确定协议并与 AVIO 绑定详细流程

剖析 FFmpeg 确定协议并与 AVIO 绑定详细流程

AVIO 的设计极其精妙,通过 URLProtocol → URLContext → AVIOContext 三层抽象,把“各种协议”统一成“标准 IO 流”。它实现了一套抽象的字节流操作层,使得上层逻辑(如 MP4 解析器)无需关心底层是本地文件、内存缓存还是网络协议(RTMP/HTTP)。

总揽

在 FFmpeg 中,从输入 URL 到最终得到 AVIOContext,核心流程可以抽象为三步:

  1. 解析输入并确定协议(URL → URLProtocol)
  2. 创建 URLContext(协议实例)
  3. 创建 AVIOContext 并绑定 URLContext(统一 IO 抽象)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
avformat_open_input
        │
        ▼
    init_input
        │
        ▼
   io_open_default
        │
        ▼
ffio_open_whitelist
        │
        ├─────────────────┐
        ▼                 ▼
ffurl_open_whitelist  ffio_fdopen
        │                 │
        ▼                 ▼
  ffurl_alloc2     avio_alloc_context
        │                 │
        ▼                 ▼
  ffurl_alloc    将URLContext和avio绑定
        │
        ▼
url_find_protocol
        │
        ▼
url_alloc_for_protocol
        │
        ▼
   ffurl_connect

FFmpeg 是如何确定协议的?

当通过 err = avformat_open_input(&ic, "https://xx.yy.com/xp5.mp4", is->iformat, &ffp->format_opts); 指定一个地址之后,FFmpeg 是如何找到对应的 URLProtocol(如 file, http, rtmp 等)的呢?

这是一个典型的字符串解析 + 静态/动态注册表查询的过程,URLProtocol 使用 C 结构体和函数指针实现了面向对象的“多态”,使得 FFmpeg 能够支持很多协议,我曾经修改源码很轻松支持了 smb2 协议足以证明 FFmpeg 的设计很好。同时在 config 时也可以启用或者禁用指定协议。ijkplayer 还搞了 fake 协议,在运行时动态替换实现动态加载协议。

avformat_open_input 入口函数

avformat_open_input 是所有输入的统一入口,它负责分配 AVFormatContext 对象,保存 URL,并调用 init_input 开始真正的 IO 初始化流程。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int avformat_open_input(AVFormatContext **ps, const char *filename,
                        const AVInputFormat *fmt, AVDictionary **options)
{
    AVFormatContext *s = *ps;
    FFFormatContext *si;
    AVDictionary *tmp = NULL;
    AVDictionary *tmp2 = NULL;
    ID3v2ExtraMeta *id3v2_extra_meta = NULL;
    int ret = 0;

    if (!s && !(s = avformat_alloc_context()))
        return AVERROR(ENOMEM);
    si = ffformatcontext(s);
    if (!s->av_class) {
        av_log(NULL, AV_LOG_ERROR, "Input context has not been properly allocated by avformat_alloc_context() and is not NULL either\n");
        return AVERROR(EINVAL);
    }
    if (fmt)
        s->iformat = fmt;

    if (options)
        av_dict_copy(&tmp, *options, 0);

    if (s->pb) // must be before any goto fail
        s->flags |= AVFMT_FLAG_CUSTOM_IO;

    if ((ret = av_opt_set_dict(s, &tmp)) < 0)
        goto fail;

    if (!(s->url = av_strdup(filename ? filename : ""))) {
        ret = AVERROR(ENOMEM);
        goto fail;
    }

    if ((ret = init_input(s, filename, &tmp)) < 0)
        goto fail;
    s->probe_score = ret;
......
}

如果用户自定义 AVIO(s->pb 已存在),则不往下走 URL 打开流程,直接用已有 IO,常见于实现内存流、自定义网络层等情况。

否则进入默认 IO 打开流程,调用 init_input 进行初始化,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static int init_input(AVFormatContext *s, const char *filename,
                      AVDictionary **options)
{
    int ret;
    AVProbeData pd = { filename, NULL, 0 };
    int score = AVPROBE_SCORE_RETRY;

    if (s->pb) {
        s->flags |= AVFMT_FLAG_CUSTOM_IO;
        if (!s->iformat)
            return av_probe_input_buffer2(s->pb, &s->iformat, filename,
                                          s, 0, s->format_probesize);
        else if (s->iformat->flags & AVFMT_NOFILE)
            av_log(s, AV_LOG_WARNING, "Custom AVIOContext makes no sense and "
                                      "will be ignored with AVFMT_NOFILE format.\n");
        return 0;
    }

    if ((s->iformat && s->iformat->flags & AVFMT_NOFILE) ||
        (!s->iformat && (s->iformat = av_probe_input_format2(&pd, 0, &score))))
        return score;

    if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)
        return ret;

    if (s->iformat)
        return 0;
    return av_probe_input_buffer2(s->pb, &s->iformat, filename,
                                  s, 0, s->format_probesize);
}

init_input 函数里会调用 s 的 io_open 函数,它是创建 AVFormatContext 的时候赋值的:

1
2
3
4
5
6
7
8
9
10
11
12
13
AVFormatContext *avformat_alloc_context(void)
{
    FFFormatContext *const si = av_mallocz(sizeof(*si));
    AVFormatContext *s;

    if (!si)
        return NULL;

    s = &si->pub;
    s->av_class = &av_format_context_class;
    s->io_open  = io_open_default;
......
}

所以调用 io_open 实际上是调用 io_open_default 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int io_open_default(AVFormatContext *s, AVIOContext **pb,
                           const char *url, int flags, AVDictionary **options)
{
    int loglevel;

    if (!strcmp(url, s->url) ||
        s->iformat && !strcmp(s->iformat->name, "image2") ||
        s->oformat && !strcmp(s->oformat->name, "image2")
    ) {
        loglevel = AV_LOG_DEBUG;
    } else
        loglevel = AV_LOG_INFO;

    av_log(s, loglevel, "Opening \'%s\' for %s\n", url, flags & AVIO_FLAG_WRITE ? "writing" : "reading");

    return ffio_open_whitelist(pb, url, flags, &s->interrupt_callback, options, s->protocol_whitelist, s->protocol_blacklist);
}

ffio_open_whitelist 分水岭

进一步调用 ffio_open_whitelist 函数,这是 AVFormat 层和 URL 层的分界点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int ffio_open_whitelist(AVIOContext **s, const char *filename, int flags,
                         const AVIOInterruptCB *int_cb, AVDictionary **options,
                         const char *whitelist, const char *blacklist
                        )
{
    URLContext *h;
    int err;

    *s = NULL;

    err = ffurl_open_whitelist(&h, filename, flags, int_cb, options, whitelist, blacklist, NULL);
    if (err < 0)
        return err;
    err = ffio_fdopen(s, h);
    if (err < 0) {
        ffurl_close(h);
        return err;
    }
    return 0;
}

下面是 ffurl_open_whitelist 的源码,核心工作是:

  1. 调用 ffurl_alloc2 创建 URLContext
  2. 设置 options / whitelist / blacklist
  3. 调用 ffurl_connect 函数,建立连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
int ffurl_open_whitelist(URLContext **puc, const char *filename, int flags,
                         const AVIOInterruptCB *int_cb, AVDictionary **options,
                         const char *whitelist, const char* blacklist,
                         URLContext *parent)
{
    AVDictionary *tmp_opts = NULL;
    AVDictionaryEntry *e;
    int ret = ffurl_alloc2(puc, filename, flags, int_cb, options);
    if (ret < 0)
        return ret;
    if (parent) {
        ret = av_opt_copy(*puc, parent);
        if (ret < 0)
            goto fail;
    }
    if (options &&
        (ret = av_opt_set_dict(*puc, options)) < 0)
        goto fail;
    if (options && (*puc)->prot->priv_data_class &&
        (ret = av_opt_set_dict((*puc)->priv_data, options)) < 0)
        goto fail;

    if (!options)
        options = &tmp_opts;

    av_assert0(!whitelist ||
               !(e=av_dict_get(*options, "protocol_whitelist", NULL, 0)) ||
               !strcmp(whitelist, e->value));
    av_assert0(!blacklist ||
               !(e=av_dict_get(*options, "protocol_blacklist", NULL, 0)) ||
               !strcmp(blacklist, e->value));

    if ((ret = av_dict_set(options, "protocol_whitelist", whitelist, 0)) < 0)
        goto fail;

    if ((ret = av_dict_set(options, "protocol_blacklist", blacklist, 0)) < 0)
        goto fail;

    if ((ret = av_opt_set_dict(*puc, options)) < 0)
        goto fail;

    ret = ffurl_connect(*puc, options);

    if (!ret)
        return 0;
fail:
    ffurl_closep(puc);
    return ret;
}

selected_http 指定 http 协议实现

ffurl_alloc2 定制了一个协议选择的逻辑,允许用户通过 options 传入一个 selected_http 的选项来指定使用哪个 http 实现协议。 代理里可以有多套 http 实现,比如 ijkhttp1、ijkhttp2、ijkhttp3 等,用户可以通过 selected_http 来选择使用哪套 http 实现协议来发送请求。 如果用户没有指定 selected_http,或者指定的协议不合法,那么就会调用 ffurl_alloc 来按照默认的协议查找逻辑来查找协议并创建 URLContext 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int ffurl_alloc2(URLContext **puc, const char *filename, int flags,
    const AVIOInterruptCB *int_cb, AVDictionary **options)
{
    if (options && *options) {
        AVDictionaryEntry *e = av_dict_get(*options, "selected_http", NULL, 0);
        const char *selected_http;
        if (e && (selected_http = e->value)) {

            char proto_str[128] = {0};
            size_t proto_len = strspn(filename, URL_SCHEME_CHARS);
            if (filename[proto_len] != ':' &&
                (strncmp(filename, "subfile,", 8) || !strchr(filename + proto_len + 1, ':')) ||
                is_dos_path(filename))
                strcpy(proto_str, "file");
            else
                av_strlcpy(proto_str, filename,
                            FFMIN(proto_len + 1, sizeof(proto_str)));
            //only apply http protocol
            if (!strcmp(proto_str, "http") || !strcmp(proto_str, "https")) {
                if (!strcmp(selected_http, "ijkhttp1") || !strcmp(selected_http, "ijkhttp2") || !strcmp(selected_http, "ijkhttp3")) {
                    const URLProtocol *p = url_find_the_protocol(selected_http);
                    if (p) {
                        av_log(NULL, AV_LOG_DEBUG, "%s use %s send request\n",proto_str,selected_http);
                        return url_alloc_for_protocol(puc, p, filename, flags, int_cb);
                    }
                    *puc = NULL;
                    av_log(NULL, AV_LOG_ERROR, "some thing is fault,check %s protocol\n", selected_http);
                    return AVERROR_PROTOCOL_NOT_FOUND;
                } else {
                    av_log(NULL, AV_LOG_ERROR, "invalid selected_http value: %s\n", selected_http);
                    av_assert0(0);
                    return AVERROR_PROTOCOL_NOT_FOUND;
                }
            } else {
                av_log(NULL, AV_LOG_DEBUG, "%s not use %s\n",proto_str,selected_http);
            }
        }
    }

    return ffurl_alloc(puc, filename, flags, int_cb);
}

接下来根据协议的 scheme 进行查找:

1
2
3
4
5
6
7
8
9
10
11
12
int ffurl_alloc(URLContext **puc, const char *filename, int flags,
                const AVIOInterruptCB *int_cb)
{
    const URLProtocol *p = NULL;

    p = url_find_protocol(filename);
    if (p)
       return url_alloc_for_protocol(puc, p, filename, flags, int_cb);

    *puc = NULL;
    return AVERROR_PROTOCOL_NOT_FOUND;
}

查找确定协议过程:

  1. 解析协议名,比如 “http://example.com/file” 解析出协议名 “http”。
  2. 遍历 FFmpeg 内置的协议列表(ffurl_get_protocols),找到匹配的协议。
  3. 如果协议支持嵌套(URL_PROTOCOL_FLAG_NESTED_SCHEME),还会检查嵌套协议名(比如 “http+cache” 中的 “http”)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
static const struct URLProtocol *url_find_protocol(const char *filename)
{
    const URLProtocol **protocols;
    char proto_str[128], proto_nested[128], *ptr;
    size_t proto_len = strspn(filename, URL_SCHEME_CHARS);
    int i;

    if (filename[proto_len] != ':' &&
        (strncmp(filename, "subfile,", 8) || !strchr(filename + proto_len + 1, ':')) ||
        is_dos_path(filename))
        strcpy(proto_str, "file");
    else
        av_strlcpy(proto_str, filename,
                   FFMIN(proto_len + 1, sizeof(proto_str)));

    av_strlcpy(proto_nested, proto_str, sizeof(proto_nested));
    if ((ptr = strchr(proto_nested, '+')))
        *ptr = '\0';

    protocols = ffurl_get_protocols(NULL, NULL);
    if (!protocols)
        return NULL;
    for (i = 0; protocols[i]; i++) {
            const URLProtocol *up = protocols[i];
        if (!strcmp(proto_str, up->name)) {
            av_freep(&protocols);
            return up;
        }
        if (up->flags & URL_PROTOCOL_FLAG_NESTED_SCHEME &&
            !strcmp(proto_nested, up->name)) {
            av_freep(&protocols);
            return up;
        }
    }
    av_freep(&protocols);
    if (av_strstart(filename, "https:", NULL) || av_strstart(filename, "tls:", NULL))
        av_log(NULL, AV_LOG_WARNING, "https protocol not found, recompile FFmpeg with "
                                     "openssl, gnutls or securetransport enabled.\n");

    return NULL;
}

url_alloc_for_protocol 创建协议实现

url_alloc_for_protocol 的逻辑会在后续的博客展开介绍,这里简单提下,URLContext 是一个胶水层,它使得 avio 能够使用任何 URLProtocol 实现。

创建好了 URLContext 对象后,我们是视线拉回到 ffio_open_whitelist 函数,接着调用的 ffio_fdopen 函数十分关键, 目的是创建 AVIOContext 对象,并将 URLContext 对象的相关函数指针赋值给 avio,这样后续对 avio 的操作就会调用 URLContext 里对应的函数了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
int ffio_fdopen(AVIOContext **sp, URLContext *h)
{
    AVIOContext *s;
    uint8_t *buffer = NULL;
    int buffer_size, max_packet_size;

    max_packet_size = h->max_packet_size;
    if (max_packet_size) {
        buffer_size = max_packet_size; /* no need to bufferize more than one packet */
    } else {
        buffer_size = IO_BUFFER_SIZE;
    }
    if (!(h->flags & AVIO_FLAG_WRITE) && h->is_streamed) {
        if (buffer_size > INT_MAX/2)
            return AVERROR(EINVAL);
        buffer_size *= 2;
    }
    buffer = av_malloc(buffer_size);
    if (!buffer)
        return AVERROR(ENOMEM);

    *sp = avio_alloc_context(buffer, buffer_size, h->flags & AVIO_FLAG_WRITE, h,
                             ffurl_read2, ffurl_write2, ffurl_seek2);
    if (!*sp) {
        av_freep(&buffer);
        return AVERROR(ENOMEM);
    }
    s = *sp;
    if (h->protocol_whitelist) {
        s->protocol_whitelist = av_strdup(h->protocol_whitelist);
        if (!s->protocol_whitelist) {
            avio_closep(sp);
            return AVERROR(ENOMEM);
        }
    }
    if (h->protocol_blacklist) {
        s->protocol_blacklist = av_strdup(h->protocol_blacklist);
        if (!s->protocol_blacklist) {
            avio_closep(sp);
            return AVERROR(ENOMEM);
        }
    }
    s->direct = h->flags & AVIO_FLAG_DIRECT;

    s->seekable = h->is_streamed ? 0 : AVIO_SEEKABLE_NORMAL;
    s->max_packet_size = max_packet_size;
    s->min_packet_size = h->min_packet_size;
    if(h->prot) {
        s->read_pause = h->prot->url_read_pause;
        s->read_seek  = h->prot->url_read_seek;

        if (h->prot->url_read_seek)
            s->seekable |= AVIO_SEEKABLE_TIME;
    }
    ((FFIOContext*)s)->short_seek_get = ffurl_get_short_seek;
    s->av_class = &ff_avio_class;
    return 0;
}

绑定之后的效果是:

1
2
3
avio_read
  → ffurl_read2
    → URLContext->prot->url_read

总结

  1. 协议完全插件化,新增协议只需要实现 URLProtocol
  2. IO 与协议完全解耦,AVIO 统一了读写接口,URLProtocol定义了具体实现,URLContext 充当润滑剂
  3. 支持嵌套协议 crypto+http:// 或者 cache:http:// ...
  4. 自定义 selected_http 逻辑使得网络层可插拔,允许自研 QUIC 等替换 HTTP 实现
本文由作者按照 CC BY 4.0 进行授权