本系列 以 ffmpeg4.2 源码为准,下载地址:链接:百度网盘 提取码:g3k8
之前的文章分析 FFMpeg 工程的 do_video_out() 函数的时候,建议不关注 delta0
,delta
,nb0_frames
,nb_frames
等变量。
因为在之前的命令没有用帧率变换参数,-r 。所以上面这些变量赋值,有跟没有是一样的。
现在来补一下之前缺失的内容。命令行指定 -r 之后,delta0
,delta
,nb0_frames
,nb_frames
的变化。
本文章主要讲解 FFMpeg 里面是如何实现帧率变换的,例如 24fps 是如何转成 8fps的,缩小了3倍的帧率。
./ffmpeg -i a.mp4 -r 8 output.flv
本文以此命令讲解,原始视频是24fps,转成 8fps。
a.mp4 下载链接:百度网盘 ,提取码:nl0s
如果没指定 -r ,就会从 buffersink 获取输出的帧率,帧率跟输入文件帧率一样,代码如下:
如果命令行指定了 -r ,就会用命令行参数赋值 给 ost->frame_rate
。,代码如下:
上面是 命令行参数 -r 赋值给 ost->frame_rate
。然后 ost->frame_rate
会作为时间基赋值给 编码器的time_base,见下图代码:
所以,综上所述,-r 最后主要影响的是 编码器的time_base,那编码器的time_base是怎么影响帧率变换的呢?请看下面。
在讲之前,需要普及一个知识点,请看下面代码
int a = av_rescale_q(1, (AVRational){1, 24}, (AVRational){1, 12});
int b = av_rescale_q(2, (AVRational){1, 24}, (AVRational){1, 12});
这里 a 跟 b 都是1,a不会是0.5。av_rescale_q()
返回的整数,所以精度丢失了。这也是为什么 reap_filtes()
内部会搞一个 float_pts 出来,这个float_pts 是有精度的。请看下图,编码器的time_base 的影响就在这里。
可以看到,上图代码把原始视频帧 的pts 的时间基转成 编码器的time_base了。这里是一个什么概念呢。
假如原始视频帧率是每秒24帧,他AVFrame的pts的时间基也是 {1,24}。那这时候这个 filtered_frame->pts
肯定是1,2,3,4 这样递增下去的。
把 filtered_frame->pts
转成编码器时间基 {1,8} 会怎样?
播放时间pts不变,1/24 = 0.041s ,第一帧在 0.041s 播放。然后 x/8 = 0.041,这个x等于多少呢?x = 0.333。
也就是说,第二帧的 filtered_frame->pts
原本是 1,时间基从 {1,24} 转成 {1,8} 之后,pts 从1 变成了 0.333。
类推,第三帧pts变成了 0.666,第四种变成0.999。
然后,这些跟帧率变换有什么关系?请继续看下面分析。
因为帧率变换涉及到好几个场景,这里只介绍 format_video_sync = VSYNC_VFR
的场景
接着分析,虽然上面说到,原始pts转换之后变成了 0.33,0.66 之类的小数。但是对于输入文件,对于编码器来说,编码器的帧率是 8fps,{1,8},传递给编码器的frame的pts,肯定也必须是 1,2,3,4 这样的整数递增的。这里实现这个功能的是 ost->sync_opts
变量。
要好好分析一下 ost->sync_opts
这个变量,这个变量的初始值是 0,每输入一个frame给编码器,ost->sync_opts
就会+1。
如上图所示,do_video_out() 的 in_picture 帧的pts会被 ost->sync_opts
替换,达到 输入给编码器的frame pts 都是 1,2,3,递增的目的。
接下来继续讲,上面那些0.33,0.66小数是用来干嘛的?其实这些小数就是用来丢帧,实现帧率缩小的功能,或者重复上一帧,实现帧率变大功能。
如下图所示,nb0_frames 这个变量不用管,在format_video_sync = VSYNC_VFR
的场景 下,nb0_frames 总是0。
第一帧全是0,看不出上面的逻辑,直接从第二帧开始分析。第二帧 do_video_out() 传递的 sync_ipts 是0.333。但是此时 sync_opts 是 1。
大家可以仔细琢磨一下那句英文注释,老外写注释都比较简洁。
delta0 is the "drift" between the input frame (next_picture) and where it would fall in the output.
delta0 = 0.333 - 1 = -0.666,delta0 代表当前输入帧与输出给编码器的时间差距,在第二帧的时候,时间差是 0.666。解析到这里,应该有点眉目,缩小帧率,肯定要根据时间差距来丢弃某些帧。
说实话,他这个算法有点复杂,我也不太明白他为什么不 sync_ipts < sync_opts 就直接丢弃,这样不是更简单?这个问题先不纠结,接着分析ffmpeg 是如何用delta0 ,delta 实现丢帧的。补充:直接 sync_ipts < sync_opts 不行,会出错。
这个算法涉及到不同刻度表的转换,用两个刻度表来解释这个算法会更明白。
delta0 = sync_ipts - ost->sync_opts;
delta = sync_ipts - ost->sync_opts + duration;
可以看到,因为 ost->sync_opts
每次都会 +1 地递增,而 sync_ipts 每次只能 +0.3 递增。所以delta会负得越来越大,duration是固定是 0.33。所以delta也会负得越来越大,然后 delta <= -0.6 就会把 nb_frames 置为 0 ,导致后面的for循环没执行,实现丢帧。至于为什么是0.6我也不知道。实在没想好怎么表达他这个算法逻辑,反正代码逻辑它就是这么跑的,delta越来越大,就丢帧。丢帧后,ost->sync_opts 不会+1,sync_ipts 就会慢慢赶上 sync_opts
case VSYNC_VFR:
if (delta <= -0.6){
//丢弃 frame
nb_frames = 0;
}
else if (delta > 0.6)
ost->sync_opts = lrint(sync_ipts);
break;
实际丢帧情况如下。
0 0.33 0.66(x) 1(x) 1.33 1.66(x) 2(x) 2.33 2.66(x) 3(x)
只有 .33 后缀的帧才会保留,确实是缩小了3倍帧率。
实际上,他这个算法应该是一个数学公式,sync_ipts + duration + 60% 刻度 > sync_opts,如果大于 sync_opts就可以 输出给解码器,小于就丢弃。这里的刻度是1,所以60%刻度是0.6。sync_ipts + duration 是因为只要这个frame的区间跨过 sync_opts 刻度,哪怕跨过一点点,都可以输出。
如果不改变帧率,sync_opts 跟 sync_ipts 是同步+1的,duration也是1,然后 delta0 一直是一个非常小的接近0的数字,delta 一直是接近1的数字。
所以不改变帧率,delta0 跟delta 这些变量是没有作用的。
接下来继续分析 帧率放大算法是如何实现的。
把 帧率 {1,24} 转成 {1,48},实际上就是把 pts 乘以 2 。
注意,有些MP4 是 VSYNC_CFR,有些是 VSYNC_VFR。
CFR 的帧率翻倍,会插入新帧,文件大小也会翻倍。
VFR 的帧率翻倍,不会插入新帧,文件大小不变。
case VSYNC_CFR:
// FIXME set to 0.5 after we fix some dts/pts bugs like in avidec.c
if (frame_drop_threshold && delta < frame_drop_threshold && ost->frame_number) {
nb_frames = 0;
} else if (delta < -1.1)
nb_frames = 0;
else if (delta > 1.1) {
nb_frames = lrintf(delta);
if (delta0 > 1.1)
nb0_frames = lrintf(delta0 - 0.6);
}
break;
case VSYNC_VFR:
if (delta <= -0.6){
//丢弃 frame
nb_frames = 0;
}
else if (delta > 0.6)
ost->sync_opts = lrint(sync_ipts);
break;
实际上,我个人认为,命令行 参数 -r
在 ffmpeg.c
里面的实现是一个历史遗留问题,这种实现在 ffmpeg.c
里面暴露了太多的复杂性,实际上新版本的ffmpeg,例如 4.4 版本,已经有 fps
,framerate
两个新的滤镜来实现帧率转换。
所以,调 API 函数实现帧率转换,推荐使用 fps
,framerate
滤镜,就没有这么多 delta 变量之类的。
版权所属:知识星球:弦外之音,QQ:2338195090。 由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1。