FrameSync帧同步—framesync_advance

作者:罗上文,微信:Loken1,公众号:FFmpeg弦外之音

在了解 帧同步算法 之前,我们需要先熟悉 两个数据结构,FFFrameSyncFFFrameSyncIn

我习惯把 FFFrameSync 称为 帧同步器,然后 FFFrameSyncIn 是 帧同步器 里面的输入流。

这里读者可能会有疑问,帧同步器的 输入流 跟 滤镜(AVFilterContext)的 输入流 是怎么绑定、怎么关联在一起的?

答:它们是通过数组下标 [i] 关联的,代码如下:

1-1

也就是 FFFrameSync 的第一个流 对应 AVFilterContext 的第一个流,第二个流 对应 第二个流。如下图:

1-2


虽然数据结构 FFFrameSyncFFFrameSyncIn 有很多字段,但是目前我们只需要理解 同步级别同步流,就能去看帧同步最核心的逻辑 framesync_advance()

FFFrameSyncIn 里的 sync 字段就是同步级别,每个输入流都有一个同步级别,同步级别最高的流被称为 同步流

overlay 滤镜为例,第一个输入流(main)的 sync 是 2,第二个流(second)的 sync 是 1,所以同步流是 main 那个流。代码如下:

1-3

FFFrameSync 里也有一个 sync_level,这个字段存储的是 最大的 FFFrameSyncIn::sync,也就是在这么多个输入流里,同步级别最高是多少。


我们可以用不同帧率的输入流,来理解 FFFrameSync 同步算法。

假设某个滤镜支持 3 个输入流,分别是 A、B、C。其中 A 是同步流,B、C 是其他的输入流。A 的帧率是小于 B 的帧率的。如下图:

1-3-1

FFFrameSync 帧同步器 需要选出 3 帧数据丢给滤镜进行处理,他是根据怎样的算法来挑选各个流的帧的呢?如下:

首先,同步流的每一帧数据都不能丢,也不能重复进行滤波处理。同步流是逐帧进行滤波处理的。

所以他会选出 A1 帧,然后从其他流中选出那些紧挨着 A1 的帧。输入流B 里 紧挨着 A1 的是 第一帧 B1,输入流C 里 紧挨着 A1 的是 第一帧 C1,所以第一次滤波处理的组合是 A1+B1+C1。

第二次滤波处理,就会选择同步流的第二帧 A2,然后从其他流中选出那些紧挨着 A2 的帧。输入流B 里 紧挨着 A2 的是 B3,输入流C 里 紧挨着 A2 的还是 C1,所以第一次滤波处理的组合是 A2+B3+C1。

因此第二次滤波处理的同步过程中,丢弃了 B2 帧,重复使用了 C1 帧。

因此如果一个输入流不是同步流,他的帧就可能因为同步的需要而丢弃,或者被重复使用。

紧挨着 这个词就是同步算法的核心,他每次都从同步流里面取一个帧出来,这个叫同步帧,然后在其他流里面找紧挨着这个同步帧的帧。

因此在进行 滤波处理 的帧列表里面,同步流的帧的 pts 是最大的。


framesync_advance() 里面的复杂逻辑,fs->in[i].framefs->in[i].frame_next 移来移去、等等,就是为了实现 紧挨着 这个需求。

framesync_advance() 的逻辑包在一个大的循环 while (!(fs->frame_ready || fs->eof)) {...} 里面,如下:

1-4

主要有两种情况可以跳出这个 while(...) 循环:

  1. 帧同步完成的时候,也就是 fs->frame_ready 设置为 1 的时候。
  2. AVFilterLink 上没有数据可读了,也就是 consume_from_fifos() 的返回值小于或等于 0

consume_from_fifos() 函数内部会循环读取所有 AVFilterLink,当它返回 1 的时候,代表所有输入流的 frame_next 都有数据了。

因此,也可以说 framesync_advance() 函数逻辑是在 循环消耗 AVFilterLink::fifo 上的数据,直到完成帧同步。


framesync_advance() 函数的第一步是选出 frame_next 列表里面最小的 pts,如下:

pts = INT64_MAX;
for (i = 0; i < fs->nb_in; i++)
    if (fs->in[i].have_next && fs->in[i].pts_next < pts)
        pts = fs->in[i].pts_next;

然后再把 最小 ptsframe_next 放进去 frame 变量里面。代码如下:

1-5

提醒一下,因为每个流都有一个 frame_nextframe,多个流的 frame_next 是可以看成一个列表的。所以我习惯把 frame_next 叫做一个列表,frame 也是一个列表。

上图的逻辑非常绕,首先需要注意,他判断了 fs->in[i].pts_next == pts,所以除非所有输入流里面的 pts_next 都相等,要不他这个循环只是处理了其中一个输入流的 frame_next

他只是把其中一个输入流的 frame_next 赋值给了 frame,并不是所有输入流的 frame_next 都赋值给了 frame,这个要注意一下,容易看错。

这个被赋值的 framepts 是最小的,但是,但是他同时也是最大的。这是什么意思呢?

就是这个 ptsFFFrameSyncIn::frame_next 列表里面是最小的,但是在 FFFrameSyncIn::frame 列表里面是最大的,因为 frame 列表的数据就是从最小 pts 一个一个加进去的,所以后来加入的 pts 最大。

每个输入流都有一个 frame,这个 frame 就是准备给滤镜进行滤波处理的帧。前面说过,在进行滤波处理的时候,同步流的帧的 PTS 必须是最大的

所以 framesync_advance() 就要判断,这个最大的 frame 是不是同步流的帧,如果是,就可以进行滤波处理了。代码如下:

if (fs->in[i].sync == fs->sync_level && fs->in[i].frame)
        fs->frame_ready = 1;

然后,frame 列表的数据恰恰就是紧挨着 pts 最大的那个帧的。因为 frame 列表的数据是从最小 pts 一个一个加进去的。

所以当确定 frame 列表里 pts 最大的帧是同步帧的时候,就可以把 frame 列表的数据丢给 滤镜进行滤波处理了。


下面让我们通过命令实战,调试代码来理解 紧挨着 这个逻辑。

ffmpeg -an -i juren-30s.mp4 -an -i juren-30s-small.mp4  -filter_complex "[0:v][1:v]overlay=18:20" out.mp4

juren-30s.mp4 的帧率是 23.9 fps,juren-30s-small.mp4 的帧率是 9 fps。

通过调试,我们可以发现,第一个输入流的 第1、第2、第3 帧都是跟 第二个输入流的 第1帧 进行 overlay 叠加,所以第二个流的第1帧 重复使用了3 次,这是符合 紧挨着 的逻辑的。

1-6

读者也可以把 juren-30s-small.mp4 从 9 fps 转成 120 fps,这时候会发现第二个流会跳帧,把一些帧丢弃,不进行 overlay 操作。丢帧也是在 framesync_advance() 的循环里面做的。不断把 frame_next 转给 frame,直到 frame_next 列表里面 pts 最小值是同步帧。这个过程中就会丢帧。


最后还有一个重点,就是 overlay 滤镜从同步器里面取 Frame 的时候,他只拿走了同步帧,其他流的帧是保留下来的,如下:

1-7

这也是为什么 非同步流 的帧可以重复使用的原因,因为他滤镜在取的时候,没有把 FFFrameSyncIn::frame 置为 NULL,里面的数据还留着。

ff_framesync_dualinput_get() 里面的 dual 是 两个,双的意思。

这个函数不是只给 overlay 用的,alphamerge、paletteuse 滤镜是两个输入,也会用到 ff_framesync_dualinput_get()


FFmpeg书籍版权所属@罗上文 2025 all right reserved,powered by Gitbook该文件修订时间: 2026-08-14 15:25:19

results matching ""

    No results matching ""