io_open
承接上一篇,对于avformat_open_input的分析还差其中非常重要的一步,就是io_open,该函数用于打开FFmpeg的输入输出文件。
在init_input中有这么一句
if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)
return ret;
调用了AVFormatContext的成员函数指针io_open,定义如下
/**
* A callback for opening new IO streams.
*
* Whenever a muxer or a demuxer needs to open an IO stream (typically from
* avformat_open_input() for demuxers, but for certain formats can happen at
* other times as well), it will call this callback to obtain an IO context.
*
* @param s the format context
* @param pb on success, the newly opened IO context should be returned here
* 如果调用成功会返回一个AVIOContext,就填入这里。
* @param url the url to open 需要打开的url
* @param flags a combination of AVIO_FLAG_*
* 打开地址的方式。可以选择只读,只写,或者读写。取值如下。
* AVIO_FLAG_READ:只读。
* AVIO_FLAG_WRITE:只写。
* AVIO_FLAG_READ_WRITE:读写。
* @param options a dictionary of additional options, with the same
* semantics as in avio_open2()
* @return 0 on success, a negative AVERROR code on failure
*
* @note Certain muxers and demuxers do nesting, i.e. they open one or more
* additional internal format contexts. Thus the AVFormatContext pointer
* passed to this callback may be different from the one facing the caller.
* It will, however, have the same 'opaque' field.
*/
int (*io_open)(struct AVFormatContext *s, AVIOContext **pb, const char *url,
int flags, AVDictionary **options);
该值会在初始化AVFormatContext的时候被赋值:
static void avformat_get_context_defaults(AVFormatContext *s)
{
......
s->io_open = io_open_default;
s->io_close = io_close_default;
.....
}
io_open_default最终会调用 ffio_open_whitelist方法。实际上以前版本很重要的avio_open2现在也是直接调用该方法而已。
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;
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;
}
其中whitelist和blacklist分别是白名单和黑名单,在这个过程下,值是从AVFormatContext中 s->protocol_whitelist, s->protocol_blacklist 读取的,如果用户没有设置,那么就是空。
在进一步分析之前,需要了解两个结构体,一个是URLContext,还有一个是URLProtocol。具体内容可以参考 https://my.oschina.net/zzxzzg/blog/1806996 。
ffurl_open_whitelist
ffio_open_whitelist的第一步就是调用ffurl_open_whitelist,该方法主要用于初始化URLContext。
ffurl_alloc
ffio_open_whitelist进一步会调用ffurl_alloc
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_strstart(filename, "tls:", 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;
}
又到了“原来如此”环节,ffmpeg中一个神奇的地方就在于自动找到对应的muxer,codec..等,这里就是根据filename去寻找相应的URLProtocol。
url_find_protocol
使用文件名寻找对应的URLProtocol.
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;
//格式化filename,如果是文件路径,就会变成file:开头的类型。
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';
//获取所有protocol类型数组
protocols = ffurl_get_protocols(NULL, NULL);
if (!protocols)
return NULL;
//根据该protocol定义的name和flag对和当前的文件名进行匹配,如果匹配成功,那么直接返回该protocol
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);
return NULL;
}
url_alloc_for_protocol
回到ffurl_alloc,在找到URLProtocol之后,就会调用url_alloc_for_protocol方法对URLContext进行初始化,首先,检查输入的URLProtocol是否支持指定的flag。比如flag中如果指定了AVIO_FLAG_READ,则URLProtocol中必须包含url_read();如果指定了AVIO_FLAG_WRITE,则URLProtocol中必须包含url_write()。在检查无误之后,接着就可以调用av_mallocz()为即将创建的URLContext分配内存了。接下来基本上就是各种赋值工作。
至此,我们已经初始化了URLContext,接下去ffurl_open_whitelist会继续调用方法ffurl_connect
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);
实际上就是调用URLProtocol的url_open2如果没有定义就调用url_open。这个地方根据不同的协议调用的url_open()具体实现函数也是不一样的,例如file协议的url_open()对应的是file_open(),而file_open()最终调用了_wsopen(),_sopen()(Windows下)或者open()(Linux下,类似于fopen())这样的系统中打开文件的API函数;而libRTMP的url_open()对应的是rtmp_open(),而rtmp_open()最终调用了libRTMP的API函数RTMP_Init(),RTMP_SetupURL(),RTMP_Connect() 以及RTMP_ConnectStream()。
ffio_fopen
io_open_default两个核心方法之二就是ffio_fopen, 之前我们已经初始化了URLContext,现在需要做的就是根据URLContext初始化AVIOContext
int ffio_fdopen(AVIOContext **s, URLContext *h)
{
AVIOInternal *internal = NULL;
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;
}
buffer = av_malloc(buffer_size);
if (!buffer)
return AVERROR(ENOMEM);
internal = av_mallocz(sizeof(*internal));
if (!internal)
goto fail;
internal->h = h;
*s = avio_alloc_context(buffer, buffer_size, h->flags & AVIO_FLAG_WRITE,
internal, io_read_packet, io_write_packet, io_seek);
if (!*s)
goto fail;
(*s)->protocol_whitelist = av_strdup(h->protocol_whitelist);
if (!(*s)->protocol_whitelist && h->protocol_whitelist) {
avio_closep(s);
goto fail;
}
(*s)->protocol_blacklist = av_strdup(h->protocol_blacklist);
if (!(*s)->protocol_blacklist && h->protocol_blacklist) {
avio_closep(s);
goto fail;
}
(*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 = io_read_pause;
(*s)->read_seek = io_read_seek;
if (h->prot->url_read_seek)
(*s)->seekable |= AVIO_SEEKABLE_TIME;
}
(*s)->short_seek_get = io_short_seek;
(*s)->av_class = &ff_avio_class;
return 0;
fail:
av_freep(&internal);
av_freep(&buffer);
return AVERROR(ENOMEM);
}
ffio_fdopen()函数首先初始化AVIOContext中的Buffer。如果URLContext中设置了max_packet_size,则将Buffer的大小设置为max_packet_size。如果没有设置的话(似乎大部分URLContext都没有设置该值),则会分配IO_BUFFER_SIZE个字节给Buffer。IO_BUFFER_SIZE取值为32768。
然后会调用 avio_alloc_context 创建AVIOContext。之后的代码就是在给它赋初始值。
avio_alloc_context
/**
* Allocate and initialize an AVIOContext for buffered I/O. It must be later
* freed with avio_context_free().
*
* @param buffer Memory block for input/output operations via AVIOContext.
* The buffer must be allocated with av_malloc() and friends.
* It may be freed and replaced with a new buffer by libavformat.
* AVIOContext.buffer holds the buffer currently in use,
* which must be later freed with av_free().
* AVIOContext中的Buffer。
* @param buffer_size The buffer size is very important for performance.
* For protocols with fixed blocksize it should be set to this blocksize.
* For others a typical size is a cache page, e.g. 4kb.
* AVIOContext中的Buffer的大小。
* @param write_flag Set to 1 if the buffer should be writable, 0 otherwise.
* @param opaque An opaque pointer to user-specific data.
* 用户自定义数据。
* @param read_packet A function for refilling the buffer, may be NULL.
* For stream protocols, must never return 0 but rather
* a proper AVERROR code.
* 读取外部数据,填充Buffer的函数。
* @param write_packet A function for writing the buffer contents, may be NULL.
* The function may not change the input buffers content.
* 向Buffer中写入数据的函数。
* @param seek A function for seeking to specified byte position, may be NULL.
* 用于Seek的函数。
*
* @return Allocated AVIOContext or NULL on failure.
*/
AVIOContext *avio_alloc_context(
unsigned char *buffer,
int buffer_size,
int write_flag,
void *opaque,
int (*read_packet)(void *opaque, uint8_t *buf, int buf_size),
int (*write_packet)(void *opaque, uint8_t *buf, int buf_size),
int64_t (*seek)(void *opaque, int64_t offset, int whence));
该方法的参数比较多,但是方法体很简单,无非是分配内存,初始化默认值,其中非常重要的就是以下代码了,3个函数作为该AVIOContext的读,写,跳转函数。
s->write_packet = write_packet;
s->read_packet = read_packet;
s->seek = seek;
我们回到 ffio_fopen 中找到这几个函数指针实际上就是 io_read_packet, io_write_packet, io_seek。再进一步剥析就是ffurl_read,ffurl_write,ffurl_seek。
io_seek
这个方法是最简单的
int64_t ffurl_seek(URLContext *h, int64_t pos, int whence)
{
int64_t ret;
if (!h->prot->url_seek)
return AVERROR(ENOSYS);
ret = h->prot->url_seek(h, pos, whence & ~AVSEEK_FORCE);
return ret;
}
他实际上就是调用URLProtocol的url_seek方法。
ffurl_read,ffurl_write
这两个方法大同小异,代码很简单,调用retry_transfer_wrapper方法进行真正的操作,真正的操作是什么?就是retry_transfer_wrapper这个方法的最后一个参数,分别是 h->prot->url_read 和h->prot->url_write,也就是URLProtocol中的url_read方法和url_wirte方法。
retry_transfer_wrapper的作用是对URLProtocol方法调用进行一次封装,做一些校验和错误处理。
结语
至此,我们分析完了io_open。连同上一篇,我们已经基本了解了init_input方法到底是如何打开文件,判断文件格式,并且初始化一些之后需要使用到的内容。
参考文献
https://blog.csdn.net/leixiaohua1020/article/details/41199947