ffmpeg源码分析(九)解协议
本文将聚焦于FFmpeg协议处理模块,以avformat_open_input
函数为核心,详细剖析其在最新FFmpeg源码中的实现。
音视频处理流程简介
avformat_open_input概述
avformat_open_input
是FFmpeg用于打开输入多媒体数据的关键函数。它通过统一的接口处理多种协议,使用户无需关心底层实现。该函数的声明如下,位于libavformat/avformat.h
:
int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);
-
ps:指向
AVFormatContext
结构体的指针,用于存储打开的多媒体数据上下文。 -
url:要打开的资源URL。
-
fmt:指定输入格式,如果为NULL,FFmpeg会自动探测。
-
options:用于传递其他选项(字典形式)。
这个函数主要完成的功能就是探测输入的格式,然后配置格式的上下文,
比如输入一个mp4文件的地址,它会判断出这是mp4文件,然后AVFormatContext 就知道该怎么处理这个文件了。
avformat_open_input 打开数据流
/**
* @brief 打开一个输入媒体文件。
*
* @param ps 用户提供的 AVFormatContext 的指针,可能是一个指向 NULL 的指针来分配一个新的上下文。
* @param filename 要打开的文件名。
* @param fmt 如果非 NULL,此参数强制使用特定的输入格式。
* @param options 填充 AVFormatContext 和解复用器私有选项的字典。返回时,该参数将被销毁并替换为包含未找到选项的字典。
*
* @return 成功返回0,失败返回负的 AVERROR。
*/
int avformat_open_input(AVFormatContext **ps, const char *filename,
const AVInputFormat *fmt, AVDictionary **options)
{
AVFormatContext *s = *ps; // 获取提供的格式上下文
FFFormatContext *si; // 内部格式上下文
AVDictionary *tmp = NULL; // 用于选项的临时字典
int ret = 0; // 返回值
// 如果没有提供格式上下文,则分配一个新的,也就是说这个格式上下文是可以由外部分配创建的
if (!s && !(s = avformat_alloc_context()))
return AVERROR(ENOMEM);
// 获取内部格式上下文,这相当于给AVFormatContext补充额外的上下文,这在ffmepg中是比较常见的。注意补充字段的内存是在avformat_alloc_context中分配的,所以不要自己直接malloc AVFormatContext
si = ffformatcontext(s);
if (!s->av_class) {
av_log(NULL, AV_LOG_ERROR, "输入上下文未通过 avformat_alloc_context() 正确分配\n");
return AVERROR(EINVAL);
}
if (fmt)
s->iformat = fmt; // 设置输入格式
if (options)
av_dict_copy(&tmp, *options, 0);
// 如果设置了IO上下文,则设定定制的解复用标志
if (s->pb)
s->flags |= AVFMT_FLAG_CUSTOM_IO;
if ((ret = av_opt_set_dict(s, &tmp)) < 0)
goto fail; // 设置格式上下文选项失败
// 拷贝文件名
if (!(s->url = av_strdup(filename ? filename : ""))) {
ret = AVERROR(ENOMEM);
goto fail;
}
// 【重要】探测输入格式
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, "格式不在白名单 '%s' 中\n", s->format_whitelist);
ret = AVERROR(EINVAL); // 非法参数错误
goto fail;
}
avio_skip(s->pb, s->skip_initial_bytes); // 跳过初始字节
// 检查文件名是否需要图像编号
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;
// 分配私有数据
if (ffifmt(s->iformat)->priv_data_size > 0) {
if (!(s->priv_data = av_mallocz(ffifmt(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;
}
}
// mp3,读取 ID3v2 元数据
if (s->pb)
ff_id3v2_read_dict(s->pb, &si->id3v2_meta, ID3v2_DEFAULT_MAGIC, NULL);
// 【重要】调用iformat格式的 read_header 函数
if (ffifmt(s->iformat)->read_header)
if ((ret = ffifmt(s->iformat)->read_header(s)) < 0) {
if (ffifmt(s->iformat)->flags_internal & FF_INFMT_FLAG_INIT_CLEANUP)
goto close;
goto fail;
}
// 处理 ID3 标签
//...
// 分析 ID3v2 额外数据,如果是特殊格式
if (!strcmp(s->iformat->name, "mp3") || !strcmp(s->iformat->name, "aac")) {
// 处理 ID3v2 的 APIC、章节和隐私数据
// ...
}
// 队列附加的图片
if ((ret = avformat_queue_attached_pictures(s)) < 0)
goto close;
if (s->pb && !si->data_offset)
si->data_offset = avio_tell(s->pb); // 更新数据偏移
si->raw_packet_buffer_size = 0; // 初始化缓冲区大小
update_stream_avctx(s); // 更新流的解码上下文
// 清理选项字典
if (options) {
av_dict_free(options);
*options = tmp;
}
*ps = s;
return 0;
close:
//【重要】调用iformat格式的 read_close 函数
if (ffifmt(s->iformat)->read_close)
ffifmt(s->iformat)->read_close(s);
fail:
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;
}
init_input 初始化媒体输入流
该函数的主要功能是初始化输入媒体文件的处理环境,包括格式探测及文件打开操作。同时,代码设计中考虑了多种情况,包括用户提供的自定义 IO 和文件格式和网络格式,确保了高灵活性和广泛的适用性。
static int init_input(AVFormatContext *s, const char *filename,
AVDictionary **options)
{
int ret;
AVProbeData pd = { filename, NULL, 0 };
int score = AVPROBE_SCORE_RETRY;
//处理自定义io
if (s->pb) {
s->flags |= AVFMT_FLAG_CUSTOM_IO;
// 如果输入格式未指定
if (!s->iformat)
// 从自定义 IO 缓冲区中探测输入格式
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 探测输入格式
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;
int eof = 0; // 文件结束标志
// 检查并设置最大探测大小,以及偏移量
...
// 获取 MIME 类型, MIME 类型信息可以被用来指示文件的媒体类型,使ffmpeg快的处理流信息
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; // 截断 MIME 类型
if (semi) {
*semi = '\0';
}
}
// 探测循环
for (probe_size = PROBE_BUF_MIN; probe_size <= max_probe_size && !*fmt && !eof;
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)
goto fail;
score = 0; // 到达文件末尾
ret = 0;
eof = 1;
}
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(logctx, AV_LOG_WARNING, "格式 %s 仅以低得分 %d 检测到,可能存在错误识别。\n", (*fmt)->name, score);
} else {
av_log(logctx, AV_LOG_DEBUG, "格式 %s 已通过 size=%d 和 score=%d 探测到\n", (*fmt)->name, probe_size, score);
}
}
}
if (!*fmt)
ret = AVERROR_INVALIDDATA; // 如果未找到格式,则返回格式无效错误
fail:
// 回退并重用探测缓冲区
ret2 = ffio_rewind_with_probe_data(pb, &buf, buf_offset);
if (ret >= 0)
ret = ret2;
av_freep(&pd.mime_type); // 释放 MIME 类型内存
return ret < 0 ? ret : score; // 返回探测得分或错误
}
av_probe_input_format2
av_probe_input_format2实际的探测av_probe_input_format3里
const AVInputFormat *av_probe_input_format3(const AVProbeData *pd,
int is_opened, int *score_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];
enum nodat {
NO_ID3,
ID3_ALMOST_GREATER_PROBE,
ID3_GREATER_PROBE,
ID3_GREATER_MAX_PROBE,
} nodat = NO_ID3;
if (!lpd.buf)
// 如果缓冲区为空,则填充
lpd.buf = (unsigned char *) zerobuffer;
if (lpd.buf_size > 10 && ff_id3v2_match(lpd.buf, ID3v2_DEFAULT_MAGIC)) {
...
// 检查并处理 ID3v2 标签
}
// 迭代所有支持输入格式
while ((fmt1 = av_demuxer_iterate(&i))) {
if (fmt1->flags & AVFMT_EXPERIMENTAL)
continue;// 跳过实验性格式
// 特别处理 image2文件
if (!is_opened == !(fmt1->flags & AVFMT_NOFILE) && strcmp(fmt1->name, "image2"))
continue;
score = 0;
// 如果格式具有探测函数,则调用该函数进行文件格式检测
if (ffifmt(fmt1)->read_probe) {
score = ffifmt(fmt1)->read_probe(&lpd);
if (score)
av_log(NULL, AV_LOG_TRACE, "Probing %s score:%d size:%d\n", fmt1->name, score, lpd.buf_size);
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;
}
if (av_match_name(lpd.mime_type, fmt1->mime_type)) {
if (AVPROBE_SCORE_MIME > score) {
av_log(NULL, AV_LOG_DEBUG, "Probing %s score:%d increased to %d due to MIME type\n", fmt1->name, score, AVPROBE_SCORE_MIME);
score = AVPROBE_SCORE_MIME;
}
}
if (score > score_max) {
score_max = score;
fmt = 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;
}
以HLS格式为例
m3u8列表打开流程
avformat_open_input→init_input-> 格式打开入口
av_probe_input_format2→av_probe_input_format3 :探测出是hls格式
io_open→io_open_default→io_open_default→ffio_open_whitelist→ffurl_open_whitelist→ffurl_connect→http_open->http_open_cnx->http_open_cnx_internal->http_connect :打开hls,开始下载