目录
1.1.1.1.1.1 ffurl_get_protocols()
1.1.1.1.2 url_alloc_for_protocol()
1.1.1.2.1 url_open2() && url_open()
1 io_open_default()
io_open_default() 源码:
- 所属库:libavformat(lavf)
- 头文件:无,静态函数
- 源文件:libavformat/options.c
- 作用:打开URL,创建AVIOContext对象
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"); #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); }
- 输出一条打开某个url日志。注意一点:此处为啥要对image2图片封装格式做特殊处理呢?目前还不明白这点。
- 若AVFormatContext.open_cb非空,使用该函数打开URL。注意avformat_alloc_context()函数创建并初始化AVFormatContext对象时,并未初始化AVFormatContext.open_cb成员。
该成员的声明以及使用情况如下,但是注意这么几点:
1)该成员已经被标注为deprecated;
2)av_format_set_open_cb()函数和av_format_set_open_cb()函数均能找到函数的声明,但是源码中找不到定义,这一点令人奇怪,不知道是否是因为已标记为deprecated缘故;
3)源码中有专门定义一个AVOpenCallback,open_cb就是该类型的,个人觉得open_cb字段的声明以AVOpenCallback作为类型进行声明更合适,可能是因为该函数已经是被标注为deprecated的缘故吧。/** * 该函数用来打开AVIOContext对象,如果在解封装的时候需要使用I/O来读文件的话。 * Called to open further IO contexts when needed for demuxing. * * 这个函数可以被用户程序设置,在打开URLs之前对URL进行安全性验证。 * This can be set by the user application to perform security checks on * the URLs before opening them. * 这个函数的行为应该像avio_open2()一样。AVFormatContext来提供上下文信息, * 提供AVFormatContext.opaque * The function should behave like avio_open2(), AVFormatContext is provided * as contextual information and to reach AVFormatContext.opaque. * * 如果该函数指针为空,那么avio_open2会做一些简单的安全检查 * If NULL then some simple checks are used together with avio_open2(). * * 该函数指针不应该通过AVFormatContext来访问,而应该使用av_format_set_open_cb()来设置; * 使用av_format_get_open_cb()来获取。 * Must not be accessed directly from outside avformat. * @See av_format_set_open_cb() * * Demuxing: Set by user. * * @deprecated Use io_open and io_close. */ attribute_deprecated int (*open_cb)(struct AVFormatContext *s, AVIOContext **p, const char *url, int flags, const AVIOInterruptCB *int_cb, AVDictionary **options); #if FF_API_OLD_OPEN_CALLBACKS attribute_deprecated AVOpenCallback av_format_get_open_cb(const AVFormatContext *s); attribute_deprecated void av_format_set_open_cb(AVFormatContext *s, AVOpenCallback callback); #endif typedef int (*AVOpenCallback)(struct AVFormatContext *s, AVIOContext **pb, const char *url, int flags, const AVIOInterruptCB *int_cb, AVDictionary **options);
- 若AVFormatContext.open_cb为空,使用ffio_open_whitelist()函数打开URL。
1.1 ffio_open_whitelist()
ffio_open_whitelist() 声明:
- 所属库:libavformat(lavf)
- 头文件:libavformat/avio_internal.h 如其名,是IO内部使用的函数,不是public api
- 声明:
int ffio_open_whitelist(AVIOContext **s, const char *url, int flags,
const AVIOInterruptCB *int_cb, AVDictionary **options,
const char *whitelist, const char *blacklist);
ffio_open_whitelist() 源码:
- 源文件:libavformat/aviobuf.c, 源码如下所示:
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; }
1.1.1 ffurl_open_whitelist()
ffurl_open_whitelist() 声明:
- 所属库:libavformat(lavf)
- 头文件:libavformat/url.h
- 功能说明:根据URL来创建一个URLContext对象,用来访问URL所定位的资源,并打开之。对于该函数的入参以及返回值说明如下源码:
/** * Create an URLContext for accessing to the resource indicated by * url, and open it. * * puc指向成功打开后的URLContext对象 * @param puc pointer to the location where, in case of success, the * function puts the pointer to the created URLContext * * flags参数控制如何打开url指定的资源 * @param flags flags which control how the resource indicated by url * is to be opened * * int_cb 中断函数,供URLContext使用,可空 * @param int_cb interrupt callback to use for the URLContext, may be * NULL * * options参数,传入协议私有的选项。生效的选项,对应条目将被销毁,未生效的选项在本函数执 * 行之后仍保留在该参数中。该参数可空。 * @param options A dictionary filled with protocol-private options. On return * this parameter will be destroyed and replaced with a dict containing options * that were not found. May be NULL. * * parent参数代表创建成功的URLContext的父URLContext对象,父URLContext对象的通用选项 * 也应用到创建的URLContext对象中。 * @param parent An enclosing URLContext, whose generic options should * be applied to this URLContext as well. * 成功返回0或正数,失败返回负数,并且是由AVERROR宏返回的错误码 * @return >= 0 in case of success, a negative value corresponding to an * AVERROR code in case of failure */ 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);
ffurl_open_whitelist() 源码:
- 源文件:avio.c
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_alloc(puc, filename, flags, int_cb); // 创建URLContext if (ret < 0) return ret; if (parent) av_opt_copy(*puc, parent); // 拷贝父URLContext的选项->子URLContext // 设置用户传入的选项到URLContext if (options && (ret = av_opt_set_dict(*puc, options)) < 0) goto fail; // 上一步中的options含有非URLContext类的直接选项信息 // 则设置用户传入的选项到URLContext.priv_data数据中 if (options && (*puc)->prot->priv_data_class && (ret = av_opt_set_dict((*puc)->priv_data, options)) < 0) goto fail; // 传入的options为空,options指向内部临时变量,后续操作黑白名单需要用上选项 // 此处操作就是为了不论是否用户传入了options,还是传入NULL,后续操作能统一使用一个变量。 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)); // 将白名单放到options中 if ((ret = av_dict_set(options, "protocol_whitelist", whitelist, 0)) < 0) goto fail; // 将黑名单放到options中 if ((ret = av_dict_set(options, "protocol_blacklist", blacklist, 0)) < 0) goto fail; // 将options的黑白名单应用到URLContext if ((ret = av_opt_set_dict(*puc, options)) < 0) goto fail; ret = ffurl_connect(*puc, options); if (!ret) return 0; fail: ffurl_close(*puc); *puc = NULL; return ret; }
- 使用 ffurl_alloc() 创建一个与URL匹配的URLContext,并初始化;
1)根据URL中包含的协议名来查找对应的URLProtocol对象;
2)根据URLProtocol对象以及ffurl_alloc()传入的参数来构建URLContext上下文对象。 - 使用 av_opt_copy() 拷贝父URLContext(如果存在的话)的选项到新创建的URLContext;该函数的详细分析见FFMPEG源码分析之 av_opt_copy();
- 使用 av_opt_set_dict() 应用用户传入的选项options到URLContext对象:注意,若是options存在URLContext中不存在的选项,则该选项还会留存在options中,而那些应用成功的选项将会从options删除。该函数的详细分析见 FFMPEG源码分析之 av_opt_set_dict();
- 使用av_opt_set_dict() 应用用户传入的选项options到URLContext.priv_data对象:上一步遗留的options,应用到URLContext.priv_data,应用不成功的选项仍会遗留在options中;
- 对黑白名单内容进行断言:断言保证用户传入的黑白名单为空,要么与options中的保持一致。FFMPEG中的断言实现见 FFMPEG4.1源码分析之 断言;
- 使用 av_dict_set() 将黑白名单复制到options,然后使用 av_opt_set_dict() 将黑白名单应用到URLContext:av_dict_set()详细分析见 FFMPEG源码分析之 av_dict_set();
- 使用 ffurl_connect() 打开URL,如果是需要网络的话,将建立连接;
- 出错将使用ffurl_close() 进行清理。
1.1.1.1 ffurl_alloc()
ffurl_alloc() 声明:
- 所属库:libavformat(lavf)
- 头文件:ibavformat/url.h
- 功能说明:根据URL创建一个可以访问URL的URLContext对象,但是没有初始化连接。
/** * Create a URLContext for accessing to the resource indicated by * url, but do not initiate the connection yet. * * puc参数指向创建成功的URLContext * @param puc pointer to the location where, in case of success, the * function puts the pointer to the created URLContext * * flags控制如何打开url * @param flags flags which control how the resource indicated by url * is to be opened * * int_cb 是中断函数,供URLContext使用,可空 * @param int_cb interrupt callback to use for the URLContext, may be * NULL * * 该函数成功返回非负值,失败返回负值,为AVERROR宏作用后的错误码 * @return >= 0 in case of success, a negative value corresponding to an * AVERROR code in case of failure */ int ffurl_alloc(URLContext **puc, const char *filename, int flags, const AVIOInterruptCB *int_cb);
ffurl_alloc() 源码:
- 源文件:libavformat/avio.c
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;
}
-
使用url_find_protocol()查找URLProtocol对象;
-
url_find_protocol() 查找成功:使用url_alloc_for_protocol() 来创建URLContext上下文对象,并初始化其成员;
-
url_find_protocol() 查找失败:那么如果URL(或者说filename)是https协议或者是tls协议,打印WARNING级别日志告知需要重新编译ffmpeg并使能https/tls协议所依赖的openssl库,gnutls库或者是securetransport库;返回AVERROR_PROTOCOL_NOT_FOUND宏定义指代的值,该值的四个字节分别就是0xF8,'P','R','O',并且由FFERRTAG定义可知是负值。
#define AVERROR_PROTOCOL_NOT_FOUND FFERRTAG(0xF8,'P','R','O') ///< Protocol not found #define FFERRTAG(a, b, c, d) (-(int)MKTAG(a, b, c, d)) #define MKTAG(a,b,c,d) ((a) | ((b) << 8) | ((c) << 16) | ((unsigned)(d) << 24))
1.1.1.1.1 url_find_protocol()
url_find_protocol() 源码:
- 所属库:libavformat(lavf)
- 头文件:无,静态函数
- 源文件:libavformat/avio.c
- 功能:根据URL(filename)字符串来查找适配的URLContext对象。
static const struct URLProtocol *url_find_protocol(const char *filename) { const URLProtocol **protocols; char proto_str[128], proto_nested[128], *ptr; // 获取scheme长度 size_t proto_len = strspn(filename, URL_SCHEME_CHARS); int i; // 判断是否为file协议,若是file协议,则proto_str设置为file // 若不是file协议,则proto_str设置为filename中截取的scheme 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))); // 处理nested protocol scheme av_strlcpy(proto_nested, proto_str, sizeof(proto_nested)); if ((ptr = strchr(proto_nested, '+'))) *ptr = '\0'; // 获取所有支持的URLProtocol protocols = ffurl_get_protocols(NULL, NULL); if (!protocols) return NULL; // 迭代匹配,返回匹配的URLProtocol 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; }
- 计算filename字符串属于协议scheme的长度proto_len:URL_SCHEME_CHARS宏定义如下所示,其包含了协议scheme所有可能的字符; strspn() 是c语言字符串操作基础库string.h中的函数,作用是返回字符串中第一个不在指定字符串中出现的字符下标。举个例子:
1)url为 rtmp://ns8.indexforce.com/home/mystream
2) strspn(url, URL_SCHEME_CHARS); 由于 ‘:’ 不属于URL_SCHEME_CHARS中的一员,因此将返回其下标,也就是4,正好是协议“rtmp”几个字符的长度#define URL_SCHEME_CHARS \ "abcdefghijklmnopqrstuvwxyz" \ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \ "0123456789+-."
- 计算filename字符串属于协议scheme的字符串proto_str:判断URL是否指向本地磁盘文件,如果是则proto_str置为"file",如果不是本地文件,则proto_str取URL前proto_len+1字符串作为协议。 对于Windows,Mac,IOS,Linux,Android等ffmpeg兼容的系统而言,URL形式不外乎有如下几种:
1) windows的DOS path: "c:/media/video.mp4"; 该形式的URL可以由is_dos_path()方法检出,从而被判定为本地文件(该方法位于libavformat/os_support.h文件中)
2) 类unix系统的绝对路径和相对路径:"./media/video.mp4" 或 "../media/video.mp4" 或 "/root/mediafile/video.mp4" ;由于该形式的URL不包含字符 ":",所以语句filename[proto_len] != ':' 和!strchr(filename + proto_len + 1, ':') 肯定都成立,从而被判定为本地文件。static inline int is_dos_path(const char *path) { #if HAVE_DOS_PATHS if (path[0] && path[1] == ':') return 1; #endif return 0; }
3)注意subfile协议是个特例,其URL形似为"subfile,,start,32815239,end,0,,:video.ts", 因此通过strncmp(filename, "subfile,", 8)语句将其排除在file协议之外。(疑问之处:如果不加strncmp()这条语句,也能通过!strchr(filename + proto_len + 1, ':') 将其排除的?),该协议的详细描述见FFMPEG官网:ffmpeg-protocols.html#subfile
- 获取协议字符串proto_str可能嵌套的协议串proto_nested:以hls协议为例,其nested格式为 "hls+http://host/path/to/remote/resource.m3u8" 和 "hls+file://path/to/local/resource.m3u8",由源码中的处理可知对于上述URL的proto_nested为"hls"。
- ffurl_get_protocols() 获取ffmpeg支持的所有URLProtocol,存储在变量 const URLProtocol **protocols中;
- 迭代protocols中的每个URLProtocol,进行协议名匹配:先将proto_str匹配URLProtocol.name,如果不匹配则对proto_nested匹配URLProtocol.name,只要满足其一,那么就算找到了合适的URLProtocol对象。
- av_freep()释放protocols所占空间:具体分析见(FFMPEG4.1源码分析之 内存分配)
1.1.1.1.1.1 ffurl_get_protocols()
ffurl_get_protocols() 声明:
- 所属库:libavformat(lavf)
- 头文件:libavformat/url.h
- 声明:
1. 创建一个协议的list,list中的协议将匹配(满足)白名单和黑名单;
2. 白名单(whitelist):该参数要么是空,要么是以逗号分隔的协议名,这些协议名为允许的协议;若不为空,那么该函数返回的协议list必须属于白名单中的列举的协议;
3. 黑名单(blacklist):该参数要么是空,要么是以都放分隔的协议名,这些协议名为禁止的协议;若不为空,那么该函数返回的协议list中必须排除黑名单中列举的协议。/** * Construct a list of protocols matching a given whitelist and/or blacklist. * * @param whitelist a comma-separated list of allowed protocol names or NULL. If * this is a non-empty string, only protocols in this list will * be included. * @param blacklist a comma-separated list of forbidden protocol names or NULL. * If this is a non-empty string, all protocols in this list * will be excluded. * * @return a NULL-terminated array of matching protocols. The array must be * freed by the caller. */ const URLProtocol **ffurl_get_protocols(const char *whitelist, const char *blacklist);
ffurl_get_protocols() 声明:
- 所属库:libavformat(lavf)
- 源文件:libavformat/protocols.c
- 源码:
const URLProtocol **ffurl_get_protocols(const char *whitelist, const char *blacklist) { const URLProtocol **ret; int i, ret_idx = 0; ret = av_mallocz_array(FF_ARRAY_ELEMS(url_protocols), sizeof(*ret)); if (!ret) return NULL; for (i = 0; url_protocols[i]; i++) { const URLProtocol *up = url_protocols[i]; if (whitelist && *whitelist && !av_match_name(up->name, whitelist)) continue; if (blacklist && *blacklist && av_match_name(up->name, blacklist)) continue; ret[ret_idx++] = up; } return ret; }
1. url_protocols是一个定义在protocol_list.c文件中的静态数组,包含了ffmpeg所支持的所有协议,如下所示:
static const URLProtocol *url_protocols[] = { &ff_async_protocol, ...此处省略几十行... &ff_libssh_protocol, NULL };
2. ret为函数内部分配的URLProtocol *数组,其空间大小与url_protocols相同
3. 循环查找url_protocols静态数组中,协议名在白名单中,不在黑名单中的协议,并填充ret数组。
4. 返回该数组。
1.1.1.1.2 url_alloc_for_protocol()
url_alloc_for_protocol() 源码:
- 所属库:libavformat(lavf)
- 头文件:无,静态函数
- 源文件:libavformat/avio.c
- 功能:创建URLContext上下文对象,并且初始化URL各成员对象。
static int url_alloc_for_protocol(URLContext **puc, const URLProtocol *up, const char *filename, int flags, const AVIOInterruptCB *int_cb) { URLContext *uc; int err; // ffmpeg配置需要使用网络,需要进行如下判断 // 若协议是需要使用网络的,但是网络库初始化失败,则返回失败码 #if CONFIG_NETWORK if (up->flags & URL_PROTOCOL_FLAG_NETWORK && !ff_network_init()) return AVERROR(EIO); #endif // 如果要求读该协议,但是对应的URLProtocol.url_read不存在,即不支持读 // 打印错误信息,并返回错误码 if ((flags & AVIO_FLAG_READ) && !up->url_read) { av_log(NULL, AV_LOG_ERROR, "Impossible to open the '%s' protocol for reading\n", up->name); return AVERROR(EIO); } // 如果要求写该协议,但是对应的URLProtocol.url_write不存在,即不支持写 // 打印错误信息,并返回错误码 if ((flags & AVIO_FLAG_WRITE) && !up->url_write) { av_log(NULL, AV_LOG_ERROR, "Impossible to open the '%s' protocol for writing\n", up->name); return AVERROR(EIO); } // 创建URLContext上下文对象,为啥这儿不是分配sizeof(URLContext)? // 仔细思考哦~ uc = av_mallocz(sizeof(URLContext) + strlen(filename) + 1); if (!uc) { // 分配空间失败,记住失败码,跳转fail标签处理 err = AVERROR(ENOMEM); goto fail; } // 初始化URLContext各成员 uc->av_class = &ffurl_context_class; uc->filename = (char *)&uc[1]; strcpy(uc->filename, filename); uc->prot = up; uc->flags = flags; uc->is_streamed = 0; /* default = not streamed */ uc->max_packet_size = 0; /* default: stream file */ // 特别注意URLProtocol.priv_data_size的大小 // 以及URLContext.priv_data到底是哪种类型?AVClass吗? // 哈哈,再仔细追踪下代码吧~~ if (up->priv_data_size) { uc->priv_data = av_mallocz(up->priv_data_size); if (!uc->priv_data) { err = AVERROR(ENOMEM); goto fail; } if (up->priv_data_class) { int proto_len= strlen(up->name); char *start = strchr(uc->filename, ','); *(const AVClass **)uc->priv_data = up->priv_data_class; // 如果没搞清楚URLContext.priv_data真正的类型 // 这个函数的调用你就会懵逼 av_opt_set_defaults(uc->priv_data); // 这儿是对subfile这个协议的特例处理 if(!strncmp(up->name, uc->filename, proto_len) && uc->filename + proto_len == start){ int ret= 0; char *p= start; char sep= *++p; char *key, *val; p++; if (strcmp(up->name, "subfile")) ret = AVERROR(EINVAL); while(ret >= 0 && (key= strchr(p, sep)) && p<key && (val = strchr(key+1, sep))){ *val= *key= 0; if (strcmp(p, "start") && strcmp(p, "end")) { ret = AVERROR_OPTION_NOT_FOUND; } else ret= av_opt_set(uc->priv_data, p, key+1, 0); if (ret == AVERROR_OPTION_NOT_FOUND) av_log(uc, AV_LOG_ERROR, "Key '%s' not found.\n", p); *val= *key= sep; p= val+1; } if(ret<0 || p!=key){ av_log(uc, AV_LOG_ERROR, "Error parsing options string %s\n", start); av_freep(&uc->priv_data); av_freep(&uc); err = AVERROR(EINVAL); goto fail; } memmove(start, key+1, strlen(key)); } } } if (int_cb) uc->interrupt_callback = *int_cb; *puc = uc; // 成功,将URLConext对象地址给puc return 0; // 成功,返回0 fail: // 失败,进行清理内存,卸载网络库 *puc = NULL; if (uc) av_freep(&uc->priv_data); av_freep(&uc); #if CONFIG_NETWORK if (up->flags & URL_PROTOCOL_FLAG_NETWORK) ff_network_close(); #endif return err; // 失败,返回失败码 }
- 参数合法性验证:主要是传入的标识flags与URLProtocol之间的验证,有3点:
1)协议URLProtocol是否需要使用网络,初始化网络库,ff_network_init()源码如下所示
2)flags中是否要求URLProtocol进行读操作,而URLProtocol是否有读函数URLProtocol.url_read;
3) flags中是否要求URLProtocol进行读操作,而URLProtocol是否有读函数URLProtocol.url_write;int ff_network_init(void) { #if HAVE_WINSOCK2_H // windows系统的winsock2库 WSADATA wsaData; if (WSAStartup(MAKEWORD(1,1), &wsaData)) //调用该库的初始化函数WSAStartup() return 0; // 初始化失败则返回0 #endif return 1; // 其他网络库不需要初始化,因此直接返回1 }
- 创建URLContext对象,分配存储空间:av_mallocz(sizeof(URLContext) + strlen(filename) + 1); 注意这儿分配的大小不是sizeof(URLContext)!!!为的是将filename这个字符串直接拷贝到紧挨着URLContext的后续内存中。
- 初始化URLContext成员:此处进行初始化的成员有URLContext.av_class,.filename,.prot,.flags,.is_streamed,.max_packet_size,.priv_data,.interrupt_callback 。这儿只关注字段.filename, .prot,.priv_data。其他字段见(FFMPEG4.1源码分析之 URLProtocol结构体 && URLContext以及相关函数)
1)URLContext.filename: 注意源码实现,首先将filename指针指向(char *)&uc[1],这个操作使得其指向了uc之后的空间,然后拷贝filename字符串到这儿。与之前空间分配相呼应
2)URLContext.prot: 将之前查找到的URLPotocol对象挂在这个成员之下,建立这么个观点:URLContext上下文持有对应的URLPotocol对象
3)URLContext.priv_data:这个字段的初始化过程可以分几个阶段来解读
3.1)分配空间:uc->priv_data = av_mallocz(up->priv_data_size); 该字段的空间大小居然取决的是URLProtocol.priv_data_size字段;
3.2)AVClass*字段地址指派:*(const AVClass **)uc->priv_data = up->priv_data_class; 这句话是比较难以理解的事,这么来说吧,先将uc->priv_data看成是一个如同AVFormatContext这类具有AVClass*成员的结构体,该AVClass*结构体是该结构体的第一个成员,因此其地址与uc->priv_data地址相同,那么上述的语句就是将uc->priv_data->av_class指向了up->priv_data_class对象。
3.3)初始化.priv_data字段的成员字段: 通过调用av_opt_set_defaults(uc->priv_data);来实现这个功能,由于3.2,uc->priv_data->av_class已经被设置,那么就很好理解了,如果不知道原理可以查看 FFMPEG4.1源码分析之 av_opt_set_defaults()函数这篇文章,已经有对av_opt_set_defaults()很详细的分析了,此处就不再赘述。
3.4)特殊处理subfile协议:前文就已经论述过subfile协议的url格式为"subfile,,start,32815239,end,0,,:video.ts",简要的说下这个特殊处理过程所作事情:
3.4.1)提取url中的start,end的值,分别为32815239,0;
3.4.2)通过ret= av_opt_set(uc->priv_data, p, key+1, 0)将start,end值赋值给.priv_data字段所代表的结构体的start成员字段和end成员字段。
3.4.3)咦?!!!发现没,之前整个过程,我们根本就不知道.priv_data字段的结构体是啥,哪儿来的start成员字段,哪儿来的end成员字段?查看.priv_data的类型为void*,看到void*这个类型,同时通过上述分析得知其代表了一个具体的结构体(对于subfile协议来说该结构体具有start成员和end成员),那么为啥不直接声明成这个具体结构体类型呢?意味着这么一点:.priv_data字段需要根据不同情形成为不同的结构体类型。
3.4.4)正好,我们针对subfile协议进行分析:uc->priv_data 字段的大小取决于URLProtocol.priv_data_size,那么就以此为突破口吧。首先,我们需要找到subfile对应的URLProtocol,所有的协议均在libavformat/protocol_list.c文件中找到,subfile协议的URLProtocol对象为ff_subfile_protocol,定义在libavformat/subfile.c中,如下源码所示。
ff_subfile_protocol.priv_data_size取值为sizeof(SubfileContext),因此可以推知URLContext.priv_data结构体为SubfileContext这个结构体,如下所示。果然这个结构体存在start字段和end字段。是不是可以解惑了?const URLProtocol ff_subfile_protocol = { .name = "subfile", .url_open2 = subfile_open, .url_read = subfile_read, .url_seek = subfile_seek, .url_close = subfile_close, .priv_data_size = sizeof(SubfileContext), .priv_data_class = &subfile_class, .default_whitelist = "file", };
稍微再深挖一点:之前给URLContext.priv_data的首个字段.av_class指定到了URLProtocol.priv_data_class。对于subfile协议来说,就是上面源码对应的subfile_class这个静态结构体,如下源码所示,其.option属性是subfile_options,如下源码所示。来看看,是不是又印证了之前使用av_opt_set_defaults()来给URLContext.priv_data设置默认值,使用av_opt_set()来设置URLContext.priv_data成员的start和end的逻辑? 至此,该段终于解析完毕。typedef struct SubfileContext { const AVClass *class; URLContext *h; int64_t start; int64_t end; int64_t pos; } SubfileContext; static const AVClass subfile_class = { .class_name = "subfile", .item_name = av_default_item_name, .option = subfile_options, .version = LIBAVUTIL_VERSION_INT, }; static const AVOption subfile_options[] = { { "start", "start offset", OFFSET(start), AV_OPT_TYPE_INT64, {.i64 = 0}, 0, INT64_MAX, D }, { "end", "end offset", OFFSET(end), AV_OPT_TYPE_INT64, {.i64 = 0}, 0, INT64_MAX, D }, { NULL } };
- 出错处理:释放URLContext所占用的空间,包括其私有数据字段;ff_network_close()函数卸载网络库,其源码如下:
void ff_network_close(void) { #if HAVE_WINSOCK2_H // 如果网络库是winsocket库,则需要使用WSACleanup();进行清理 WSACleanup(); #endif }
1.1.1.2 ffurl_connect()
ffurl_connect() 声明:
- 所属库:libavformat(lavf)
- 头文件:libavformat/url.h
- 声明:连接使用ffurl_alloc所创建的URLContext,这个说明比较抽象,具体分析见后文源码分析。
/** * Connect an URLContext that has been allocated by ffurl_alloc * * @param options A dictionary filled with options for nested protocols, * i.e. it will be passed to url_open2() for protocols implementing it. * This parameter will be destroyed and replaced with a dict containing options * that were not found. May be NULL. */ int ffurl_connect(URLContext *uc, AVDictionary **options);
ffurl_connect() 源码:
- 所属库:libavformat(lavf)
- 源文件:libavformat/avio.c
- 源码:
int ffurl_connect(URLContext *uc, AVDictionary **options) { int err; AVDictionary *tmp_opts = NULL; AVDictionaryEntry *e; if (!options) options = &tmp_opts; // 检查URLContext中的黑白名单是否和options中的一致 // Check that URLContext was initialized correctly and lists are matching if set av_assert0(!(e=av_dict_get(*options, "protocol_whitelist", NULL, 0)) || (uc->protocol_whitelist && !strcmp(uc->protocol_whitelist, e->value))); av_assert0(!(e=av_dict_get(*options, "protocol_blacklist", NULL, 0)) || (uc->protocol_blacklist && !strcmp(uc->protocol_blacklist, e->value))); // 检查协议是否在白名单中 if (uc->protocol_whitelist && av_match_list(uc->prot->name, uc->protocol_whitelist, ',') <= 0) { av_log(uc, AV_LOG_ERROR, "Protocol '%s' not on whitelist '%s'!\n", uc->prot->name, uc->protocol_whitelist); return AVERROR(EINVAL); } // 检查协议是否在黑名单中 if (uc->protocol_blacklist && av_match_list(uc->prot->name, uc->protocol_blacklist, ',') > 0) { av_log(uc, AV_LOG_ERROR, "Protocol '%s' on blacklist '%s'!\n", uc->prot->name, uc->protocol_blacklist); return AVERROR(EINVAL); } // 如果URLContext上下文中的白名单为空,并且URLProtocol中的默认白名单不为空,那么 // 复制URLProtocol的默认白名单到URLContext上下文的白名单中 if (!uc->protocol_whitelist && uc->prot->default_whitelist) { av_log(uc, AV_LOG_DEBUG, "Setting default whitelist '%s'\n", uc->prot->default_whitelist); uc->protocol_whitelist = av_strdup(uc->prot->default_whitelist); if (!uc->protocol_whitelist) { return AVERROR(ENOMEM); } } else if (!uc->protocol_whitelist) av_log(uc, AV_LOG_DEBUG, "No default whitelist set\n"); // This should be an error once all declare a default whitelist // 将黑白名单设置回options,仍保持二者一致 if ((err = av_dict_set(options, "protocol_whitelist", uc->protocol_whitelist, 0)) < 0) return err; if ((err = av_dict_set(options, "protocol_blacklist", uc->protocol_blacklist, 0)) < 0) return err; // 使用URLProtocol的url_open2函数或者url_open函数来打开该url err = uc->prot->url_open2 ? uc->prot->url_open2(uc, uc->filename, uc->flags, options) : uc->prot->url_open(uc, uc->filename, uc->flags); // 给options的黑白名单置空 av_dict_set(options, "protocol_whitelist", NULL, 0); av_dict_set(options, "protocol_blacklist", NULL, 0); if (err) return err; // open成功则表示已连接上,设置标志位 uc->is_connected = 1; // 如果协议支持写入或者是协议是file协议,即本地文件 /* We must be careful here as ffurl_seek() could be slow, * for example for http */ if ((uc->flags & AVIO_FLAG_WRITE) || !strcmp(uc->prot->name, "file")) // 协议是非流协议并且不能seek,那么也当作流协议来处理,is_streamed标志置位。 if (!uc->is_streamed && ffurl_seek(uc, 0, SEEK_SET) < 0) uc->is_streamed = 1; return 0; }
1. 验证参数的有效性
1)验证入参URLContext和选项options中的黑白名单是否一整;
2)验证入参URLContext对应的协议是否在黑白名单中(若白名单存在,则必须在白名单中,若黑名单存在则一定不能在黑名单中);
3)如果白名单不存在,那么复制URLProtocol的默认白名单到URLContext的白名单中
4)由于3)的存在会修改白名单,因此,将黑白名单从URLContext再复制回options中,后续还要使用
2. 打开url
这个操作是通过调用URLProtocol的url_open2或者是url_open函数真正的处理url打开,对于不同的协议URLProtocol实体是不一样的,因此,后续将针对某个协议,比如常见的rtmp协议进行分析。
3. 修改URLContext的成员值
1)修改URLContext.is_connected = 1
2) 修改URLContext.is_streamed = 1,这种修改是在满足如下几个条件下才设置的:协议支持写入或者协议是file协议(即本地文件),但不支持按字节seek(ffurl_seek函数内部是按字节seek,而非按时间戳seek)
1.1.1.2.1 url_open2() && url_open()
url_open2()或者是url_open()函数是URLProtocol的成员,不同协议的URLProtocol实体不同,因此url_open函数的内容不尽相同,但是作用是类似的。接下来,以常见的RTMP协议来进行分析。
rtmp协议的URLProtocol是名为ff_rtmp_protocol的常量,其定义在rtmpproto.c文件中,详见如下源码:
#define RTMP_PROTOCOL(flavor) \
static const AVClass flavor##_class = { \
.class_name = #flavor, \
.item_name = av_default_item_name, \
.option = rtmp_options, \
.version = LIBAVUTIL_VERSION_INT, \
}; \
\
const URLProtocol ff_##flavor##_protocol = { \
.name = #flavor, \
.url_open2 = rtmp_open, \
.url_read = rtmp_read, \
.url_read_seek = rtmp_seek, \
.url_read_pause = rtmp_pause, \
.url_write = rtmp_write, \
.url_close = rtmp_close, \
.priv_data_size = sizeof(RTMPContext), \
.flags = URL_PROTOCOL_FLAG_NETWORK, \
.priv_data_class= &flavor##_class, \
};
RTMP_PROTOCOL(rtmp)
RTMP_PROTOCOL(rtmpe)
RTMP_PROTOCOL(rtmps)
RTMP_PROTOCOL(rtmpt)
RTMP_PROTOCOL(rtmpte)
RTMP_PROTOCOL(rtmpts)
1. RTMP_PROTOCOL宏相当于定义了协议相关的两个结构体实例,以rtmp为例:一个是AVClass类型的rtmp_class;一个是URLProtocol类型的ff_rtmp_protocol
2. rtmp协议存在多个变种协议:
rtmpe:rtmp的加密版本,基于Adobe私有加密协议。
rtmps:rtmp的加密版本,基于TLS/SSL加密。
rtmpt:将rtmp协议包封装在http协议之上,用以穿越firewall,通常传输接口也变为80或者是443
rtmpte:rtmp协议+Adobe私有加密协议+http协议。
rtmpts:rtmp协议+TLS/SSL加密协议+http协议。
当前,我们只关注url_open2() && url_open()函数,由上源码可知,其实就是rtmp_open()函数。由于rtmp_open函数的代码太长,本文不在此处解析。rtmp_open() 函数中将试图与服务器建立链接,验证url是否可以推送流或者拉取流
1.1.1.2.2 ffurl_seek()
ffurl_seek() 声明:
- 所属库:libavformat(lavf)
- 头文件:libavformat/url.h
- 声明:功能:将改变URLContext访问的资源中的偏移量,下次的read/write操作将使用该偏移量。
参数pos:指定新的偏移量
参数whence:与unix的文件io含义是一样的,可取SEEK_SET,SEEK_END,SEEK_CUR三个。不同之处在于还可有取值AVSEEK_SIZE,此时,函数返回资源的文件大小,而不进行seek操作。
返回值:失败返回负的错误码,成功返回相对于文件起始位置的偏移量,以字节数来度量。 /** * Change the position that will be used by the next read/write * operation on the resource accessed by h. * * @param pos specifies the new position to set * @param whence specifies how pos should be interpreted, it must be * one of SEEK_SET (seek from the beginning), SEEK_CUR (seek from the * current position), SEEK_END (seek from the end), or AVSEEK_SIZE * (return the filesize of the requested resource, pos is ignored). * @return a negative value corresponding to an AVERROR code in case * of failure, or the resulting file position, measured in bytes from * the beginning of the file. You can use this feature together with * SEEK_CUR to read the current file position. */ int64_t ffurl_seek(URLContext *h, int64_t pos, int whence);
ffurl_seek() 源码:
- 所属库:libavformat(lavf)
- 头文件:libavformat/avio.c
- 源码:
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()函数,对于rtmp协议而言,参考上节内容可知:url_seek是没有被赋值的,是个空指针。因此,对于RTMP协议来说,直接返回错误码AVERROR(ENOSYS)。这儿重点提一下:
URLProtocol由两个seek函数,分别如下声明:
1) int64_t (*url_seek)( URLContext *h, int64_t pos, int whence);
表示该资源可以按照字节seek,其参数列表与本地文件IO的seek函数类似;
2)int64_t (*url_read_seek)(URLContext *h, int stream_index, int64_t timestamp, int flags);
表示该资源可以按照时间戳seek;
那么URLContext.is_streamed大多数取决于URLProtocol是否支持按字节seek。
1.1.2 ffio_fdopen
ffio_fdopen() 声明:
- 所属库:libavformat(lavf)
- 头文件:libavformat/avio_internal.h
- 声明:创建并初始化AVIOContext对象,用以访问URLContext所引用的资源
/** * Create and initialize a AVIOContext for accessing the * resource referenced by the URLContext h. * @note When the URLContext h has been opened in read+write mode, the * AVIOContext can be used only for writing. * * @param s Used to return the pointer to the created AVIOContext. * In case of failure the pointed to value is set to NULL. * @return >= 0 in case of success, a negative value corresponding to an * AVERROR code in case of failure */ int ffio_fdopen(AVIOContext **s, URLContext *h);
ffio_fdopen() 源码:
- 所属库:libavformat
- 源文件:libavformat/aviobuf.c
- 源码:
int ffio_fdopen(AVIOContext **s, URLContext *h) { AVIOInternal *internal = NULL; uint8_t *buffer = NULL; int buffer_size, max_packet_size; // 根据max_packet_size来创建缓冲 // 若用户设置了max_packet_size,则使用用户设置的 // 若为0(创建URLContext时默认设置为0),则使用 // 默认的IO缓冲大小->IO_BUFFER_SIZE为32768 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); // 创建AVIOInternal对象 internal = av_mallocz(sizeof(*internal)); if (!internal) goto fail; // 将URLContext挂到AVIOInternal上 internal->h = h; // 创建AVIOContext对象 *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; // 拷贝URLContext黑白名单到AVIOContext黑白名单 (*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; } // 是否直接调用底层接口而不使用buffer (*s)->direct = h->flags & AVIO_FLAG_DIRECT; // 根据URLContext.is_streamed来判定是否支持seek操作 // 注意AVIO_SEEKABLE_NORMAL表征的是可以像对本地文件那样按字节进行seek (*s)->seekable = h->is_streamed ? 0 : AVIO_SEEKABLE_NORMAL; // 设置包最大值和包最小值 (*s)->max_packet_size = max_packet_size; (*s)->min_packet_size = h->min_packet_size; // 如果URLProtocol存在,那么给如下几个函数指针赋值 // AVIOContext.read_pause,AVIOContext.read_seek // 注意上述两个函数指针,最终还是需要调用URLProtocol的 // url_read_pause和url_read_seek函数。 if(h->prot) { (*s)->read_pause = io_read_pause; (*s)->read_seek = io_read_seek; // 当URLProtocol.url_read_seek存在,那么说明可以按照时间戳进行seek // 因此AVIOContext.seekable加上可按时间戳进行seek的标记 if (h->prot->url_read_seek) (*s)->seekable |= AVIO_SEEKABLE_TIME; } // 设置short_seek回调 (*s)->short_seek_get = io_short_seek; // 设置AVIOContext的AVClass (*s)->av_class = &ff_avio_class; return 0; fail: av_freep(&internal); av_freep(&buffer); return AVERROR(ENOMEM); }
上述源码可以分三个步骤来看
1. 为创建AVIOContext——IO上下文对象对象做准备
1)创建缓冲区:根据URLContext .max_packet_size来判断缓存区大小,由上文的URLContext创建过程可知,max_packet_size默认为0,如无意外,将使用IO_BUFFER_SIZE作为要创建的缓冲区的大小,该值为32768,我们知道linux系统blocksize一般为4096,缓冲区32748 = 8 * blocksize。创建这个大小的缓冲区可达到比较高效的读写性能,见<<unix环境高级编程>> 3.9节 I/O的效率。
2)创建AVIOInternal对象:该对象只是简单的持有URLContext指针,源码如下typedef struct AVIOInternal { URLContext *h; } AVI
2. 使用avio_alloc_context()函数创建AVIOConext对象:后文分析该函数的实现。
3. 修改AVIOConext成员值:
1)AVIOContext.whitelist && AVIOContext.blacklist:拷贝URLContext的黑白名单
2)AVIOContext.direct:
2.1)首先,看下这个字段代表的含义:该字段将影响avio_read(),avio_write(),avio_seek(),这几个函数将使得这3个函数不会使用AVIOContext内部的buffer,并且avio_seek将直接调用底层seek函数而不再在AVIOContext的buffer中seek;
2.2)其次,direct取值取决于URLContext .flags,而这个flags参数是由最开始io_open_default()函数一路传递进来赋值给URLContext构建函数的。/** * avio_read and avio_write should if possible be satisfied directly * instead of going through a buffer, and avio_seek will always * call the underlying seek function directly. */ int direct;
3)AVIOContext.protocol_whitelist && protocol_blacklist:取自URLContext.protocol_whitelist && protocol_blacklist
4)AVIOContext.seekable:该字段表征该资源是否可seek,可seek分两种,按字节seek,按时间戳seek
4.1)若不可seek则为0;
4.2)可按字节seek,seekable的AVIO_SEEKABLE_NORMAL置位:标志支持按字节seek,就像对本地文件进行seek一样。取决于URLContext.is_streamed字段是否为1,而该值如前文分析,又取决于:协议支持写入或者协议是file协议(即本地文件),但不支持按字节seek(最终却决于URLProtocol.url_seek函数指针是否为空)。/** * Seeking works like for a local file. */ #define AVIO_SEEKABLE_NORMAL (1 << 0)
4.3)可按时间戳seek,seekable的AVIO_SEEKABLE_TIME置位:标志支持按时间戳seek。取决于URLProtocol.url_read_seek是否存在。
/** * Seeking by timestamp with avio_seek_time() is possible. */ #define AVIO_SEEKABLE_TIME (1 << 1)
4.4)注意这两个宏定义并不冲突,即一个资源即可以支持按字节seek,也可以按照时间戳seek。
5)AVIOContext.read_pause:赋值io_read_pause,该函数源码如下,可见最终还是调用的URLProtocol的url_read_pause函数。static int io_read_pause(void *opaque, int pause) { AVIOInternal *internal = opaque; if (!internal->h->prot->url_read_pause) return AVERROR(ENOSYS); return internal->h->prot->url_read_pause(internal->h, pause); }
6)AVIOContext.read_seek:赋值io_read_seek,该函数源码如下,可以最终还是调用URLProtocol的url_read_seek()函数,该函数表征的是该协议是否支持按时间戳seek
static int64_t io_read_seek(void *opaque, int stream_index, int64_t timestamp, int flags) { AVIOInternal *internal = opaque; if (!internal->h->prot->url_read_seek) return AVERROR(ENOSYS); return internal->h->prot->url_read_seek(internal->h, stream_index, timestamp, flags); }
7) AVIOContext.short_seek_get:赋值io_short_seek,最终还是调用的URLProtocol的url_get_short_seek函数。当前没有从基本概念上理解short_seek_get是什么,做什么用。
static int io_short_seek(void *opaque) { AVIOInternal *internal = opaque; return ffurl_get_short_seek(internal->h); } int ffurl_get_short_seek(URLContext *h) { if (!h || !h->prot || !h->prot->url_get_short_seek) return AVERROR(ENOSYS); return h->prot->url_get_short_seek(h); }
1.1.2.1 avio_alloc_context()
avio_alloc_context() 声明:
- 所属库:libavformat(lavf)
- 头文件:libavformat/avio.h
- 声明:
功能:创建并初始化AVIOContext,并且是带缓冲的io,并且需要使用avio_context_free()来释放该对象。
入参buffer:AVIOContext的输入输出操作都通过该缓冲区进行,该缓冲区必须使用av_malloc()函数族进行创建,使用 av_free()函数进行释放。该缓冲区可能被释放或者是被一个新的缓冲区替代。当前使用的缓冲区由AVIOContext.buffer字段持有。
入参buffer_size:缓冲区大小对于IO性能非常重要,对于那些由固定块大小的协议,必须设置为使用该缓冲区大小;一般的典型的大小为缓存页大小4kb
入参write_flag:如果缓冲区支持写入,则该参数设置为1。
入参read_packet ,write_packet,seek 将在后文分析。/** * 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(). * @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. * @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. * @param write_packet A function for writing the buffer contents, may be NULL. * The function may not change the input buffers content. * @param seek A function for seeking to specified byte position, may be NULL. * * @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));
avio_alloc_context() 源码:
- 所属库:libavformat(lavf)
- 源文件:aviobuf.c
- 源码: 函数调用av_malloc()创建AVIOContext之后,简单的调用ffio_init_context()对AVIOContext进行初始化,此处不再赘述
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)) { AVIOContext *s = av_malloc(sizeof(AVIOContext)); if (!s) return NULL; ffio_init_context(s, buffer, buffer_size, write_flag, opaque, read_packet, write_packet, seek); return s; }
1.1.2.1.1 ffio_init_context()
ffio_init_context() 声明:
- 所属库:libavformat(lavf)
- 头文件:libavformat/avio_internal.h
- 声明:此处就不多说了,该函数主要是对s的各项成员进行初始化工作
int ffio_init_context(AVIOContext *s, 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));
ffio_init_context() 源码:
- 所属库:libavformat(lavf)
- 头文件:libavformat/aviobuf.c
- 源码:
int ffio_init_context(AVIOContext *s, 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)) { memset(s, 0, sizeof(AVIOContext)); s->buffer = buffer; s->orig_buffer_size = s->buffer_size = buffer_size; s->buf_ptr = buffer; s->buf_ptr_max = buffer; s->opaque = opaque; s->direct = 0; url_resetbuf(s, write_flag ? AVIO_FLAG_WRITE : AVIO_FLAG_READ); s->write_packet = write_packet; s->read_packet = read_packet; s->seek = seek; s->pos = 0; s->eof_reached = 0; s->error = 0; s->seekable = seek ? AVIO_SEEKABLE_NORMAL : 0; s->min_packet_size = 0; s->max_packet_size = 0; s->update_checksum = NULL; s->short_seek_threshold = SHORT_SEEK_THRESHOLD; if (!read_packet && !write_flag) { s->pos = buffer_size; s->buf_end = s->buffer + buffer_size; } s->read_pause = NULL; s->read_seek = NULL; s->write_data_type = NULL; s->ignore_boundary_point = 0; s->current_type = AVIO_DATA_MARKER_UNKNOWN; s->last_time = AV_NOPTS_VALUE; s->short_seek_get = NULL; s->written = 0; return 0; }
1. 使用memset()将分配的AVIOContext 空间全初始化为0;
2. 设置buffer相关的5个成员:
2.1)AVIOContext.buffer:指向buffer的指针,由该成员持有当前使用的缓冲区,该缓冲区有可能会被其他缓冲区替换掉,但是.buffer成员会一直指向当前有效的缓冲区(即替换后的缓冲区);
2.2)AVIOContext.buffer_size:缓冲区大小;
2.3)AVIOContext.orig_buffer_size:缓冲区的原始大小,设置为buffer_size大小,作用在于进行文件格式探测时,buffer_size的大小会被改变,文件格式探测结束后需要恢复原始的缓冲区大小,那么.orig_buffer_size就起作用了,因为其保存着该原始值。当然,该成员只在libavformat库内部使用,非公共接口,因此,用户层面不要访问该值。
2.4)AVIOContext.buf_ptr:buffer中的当前位置;该指针在读,写过程中均起作用;
2.5)AVIOContext.buf_ptr_max: 仅有用在写过程中,保存着缓存中已经处理过的数据的位置,调用flush函数将使得buffer指针到buf_ptr_max之前的内容被从缓冲中flush掉;
2.6)与buffer相关的还有两个成员:.pos 与 .buffer_end,在后续过程中会初始化。
3. AVIOContext.opaque && AVIOContext.direct:已经在前文进行了分析,此处不再赘述
4. 调用url_resetbuf重置buffer:根据是读还是写,来修改AVIOContext.buffer_end与.write_flag,源码如下:注意一点的是buffer_end这个成员在读,写中含义不一样。static int url_resetbuf(AVIOContext *s, int flags) { av_assert1(flags == AVIO_FLAG_WRITE || flags == AVIO_FLAG_READ); if (flags & AVIO_FLAG_WRITE) { s->buf_end = s->buffer + s->buffer_size; s->write_flag = 1; } else { s->buf_end = s->buffer; s->write_flag = 0; } return 0; }
5. AVIOContext.write_packet: 这个参数取值为io_write_packet,该函数是aviobuf.c文件中的静态函数。将在后文分析。
6. AVIOContext.read_packet: 这个参数取值为io_read_packet,该函数是aviobuf.c文件中的静态函数。将在后文分析。
7. AVIOContext.seek:这个参数取值为io_seek,该函数是aviobuf.c文件中的静态函数。将在后文分析。
8. 后续的字段就不再一一赘述,以后会写文章专门分析AVIOContext结构及其各字段的含义,后续主要分析5,6,7三个字段的含义。
此处特别提出一点:由ffio_fdopen(),avio_alloc_context(),ffio_init_context()分析可以得知,AVIOContext结构体的几个函数指针成员read_packet,write_packet,seek,read_pause,read_seek,short_seek_get赋值的都是在libavformat/aviobuf.c 文件中的对应的以io_开头的静态函数:io_read_packet,io_write_packet,io_seek,io_read_pause,io_read_seek,io_short_seek。这些函数最终会调用到URLProtocol中对应的url_开头的函数。
这些函数将在 FFMPEG4.1源码分析之 IO操作相关Structures && APIs 进行分析。
1.1.3 ffurl_close()
ffurl_close() 声明:
- 所属库:libavformat(lavf)
- 头文件:libavformat/url.h
- 声明:这两个函数所起的作用是关闭URLContext所访问的资源;释放URLContext所占用的内存空间,并且将指向URLContext的指针置为空。
/** * Close the resource accessed by the URLContext h, and free the * memory used by it. Also set the URLContext pointer to NULL. * * @return a negative value if an error condition occurred, 0 * otherwise */ int ffurl_closep(URLContext **h); int ffurl_close(URLContext *h);
ffurl_close() 源码:
- 所属库:libavformat(lavf)
- 源文件:libavformat/avio.c
- 源码:ffurl_close函数简简单单就调用了ffurl_closep函数,注意传入的参数是&h,可以解决悬空指针问题,即最终h=NULL
int ffurl_close(URLContext *h) { return ffurl_closep(&h); }
int ffurl_closep(URLContext **hh) { URLContext *h= *hh; int ret = 0; if (!h) return 0; /* can happen when ffurl_open fails */ // 如果is_connected被置位,即url_open2或者url_open函数执行成功 // 并且URLProtocol的url_close函数存在,则调用url_close关闭URLContext // 访问的资源 if (h->is_connected && h->prot->url_close) ret = h->prot->url_close(h); // 如果配置了访问访问,并且协议是需要网络访问的,那么意味着初始化了网络库 // 因此需要调用ff_network_close()函数来关闭网络库 #if CONFIG_NETWORK if (h->prot->flags & URL_PROTOCOL_FLAG_NETWORK) ff_network_close(); #endif // 释放URLContext所占用的空间,并且使用av_freep函数来将指针置空 if (h->prot->priv_data_size) { if (h->prot->priv_data_class) av_opt_free(h->priv_data); av_freep(&h->priv_data); } av_opt_free(h); av_freep(hh); return ret; }
上述函数还是比较简单的,源码上进行了注释,过程就不再赘述。此处再关注下网络库清理函数 ff_network_close()。源码如下:发现,如果是windows环境,使用的是winsock网络库,那么需要调用WSACleanup();的windows api来卸载网络库
void ff_network_close(void) { #if HAVE_WINSOCK2_H WSACleanup(); #endif }
2 io_close_default()
io_close_default() 源码:
- 所属库:libavformat(lavf)
- 头文件:无,静态函数
- 源文件:libavformat/options.c
- 作用:打开URL,创建AVIOContext对象
static void io_close_default(AVFormatContext *s, AVIOContext *pb)
{
avio_close(pb);
}