在 FFmpeg 中为 DASH 修复关键帧的正确方法是什么?

在 FFmpeg 中为 DASH 修复关键帧的正确方法是什么?

在调节流以进行 DASH 播放时,所有流中的随机访问点必须位于完全相同的源流时间。通常的做法是强制使用固定帧速率和固定 GOP 长度(即每 N 帧一个关键帧)。

在 FFmpeg 中,固定帧速率很容易(-r NUMBER)。

但对于固定关键帧位置(GOP 长度),有三种方法...哪一种是“正确的”?FFmpeg 文档对此含糊不清。

方法 1:修改 libx264 的参数

-c:v libx264 -x264opts keyint=GOPSIZE:min-keyint=GOPSIZE:scenecut=-1

关于场景切换是否应该关闭似乎存在一些争论,因为不清楚场景切换时关键帧“计数器”是否重新启动。

方法二:设置固定GOP大小:

-g GOP_LEN_IN_FRAMES

不幸的是,这只是在 FFMPEG 文档中顺便记录的,因此这个论点的效果非常不明确。

方法三:每隔N秒插入一个关键帧(或许?):

-force_key_frames expr:gte(t,n_forced*GOP_LEN_IN_SECONDS)

有明确记录。但目前还不清楚“时间计数器”是否在每个关键帧后重新启动。例如,在预期为 5 秒的 GOP 中,如果scenecutlibx264 在 3 秒内注入了一个关键帧,下一个关键帧是 5 秒后还是 2 秒后?

事实上,FFmpeg 文档区分了此选项和该-g选项,但它并没有真正说明上述这两个选项有何不同(显然,-g需要固定的帧速率)。

哪个是对的?

看起来会-force_key_frames更好,因为它不需要固定的帧速率。然而,这需要

  • 它符合 H.264 中的 GOP 规范(如果有的话
  • 它保证会有一个固定节奏的关键帧,而不管 libx264scenecut关键帧。

-g如果不强制固定帧速率,似乎也无法工作( -r,因为无法保证ffmpeg使用不同的编解码器参数多次运行会在每个分辨率下提供相同的瞬时帧速率。固定帧速率可能会降低压缩性能(在 DASH 场景中很重要!)。

最后,keyint方法似乎是一种黑客攻击我怀着一丝希望,希望这不是正确答案。

参考:

-force_key_frames使用该方法的示例

keyint使用该方法的示例

FFmpeg 高级视频选项部分

答案1

总结

我建议如下:

  • libx264: (也可以选择添加)-g X -keyint_min X-force_key_frames "expr:gte(t,n_forced*N)"
  • libx265-x265-params "keyint=X:min-keyint=X"
  • libvpx-vp9-g X

其中X是帧间隔,N是秒间隔。例如,对于 30fps 的视频,间隔为 2 秒,X= 60 且N= 2。

关于不同框架类型的说明

为了正确解释这个主题,我们首先必须定义两种类型的 I 帧/关键帧:

  • 即时解码器刷新 (IDR) 帧:这些帧允许独立解码以下帧,而无需访问 IDR 帧之前的帧。
  • 非 IDR 帧:这些帧需要先前的 IDR 帧才能进行解码。非 IDR 帧可用于 GOP(图片组)中间的场景切换。

对于流媒体有什么推荐?

对于流媒体案例,您需要:

  • 确保所有 IDR 帧都位于规则位置(例如,2、4、6、...秒),以便可以将视频分成等长度的片段。
  • 启用场景切换检测,以提高编码效率/质量。这意味着允许将 I 帧放置在 IDR 帧之间。您仍然可以在禁用场景切换检测的情况下工作(这仍然是许多指南的一部分),但这不是必需的。

参数起什么作用?

为了配置编码器,我们必须了解关键帧参数的作用。我做了一些测试,发现了以下三个编码器libx264和FFmpeg 中的libx265关键帧参数:libvpx-vp9

  • libx264

    • -g设置关键帧间隔。
    • -keyint_min设置最小关键帧间隔。
    • -x264-params "keyint=x:min-keyint=y"是相同的-g x -keyint_min y
    • 笔记:当将两者设置为相同的值时,最小值内部设置为一半最大间隔加一,如代码所示x264

      h->param.i_keyint_min = x264_clip3( h->param.i_keyint_min, 1, h->param.i_keyint_max/2+1 );
      
  • libx265

    • -g未实现。
    • -x265-params "keyint=x:min-keyint=y"作品。
  • libvpx-vp9

    • -g设置关键帧间隔。
    • -keyint_min设置最小关键帧间隔
    • 笔记:由于 FFmpeg 的工作方式,-keyint_min只有与 相同时才会转发到编码器-g。在 FFmpeg 的代码中libvpxenc.c我们可以找到:

      if (avctx->keyint_min >= 0 && avctx->keyint_min == avctx->gop_size)
          enccfg.kf_min_dist = avctx->keyint_min;
      if (avctx->gop_size >= 0)
          enccfg.kf_max_dist = avctx->gop_size;
      

      这可能是一个错误(或缺少功能?),因为libvpx肯定支持为设置不同的值kf_min_dist

你应该使用吗-force_key_frames

-force_key_frames选项强制以给定间隔(表达式)插入关键帧。这适用于所有编码器,但可能会干扰速率控制机制。特别是对于 VP9,我注意到严重的质量波动,因此我不建议在这种情况下使用它。

答案2

这是我对此案的五毛钱意见。

方法 1:

弄乱 libx264 的参数

-c:v libx264 -x264opts keyint=GOPSIZE:min-keyint=GOPSIZE:场景剪切=-1

仅按照所需的间隔生成 iframe。

示例 1:

ffmpeg -i test.mp4 -codec:v libx264 \
-r 23.976 \
-x264opts "keyint=48:min-keyint=48:no-scenecut" \
-c:a copy \
-y test_keyint_48.mp4

按预期生成 iframe,如下所示:

Iframes     Seconds
1           0
49          2
97          4
145         6
193         8
241         10
289         12
337         14
385         16
433         18
481         20
529         22
577         24
625         26
673         28
721         30
769         32
817         34
865         36
913         38
961         40
1009        42
1057        44
1105        46
1153        48
1201        50
1249        52
1297        54
1345        56
1393        58

方法 2 已弃用。省略。

方法 3:

每 N 秒插入一个关键帧(可能):

-force_key_frames expr:gte(t,n_forced*GOP_LEN_IN_SECONDS)

示例 2

ffmpeg -i test.mp4 -codec:v libx264 \
-r 23.976 \
-force_key_frames "expr:gte(t,n_forced*2)"
-c:a copy \
-y test_fkf_2.mp4

以稍微不同的方式生成 iframe:

Iframes     Seconds
1           0
49          2
97          4
145         6
193         8
241         10
289         12
337         14
385         16
433         18
481         20
519         21.58333333
529         22
577         24
625         26
673         28
721         30
769         32
817         34
865         36
913         38
931         38.75
941         39.16666667
961         40
1008        42
1056        44
1104        46
1152        48
1200        50
1248        52
1296        54
1305        54.375
1344        56
1367        56.95833333
1392        58
1430        59.58333333
1440        60
1475        61.45833333
1488        62
1536        64
1544        64.33333333
1584        66
1591        66.29166667
1632        68
1680        70
1728        72
1765        73.54166667
1776        74
1811        75.45833333
1824        75.95833333
1853        77.16666667
1872        77.95833333
1896        78.95833333
1920        79.95833333
1939        80.75
1968        81.95833333

正如您所看到的,它每 2 秒放置一次 iframe,并且在场景切换(带有浮动部分的秒数)上放置一次,我认为这对于视频流的复杂性很重要。

生成的文件大小几乎相同。奇怪的是,即使关键帧更多,方法 3它有时比标准 x264 库算法生成的文件更少。

为了生成用于 HLS 流的多个比特率文件,我们选择了方法三。它完美地与块之间的 2 秒对齐,它们在每个块的开头都有 iframe,并且在复杂场景中还有额外的 iframe,这为拥有过时设备且无法播放 x264 高级配置文件的用户提供了更好的体验。

希望它能对某人有所帮助。

答案3

我想在这里添加一些信息,因为在尝试寻找按照我想要的方式分段 DASH 编码的信息时,我用谷歌搜索了很多次,但找到的信息都不是完全正确的。

首先要消除几个错误观念:

  1. 并非所有 I 帧都相同。有大“I”帧和小“i”帧。或者用正确的术语来说,IDR I 帧和非 IDR I 帧。IDR I 帧(有时称为“关键帧”)将创建新的 GOP。非 IDR 帧则不会。在场景发生变化的 GOP 中,它们很有用。

  2. -x264opts keyint=GOPSIZE:min-keyint=GOPSIZE← 它并没有像你想象的那样工作。我花了一段时间才弄清楚。原来min-keyint代码中的 是有限的。它不允许大于(keyint / 2) + 1。因此,将相同的值分配给这两个变量会导致 的值min-keyint在编码时减少一半。

事情是这样的:场景剪切真的很棒,尤其是在有快速硬剪切的视频中。它保持画面清晰,所以我不想禁用它,但同时,只要启用它,我就无法获得固定的 GOP 大小。我想启用场景剪切,但只让它使用非 IDR I 帧。但它不起作用。直到我(通过大量阅读)弄清楚了误解 #2。

事实证明我需要将设置为keyint所需 GOP 大小的两倍。这意味着min-keyint可以将设置为所需 GOP 大小(无需内部代码将其减半),这可以防止场景切换检测在 GOP 大小内使用 IDR I 帧,因为自上一个 IDR I 帧以来的帧数始终小于min-keyinit

最后,设置force_key_frame选项将覆盖双倍大小keyint。因此,以下是有效的方法:

我更喜欢 2 秒的片段,所以我的 GOPSIZE = 帧率 * 2

ffmpeg <other_options> -force_key_frames "expr:eq(mod(n,<GOPSIZE>),0)" -x264opts rc-lookahead=<GOPSIZE>:keyint=<GOPSIZE * 2>:min-keyint=<GOPSIZE> <other_options>

您可以使用 ffprobe 进行验证:

ffprobe <SRC_FLE> -select_streams v -show_frames -of csv -show_entries frame=coded_picture_number,key_frame,pict_type > frames.csv

在生成的 CSV 文件中,每一行都会告诉您frame, [is_an_IDR_?], [frame_type], [frame_number]::

frame,1,I,60  <-- frame 60, is I frame, 1 means is an IDR I-frame (aka KeyFrame)
frame,0,I,71  <-- frame 71, is I frame, 0 means not an IDR I_frame

结果是您只能以固定GOPSIZE间隔看到 IDR I 帧,而所有其他 I 帧都是根据场景剪切检测的需要插入的非 IDR I 帧。

答案4

Twitch 有一篇关于此的帖子。他们解释说,他们决定使用自己的程序有几个原因;其中之一是 ffmpeg 不允许您在不同的线程中运行不同的 x264 实例,而是将所有指定的线程专用于一个输出中的一个帧,然后再转到下一个输出。

如果您不进行实时流式传输,则您可以享受更多便利。“正确”的方法可能是使用 -g 指定 GOP 大小,以某一分辨率进行编码,然后以其他分辨率进行编码,强制关键帧位于同一位置。

如果您想这样做,您可以使用 ffprobe 来获取关键帧时间,然后使用 shell 脚本或实际编程语言将其转换为 ffmpeg 命令。

但对于大多数内容而言,每 5 秒有一个关键帧和每 5 秒有两个关键帧(一个强制,一个来自场景切换)之间几乎没有区别。这是关于平均 I 帧大小与 P 帧和 B 帧大小的比较。如果您使用具有典型设置的 x264(我认为您应该采取任何措施来影响这些设置的唯一原因是如果您设置 -qmin,这是一种阻止 x264 在简单内容上使用比特率的糟糕方法;我认为这将所有帧类型限制为相同的值)并获得 I 帧平均大小为 46 kB、P 帧 24 kB、B 帧 17 kB(频率是 P 帧的一半)的结果,那么每秒 30 fps 的额外 I 帧只会增加 3% 的文件大小。h264 和 h263 之间的差异可能是由一堆 3% 的减少组成的,但单个减少并不重要。

对于其他类型的内容,帧大小会有所不同。公平地说,这与时间复杂性有关,而不是空间复杂性,因此这不仅仅是简单内容与困难内容之间的区别。但一般来说,流媒体视频网站都有比特率限制,而 I 帧相对较大的内容是简单内容,无论添加多少额外的关键帧,都会以高质量进行编码。这很浪费,但这种浪费通常不会被注意到。最浪费的情况可能是视频只是一首歌曲的静态图像,其中每个关键帧都完全相同。

我不确定的一件事是强制关键帧如何与使用 -maxrate 和 -bufsize 设置的速率限制器交互。我认为即使 YouTube 最近也遇到了无法正确配置缓冲区设置以提供一致质量的问题。如果您只是使用一些网站可以看到的平均比特率设置(因为您可以使用十六进制编辑器检查 header/mov atom 中的 x264 选项?),那么缓冲区模型不是问题,但如果您提供用户生成的内容,平均比特率会鼓励用户在视频末尾添加黑屏。

Ffmpeg 的 -g 选项或您使用的任何其他编码器选项都映射到特定于编码器的选项。因此“-x264-params keyint=GOPSIZE”相当于“-g GOPSIZE”。

使用场景检测的一个问题是,如果您出于某种原因更喜欢特定数字附近的关键帧。如果您每 5 秒指定一个关键帧并使用场景检测,并且在 4.5 处发生场景变化,那么应该检测到它,但下一个关键帧将在 9.5 处。如果时间继续像这样增加,您最终可能会在 42.5、47.5、52.5 等处得到关键帧,而不是 40、45、50、55。相反,如果在 5.5 处发生场景变化,那么在 5 处将有一个关键帧,而 5.5 处对于另一个关键帧来说太早了。Ffmpeg 不允许您指定“如果接下来的 30 帧内没有场景变化,则在此处创建关键帧”。不过,了解 C 的人可以添加该选项。

对于可变帧率视频,当您不像 Twitch 那样进行直播时,您应该能够使用场景变化,而无需永久转换为恒定帧率。如果您在 ffmpeg 中使用“select”过滤器并在表达式中使用“scene”常量,则调试输出(-v debug 或在编码时多次按“+”)会显示场景变化编号。这可能与 x264 使用的数字不同,并且没有那么有用,但它仍然有用。

那么,该过程可能是制作一个仅用于关键帧更改的测试视频,但如果使用 2-pass,也许可以用于速率控制数据。(不确定生成的数据是否对不同的分辨率和设置有用;宏块树数据不会。)将其转换为恒定帧速率视频,但请参阅这个错误如果您决定将 fps 过滤器用于其他目的,请考虑将帧速率减半时输出卡顿的问题。使用您想要的关键帧和 GOP 设置通过 x264 运行它。

然后只需将这些关键帧时间与原始可变帧率视频一起使用即可。

如果您允许完全疯狂的用户生成内容,并且帧之间有 20 秒的间隙,那么对于可变帧速率编码,您可以分割输出,使用 fps 过滤器,以某种方式使用选择过滤器(也许构建一个包含每个关键帧时间的非常长的表达式)...或者您可以使用测试视频作为输入并仅解码关键帧(如果该 ffmpeg 选项有效),或者使用选择过滤器选择关键帧。然后将其缩放到正确的大小(甚至有一个 scale2ref 过滤器)并将原始视频叠加在其上。然后使用交错过滤器将这些注定强制的关键帧与原始视频相结合。如果这导致两个相距 0.001 秒的帧,而交错过滤器无法阻止,那么请使用另一个选择过滤器自己解决这个问题。处理交错过滤器的帧缓冲区限制可能是这里的主要问题。这些都可以工作:使用某种过滤器来缓冲更密集的流(fifo 过滤器?);多次引用输入文件,以便对其进行多次解码,并且无需存储帧;在关键帧的时间准确使用“streamselect”过滤器(我从来没有这样做过);通过更改其默认行为或添加选项来输出缓冲区中最旧的帧而不是丢弃帧来改进交错过滤器。

相关内容