从学龄前开始解读FFMPEG代码 之 avformat_open_input函数 三
开始学龄前学习前想说的话
这一部分的内容是回到init_input函数,并在完成init_input函数的学习之后,解决avformat_open_input剩下的内容,其实在第二章中,搞懂了三个主要的函数,剩下的内容就不是很多了(坑了许久,捡起来拍拍灰尘)。
init_input()使用三个函数做了什么
在这一部分的第一篇文章中,简单展示了init_input函数的作用,现在,再回顾一下:
static int init_input(AVFormatContext *s, const char *filename,
AVDictionary **options)
{
int ret;
AVProbeData pd = { filename, NULL, 0 };
int score = AVPROBE_SCORE_RETRY;
if (s->pb) {
s->flags |= AVFMT_FLAG_CUSTOM_IO;
if (!s->iformat)
return av_probe_input_buffer2(s->pb, &s->iformat, filename,
s, 0, s->format_probesize);
else if (s->iformat->flags & AVFMT_NOFILE)
av_log(s, AV_LOG_WARNING, "Custom AVIOContext makes no sense and "
"will be ignored with AVFMT_NOFILE format.\n");
return 0;
}
if ((s->iformat && s->iformat->flags & AVFMT_NOFILE) ||
(!s->iformat && (s->iformat = av_probe_input_format2(&pd, 0, &score))))
return score;
if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)
return ret;
if (s->iformat)
return 0;
return av_probe_input_buffer2(s->pb, &s->iformat, filename,
s, 0, s->format_probesize);
}
在第二篇文章中,我们对这里面所调用的三个函数av_probe_input_buffer2(),av_probe_input_format2()以及avio_read,进行了总结,分别用一句话总结功能就是:
av_probe_input_buffer2()函数的主要功能就是从输入的url中推断出文件的format
avio_read()函数所做的事就是从IO上下文的文件数据读入到缓存buffer中
av_probe_input_format2()函数所做的事就是循环查format表,让注册的format调用自己的probe函数来判断文件是否属于该种文件format
init_input函数整体并不算长,主要由四个if以及最后一个return构成:
init_input中进来的第一个if,首先判断输入的AVFormatContext中的pb是否设置好。这个pb实际上是一个AVIOContext,这pb,要么就在执行avformat_open_input之前,由调用函数的用户手动设置好,要么,就由avformat_open_input来设置好。在pb已经设置好的时候,情况就很简单了,这代表着用户(实际上代码里写作caller,也就是调用接口的一方,不过我喜欢称作为用户,我就算是ffmpeg的用户嘛)已经设置好了,这个时候,对s所包含的flag标记位置中加上一个 AVFMT_FLAG_CUSTOM_IO,意味着,这个format是由用户来设置好IO的,不需要你open_input多插手。(当然也有可能open_input_format被重复调用了,设置好了之后这也防止了重复设置,没有意义了)
然后,判断s中的输入format,即iformat有没有设置好,没设置好那么就直接调用av_probe_input_buffer2()来用buffer的方式查出文件的format就好了。如果设置好了,那也不需要去probe了,唯一的异常情况就是输入iformat中设置了AVFMT_NOFILE的标记位,这代表着没有输入文件,这种情况下,给个warning,ffmpeg也就撒手不管了(return 0)。
接下来,来到了第二个if,在第二个if中,有两种可能,第一种是输入文件不存在的情况,还有一种可能是s中没有设置好输入iformat,并且进行一次av_probe_input_format2来猜出了具体的format是什么,这个时候,score已经确定下来了(第一种可能,直接返回25分,第二种,通过probe已经得到了对应的得分),那么直接返回score就好。
然后是第三个if,这里,调用了s的io_open函数,并传入了所需要的参数,来获取到最终的ret,一开始看到io_open函数我还以为漏分析了函数,后来才发现这个io_open是回调函数,也就是说,需要每种AVFormatContext在注册时自己就写好的,不同的AVFormatContext拥有自己独特的io_open函数,这个函数的用法就是创建并设置好一个AVIOContext,在这里的话也就是s的pb了,就像第一个if中说的,这个pb也可能由avformat_open_input来做好设置。通过s的io_open,就可以创建IOContext并打开文件里视频流了,这个回调函数最终也会返回一个int的ret
第四个if里,如果文件已经设置好了iformat,那么不需要做无意义的检查,ffmpeg直接撒手摆烂。(return 0)
最后的return中,还是av_probe_input_buffer2()来终结一切可能存在的不知道文件格式的情况,通过载入数据来probe出文件内容。
从上面来看,init_input确实很实诚,函数如其名,很好地完成了初始化IO并推测出文件格式的任务,最后它还能返回给你一个评分,来表示自己的推测靠谱程度有多少,很人性化了。
最终回到了avformat_open_input中
回到这个函数中,剩下的内容看起来很多,但操作都不是那么复杂了,完整版的代码就不贴了,在同文章第一篇中有贴过,直接从init_input开始往后贴然后一起看一下。
if ((ret = init_input(s, filename, &tmp)) < 0)
goto fail;
s->probe_score = ret;
if (!s->protocol_whitelist && s->pb && s->pb->protocol_whitelist) {
s->protocol_whitelist = av_strdup(s->pb->protocol_whitelist);
if (!s->protocol_whitelist) {
ret = AVERROR(ENOMEM);
goto fail;
}
}
if (!s->protocol_blacklist && s->pb && s->pb->protocol_blacklist) {
s->protocol_blacklist = av_strdup(s->pb->protocol_blacklist);
if (!s->protocol_blacklist) {
ret = AVERROR(ENOMEM);
goto fail;
}
}
if (s->format_whitelist && av_match_list(s->iformat->name, s->format_whitelist, ',') <= 0) {
av_log(s, AV_LOG_ERROR, "Format not on whitelist \'%s\'\n", s->format_whitelist);
ret = AVERROR(EINVAL);
goto fail;
}
avio_skip(s->pb, s->skip_initial_bytes);
/* Check filename in case an image number is expected. */
if (s->iformat->flags & AVFMT_NEEDNUMBER) {
if (!av_filename_number_test(filename)) {
ret = AVERROR(EINVAL);
goto fail;
}
}
s->duration = s->start_time = AV_NOPTS_VALUE;
/* Allocate private data. */
if (s->iformat->priv_data_size > 0) {
if (!(s->priv_data = av_mallocz(s->iformat->priv_data_size))) {
ret = AVERROR(ENOMEM);
goto fail;
}
if (s->iformat->priv_class) {
*(const AVClass **) s->priv_data = s->iformat->priv_class;
av_opt_set_defaults(s->priv_data);
if ((ret = av_opt_set_dict(s->priv_data, &tmp)) < 0)
goto fail;
}
}
/* e.g. AVFMT_NOFILE formats will not have a AVIOContext */
if (s->pb)
ff_id3v2_read_dict(s->pb, &s->internal->id3v2_meta, ID3v2_DEFAULT_MAGIC, &id3v2_extra_meta);
if (!(s->flags&AVFMT_FLAG_PRIV_OPT) && s->iformat->read_header)
if ((ret = s->iformat->read_header(s)) < 0)
goto fail;
if (!s->metadata) {
s->metadata = s->internal->id3v2_meta;
s->internal->id3v2_meta = NULL;
} else if (s->internal->id3v2_meta) {
int level = AV_LOG_WARNING;
if (s->error_recognition & AV_EF_COMPLIANT)
level = AV_LOG_ERROR;
av_log(s, level, "Discarding ID3 tags because more suitable tags were found.\n");
av_dict_free(&s->internal->id3v2_meta);
if (s->error_recognition & AV_EF_EXPLODE) {
ret = AVERROR_INVALIDDATA;
goto close;
}
}
if (id3v2_extra_meta) {
if (!strcmp(s->iformat->name, "mp3") || !strcmp(s->iformat->name, "aac") ||
!strcmp(s->iformat->name, "tta") || !strcmp(s->iformat->name, "wav")) {
if ((ret = ff_id3v2_parse_apic(s, &id3v2_extra_meta)) < 0)
goto close;
if ((ret = ff_id3v2_parse_chapters(s, &id3v2_extra_meta)) < 0)
goto close;
if ((ret = ff_id3v2_parse_priv(s, &id3v2_extra_meta)) < 0)
goto close;
} else
av_log(s, AV_LOG_DEBUG, "demuxer does not support additional id3 data, skipping\n");
}
ff_id3v2_free_extra_meta(&id3v2_extra_meta);
if ((ret = avformat_queue_attached_pictures(s)) < 0)
goto close;
if (!(s->flags&AVFMT_FLAG_PRIV_OPT) && s->pb && !s->internal->data_offset)
s->internal->data_offset = avio_tell(s->pb);
s->internal->raw_packet_buffer_remaining_size = RAW_PACKET_BUFFER_SIZE;
update_stream_avctx(s);
for (i = 0; i < s->nb_streams; i++)
s->streams[i]->internal->orig_codec_id = s->streams[i]->codecpar->codec_id;
if (options) {
av_dict_free(options);
*options = tmp;
}
*ps = s;
return 0;
close:
if (s->iformat->read_close)
s->iformat->read_close(s);
fail:
ff_id3v2_free_extra_meta(&id3v2_extra_meta);
av_dict_free(&tmp);
if (s->pb && !(s->flags & AVFMT_FLAG_CUSTOM_IO))
avio_closep(&s->pb);
avformat_free_context(s);
*ps = NULL;
return ret;
}
从第一行开始,用s的成员probe_score记录init_open推测格式的最终得分,在这个时间段的时候,作为IOContext的pb已经被打开了,之后对s的pb中存在的协议黑白名单拷贝到s的黑白名单,黑名单中是禁止的协议protocol,白名单是允许的protocol,这些protocal都是用char*的方式来存储的,多个protocal中间用逗号隔开就行。
之后,如果s的IOContext pb,在设置了黑白名单的情况下,就把黑白名单复制一下,复制到s的黑白名单中。
完成了黑白名单复制之后,如果s设置了format_whitelist,也就是说只接受某些特定格式文件,那么还需要检查输入文件格式是否在白名单中,不在白名单中一律枪毙。
27行中。s的pb指针还需要做一个前移,移动到打开视频stream的位置。
之后检查flags中是否需要数字表示,比如在做一系列的解码任务是,会存在类似于test%03d.jpg 这种表示法,代表着(test001.jpg,test002.jpg …)一序列的图片,进行设置检查,然后再设置好s的duration和start_time(设置为0);之后,如果AVFormatContext还有外接的内存(可能在不同开发者自己注册的AVFormatContext中,会需要额外的数据结构来维护一些功能,所以需要额外进行划分)为s->priv_data设置划分好数据空间。
之后,从54行开始,需要对文件的ID3v2数据做好处理。首先,使用ff_id3v2_read_dict函数,该函数的备注说明该函数功能是将文件中的ID3v2数据置入到AVDictionary中,该函数实际的调用是libavformat/id3v2.c中的函数,
id3v2_read_internal(AVIOContext *pb, AVDictionary **metadata,
AVFormatContext *s, const char *magic,
ID3v2ExtraMeta **extra_meta, int64_t max_search_size)
但是在这里调用的时候,最后传入的max_search_size被设置为了0,这样在实际的操作过程中,直接在该函数处理的第一步就直接被返回了,所以是没有做操作
在58行,使用了另外一个比较重要的函数,在iformat对应的格式设置了read_header回调函数时,需要进行该函数功能,读取文件头并做好初始化工作。基本上对于每个格式的read_header函数,都会创建好AVStream,并等待在后续的流程中,可以取出或写入音视频流信息。
之后,是61行到74行,对id3v2的额外数据的处理,对于相应的音频格式,处理好id3v2数据,而其他格式则不需要。
在90行,则是用函数avformat_queue_attached_pictures来处理好音频流中可能需要附加的照片(这里可以举个例子说明一下:在播放音频时出现的类似专辑封面的照片,就是所谓的音频附加的照片,这种经常可以在各种音乐播放器中看见。)
93行的位置,做好处理完成数据之后的数据指针位移,然后98-101行,将s中已经打开的AVStream进行更新,完成每个音视频流的配置。这里的update_stream_avctx()函数也是一个很重要的函数,下面可以看一看它是干啥的。在update之后,由于一个视频文件中可能包含多个码流stream,所以就遍历所有的码流,并设置好orig_codec_id初始的codec id,也就是说,最后的这一点工作,也涉及到了编解码器的配置任务,在这个任务之后,avformat_open_input收尾工作开始,该设置的设置,该销毁的销毁。
update_stream_avctx()的作用
这个函数只在open_input这里曾经用过,其实算是处理文件中视频码流(AVStream)的开头,相当于太极拳里的起势,一般做解码的话,接下来的工作就是围绕着AVStream来展开的。
update_stream_avctx()的实现在文件libavformat/utils.c中,内容如下:
static int update_stream_avctx(AVFormatContext *s)
{
int i, ret;
for (i = 0; i < s->nb_streams; i++) {
AVStream *st = s->streams[i];
if (!st->internal->need_context_update)
continue;
/* close parser, because it depends on the codec */
if (st->parser && st->internal->avctx->codec_id != st->codecpar->codec_id) {
av_parser_close(st->parser);
st->parser = NULL;
}
/* update internal codec context, for the parser */
ret = avcodec_parameters_to_context(st->internal->avctx, st->codecpar);
if (ret < 0)
return ret;
#if FF_API_LAVF_AVCTX
FF_DISABLE_DEPRECATION_WARNINGS
/* update deprecated public codec context */
ret = avcodec_parameters_to_context(st->codec, st->codecpar);
if (ret < 0)
return ret;
FF_ENABLE_DEPRECATION_WARNINGS
#endif
st->internal->need_context_update = 0;
}
return 0;
}
首先进入st的一个大的遍历中,不需要update信息的stream则进行跳过,需要update的,那么继续接下来的流程。
接下来是一个判断,如果遍历到的当前的stream拥有分析器parser,且st解码器id与parser的解码器id不一致,那么就关闭parser,正如同注释中所说的那样:close parser, because it depends on the codec,这都是取决于解码器的。
之后,按照注释里写的,update internal codec context, for the parser,也就是说从parser中取出信息,并更新stream internal对应的codec编解码器,同样的招式对codec也是有效的,那么就再使一次,更新一下每个stream所设置的codec;
完成了update之后,这个就不需要update惹,所以将标记变量设置为不需要update,结束整个函数的执行。
总结与最后的一些话
鸽了许久,把open_input可算是更新完了,这几个月事情比较多,更新有所暂停,接下来的目标是一到二周更新一下,下一篇文章,就可以从一些具体的文件类型入手,熟悉一下不同的文件类型到底是怎么分辨自己的文件的,以mp4或者flv为例子,这样会更好去理解read_probe()以及read_head()这两个函数大概是要做什么的。