Avformat_open_input函数的分析之--HTTP篇

前段时间在做直播的优化,主要是优化首屏时间,因为直播播放器大部分都会采用ffmpeg来处理,所以就会用到avformat_open_input这个函数,这也是首屏耗时比较多的一个地方,这里我主要跟踪一下http的请求以及rtmp的请求,源码都是开源的,这里主要是记录下来以备自己查询,本篇文章主要是是以ijkplayer源码为基础分析的。

avformat_open_input这个函数的作用是打开文件的链接,如果是网络连接,还会发起网络请求,并一直等待网络数据的返回,然后读取视频流的数据。接下来进行详细的分析。

1.接口参数的解析

首先看函数的声明

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这个头文件。

2.函数的关键函数实现

avformat_open_input的具体实现在libavformat/utils.c文件。

init_input函数

第一次调用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函数

继续跟进函数的定义和调用发现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函数实现中。

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.cff_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里面提取出hostnameport,以及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_cnxhttp_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函数了,后面的都是些其他细节了,这里就不再细讲了。

多谢支持!!!

你可能感兴趣的:(Avformat_open_input函数的分析之--HTTP篇)