持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第31天,点击查看活动详情
transcode_step() 函数是 ffmpeg.exe 转换编码格式,或者转换封装格式的总函数,它在函数调用中的位置如下:
transcode_step() 内部每次都会读取一个AVPacket,发送给解码器进行解码,解码器可能会输出 0个 ~ N个 AVFrame,然后把 AVFrame 发送给滤镜容器,如果滤镜容器有数据能出来,就会接着发送给编码器进行编码,如果编码器有 AVPacket 数据可以出来,就把 AVPacket 写入文件保存。
这就是 transcode_step() 总函数的内部逻辑,不过有几点需要注意的。
1, 往解码器发送一个 AVPacekt ,解码器可能没数据输出,这样就会回到 while 循环,再次调 transcode_step() 读第二个 AVPacekt 往解码器丢。
2, 往滤镜容器发送一个 AVFrame,滤镜容器不一定就立马有数据可读,没数据可读,又会回到 while 循环。
3, 往编码器发送一个 AVFrame,编码器不一定就立即输出 AVPacekt,所以又会回到 while 循环,再次调 transcode_step() 解码出第二个 AVFrame 往编码器丢。
这个函数之所以有 step,step 就是一步的意思,每次只从文件读取一个 AVPacekt,只处理一个 AVPacket
下面一起来学习一下 transcode_step() 转码总函数的代码,重点如下:
choose_output() 函数的作用是在所有输出流(OutputStream)中选出最合适的一个。 代码如下:
可以看到,会优选选择未初始化的 OutputStream,如果所有输出流都初始化了,就选择时间最短的那个输出流。最短时间通过 ost->st->cur_dts 来判断,例如音频流已经输出了2分钟了,视频流才输出1分钟,choose_output() 函数就会返回 视频流。
ost->initialized 在出口滤镜输出 AVFrame 的时候就会初始化,被赋值为 1,注意不是在解码器输出 AVFrame 的时候,而是出口滤镜输出 AVFrame 的时候才会初始化输出流,因为这时候才知道具体要编码的像素格式等信息。(不过对于音频输出流,会提前进行初始化)
//init_output_stream 函数的代码
ost->initialized = 1;
ost->inputs_done 代表没有找到输出流对应的输入流,通常不会没找到,所以通常一直都是 0
第二个重点是三个 if 判断,这 3 个 if 判断分别是处理不同的场景的,如下:
第一个 if 条件, 不清楚是处理什么场景的 。
if (ost->filter && !ost->filter->graph->graph) {
if (ifilter_has_all_input_formats(ost->filter->graph)) {
ret = configure_filtergraph(ost->filter->graph);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Error reinitializing filters!\n");
return ret;
}
}
}
上面这个判断,肯定不是复杂滤镜的代码,因为 init_complex_filtergraph() 会对 ost->filter->graph->graph 进行赋值。
但是一般的 简单滤镜场景,所有的输入流的 format 都是 -1,所以 ifilter_has_all_input_formats() 也会返回 false,所以不会跑进去。
在 ifilter_parameters_from_frame() 里,会把 format 从 -1 改成别的。但是通常当所有的都变成 非 -1,就会调 configure_filtergraph() 配置滤镜了,
具体第一个 if 条件是处理什么场景的,我也不清楚,后面补充。
第二个 if 条件,从滤镜容器选出最合适的输入流,如下:
if (ost->filter && ost->filter->graph->graph) {
/*
* 省略注释
*/
if (av_buffersink_get_type(ost->filter->filter) == AVMEDIA_TYPE_AUDIO)
init_output_stream_wrapper(ost, NULL, 1);
if ((ret = transcode_from_filter(ost->filter->graph, &ist)) < 0)
return ret;
if (!ist)
return 0;
}
上面的代码主要是调用了 transcode_from_filter() 函数,确定应该从哪个输入流读取 AVPacekt。
具体的算法,是用 av_buffersrc_get_nb_failed_requests() 获取每个 buffer入口滤镜的失败次数,选出失败次数最多的入口滤镜,最后因为入口滤镜绑定了输入流。所以最合适的输入流也就确定了。
第三个 if 条件,当还未打开滤镜容器的时候,选一个从未解码出 AVFrame 的输入流来进行处理,如下:
else if (ost->filter) {
int i;
for (i = 0; i < ost->filter->graph->nb_inputs; i++) {
InputFilter *ifilter = ost->filter->graph->inputs[i];
if (!ifilter->ist->got_output && !input_files[ifilter->ist->file_index]->eof_reached) {
ist = ifilter->ist;
break;
}
}
if (!ist) {
ost->inputs_done = 1;
return 0;
}
}
上面的 got_output 字段,代表这个输入流是否已经解码出来 AVFrame 了,解码出来 AVFrame 就可以配置对应的入口滤镜的参数。
当所有的入口滤镜都配置完成(format 等于 -1),就可以成功打开滤镜容器,函数的调用流程如下:
关于 configure_filtergraph 函数的介绍,推荐阅读《configure_filtergraph配置滤镜容器》。
第四个 else 条件,是最后的保留策略,直接用输出流对应的输入流,如下:
else {
av_assert0(ost->source_index >= 0);
ist = input_streams[ost->source_index];
}
我也不太清楚什么场景会跑进这里,ffmpeg.exe 转换器,就是分为简单滤镜 与 复杂滤镜,不能不使用滤镜,即使不使用滤镜,默认也会创建一个空滤镜。
所以我倾向于认为最后这个保留策略是多余的。
做个阶段性的小总结,transcode_step() 内部选出输入流的策略只有两个需要关注的地方,就是第二 跟 第三个条件。
1,第二个条件是滤镜容器已经打开了,所以从 transcode_from_filter() 选出输入流来进行处理。
2,第三个条件是,只要有一个入口滤镜未配置好,也就是 format 等于 -1 的时候,滤镜容器就不能打开。这时候,就选出一个从未解码出 AVFrame 的输入流出来,进行处理。
所以,无论你是用简单滤镜,还是复杂滤镜,ffmpeg.exe 它都会先跑到 第三个条件,把所有的输入流都至少解码出一个 AVFrame。然后打开滤镜容器,这样才会跑进去第二个条件。
transcode_step() 转码总函数的流程图如下:
从上图可以看到,选出输入流之后,就会调 process_input() 从输入流关联的输入文件里面读取 AVPacekt。
process_input() 函数的主要逻辑是,读取一个 AVPacekt,然后往解码器丢,解码器可能不会出数据,也可能会出来多个 AVFrame,无论出来多少个 AVFrame,都调 send_frame_to_filters() 把它们往入口滤镜发送。
process_input()只会往解码器发送一个 AVPacket,但是会不断往解码器读 AVFrame,直到解码器返回 EAGAIN。
而最后的 reap_filter() 函数,就负责从出口滤镜里 读取 AVFrame,然后发送给编码器编码,最后保存进去文件。
更详细的介绍,推荐阅读《process_input处理输入文件》《reap_filter收割滤镜数据》