搜索
您的当前位置:首页正文

Avformat_open_input函数的分析之--HTTP篇

来源:二三娱乐

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

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

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;
}
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);//先从文件名中获取协议名的长度

这时再回到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);
.url_open2           = http_open,

http_open函数

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_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);
ff_url_join(buf, sizeof(buf), lower_proto, NULL, hostname, port, NULL);
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;
}

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握手。

err = http_connect(h, path, local_path, hoststr, auth, proxyauth, &location_changed);
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);
}
/* wait for header */
err = http_read_header(h, new_location);
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函数了,后面的都是些其他细节了,这里就不再细讲了。

多谢支持!!!

Top