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

Mar*_*tos 54 video ffmpeg streaming video-conversion

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

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

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

方法 1:弄乱 libx264 的参数

-c:v libx264 -x264opts keyint=GOPSIZE:min-keyint=GOPSIZE:scenecut=-1
Run Code Online (Sandbox Code Playgroud)

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

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

-g GOP_LEN_IN_FRAMES
Run Code Online (Sandbox Code Playgroud)

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

方法 3:每 N 秒插入一个关键帧(可能?):

-force_key_frames expr:gte(t,n_forced*GOP_LEN_IN_SECONDS)
Run Code Online (Sandbox Code Playgroud)

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

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

哪个是对的?

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

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

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

最后,这个keyint方法看起来就像一个 hack。我希望这不是正确的答案。

参考:

使用该-force_key_frames方法的示例

使用该keyint方法的示例

FFmpeg 高级视频选项部分

slh*_*hck 41

TL;DR

I would recommend the following:

  • libx264: -g X -keyint_min X (and optionally add -force_key_frames "expr:gte(t,n_forced*N)")
  • libx265: -x265-params "keyint=X:min-keyint=X"
  • libvpx-vp9: -g X

where X is the interval in frames and N is the interval in seconds. For example, for a 2-second interval with a 30fps video, X = 60 and N = 2.

A note about different frame types

In order to properly explain this topic, we first have to define the two types of I-frames / keyframes:

  • Instantaneous Decoder Refresh (IDR) frames: These allow independent decoding of the following frames, without access to frames previous to the IDR frame.
  • Non-IDR-frames: These require a previous IDR frame for the decoding to work. Non-IDR frames can be used for scene cuts in the middle of a GOP (group of pictures).

What is recommended for streaming?

For the streaming case, you want to:

  • Ensure that all IDR frames are at regular positions (e.g. at 2, 4, 6, … seconds) so that the video can be split up into segments of equal length.
  • Enable scene cut detection, so as to improve coding efficiency / quality. This means allowing I-frames to be placed in between IDR frames. You can still work with scene cut detection disabled (and this is part of many guides, still), but it's not necessary.

What do the parameters do?

In order to configure the encoder, we have to understand what the keyframe parameters do. I did some tests and discovered the following, for the three encoders libx264, libx265 and libvpx-vp9 in FFmpeg:

  • libx264:

    • -g sets the keyframe interval.
    • -keyint_min sets the minimum keyframe interval.
    • -x264-params "keyint=x:min-keyint=y" is the same as -g x -keyint_min y.
    • Note: When setting both to the same value, the minimum is internally set to half the maximum interval plus one, as seen in the x264 code:

      h->param.i_keyint_min = x264_clip3( h->param.i_keyint_min, 1, h->param.i_keyint_max/2+1 );
      
      Run Code Online (Sandbox Code Playgroud)
  • libx265:

    • -g is not implemented.
    • -x265-params "keyint=x:min-keyint=y" works.
  • libvpx-vp9:

    • -g sets the keyframe interval.
    • -keyint_min sets the minimum keyframe interval
    • Note: Due to how FFmpeg works, -keyint_min is only forwarded to the encoder when it is the same as -g. In the code from libvpxenc.c in FFmpeg we can find:

      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;
      
      Run Code Online (Sandbox Code Playgroud)

      This might be a bug (or lack of feature?), since libvpx definitely supports setting a different value for kf_min_dist.

Should you use -force_key_frames?

The -force_key_frames option forcibly inserts keyframes at the given interval (expression). This works for all encoders, but it might mess with the rate control mechanism. Especially for VP9, I've noticed severe quality fluctuations, so I cannot recommend using it in this case.

  • 我刚刚在 Excel 中制作了它,粘贴了我从三轮`ffprobe -i input.mp4 -select_streams v -show_frames -of csv -show_entries frame=pict_type` 中获得的输出,然后为单元格着色。恐怕没有公开讨论,但我会看看我是否可以挖掘我当时找到的一些链接。 (2认同)

小智 14

这是我的案子五十美分。

方法一:

搞乱 libx264 的参数

-c:v libx264 -x264opts keyint=GOPSIZE:min-keyint=GOPSIZE:scenecut=-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
Run Code Online (Sandbox Code Playgroud)

按预期生成 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
Run Code Online (Sandbox Code Playgroud)

方法二折旧。省略。

方法三:

每 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
Run Code Online (Sandbox Code Playgroud)

以稍微不同的方式生成 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
Run Code Online (Sandbox Code Playgroud)

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

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

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

希望它可以帮助某人。


Mar*_*tos 8

因此,答案似乎是:

  • 方法1验证工作,但libx264特异性,并配备消除了非常有用的成本scenecut的选项libx264
  • 方法 3 从 2015 年 4 月的 FFMPEG 版本开始工作,但您应该使用本文底部包含的脚本验证您的结果,因为 FFMPEG 文档不清楚该选项的效果。如果它有效,它是两个选项中的优越者。
  • 请勿使用方法 2,-g似乎已被弃用。它似乎不起作用,也没有在文档中明确定义,也没有在帮助中找到,也没有出现在代码中。代码检查表明该-g选项可能适用于 MPEG-2 流(甚至有代码节指的是 PAL 和 NTSC!)。

还:

  • 使用方法 3 生成的文件可能比方法 1 稍大,因为允许使用间隙 I 帧(关键帧)。
  • 在这两种情况下,您都应该明确设置“-r”标志,即使方法 3 在指定时间或之后将I 帧放置在下一个帧槽中。未能设置“-r”标志会使您受源文件的支配,可能具有可变的帧速率。可能会导致不兼容的 DASH 转换。
  • 尽管 FFMPEG 文档中有警告,但方法 3并不比其他方法效率低。事实上,测试表明它可能比方法 1 更有效。

-force_key_frames选项的脚本

这是一个简短的 PERL 程序,我用来根据 slhck 的 ffprobe 建议的输出来验证 I 帧节奏。它似乎验证了该-force_key_frames方法也将起作用,并且具有允许scenecut帧的额外好处。我完全不知道 FFMPEG 是如何完成这项工作的,或者我是否只是因为我的流恰好处于良好状态而幸运。

就我而言,我以 30fps 编码,预期 GOP 大小为 6 秒或 180 帧。我使用 180 作为该程序的 gopsize 参数验证了每个 180 的倍数处的 I 帧,但将其设置为 181(或任何其他不是 180 的倍数的数字)使它抱怨。

#!/usr/bin/perl
use strict;
my $gopsize = shift(@ARGV);
my $file = shift(@ARGV);
print "GOPSIZE = $gopsize\n";
my $linenum = 0;
my $expected = 0;
open my $pipe, "ffprobe -i $file -select_streams v -show_frames -of csv -show_entries frame=pict_type |"
        or die "Blah";
while (<$pipe>) {
  if ($linenum > $expected) {
    # Won't catch all the misses. But even one is good enough to fail.
    print "Missed IFrame at $expected\n";
    $expected = (int($linenum/$gopsize) + 1)*$gopsize;
  }
  if (m/,I\s*$/) {
    if ($linenum < $expected) {
      # Don't care term, just an extra I frame. Snore.
      #print "Free IFrame at $linenum\n";
    } else {
      #print "IFrame HIT at $expected\n";
      $expected += $gopsize;
    }
  }
  $linenum += 1;
}
Run Code Online (Sandbox Code Playgroud)

  • @MarkGerolimatos :关于`-g`,你说,“它似乎不起作用,......似乎也没有在代码中使用。”。我检查过,`g` 的输入存储在 `avctx-&gt;gop_size` 中,而 libx264 使用了它:`x4-&gt;params.i_keyint_max = avctx-&gt;gop_size;`。当我探测这个生成的测试文件时:`ffmpeg -i a-test-file.mp4 -g 37 -t 15 gtest.mp4`,我得到的关键帧恰好是 `0,37,74,111,148,185,222,259,296,333,370`。如果触发场景变化,GOP 可以被缩短,为此可以设置 `-sc_threshold`,它也被 x264 接收。 (5认同)
  • 我只是有一个想法(实际上它是在 FFmpeg 邮件列表中提出的)。当你使用 `force_key_frames` 时,它会搞乱 x264 位分配算法,所以它可能比简单地设置一个固定的关键帧间隔给你更糟糕的质量。 (2认同)

Reu*_*ben 8

我想在这里添加一些信息,因为我的谷歌搜索在我寻求找到有关尝试找到一种方法以我想要的方式分割我的 DASH 编码的信息时相当多地提出了这个讨论,而且我发现的信息都不是完全正确的。

首先要摆脱的几个误解:

  1. 并非所有 I 帧都相同。有大的“I”框架和小的“i”框架。或者使用正确的术语,IDR I 帧和非 IDR I 帧。IDR I 帧(有时称为“关键帧”)将创建一个新的 GOP。非 IDR 帧不会。它们可以方便地放在有场景变化的 GOP 内部。

  2. -x264opts keyint=GOPSIZE:min-keyint=GOPSIZE← 这并不像你认为的那样。这花了我一点时间才弄明白。事实证明min-keyint,代码中的is 是有限的。不允许大于(keyint / 2) + 1。因此,为这两个变量分配相同的值会导致min-keyint编码时的值减半。

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

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

最后设置force_key_frame选项会覆盖 double size keyint。所以这是有效的:

我更喜欢 2 秒的片段,所以我的 GOPSIZE = Framerate * 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>
Run Code Online (Sandbox Code Playgroud)

您可以使用 ffprobe 进行验证:

ffprobe <SRC_FLE> -select_streams v -show_frames -of csv -show_entries frame=coded_picture_number,key_frame,pict_type > frames.csv
Run Code Online (Sandbox Code Playgroud)

在生成的 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
Run Code Online (Sandbox Code Playgroud)

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