ffmpeg命令分析-r - C语言音视频技术

/ 0评 / 1

本系列 以 ffmpeg4.2 源码为准,下载地址:链接:百度网盘 提取码:g3k8

之前的文章分析 FFMpeg 工程的 do_video_out() 函数的时候,建议不关注 delta0deltanb0_framesnb_frames 等变量。

因为在之前的命令没有用帧率变换参数,-r 。所以上面这些变量赋值,有跟没有是一样的。

现在来补一下之前缺失的内容。命令行指定 -r 之后,delta0deltanb0_framesnb_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;

实际上,我个人认为,命令行 参数 -rffmpeg.c 里面的实现是一个历史遗留问题,这种实现在 ffmpeg.c 里面暴露了太多的复杂性,实际上新版本的ffmpeg,例如 4.4 版本,已经有 fpsframerate 两个新的滤镜来实现帧率转换。

所以,调 API 函数实现帧率转换,推荐使用 fpsframerate 滤镜,就没有这么多 delta 变量之类的。


版权所属:知识星球:弦外之音,QQ:2338195090。 由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注