与 ffmpeg 的帧数不一致

Inconsistent frame number with ffmpeg

我经常遇到 hvc1 视频在 ffprobe 信息和 FFmpeg 信息之间获取不一致帧数的问题,我想知道这个问题的可能原因是什么,如果没有,如何解决它重新编码视频。

我用我的测试视频编写了以下示例脚本

我将视频分成 5 秒的片段,我得到 ffprobe 给出了预期的视频长度,但 FFmpeg 给出的每个片段都比预期少 3 帧,但第一个片段除外。

如果我分割 10 秒或任何分割,问题完全相同,我总是丢失 3 帧。

我注意到第一个片段总是比其他片段小 3 帧(在 ffprobe 上),并且它是唯一一致的片段。

这是我为测试此问题而编写的示例脚本:

# get total video frame number using ffprobe or ffmpeg
total_num_frames=$(ffprobe -v quiet -show_entries stream=nb_read_packets -count_packets -select_streams v:0 -print_format json test_video.mp4 | jq '.streams[0].nb_read_packets' | tr -d '"')
echo $total_num_frames
ffmpeg -hwaccel cuda -i test_video.mp4 -vsync 2 -f null -

# Check ffprobe of each segment is consistent 
rm -rf clips && mkdir clips && \
ffmpeg -i test_video.mp4 -acodec copy -f segment -vcodec copy -reset_timestamps 1 -segment_time 5 -map 0 clips/part_%d.mp4
count_frames=0
for i in {0..5}
do
    num_packets=$(ffprobe -v quiet -show_entries stream=nb_read_packets -count_packets -select_streams v:0 -print_format json clips/part_$i.mp4 | jq '.streams[0].nb_read_packets' | tr -d '"')
    count_frames=$(($count_frames+$num_packets))
    echo $num_packets $count_frames $total_num_frames
done

输出如下

3597
ffmpeg version 4.2.4-1ubuntu0.1 Copyright (c) 2000-2020 the FFmpeg developers
  built with gcc 9 (Ubuntu 9.3.0-10ubuntu2)
  configuration: --prefix=/usr --extra-version=1ubuntu0.1 --toolchain=hardened --libdir=/usr/lib/x86_64-linux-gnu --incdir=/usr/include/x86_64-linux-gnu --arch=amd64 --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-avisynth --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-nvenc --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared
  libavutil      56. 31.100 / 56. 31.100
  libavcodec     58. 54.100 / 58. 54.100
  libavformat    58. 29.100 / 58. 29.100
  libavdevice    58.  8.100 / 58.  8.100
  libavfilter     7. 57.100 /  7. 57.100
  libavresample   4.  0.  0 /  4.  0.  0
  libswscale      5.  5.100 /  5.  5.100
  libswresample   3.  5.100 /  3.  5.100
  libpostproc    55.  5.100 / 55.  5.100
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'test_video.mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2mp41
    encoder         : Lavf58.29.100
  Duration: 00:00:59.95, start: 0.035000, bitrate: 11797 kb/s
    Stream #0:0(und): Video: hevc (Main) (hvc1 / 0x31637668), yuv420p(tv, bt709), 1920x1080, 11692 kb/s, 60.01 fps, 60 tbr, 19200 tbn, 19200 tbc (default)
    Metadata:
      handler_name    : Core Media Video
    Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, mono, fltp, 91 kb/s (default)
    Metadata:
      handler_name    : Core Media Audio
Stream mapping:
  Stream #0:0 -> #0:0 (hevc (native) -> wrapped_avframe (native))
  Stream #0:1 -> #0:1 (aac (native) -> pcm_s16le (native))
Press [q] to stop, [?] for help
Output #0, null, to 'pipe:':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2mp41
    encoder         : Lavf58.29.100
    Stream #0:0(und): Video: wrapped_avframe, nv12, 1920x1080, q=2-31, 200 kb/s, 60 fps, 60 tbn, 60 tbc (default)
    Metadata:
      handler_name    : Core Media Video
      encoder         : Lavc58.54.100 wrapped_avframe
    Stream #0:1(und): Audio: pcm_s16le, 44100 Hz, mono, s16, 705 kb/s (default)
    Metadata:
      handler_name    : Core Media Audio
      encoder         : Lavc58.54.100 pcm_s16le
frame= 3597 fps=788 q=-0.0 Lsize=N/A time=00:00:59.95 bitrate=N/A speed=13.1x    
video:1883kB audio:5162kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown

然后

297 297 3597
300 597 3597
300 897 3597
300 1197 3597
300 1497 3597
300 1797 3597 <--- output are consistent based on ffprobe

但是如果我使用以下命令检查 ffmpeg 的段大小

ffmpeg -hwaccel cuda -i clips/part_$i.mp4 -vsync 2 -f null - 

第 0 部分没问题

frame=  297 fps=0.0 q=-0.0 Lsize=N/A time=00:00:04.95 bitrate=N/A speed=12.5x    
video:155kB audio:424kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown

所有其他部分不一致,应该是 300

frame=  297 fps=0.0 q=-0.0 Lsize=N/A time=00:00:04.95 bitrate=N/A speed=12.3x    
video:155kB audio:423kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown

问题与任何其他间隔大小完全相同,例如 10 秒我会得到以下视频大小:

ffprobe 597 - 600 ...
ffmpeg 597 597 ...

我认为这可能与源 vfr 或 cfr 有关,但我尝试将输入转换为 cfr,但没有任何改变。

此外,我尝试每秒强制关键帧检查是否是以下参数的关键帧问题:-force_key_frames“expr:gte(t,n_forced*1)”,但问题完全一样。

我做错了什么?我经常遇到 hvc1 中的文件,我真的不知道如何处理。

差异的来源是 FFprobe 计算丢弃的数据包,而 FFmpeg 不将丢弃的数据包计为帧。


您的结果与使用 3 个 B 帧(每个 P 帧或 I 帧对应 3 个连续的 B 帧)创建的视频流一致。

根据Wikipedia

I‑frames are the least compressible but don't require other video frames to decode.
P‑frames can use data from previous frames to decompress and are more compressible than I‑frames.
B‑frames can use both previous and forward frames for data reference to get the highest amount of data compression.

在不重新编码的情况下将具有 P 帧和 B 帧的视频分割成片段时,依赖链会中断。

  • (几乎)总是有帧依赖于前一段或下一段的帧。
  • 保留上述帧,但匹配的数据包被标记为“丢弃”(标记为AV_PKT_FLAG_DISCARD标志)。

为了处理同一个数据集,我们构建了合成视频(用作输入)。

使用以下命令构建合成视频:

ffmpeg -y -r 60 -f lavfi -i testsrc=size=384x256:rate=1 -vf "setpts=N/60/TB" -g 60 -vcodec libx265 -x265-params crf=28:bframes=3:b-adapt=0 -tag:v hvc1 -pix_fmt yuv420p -t 20 test_video.mp4
  • -g 60 将 GOP 大小设置为 60 帧(每 60 帧插入一个关键帧)。
  • bframes=3:b-adapt=0 强制 3 个连续的 B 帧。

为了验证I/P/B帧的数量,我们可以使用FFprobe:

ffprobe -i test_video.mp4 -show_frames -show_entries frame=pict_type

输出如下:

pict_type=I
pict_type=B
pict_type=B
pict_type=B
pict_type=P
pict_type=B
pict_type=B
pict_type=B
...


按时间对视频进行分段(每段 5 秒):

ffmpeg -i test_video.mp4 -f segment -vcodec copy -reset_timestamps 1 -segment_time 5 clips/part_%d.mp4

FFprobe 计数:
297 1497 1200
300 1797 1200
300 2097 1200
303 2400 1200

FFmpeg 计数:
frame= 297
frame= 297
frame= 297
frame= 300

可以看到,结果和你的输出是一致的。


我们可以使用 FFprobe 识别“丢弃”的数据包:

ffprobe -i part_1.mp4 -show_packets

寻找flags=_D.
flags=_D 的数据包被标记为“已丢弃”
注意:在视频流中,每个数据包都匹配一个帧。

FFprobe 输出开始于:
flags=K_
flags=_D
flags=_D
flags=_D
flags=__
flags=__
flags=__
...

对于每个中间段,有 3 个数据包被标记为“丢弃”,这就是 FFmpeg 与 FFprobe 相比丢失 3 个帧的原因。