函数原型
int avformat_open_input(
AVForamtContext** ps, // 跟容器相关,比如文件格式、元数据. 通用信息存放在这里
const char* filename, // 文件名
const AVInputFormat* fmt, // 输入格式结构体,特定信息存放在这里
AVDictionary** options) // 选项
在ffplay中就会用到这个函数
参数如下:
在源码分析之前先看一下AVInputFormat和AVFormatContext这俩有啥区别
AVInputFormat
AVFormatContext
AVFormatContext主要用于描述和管理媒体文件的总体信息,甚至还包含了AVInputFormat.
AVInputFormat标识输入格式的一些信息,如何解析某种格式的文件,提供了相关的回调函数
源码剖析
1. 设置变量
上面代码分配了AVFormatContext结构体,设置文件名url、将选项设置到AVFormatContext里等等.
2. 调用init_input函数
传入三个参数: AVFormatContext、文件名、参数选项
这个函数代码较短,主要做的工作就是猜测文件格式
static int init_input(AVFormatContext* s, const char* filename,
AVDictionary** options)
{
int ret;
AVProbeData pd = {filename, NULL, 0};
int score = AVPROBE_SCORE_RETRY; // 等于25,用来计算探测后的分数
if (s->pb) // 如果开始就设置了AVIOContext
{
s->flags |= AVFMT_FLAG_CUSTOM_IO; // 那么就设置为自定义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(...); // 日志
return 0;
}
// 由ffmpeg来创建AVInputFormat,然后猜测格式
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);
}
这里面用到了个结构体AVProbeData,这个主要是包含探测文件的格式所需的数据
struct AVProbeData {
const char* filename; // 文件名
unsigned char* buf; // 缓冲区
int buf_size; // 缓冲区大小
const char* mime_type;
}
这个函数用来探测格式,然后定义了分数然后返回.最主要的核心函数就是av_probe_input_buffer2.所以整个函数看起来非常短小.
一般ifromat默认都是为NULL,所以我们会进入到第二个if条件里,然后调用av_probe_input_format2函数
3. av_probe_input_format2函数
同样,这个函数代码量也很小.
const AVInputFormat *av_probe_input_format2(ff_const59 AVProbeData *pd,
int is_opened,
int *score_max) // 默认传入分数25
{
int score_ret; // 存储探测格式后的分数
// 从文件读取一些数据用于探测格式,并返回AVInputFormat对象
const AVInputFormat *fmt = av_probe_input_format3(pd, is_opened, &score_ret);
if (score_ret > *score_max)
{
*score_max = score_ret; // 设置探测后的分数
return fmt;
}
else
return NULL;
}
这个函数并没有做什么操作,主要调用av_probe_input_format3函数用于探测.最后将AVInputFormat对象返回并设置探测后的分数
4. av_probe_input_format3函数
const AVInputFormat* av_probe_input_format3(const AVProbeData* pd, int is_opned,
int* socre_ret)
{
AVProbeData lpd = *pd;
const AVInputFormat* fmt1 = NULL;
const AVInputFormat* fmt = NULL;
int score, score_max = 0;
void* i = 0; // 下标,后续遍历会使用到
const static uint8_t zerobuffer[AVPROBE_PADDING_SIZE]; // 数组32大小
enum nodat {
NO_ID3,
ID3_ALMOST_GREATER_PROBE,
ID3_GREATER_PROBE,
ID3_GREATER_MAX_PROBE,
} nodat = NO_ID3; // 默认设置为NO_ID3
if (!lpd.buf) // 将AVProbeData的buf缓冲区指向zerobuffer
lpd.buf = (unsigned char*)zerobuffer;
... 暂时省略
}
上面代码设置一些变量,这里定义了一个数组,然后让AVProbeData的buf指向这个数组.
然后设置了一个枚举变量设置为NO_ID3
然后继续后面的代码
if (lpd.buf_size > 10 && ff_id3v2_match(lpd.buf, ID3v2_DEFAULT_MAGIC))
{
int id3len = ff_id3v2_tag_len(lpd.buf);
if (lpd.buf_size > id3len + 16)
{
if (lpd.buf_size < 2LL * id3len + 16)
nodat = ID3_ALMOST_GREATER_PROBE;
lpd.buf += id3len;
lpd.buf_size -= id3len;
}
else if (id3len >= PROBE_BUF_MAX)
nodat = ID3_GREATER_MAX_PROBE;
else
nodat = ID3_GREATER_PROBE;
}
上面代码主要检测ID3v2标签(通常用于MP3文件元数据).如果缓冲区大小大于10,并且缓冲区的开头与ID3v2的魔数匹配,则检测是否存在ID3标签. 根据ID3标签的大小调用lpd.buf和lpd.buf_size,使得后续的格式探测从ID3标签之后的数据开始
接下来遍历格式列表,进行格式探测,然后调整分数
while ((fmt1 = av_demuxer_iterate(&i)) // 从demuxer_list列表中获取AVInputFormat
{
// 如果is_opened == 0 且格式要求不能打开文件(AVFMT_NOFILE),则跳过
// 如果格式为image2则跳过,因为image2不能进行格式探测
if (!is_opened == !(fmt1->flags & AVFMT_NOFILE) && strcmp(fmt1->name, "image2"))
continue;
score = 0;
if (fmt1->read_probe) // 如果设置了read_probe回调函数
{
socre = fmt1->read_probe(&lpd); // 则调用read_probe进行探测并返回得分
if (score)
av_log(...); // 日志输出
// 如果支持文件扩展名,并且文件名与扩展名匹配,则根据探测到的ID3标签调整分数
if (fmt1->extensions && av_match_ext(lpd.filename, fmt1->extensions))
{
switch (nodat)
{
case NO_ID3:
score = FFMAX(score, 1);
break;
case ID3_GREATER_PROBE:
case ID3_ALMOST_GREATER_PROBE:
score = FFMAX(score, AVPROBE_SCORE_EXTENSION / 2 - 1);
break;
case ID3_GREATER_MAX_PROBE:
score = FFMAX(score, AVPROBE_SCORE_EXTENSION);
break;
}
}
}
else if (fmt1->extensions)
{
if (av_match_ext(lpd.filename, fmt1->extensions))
score = AVPROBE_SCORE_EXTENSION; // 扩展格式分数为 50
}
if (av_match_name(lpd.mime_type, fmt1->mime_type))
{
// 如果输入数据的MIME类型与当前格式的MIME类型匹配,则分数设置为75(较高分数)
if (AVPROBE_SCORE_MIME > score)
{
av_log(..); // 日志输出
score = AVPROBE_SCORE_MIME; // MIME分数为 75
}
}
if (score > score_max) // 更新最高分数和匹配格式
{
score_max = score;
fmt = (AVInputFormat*)fmt1;
}
else if (score == score_max)
fmt = NULL;
}
if (nodat == ID3_GREATER_PROBE)
score_max = FFMIN(AVPROBE_SCORE_EXTENSION / 2 - 1, score_max);
*score_ret = score_max;
return fmt;
上面代码都加了注释,在这里总结一下这个函数所做的事情.
默认有个
demuxer_list列表,这个大小是325,类型是AVInputFormat. 这个函数遍历完整个这个列表,然后调用read_probe回调函数探测格式设置分数,这个是每个格式都有自己的格式解析函数. 然后设置最高分数.
如果设置了最高分数还会继续遍历,直到把这个列表遍历完,最终返回分数最高demuxer_list中的某个AVInputFormat
所以时间复杂度还是挺高的,O(N)
上面代码av_demuxer_iterate是怎么做的呢?下面来分析一下:
const AVInputFormat* av_demuxer_iterate(void** opaque)
{
// 计算这个数组的大小
static const uintptr_t size = sizeof(demuxer_list) / sizeof(demuxer_list[0]) - 1;
uintptr_t i = (uintptr_t)*opaque; // 设置数组下标,默认是0
const AVInputFormat* f = NULL;
if (i < size)
f = demuxer_list[i]; // 读取列表的AVInputFormat对象
else if (indev_list)
f = indev_list[i - size];
if (f)
*opaque = (void*)(i + 1); // 下标加1
return f;
}
参数opaque存储的是0,在这里就代表数组下标,从0开始获取.然后遍历demuxer_list列表,从中获取AVInputFormat对象
demuxer_list这个列表定义在libavformat/demuxer_list.c
然后每个解复用器都会自己定义上图的结构,比如
ff_aa_demuxer
至此av_probe_input_format2函数解析完成,让我们把视角再次回到init_input函数中
前面我们分析的是这个函数,那么计算完分数后,该执行下面的
s->io_open函数
5. s->io_open函数
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;
if (s->open_cb)
return s->open_cb(s, pb, url, flags, &s->interrupt_callback, options);
return ffio_open_whitelist(pb, url, flags, &s->interrupt_callback, options,
s->protocol_whitelist, s->protocol_blacklist);
}
这个函数最主要的就是最后一行代码,ffio_open_whitelist,用来设置AVIOContext的读、写、seek回调函数.还有就是设置AVFormatContext的AVIOContext成员,用于文件IO
然后再回到init_input函数,接下来剩下最后一个函数分析了.
6. av_probe_input_buffer2函数
int av_probe_input_buffer2(AVIOContext* pb, const AVInputFormat** fmt,
const char* filename, void* logctx, unsigned int offset,
unsigned int max_probe_size)
{
AVProbeData pd = {filename ? filename : ""};
uint8_t buf = NULL;
int ret = 0, probe_size, buf_offset = 0;
int score = 0;
int ret2;
if (!max_probe_size)
max_probe_size = PROBE_BUF_MAX;
else if (max_probe_size < PROBE_BUF_MIN)
{
av_log(...); // 日志输出
return AVERROR(EINVAL);
}
if (offset >= max_probe_size)
return AVERROR(EINVAL);
if (pb->av_class)
{
uint8_t mime_type_opt = NULL;
char* semi;
av_opt_get(pb, "mime_type", AV_OPT_SEARCH_CHILDREN, &mime_type_opt);
pd.mime_type = (const char*)mime_type_opt;
semi = pd.mime_type ? strchr(pd.mime_type, ';') : NULL;
if (semi)
*semi = '\0';
}
.. 暂时省略代码,后续继续分析
}
上面代码就是做一些简单的设置工作,没什么好解释的.下面讲解后面的for循环读取数据然后探测格式
// 探测数据大小默认2048,每次循环后增加2倍
for (probe_size = PROBE_BUF_MIN; probe_size <= max_probe_size && !*fmt;
probe_size = FFMIN(probe_size << 1, FFMAX(max_probe_size, probe_size + 1)))
{
score = probe_size < max_probe_size ? AVPROBE_SCORE_RETRY : 0;
// 分配缓冲区
if ((ret = av_reallocp(&buf, probe_size + AVPROBE_PADDING_SIZE)) < 0)
goto fail;
// 读取数据到缓冲区里
if ((ret = avio_read(pb, buf + buf_offset, probe_size - buf_offset)) < 0)
{
if (ret != AVERROR_EOF) // 读取失败则跳转到fail标号
goto fail;
score = 0;
ret = 0;
}
buf_offset += ret; // 记录已读取的字节数
if (buf_offset < offset)
continue;
pd.buf_size = buf_offset - offset;
pd.buf = &buf[offset];
memset(pd.buf + pd.buf_size, 0, AVPROBE_PADDING_SIZE);
// 探测数据格式
*fmt = av_probe_input_format2(&pd, 1, &score);
if (*fmt)
{
if (score <= AVPROBE_SCORE_RETRY) // 分数较低发出警告可能存在误判
{
av_log(...); // 日志输出
}
else
// 同样也是日志 <--- 这里是分数较高
}
}
if (!*fmt) // 未找到匹配的格式
ret = AVERROR_INVALIDDATA;
fail:
// 重置缓冲区
ret2 = ffio_rwind_with_probe_data(pb, &buf, buf_offset);
if (ret >= 0)
ret = ret2;
av_freep(&pd.mime_type); // 释放MIME内存,返回探测分数或错误码
return ret < 0 ? ret : score;
上面的代码总结就是分配缓冲区,默认大小是2048,每次增加2倍直到max_probe_size大小(1048576). 然后调用avio_read读取数据到缓冲区,调用av_probe_input_format2探测格式.最终返回分数和AVInputFormat对象
至此init_input函数到此解析完成.让我们把视角重新回到avformat_open_input调用init_input函数后
7. avformat_open_input函数后续代码
此处的
ret返回值应该是100,表示成功探测出了格式.
然后剩余后续代码工作:
- 设置duration和start_time
- 分配私有数据priv_data内存
- 调用s->iformat->read_header函数确定流的信息,有几路流
- 处理元数据
- 设置编解码器id
最后总结
- 初始化
AVFormatContext - 打开输入源
- 探测输入格式
- 读取并解析文件头,确定输入流信息.比如有几路流,编解码器id是什么