FFmpeg avformat_find_stream_info () 函数源码解析

avformat_find_stream_info() 函数的作用

先来看一下 avformat_find_stream_info() 的头文件里的注释对该函数的介绍,本文我们基于 FFmpeg n4.2 版本的源码分析。

1
2
3
4
5
6
7
8
9
10
/**
* Read packets of a media file to get stream information. This
* is useful for file formats with no headers such as MPEG. This
* function also computes the real framerate in case of MPEG-2 repeat
* frame mode.
* The logical file position is not changed by this function;
* examined packets may be buffered for later processing.
* ...
*/
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

注释里说这个方法通过读取媒体文件中若干个 packet 来获取流信息,对于 MPEG 这种没有 header 的文件格式比较有用,也可以计算像 MPEG-2 这种支持 repeat mode 的真实帧率。(MPEG-2 支持对于大量静止的画面设置 repeat mode,重复的帧不用编码和存储,可以减少体积)

另外提到这个函数不会修改逻辑文件位置,为了探测流信息所读取到的 packet 不会丢掉,会缓存下来为后面使用。

上面提到的流信息包括音频流的采样率、通道数等,视频包括视频的宽高、pixel format、码率、帧率等信息。

avformat_find_stream_info() 函数体有 600 行左右的代码,我们拆开来看,一些不太重要的部分,这里就直接跳过了。

avformat_find_stream_info() 函数源码解析

我们从这两个循环开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
for (i = 0; i < ic->nb_streams; i++) {
const AVCodec *codec;
AVDictionary *thread_opt = NULL;
st = ic->streams[i];
avctx = st->internal->avctx;

if (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO ||
st->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
/* if (!st->time_base.num)
st->time_base = */
if (!avctx->time_base.num)
avctx->time_base = st->time_base;
}
//省略代码
}

for (i = 0; i < ic->nb_streams; i++) {
#if FF_API_R_FRAME_RATE
ic->streams[i]->info->last_dts = AV_NOPTS_VALUE;
#endif
ic->streams[i]->info->fps_first_dts = AV_NOPTS_VALUE;
ic->streams[i]->info->fps_last_dts = AV_NOPTS_VALUE;
}

这两个循环我们可以先跳过,原因是如果在 avformat_open_input() 之后第一次调用 avformat_find_stream_info(),此时还没有 stream 的信息,所以 ic->nb_streams 为 0(nb_streams 是 stream 的个数),进不去循环体,所以我们可以直接跳过,不影响理解。

接下来这个看着像’死循环’的 for-loop,就是我们重点的分析对象了,为了代码的简洁,这里省略掉一些不影响我们理解整体逻辑的代码。既然是个‘死循环’,如果想跳出来就只有 break 和 goto 语句,我们看的时候多留意一下这两种 case. 我也会在代码的注释里加上 break 的标记,同时也会把一些需要注意的地方加上了我自己的理解(中文部分)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
for (;;) {
int analyzed_all_streams;
//break1: 检查是否被打断(或者说取消了继续探测),如果是,直接 break 退出
if (ff_check_interrupt(&ic->interrupt_callback)) {
ret = AVERROR_EXIT;
av_log(ic, AV_LOG_DEBUG, "interrupted\n");
break;
}

/* check if one codec still needs to be handled */
//这个 for-loop 里做了一些对流信息的检测,如果循环能正常结束,
//说明流信息的探测基本完成,这时 i == ic->nb_streams;
//如果中间被 break 了,也就是说某个流的信息还没有完全得到,
//此时 i < ic->nb_streams 的。
//(第一次执行的时候,因为还没有流,所以会直接跳过)
for (i = 0; i < ic->nb_streams; i++) {
int fps_analyze_framecount = 20;
int count;

st = ic->streams[i];
//codec信息是否完整
if (!has_codec_parameters(st, NULL))
break;
/* If the timebase is coarse (like the usual millisecond precision
* of mkv), we need to analyze more frames to reliably arrive at
* the correct fps. */
if (av_q2d(st->time_base) > 0.0005)
fps_analyze_framecount *= 2;
if (!tb_unreliable(st->internal->avctx))
fps_analyze_framecount = 0;
if (ic->fps_probe_size >= 0)
fps_analyze_framecount = ic->fps_probe_size;
if (st->disposition & AV_DISPOSITION_ATTACHED_PIC)
fps_analyze_framecount = 0;
/* variable fps and no guess at the real fps */
count = (ic->iformat->flags & AVFMT_NOTIMESTAMPS) ?
st->info->codec_info_duration_fields/2 :
st->info->duration_count;
if (!(st->r_frame_rate.num && st->avg_frame_rate.num) &&
st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
if (count < fps_analyze_framecount)
break;
}
// Look at the first 3 frames if there is evidence of frame delay
// but the decoder delay is not set.
if (st->info->frame_delay_evidence && count < 2 && st->internal->avctx->has_b_frames == 0)
break;
if (!st->internal->avctx->extradata &&
(!st->internal->extract_extradata.inited ||
st->internal->extract_extradata.bsf) &&
extract_extradata_check(st))
break;
if (st->first_dts == AV_NOPTS_VALUE &&
!(ic->iformat->flags & AVFMT_NOTIMESTAMPS) &&
st->codec_info_nb_frames < ((st->disposition & AV_DISPOSITION_ATTACHED_PIC) ? 1 : ic->max_ts_probe) &&
(st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO ||
st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO))
break;
}
analyzed_all_streams = 0;
//上面提到的 missing_streams 起到作用了,判断是否所有流都找到了
if (!missing_streams || !*missing_streams)
//上面也提到了,i == ic->nb_streams 时,说明所有的流,以及流信息都没有问题了
if (i == ic->nb_streams) {
analyzed_all_streams = 1;
/* NOTE: If the format has no header, then we need to read some
* packets to get most of the streams, so we cannot stop here. */
//如果是有头的封装格式,直接break退出了,如果是像 MPEG 这种没有头的封装格式,需要解析更多的 packets 来探测。
if (!(ic->ctx_flags & AVFMTCTX_NOHEADER)) {
/* If we found the info for all the codecs, we can stop. */
ret = count;
av_log(ic, AV_LOG_DEBUG, "All info found\n");
flush_codecs = 0;
//break2: 所有的流以及流信息都没有问题,正常退出 for 循环
break;
}
}

//走到这里,说明还有些流没探测出来,或者有些流信息还没完善。

/* We did not get all the codec info, but we read too much data. */
//虽然流信息还没完全探测出来,如果已读取到的大小超过了设定的 probesize,也会退出
if (read_size >= probesize) {
ret = count;
for (i = 0; i < ic->nb_streams; i++)
if (!ic->streams[i]->r_frame_rate.num &&
ic->streams[i]->info->duration_count <= 1 &&
ic->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO &&
strcmp(ic->iformat->name, "image2"))
av_log(ic, AV_LOG_WARNING,
"Stream #%d: not enough frames to estimate rate; "
"consider increasing probesize\n", i);
//break3: 读取到的大小超过了设定的 probesize,退出
break;
}

//接下来就需要从网络/文件中读取 packet,这个函数里面做的事情很多,
//拿 flv 来举例子🌰,执行完 read_frame_internal() 函数,
//正常情况下,音视频对应的 AVStream 结构体会被创建,
//并且 ic->nb_streams,也就是流的个数也会是正常的值,
//比如如果包含音频和视频,nb_streams 的值会是 2。

/* NOTE: A new stream can be added there if no header in file
* (AVFMTCTX_NOHEADER). */
ret = read_frame_internal(ic, &pkt1);

//...

pkt = &pkt1;

st = ic->streams[pkt->stream_index];
//读完packet,增加 read_size,下一轮循环会跟 probesize 做对比
if (!(st->disposition & AV_DISPOSITION_ATTACHED_PIC))
read_size += pkt->size;

avctx = st->internal->avctx;
if (!st->internal->avctx_inited) {
ret = avcodec_parameters_to_context(avctx, st->codecpar);
if (ret < 0)
goto find_stream_info_err;
st->internal->avctx_inited = 1;
}

//处理和更新 dts: st->info->fps_first_dts 和 st->info->fps_last_dts
if (pkt->dts != AV_NOPTS_VALUE && st->codec_info_nb_frames > 1) {
//...
/* update stored dts values */
if (st->info->fps_first_dts == AV_NOPTS_VALUE) {
st->info->fps_first_dts = pkt->dts;
st->info->fps_first_dts_idx = st->codec_info_nb_frames;
}
st->info->fps_last_dts = pkt->dts;
st->info->fps_last_dts_idx = st->codec_info_nb_frames;
}
if (st->codec_info_nb_frames>1) {
int64_t t = 0;
int64_t limit;

//计算已经读取到的时间长度
//codec_info_duration:已经取到的packet的总时长
if (st->time_base.den > 0)
t = av_rescale_q(st->info->codec_info_duration, st->time_base, AV_TIME_BASE_Q);
//根据已经读取到的帧数/帧率来计算
if (st->avg_frame_rate.num > 0)
t = FFMAX(t, av_rescale_q(st->codec_info_nb_frames, av_inv_q(st->avg_frame_rate), AV_TIME_BASE_Q));
//根据 fps_last_dts - fps_first_dts 来计算
if (t == 0
&& st->codec_info_nb_frames>30
&& st->info->fps_first_dts != AV_NOPTS_VALUE
&& st->info->fps_last_dts != AV_NOPTS_VALUE)
t = FFMAX(t, av_rescale_q(st->info->fps_last_dts - st->info->fps_first_dts, st->time_base, AV_TIME_BASE_Q));


//如果流信息都探测完(analyzed_all_streams = 1),limit = max_analyze_duration
if (analyzed_all_streams)
limit = max_analyze_duration;
else if (avctx->codec_type == AVMEDIA_TYPE_SUBTITLE)
limit = max_subtitle_analyze_duration;
else limit = max_stream_analyze_duration;

//如果当前已经读取到packet的总时长 >= 上面的 max_analyze_duration,退出
if (t >= limit) {
av_log(ic, AV_LOG_VERBOSE, "max_analyze_duration %"PRId64" reached at %"PRId64" microseconds st:%d\n",
limit,
t, pkt->stream_index);
if (ic->flags & AVFMT_FLAG_NOBUFFER)
av_packet_unref(pkt);
//break4: 读取到packet的总时间 >= max_analyze_duration
break;
}

//更新已经读取到的packet的总时长
if (pkt->duration) {
//...
st->info->codec_info_duration += pkt->duration;
//...
}
}

if (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
#if FF_API_R_FRAME_RATE
ff_rfps_add_frame(ic, st, pkt->dts);
#endif
if (pkt->dts != pkt->pts && pkt->dts != AV_NOPTS_VALUE && pkt->pts != AV_NOPTS_VALUE)
st->info->frame_delay_evidence = 1;
}
if (!st->internal->avctx->extradata) {
ret = extract_extradata(st, pkt);
if (ret < 0)
goto find_stream_info_err;
}

/* If still no information, we try to open the codec and to
* decompress the frame. We try to avoid that in most cases as
* it takes longer and uses more memory. For MPEG-4, we need to
* decompress for QuickTime.
*
* If AV_CODEC_CAP_CHANNEL_CONF is set this will force decoding of at
* least one frame of codec data, this makes sure the codec initializes
* the channel configuration and does not only trust the values from
* the container. */

//到这里,调用 try_decode_frame() 对获取的 packet 进行音视频的解码,
//正常情况下,会得到当前流的所有的解码期信息,
//比如视频的宽高、pixel format,音频的 sample format, 采样率、通道数等。
try_decode_frame(ic, st, pkt,
(options && i < orig_nb_streams) ? &options[i] : NULL);

if (ic->flags & AVFMT_FLAG_NOBUFFER)
av_packet_unref(pkt);

//已经探测的帧数+1,count总数+1
st->codec_info_nb_frames++;
count++;
}

我们用伪代码来简化一下上面代码的主要逻辑:

1
2
3
4
5
6
7
8
9
10
11
for (;;) {
if 所有stream 满足 has_codec_parameters(st, ..)
|| probe_size > 设置值 {
break 退出;
} else {
//继续读取 packet
read_frame_internal(ic, &pkt1);
//尝试对读取到的 packet 解码
try_decode_frame(ic, st, pkt, ...);
}
}

下面我们详细地来看一下这三个函数的作用。

has_codec_parameters () 、read_frame_internal ()、try_decode_frame () 函数的作用

上面提到的 has_codec_parameters() 函数,是一个很重要的函数,它来检测当前的音视频流信息是否完整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
static int has_codec_parameters(AVStream *st, const char **errmsg_ptr)
{
AVCodecContext *avctx = st->internal->avctx;
//...
if ( avctx->codec_id == AV_CODEC_ID_NONE
&& avctx->codec_type != AVMEDIA_TYPE_DATA)
FAIL("unknown codec");
switch (avctx->codec_type) {
case AVMEDIA_TYPE_AUDIO:
if (!avctx->frame_size && determinable_frame_size(avctx))
FAIL("unspecified frame size");
if (st->info->found_decoder >= 0 &&
avctx->sample_fmt == AV_SAMPLE_FMT_NONE)
FAIL("unspecified sample format");
if (!avctx->sample_rate)
FAIL("unspecified sample rate");
if (!avctx->channels)
FAIL("unspecified number of channels");
if (st->info->found_decoder >= 0 && !st->nb_decoded_frames && avctx->codec_id == AV_CODEC_ID_DTS)
FAIL("no decodable DTS frames");
break;
case AVMEDIA_TYPE_VIDEO:
if (!avctx->width)
FAIL("unspecified size");
if (st->info->found_decoder >= 0 && avctx->pix_fmt == AV_PIX_FMT_NONE)
FAIL("unspecified pixel format");
if (st->codecpar->codec_id == AV_CODEC_ID_RV30 || st->codecpar->codec_id == AV_CODEC_ID_RV40)
if (!st->sample_aspect_ratio.num && !st->codecpar->sample_aspect_ratio.num && !st->codec_info_nb_frames)
FAIL("no frame in rv30/40 and no sar");
break;
case AVMEDIA_TYPE_SUBTITLE:
if (avctx->codec_id == AV_CODEC_ID_HDMV_PGS_SUBTITLE && !avctx->width)
FAIL("unspecified size");
break;
case AVMEDIA_TYPE_DATA:
if (avctx->codec_id == AV_CODEC_ID_NONE) return 1;
}

return 1;
}

可以看到音频要检测是否拿到 frame size,sample format, sample rate, channels 等重要参数,视频则会检测视频的 width, pixel format 等等。

代码中,我们可以看到,为了获取音视频流信息,涉及到了两个重要的函数调用:

  1. read_frame_internal() 函数
  2. try_decode_frame() 函数

在讨论他们的作用之前,我们首先以 FLV 封装格式为例(以音频编码为 AAC,视频编码为 H.264 为例),来解释一下为什么需要这两个函数。

FLV 封装格式的大概示意图如下(为了简洁,省略了一些信息,详细细节参考 FLV specification),

-w510

我们可以这么来理解 FLV 封装格式,除了 header 之外,它里面包含了一系列的 Tag,可能是 Video Tag,也可能是 Audio Tag, 这个是 FLV 文档来定义的。其中每个 Tag 里面包含了一些描述性信息,以及对应的编码后的 AAC 或者 H.264 数据。如果我们分为两层来看的话,一层是 FLV Tag,一层是编码后的音视频数据。

了解这个之后,我们再来试着理解上面提到的 read_frame_internal() 函数和 try_decode_frame() 在这里的作用。

read_frame_internal() 函数本质上就是从网络中读取音视频的 packet,对应到 FLV 格式的话,其实就是读取第一层,也就是 也就是 FLV Tag 信息,从 tag 里可以读取 tag 的一些描述性信息 , 这些描述性信息包括(参考自 FLV Specification):

音频:

  1. 编码格式,是 AAC 还是 MP3,还是 Linear PCM?
  2. Sample rate, 采样率,比如 48000
  3. 通道数,单声道还是双声道?(FLV 最多支持两个声道)
  4. Bit depth

视频:

  1. 帧类型,关键帧还是中间帧?
  2. 编码格式,H.264 还是 H.263,还是 VP6 等其他格式?

read_frame_internal() 函数能拿到的就是上面的这些信息,这个信息是否够全呢?跟上面提到的 has_codec_parameters() 检测函数里的要求相比,确实还差了一些信息。比如音频的 sample format(比如 AV_SAMPLE_FMT_FLTP),它需要打开音频解码器之后才能确定,sample rate 等信息也是解码后得到的更加准确。

视频的宽高、pixel format 信息是存放在 H.264 流信息里,所以也需要解码之后才能获取到,所以,这里才需要 try_decode_frame() 函数去做解码的工作才能拿到完整的信息。

也就是说 read_frame_internal() 函数负责从第一层 FLV Tag 里获得流信息,try_decode_frame() 函数负责解码第二层的编码数据,来获取更多的流编码信息,最终汇总为完整的流信息,所以两处函数调用都是必要的。

OK,我们这里不展开 read_frame_internal() 函数 和 try_decode_frame() 函数内部的实现,有兴趣的小伙伴可以自己读读源码。

如何合理设置 probesize 来降低播放首屏时间?

在直播场景下,我们为了提高用户的体验,减少首屏时间,希望直播流是秒开的。这个时候我们会希望 avformat_find_stream_info() 函数在可以完成流信息探测完整的情况下,尽可能的早一些返回。根据前面的分析,我们可以知道,read_frame_internal() 函数的耗时依赖网络的情况,try_decode_frame() 函数负责解码,依赖软件 / 硬件执行的效率,这两者可能都会比较耗时,所以我们要尽可能的减少这两个方法的调用,从而减少 avformat_find_stream_info() 函数执行的时间。

avformat_find_stream_info() 退出的条件有很多个,probesizemax_analyze_durationfps_analyze_framecount 以及是其中的三个:

  1. 已读取的数据 > probesize
  2. 已读取视频帧播放时间长度 > max_analyze_duration

我们先来看 probesizemax_analyze_duration,这两个判断是强制性的,就是说不管是否获取到了完整的流信息,只要达到了这两个条件,就会退出。下面这段代码摘自 avformat_find_stream_info() 函数的前一部分:

1
2
3
4
5
6
7
8
9
10
11
...
int64_t max_analyze_duration = ic->max_analyze_duration;
...
int64_t probesize = ic->probesize;
int *missing_streams = av_opt_ptr(ic->iformat->priv_class, ic->priv_data, "missing_streams");


if (!max_analyze_duration) {
max_stream_analyze_duration =
max_analyze_duration = 5*AV_TIME_BASE;
}

这两个值用来设置函数探测流信息的最大 size,以及最大时长。probesize 默认值是 5,000,000 bytes 也就是 5MB 大小,max_analyze_duration 默认值是 5*AV_TIME_BASE, 也就是 5,000,000 micro-seconds 也就是 5 秒。

PS: 另外 missing_streams 是个指向 int 的指针,*missing_streams 只要是 > 0,就说明还有某些流没探测到,这个后面的循环有个关键判断会用到。(假设我们要播放的是 flv 流,有兴趣的同学可以到 flvdec.c 文件搜索一下这个属性)

默认值对直播来说都蛮大的,不过他们都支持在调用 avformat_find_stream_info() 之前手动地设置。那设置多少比较合适呢?

通过我们刚才的分析,理论上获取到一帧视频和一帧音频,并对他们解码,我们就可以拿到完整的音视频信息。所以理论上我们把 probe size 设置为第一次获取完音频和视频帧时所需读取的长度即可。当然这是理想的情况,实际中有诸多意外因素。比如第一帧视频帧通常是关键帧会比较大,对于不同的码率的流,大小差异会比较大,还有些 CDN 下发的流中,前面放了很多的视频帧之后,才有一个音频帧(这种最好要求 CDN 厂商修改)。

可见 probe size 如果设置的太大会导致首帧时间比较长,设置的太小,可能一些 case 下会获取流信息失败,所以需要根据自己流信息的情况(特别是码率,因为码率会影响音频、视频帧的大小)去设置。我们目前用的一种策略是,设置 probe size 为一个针对我们目前直播稍大于上面所说的长度的一个值,应对大部分 case,对于一小部分 case,比如可能要播放外部流,我们除了支持服务器动态配置之外,还会在失败之后,对 probe sizemax_analyze_duration 乘以一个系数之后再重试。

PS: 对于 FLV 格式,具体到理论上的最小的 probe size 的大小,大概等于

1
2
3
4
5
6
>smallest probe_size =  sizeof(FLV header) +
sizeof(script tag) +
sizeof(audio tag of AAC sequence header) +
sizeof(audio tag of AAC raw data) +
sizeof(video tag of H.264 sequence header) +
sizeof(video tag of H.264 NALU data)