前段时间在做直播的优化,主要是优化首屏时间,因为直播播放器大部分都会采用ffmpeg来处理,所以就会用到avformat_open_input这个函数,这也是首屏耗时比较多的一个地方,这里我主要跟踪一下http的请求以及rtmp的请求,源码都是开源的,这里主要是记录下来以备自己查询,本篇文章主要是是以ijkplayer源码为基础分析的。
avformat_open_input
这个函数的作用是打开文件的链接,如果是网络连接,还会发起网络请求,并一直等待网络数据的返回,然后读取视频流的数据。接下来进行详细的分析。
首先看函数的声明
int avformat_open_input(AVFormatContext **ps, const char *filename,
AVInputFormat *fmt, AVDictionary **options)
AVFormatContext **ps
该函数的主要作用是填充好AVFormatContext **ps
这个结构体。AVFormatContext
这个结构体里面的参数比较多,这里就不一一列举了,详细可以参考avformat.h
这个头文件,具体用到啥到时再详细说明。
const char *filename
文件的全部路径,比如http://flv-meipai.8686c.com/meipai-live/58e03ffd20a05d7a1410d08c.flv
AVInputFormat *fmt
AVInputFormat
的结构体也比较复杂,主要是封装媒体数据封装类型的结构体,比如flv, mpegts, mp4等。在这里可以传入空,如果为空,后面就会从网络数据中读出。当然如果我们知道文件的类型,先用av_find_input_format("flv")
初始化出对应的结构体,这里我们用的是flv,先初始化好这个结构体,对于直播来说,会比较节约时间。
AVDictionary **options
struct AVDictionary {
int count;
AVDictionaryEntry *elems;
};
typedef struct AVDictionaryEntry {
char *key;
char *value;
} AVDictionaryEntry;
字典类型的可选参数,可以向ffmpeg中传入指定的参数的值。比如我们这里传入了 av_dict_set_int(&ffp->format_opts, "fpsprobesize", 0, 0);
表示fpsprobesize
对应的参数值为0,当然还可以传入更多值,具体可以参考options_table.h
这个头文件。
avformat_open_input
的具体实现在libavformat/utils.c
文件。
第一次调用avformat_open_input
函数时,传入的ps
是属于初始化状态,很多部分可以忽略,直接跳到以下部分
if ((ret = init_input(s, filename, &tmp)) < 0)
goto fail;
init_input
函数的声明如下
/* Open input file and probe the format if necessary. */
static int init_input(AVFormatContext *s, const char *filename,
AVDictionary **options)
函数的主要功能如注释一样,打开一个文件链接,并尽可能解析出该文件的格式。它里面关键的调用是
if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ |
s->avio_flags, options)) < 0)
return ret;
io_open
函数是一个回调函数。一般情况下是采用的默认函数io_open_default
,具体的赋值是在libavformat/option.c
文件中,调用过程如下:
avformat_alloc_context->avformat_get_context_defaults->(s->io_open = io_open_default;)
其中io_open_default的函数实现
static int io_open_default(AVFormatContext *s, AVIOContext **pb,
const char *url, int flags, AVDictionary **options)
{
printf("io_open_default called\n");
#if FF_API_OLD_OPEN_CALLBACKS
FF_DISABLE_DEPRECATION_WARNINGS
if (s->open_cb)
return s->open_cb(s, pb, url, flags, &s->interrupt_callback, options);
FF_ENABLE_DEPRECATION_WARNINGS
#endif
return ffio_open_whitelist(pb, url, flags, &s->interrupt_callback, options, s->protocol_whitelist, s->protocol_blacklist);
}
这里一般都是没有定义FF_API_OLD_OPEN_CALLBACKS
宏的,所以实际是调用ffio_open_whitelist
函数。
继续跟进函数的定义和调用发现ffio_open_whitelist
的实现是在libavformat/aviobuf.c
文件中。
err = ffurl_open_whitelist(&h, filename, flags, int_cb, options,
whitelist, blacklist, NULL);
ffurl_open_whitelist
函数的功能主要是打开文件链接,并填充一个URLContext *h
结构体。该结构体的声明是在url.h
文件里面,源码里面有,不过我这里也提一下
typedef struct URLContext {
const AVClass *av_class; /**< information for av_log(). Set by url_open(). */
const struct URLProtocol *prot;
void *priv_data;
char *filename; /**< specified URL */
int flags;
int max_packet_size; /**< if non zero, the stream is packetized with this max packet size */
int is_streamed; /**< true if streamed (no seek possible), default = false */
int is_connected;
AVIOInterruptCB interrupt_callback;
int64_t rw_timeout; /**< maximum time to wait for (network) read/write operation completion, in mcs */
const char *protocol_whitelist;
const char *protocol_blacklist;
} URLContext;
这个结构体很重要,里面prot
指向了具体URLProtocol
结构体,该结构体里面包含有打开该协议的url的回调函数,如http,tcp,都有对应的open函数来处理。ok,现在我们深入到ffurl_open_whitelist
函数实现中。
该函数的实现是在avio.c
里面,它会先调用下面的函数初始化相应的结构体
int ret = ffurl_alloc(puc, filename, flags, int_cb);
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;
if (av_strstart(filename, "https:", NULL))
av_log(NULL, AV_LOG_WARNING, "https protocol not found, recompile FFmpeg with "
"openssl, gnutls "
"or securetransport enabled.\n");
return AVERROR_PROTOCOL_NOT_FOUND;
}
这个函数实现比较简单,先调用url_find_protocol
函数根据url的name,找到对应的URLProtocol
结构。这个函数一般会调用多次,http和tcp协议解析的时候都会调用,这里传入的是http://flv-meipai.8686c.com/meipai-live/58e1c0e9d425e1464018167c.flv
,所以解析出来的URLProtocol
里面应该是HTTP协议的结构体。具体再来看下什么如何初始化的。
protocols = ffurl_get_protocols(NULL, NULL);
首先通过ffurl_get_protocols
获取到已知所有的支持协议的数组。下面是当前ffmpeg支持的协议列表,在protocol_list.c
文件中定义。
static const URLProtocol *url_protocols[] = {
&ff_async_protocol,
&ff_cache_protocol,
&ff_data_protocol,
&ff_ffrtmphttp_protocol,
&ff_file_protocol,
&ff_ftp_protocol,
&ff_hls_protocol,
&ff_http_protocol,
&ff_httpproxy_protocol,
&ff_ijkhttphook_protocol,
&ff_ijklongurl_protocol,
&ff_ijkmediadatasource_protocol,
&ff_ijksegment_protocol,
&ff_ijktcphook_protocol,
&ff_ijkio_protocol,
&ff_pipe_protocol,
&ff_rtmp_protocol,
&ff_rtmpt_protocol,
&ff_tee_protocol,
&ff_tcp_protocol,
&ff_udp_protocol,
&ff_udplite_protocol,
NULL };
其中每个protocol的具体定义在每个具体的c文件中,可以搜索。我们再回到url_find_protocol
函数中,
size_t proto_len = strspn(filename, URL_SCHEME_CHARS);//先从文件名中获取协议名的长度
有了长度,就可以获取协议的名字,这里获取到的就是http
协议名。获取到协议名以后,就可以从protocols
中获取到对应的http protocol了,那么这里的url_find_protocol
函数返回的就是ff_http_protocol
结构体了,它的结构体定义在libavformat/http.c
文件中。
这时再回到ffurl_alloc
函数中,找到协议URLProtocol *p
后,然后会调用:
p = url_find_protocol(filename);
if (p)
return url_alloc_for_protocol(puc, p, filename, flags, int_cb);
接下来进入到url_alloc_for_protocol
函数中。它的主要作用是根据URLProtocol
参数初始化URLContext
结构体,具体有什么作用用到之后再细说。接下来就是
ret = ffurl_connect(*puc, options);
ffurl_connect函数
该函数中唯一一个比较重要的函数就是
err = uc->prot->url_open2 ? uc->prot->url_open2(uc,
uc->filename,
uc->flags,
options) :
uc->prot->url_open(uc, uc->filename, uc->flags);
首先判断是否prot->url_open2
函数指针是否有赋值。我们从http.c
中ff_http_protocol
结构体的定义中可以发现
.url_open2 = http_open,
所以在ffurl_connect
函数中这里实际调用的是http_open
函数。那么接下来就进入到我们的关键函数了。
http_open函数
http协议的基本实现都是在http.c
文件中实现的。首先我们看第5行代码:
s->app_ctx = (AVApplicationContext *)(intptr_t)s->app_ctx_intptr;
它的功能是赋值AVApplicationContext
类型的指针,它主要是用于发送消息给应用层的。但问题是app_ctx_intptr
具体是在哪里赋值的呢。通过查找相关代码发现
av_dict_set_int(options, "ijkapplication", (int64_t)(intptr_t)s->app_ctx, 0);
它是通过从options
里面查找"ijkapplication"相关的int
类型的值作为指针赋值。
再从ijkplayer
的上层源代码中可以找到
ffp_set_option_int(ffp, FFP_OPT_CATEGORY_FORMAT, "ijkapplication", (int64_t)(intptr_t)ffp->app_ctx);
可以看出app_ctx
是在上层创建的。
av_application_open(&ffp->app_ctx, ffp);
该函数就是创建一个AVApplicationContext
类型的结构体并赋值给ffp->app_ctx
。
好了,回到http_open
函数,接下来是http_open_cnx
函数,它主要也是调用http_open_cnx_internal
函数。
http_open_cnx_internal函数
首先调用的是:
av_url_split(proto, sizeof(proto), auth, sizeof(auth),
hostname, sizeof(hostname), &port,
path1, sizeof(path1), s->location);
ff_url_join(hoststr, sizeof(hoststr), NULL, NULL, hostname, port, NULL);
它们的功能是根据传入的s->location
其实就是视频的url,从url里面提取出hostname
,port
,以及path
。
同时ff_url_join
解析出host地址,如果不是ipv6,那么hoststr
就是hostname
。然后还有
use_proxy = !ff_http_match_no_proxy(getenv("no_proxy"), hostname) &&
proxy_path && av_strstart(proxy_path, "http://", NULL);
由于我们的http链接,基本都没用到proxy
,所以use_proxy
用不上。接下来是
ff_url_join(buf, sizeof(buf), lower_proto, NULL, hostname, port, NULL);
ff_url_join
前面用到的时候是解析出hostname
,但在这里,由于传入了lower_proto
(它表示http 协议的下一层协议,一般都是tcp
,所以该值初始化的时候就是tcp
),所以buf的值是有lower_proto
拼凑起来的tcp链接tcp://flv-meipai.8686c.com:80
,就是tcp+域名。接下来就是
if (!s->hd) {
av_dict_set_int(options, "ijkapplication", (int64_t)(intptr_t)s->app_ctx, 0);
err = ffurl_open_whitelist(&s->hd, buf, AVIO_FLAG_READ_WRITE,
&h->interrupt_callback, options,
h->protocol_whitelist, h->protocol_blacklist, h);
if (err < 0)
return err;
}
首先判断s->hd
是否存在。默认情况下,是为NULL
值,所以调用ffurl_open_whitelist
开始打开tcp://flv-meipai.8686c.com:80
,现在回到ffurl_open_whitelist
函数了。这时通过url_find_protocol
找到的就是tcp
类型的URLProtocol
,那么在ffurl_connect
调用时调用的就是tcp.c
里面的tcp_open
函数,那么tcp的握手连接就在这个函数里面解析了。首先
av_url_split(proto, sizeof(proto), NULL, 0, hostname, sizeof(hostname),
&port, path, sizeof(path), uri);
先根据uri解析出协议名以及hostname,然后调用以下的
ret = ijk_tcp_getaddrinfo_nonblock(hostname, portstr, &hints, &ai, s->addrinfo_timeout, &h->interrupt_callback, s->addrinfo_one_by_one);
做DNS解析。这个函数是ijkplayer
作者加上去的,标准的ffmpeg 里面并没有。它的功能是利用多线程来解析DNS。但实际上从代码上并没有看到有什么优势,其实还是阻塞等结果解析出来了才返回的,这个地方不是很懂为什么要这么改。
接下来就是创建socket
了。
fd = ff_socket(cur_ai->ai_family, cur_ai->ai_socktype, cur_ai->ai_protocol);
调用ff_listen_connect
函数进行tcp握手。
至此,调用tcp协议的ffurl_open_whitelist
函数就调用完成了,tcp握手连接也建立成功。再回到http_open
函数。继续调用
err = http_connect(h, path, local_path, hoststr, auth, proxyauth, &location_changed);
该函数的功能就是发送http request
并等待http response
了。我们细看下http_connect
函数的实现,在调用ffurl_write
以前的代码都是在填充http request
的头部。
int ffurl_write(URLContext *h, const unsigned char *buf, int size)
{
if (!(h->flags & AVIO_FLAG_WRITE))
return AVERROR(EIO);
/* avoid sending too big packets */
if (h->max_packet_size && size > h->max_packet_size)
return AVERROR(EIO);
return retry_transfer_wrapper(h, (unsigned char *)buf, size, size,
(int (*)(struct URLContext *, uint8_t *, int))
h->prot->url_write);
}
从ffurl_write
代码中可以看出,它实际调用的是url_write
方法,而该prot的write方法,是http_write
,它又是调用的ffurl_write(s->hd, buf, size);
就是指http协议下一层的协议tcp的tcp_write
方法。tcp_write
方法最终调用就是ret = send(s->fd, buf, size, MSG_NOSIGNAL);
系统的send方法。所以最终都是调用系统实现的Socket
接口。至此,http_connect
方法的发送request的请求就完毕了。剩下就是等待响应了。
/* wait for header */
err = http_read_header(h, new_location);
http_read_header
就是不断的读取网络返回的数据,并解析出来。
至此http_open_cnx_internal
函数也调用完了。回到http_open_cnx
函数。这时如果能正常获取数据,那么s->http_code
的值应该是200,至此,http_open_cnx
,http_open
函数也返回了,流程可以直接返回到ffio_open_whitelist
函数中,ffio_fdopen
函数只是对AVIOContext
结构体根据http request
获取的数据进行一些赋值。那就可以直接返回到init_input
函数了。接下来是
if (s->iformat)
return 0;
return av_probe_input_buffer2(s->pb, &s->iformat, filename,
s, 0, s->format_probesize);
判断如果s->iformat
没有值,就根据filename
解析出s->iformat
。这也是在前面开头提到的,如果没有加av_find_input_format("flv")
这个代码,那就要重新根据filename
来解析数据了,这个函数比较耗时,需要读取到一定数据后才能解析出来。
至此,init_input
函数解析完毕,虽然还有大量的细节没有解析,后面有机会继续再细讲。
avformat_open_input
函数最耗时,最重要的就是init_input
函数了,后面的都是些其他细节了,这里就不再细讲了。