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


38

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

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

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

方法1:弄乱libx264的参数

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

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

方法2:设定固定的GOP大小:

-g GOP_LEN_IN_FRAMES

不幸的是,这仅是在FFMPEG文档中记录的,因此该论据的效果还不清楚。

方法3:每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规范(如果有
  • 保证将有一个固定节奏的关键帧,而与libx264 scenecut关键帧无关。

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

最终,keyint方法似乎像个hack。我希望这不是正确的答案。

参考文献:

使用-force_key_frames方法的例子

使用keyint方法的例子

FFmpeg高级视频选项部分

Answers:


27

TL; DR

我建议以下内容:

  • 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为单位的间隔,是以秒为单位的间隔。例如,对于2秒间隔和30fps视频,X= 60和N= 2。

关于不同框架类型的注释

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

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

对于流媒体,推荐什么?

对于流式传输情况,您想要:

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

参数有什么作用?

为了配置编码器,我们必须了解关键帧参数的作用。我进行了一些测试libx264libx265libvpx-vp9在FFmpeg中针对三个编码器发现了以下内容:

  • 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仅当与FFmpeg 相同时才转发到编码器-g。在libvpxenc.cFFmpeg 的代码中,我们可以找到:

      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,我注意到质量出现了严重的波动,因此在这种情况下我不建议使用它。


谢谢!这是很好的反馈。我有一个问题是您是如何生成该表格的。我完全可以使用这样的东西。
Mark Gerolimatos 2015年

(似乎没有办法直接写信给您)您能指出我指向此ITU-T讨论中任何线程的链接吗?谢谢!
Mark Gerolimatos 2015年

2
我只是在Excel中进行了此操作,粘贴了从的三个运行中获得的输出ffprobe -i input.mp4 -select_streams v -show_frames -of csv -show_entries frame=pict_type,然后为单元着色。恐怕没有公开讨论,但是我看看是否可以挖掘我当时发现的一些链接。
slhck

您可以使用该-force_key_frames expr:gte(t,n_forced*GOP_LEN_IN_SECONDS)表格重试一下吗?我只是尝试了一下,发现虽然流中有多余的I帧,但DID似乎遵守了我的规则。PERL程序将作为“答案”,因为您显然不能在注释中使用标记。
Mark Gerolimatos 2015年

有趣。我认为,如果您发现它可行,则值得单独的“真实”答案。(Stack Exchange网站对于这种讨论式的回复并不是很有效。)我上次检查时,-force_key_frames它对我没有用,所以我再也没有尝试过。那是一年多以前了。也许这是一个错误。我会尽快重试。
slhck

12

这是我的案件的五十美分。

方法1:

弄乱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

按预期方式生成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秒插入一个关键帧(MAYBE):

-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高配置文件的用户提供了更好的体验。

希望它可以帮助某人。


太好了,谢谢你的50美分!
BrunoFenzl

7

因此,答案似乎是:

  • 方法1已被验证可以工作,但是是libx264特定于方法的,其代价是消除了中非常有用的scenecut选项libx264
  • 方法3从2015年4月的FFMPEG版本开始工作,但是您应该使用本文底部的脚本验证结果,因为FFMPEG文档尚不清楚该选项的效果。如果可行,则这是两个选项中的佼佼者。
  • 不要使用方法2,-g似乎已弃用。它既不起作用,也未在文档中明确定义,也未在帮助中找到,也未在代码中使用。代码检查表明,该-g选项可能适用于MPEG-2流(甚至有引用PAL和NTSC的代码节!)。

也:

  • 使用方法3生成的文件可能比方法1稍大,因为允许插页式I帧(关键帧)。
  • 在两种情况下,即使方法3在指定的时间或之后的时间在下一个帧位置放置I帧,也应显式设置“ -r”标志。未能设置“ -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;
}

请注意:由于这是一个问答网站,而不是一个按时间顺序排列帖子的讨论论坛,因此最好将所有信息都放在一个答案中,这样,寻求解决方案的人们只需要阅读一篇帖子而不用看在谁发布什么内容的时候:)我合并了您的答案,并为此给了+1。由于不允许交叉发布,因此建议您在视频网站上删除您的问题。人们会在这里找到答案。
slhck

1
我只是想了一下(实际上是在FFmpeg邮件列表中提出的)。使用时force_key_frames,它会弄乱x264位分配算法,因此与仅设置固定的关键帧间隔相比,它可能给您带来较差的质量。
slhck

哇靠。还有一个理由让FFMPEG提供一种非编解码器的方式来做到这一点,这一论点“将对所讨论的编解码器做出最好的贡献”。我曾尝试用FFMPEG的
追踪

@slhck:请您提供更多细节吗?我查看了2015年5月的邮件列表档案,但找不到任何东西。底线是忘记“方法3”并坚持“方法1”。
schieferstapel

3
@MarkGerolimatos:关于-g,您说,“它似乎既不起作用,也似乎未在代码中使用。”。我检查和的输入g被存储在avctx->gop_size和libx264利用了它:x4->params.i_keyint_max = avctx->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也可以设置该GOP 。
Gyan

4

我想在此处添加一些信息,因为我在谷歌搜索中找到了有关尝试找到一种方法以希望的方式对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-Frame以来的帧数为总是少于min-keyinit

最后设置该force_key_frame选项将覆盖double大小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帧,它们是根据场景切换检测插入的。


太棒了!这总是很违反直觉的,谢谢您的努力。概括地说,我假设您对“ I帧”和“ i帧”的定义是概念性的(也就是说,在libx264中不是可明确配置的),并且“ max * 2”是您实施它的方式吗?
Mark Gerolimatos

是的,这只是概念上的,尽管我已经看到人们使用“ I”和“ i”来区分IDR和非IDR I帧。是的,将keyinit设置为所需的gop大小* 2是一种将gop内的所有I帧强制为非IDR I帧的方法。然后,ffmpeg -force-key-frames会覆盖x264opts中的key-init。基本上,这是获得期望结果的一种真正向后的方式,如果x264代码允许您将min-keyinit和keyinit设置为相同的值,那将是可能的。
Reuben

...同时还能够保持场景切换检测处于打开状态并获得固定的GOP大小。
Reuben

再次感谢您的出色工作!听起来我们需要一种不太“落后”的方式来实现它
Mark Gerolimatos

在这里需要rc-lookahead吗?它会影响mbtree和VBV,但会影响i帧生成吗?
亚历山大·斯维特金

0

似乎该语法始终无法正常工作。.我已经在我们的VOD内容以及实时内容(文件转储)上进行了大量测试,有时Scenecut无法正常工作并触发中间iframe:

i50-> p50上转换的语法,gop / segment 2秒,开始时IDR,如果需要,在它们之间插入iframe

ffmpeg.exe -loglevel verbose -i avc_50i.ts -pix_fmt yuv420p -filter_complex yadif=1,scale=1920:1080 -vcodec libx264 -preset fast -x264-params "rc-lookahead=100:keyint=200:min-keyint=100:hrd=1:vbv_maxrate=12000:vbv_bufsize=12000:no-open-gop=1" -r 50 -crf 22 -force_key_frames "expr:eq(mod(n,100),0)" -codec:a aac -b:a 128k -y target.ts

0

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中使用“选择”过滤器并在表达式中使用“场景”常量,则调试输出(-v调试或在编码时按几次“ +”)将显示场景更改编号。这可能与x264使用的数字不同,但没有x264使用的数字有用,但是它仍然有用。

然后,该过程可能是制作仅用于关键帧更改的测试视频,但如果使用2遍,则可以将其用于速率控制数据。(不确定所生成的数据是否对于不同的分辨率和设置完全有用;宏块树数据将无效。)将其转换为恒定帧率的视频,但是如果您决定将帧率减半,则会看到此错误的输出将fps过滤器用于其他目的。使用所需的关键帧和GOP设置通过x264运行它。

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.