剖析 FFmpeg 确定协议并与 AVIO 绑定详细流程
AVIO 的设计极其精妙,通过 URLProtocol → URLContext → AVIOContext 三层抽象,把“各种协议”统一成“标准 IO 流”。它实现了一套抽象的字节流操作层,使得上层逻辑(如 MP4 解析器)无需关心底层是本地文件、内存缓存还是网络协议(RTMP/HTTP)。
总揽
在 FFmpeg 中,从输入 URL 到最终得到 AVIOContext,核心流程可以抽象为三步:
- 解析输入并确定协议(URL → URLProtocol)
- 创建 URLContext(协议实例)
- 创建 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 的源码,核心工作是:
- 调用 ffurl_alloc2 创建 URLContext
- 设置 options / whitelist / blacklist
- 调用 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;
}
查找确定协议过程:
- 解析协议名,比如 “http://example.com/file” 解析出协议名 “http”。
- 遍历 FFmpeg 内置的协议列表(ffurl_get_protocols),找到匹配的协议。
- 如果协议支持嵌套(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
总结
- 协议完全插件化,新增协议只需要实现 URLProtocol
- IO 与协议完全解耦,AVIO 统一了读写接口,URLProtocol定义了具体实现,URLContext 充当润滑剂
- 支持嵌套协议
crypto+http:// 或者 cache:http:// ... - 自定义 selected_http 逻辑使得网络层可插拔,允许自研 QUIC 等替换 HTTP 实现