如何使用FFMPEG拆分视频,以便每个块都从关键帧开始?

59

我们需要将一个大型的实时WMV视频流分成相同大小的小块。我们编写了一个脚本来完成这个任务,但有一个问题:视频块没有以关键帧开始,所以在播放大多数视频块时,直到最终到达原始视频的关键帧之前都不会显示任何图像。

有没有办法告诉FFMPEG使输出视频从关键帧开始?

以下是我们目前的命令行:

ffmpeg.exe -i "C:\test.wmv" -ss 00:00:00 -t 00:00:05 -acodec copy -vcodec copy -async 1 -y  "0000.wmv"
ffmpeg.exe -i "C:\test.wmv" -ss 00:00:05 -t 00:00:05 -acodec copy -vcodec copy -async 1 -y  "0001.wmv"

and so on...


1
你是否正在使用最新版本的ffmpeg构建? - llogan
1
是的,最新版为 ffmpeg version N-47062-g26c531c。我们还尝试了使用 -force_key_frames:v 00:00:00 或者 -force_key_frames:v 00:00:05,但输出结果并没有任何变化。我认为使用 -vcodec copy 实际上只是在新文件中复制图片组,而不会进行任何重新编码以添加关键帧。这是正确的吗? - sboisse
“-vcodec copy” 不重新编码:它只执行分离和复用。 - llogan
1
有没有一种方法可以将我的文件分成块,使得每个块都以一个关键帧开头?它们不必具有相同的持续时间,但必须完全连续。如果我在输入文件上指定-ss,它会在关键帧处切割,但是我会看到我的视频重叠,因为同一个GOP被重新包含在连续的两个块中。 - sboisse
6个回答

53
最新版本的FFMPEG包含了一个新选项"segment",它正好满足了我认为你所需要的功能。
ffmpeg -i INPUT.mp4 -acodec copy -f segment -vcodec copy \ 
    -reset_timestamps 1 -map 0 OUTPUT%d.mp4

这将产生一系列按关键帧分割的编号输出文件。在我的测试中,它的效果很好,尽管我只在几分钟的视频上使用过,并且只在MP4格式下。

1
似乎示例已经移至此处:https://www.ffmpeg.org/ffmpeg-formats.html#Examples-9 - jox
1
@jox 看起来他们又搬家了,新地址为:https://www.ffmpeg.org/ffmpeg-formats.html#segment_002c-stream_005fsegment_002c-ssegment - TheAudiophile
@TheAudiophile 你说得对,谢谢。以下链接直接跳转到示例:https://www.ffmpeg.org/ffmpeg-formats.html#Examples-10 - jox

45
根据官方FFMPEG文档所述,我发现在指定-ss timestart 之前指定-i input_file.ext效果更好,因为它会将生成的视频的开始时间设置为您指定时间戳之前的最近的关键帧(至少我是这样理解的)。
请将您的示例更改为:
ffmpeg.exe -ss 00:00:00 -i "C:\test.wmv" -t 00:00:05 -acodec copy \
    -vcodec copy -async 1 -y  "0000.wmv"

我已经测试过这种方法可以用于.flv和.mp4文件。

3
尽管这有点混乱了视频的持续时间,比如我拿到的视频显示为8秒长,尽管我指定了视频持续5秒钟。实际上视频确实在5秒结束了,但在所有媒体播放器中都显示为8秒长。有什么想法吗? - IanDess
11
我认为这是不正确的。在寻求页面向下滚动,有一个特别针对codec copy的声明:当使用-ss作为输入选项并与-c:v copy一起使用时,可能不准确,因为ffmpeg被强制只使用/分割I帧。尽管如果可能的话,它会调整流的开始时间为负值以进行补偿。基本上,如果你指定“第157秒”,但直到第159秒才出现关键帧,它将在开头包含两秒音频(没有视频),然后从第一个关键帧开始。所以在拆分和进行编解码复制时要小心。 - Ondra Žižka
1
@Ondra:好的,但是怎样才能防止这种情况发生,生成一个从头到尾都能正确播放的视频呢? - MRule
1
@OndraŽižka 注意这一点是很好的。当剪辑视频的开头部分时,最好分两步进行:第一步在输入之前使用-ss参数,不准确地跳到你想要开始的位置之前,并将输出保存为临时文件;第二步在输入之后再次使用-ss参数,以临时文件作为输入,并更精确地设置-ss值。这样可以让你找出精确的-ss值,而无需使用冗长但准确的方法反复寻找所需的截止点。 - BallpointBen
1
@BallpointBen - 对的。@MRule - 看上面的评论。虽然有点繁琐,但是是正确的。我希望 ffmpeg 能够自动化这个过程。 - Ondra Žižka
显示剩余2条评论

22

这是我找到的解决方案:

像av501和d33pika建议的那样,我使用了ffprobe来查找关键帧的位置。由于ffprobe非常冗长,并且可能需要几秒甚至几分钟才能输出所有关键帧,而且没有办法从漫长的视频中确定我们想要的帧范围,因此我分成了5个步骤:

  1. Export a video chunk from the original file, around the double of the desired chunk size.

    ffmpeg -i source.wmv -ss 00:00:00 -t 00:00:06 -acodec copy -vcodec copy -async 1 -y  0001.wmv
    
  2. Use ffprobe to find where the keyframes are. Choose closest keyframe after desired chunk size.

    ffprobe -show_frames -select_streams v -print_format json=c=1 0001.wmv
    
  3. From the output of ffprobe get the pkt_dts_time of the frame just before that key frame.

  4. ffmpeg on the exported chunk of step 1, specifying the same input and output file, and specifying -ss 00:00:00 and -t [value found in step 3].

    ffmpeg -i 0001.wmv -ss 00:00:00 -t 00:00:03.1350000 -acodec copy -vcodec copy -async 1 -y 0001.wmv
    
  5. Restart at step 1, using -ss [cumulated sum of values found in step 3 over iterations].

按照这种方式进行操作,我能够高效而稳定地在关键帧处分割视频。

1
在您的帮助下,我也能够做到同样的事情,但是初始的dts/pts有一点偏移(这里是500毫秒)。文档表明,如果在-i之前传递-ss,则它会作为一个seek操作,并且在我的情况下,它修复了偏移。请阅读man ffmpeg中的-ss部分 :) - tito
1
为什么在关键帧之前使用帧而不是关键帧本身?回复:“从ffprobe的输出中获取紧挨着那个关键帧的帧的pkt_dts_time。” - felwithe
1
如果你不在关键帧之前剪切,那么该关键帧将被包含在块的最后一帧中,并且下一个块将不以关键帧开始。 - sboisse
1
ffprobe的语句“无法限定我们想要从长视频中获取的帧范围”是不正确的,或者至少不再正确。例如,添加选项-read_intervals 18.6%+4.2会从第18.6秒开始读取,并持续4.2秒。 - Sebastian

16

使用更新版本的ffmpeg,可以通过使用ffprobeffmpegmuxer来实现此目的。

  1. 使用ffprobe和awk尽可能地标识关键帧,以符合您所需的块长度。
ffprobe -show_frames -select_streams v:0 \
        -print_format csv [SOURCE_VIDEO] 2>&1 |
grep -n frame,video,1 |
awk 'BEGIN { FS="," } { print $1 " " $5 }' |
sed 's/:frame//g' |
awk 'BEGIN { previous=0; frameIdx=0; size=0; } 
{
  split($2,time,".");
  current=time[1];
  if (current-previous >= [DURATION_IN_SECONDS]){
    a[frameIdx]=$1; frameIdx++; size++; previous=current;
  }
}
END {
  str=a[0];
  for(i=1;i<size;i++) { str = str "," a[i]; } print str;
}'

如下是说明:

  • [SOURCE_VIDEO] 表示你想分段的视频路径
  • [DURATION_IN_SECONDS] 表示所需的每个片段长度(以秒为单位)

输出结果是逗号分隔的关键帧字符串。

  1. 使用上面输出的关键帧作为输入到 ffmpeg
ffmpeg -i [SOURCE_VIDEO] -codec copy -map 0 -f segment \
       -segment_frames [OUTPUT_OF_STEP_1] [SEGMENT_PREFIX] \
       _%03d.[SOURCE_VIDEO_EXTENSION]

在哪里

  • [SOURCE_VIDEO] = 您想要分割的视频路径
  • [OUTPUT_OF_STEP_1] = 关键帧的逗号分隔字符串
  • [SEGMENT_PREFIX] = 分段输出的名称
  • [SOURCE_VIDEO_EXTENSION] = 源视频的扩展名(例如mp4、mkv)

5
使用ffprobe -show_frames -pretty <stream>来识别关键帧。

看起来是个很有前途的解决方案。我会进行调查。我在文档中检查了一下,但没有轻松找到一种方法来ffprobe视频的一个小部分(比如只有前10秒)。由于视频非常长,它会生成几MB的输出,需要几秒钟才能生成。在ffprobe中是否有选项可以仅输出特定流的帧,并且在特定范围内? - sboisse
ffprobe -show_frames -select_streams v 过滤只包含视频帧。现在我只需要能够指定一段时间或一段帧范围。 - sboisse
我查阅了文档,没有找到关于限制视频特定时间范围的内容。所以我想我会使用ffmpeg获取一个块,然后调用ffprobe获取关键帧,最后在关键帧之前再次调用ffmpeg进行最终分割。 - sboisse
2
@sboisse,“-read_intervals”现在允许您指定时间或数据包间隔来探测。例如,ffprobe -show_frames -read_intervals 10%+20,01:30%01:45 -i <file>将从10-30秒和1:30-1:45进行探测。 - user2975337

2
如果您想要在特定时间间隔内进行I帧的脚本编写,那么一种方法是:
1. 运行ffprobe并从输出中收集I帧的位置。
ffprobe -show_streams
2. 使用相同的脚本运行一系列-ss -t命令以获取所需的块。
然后,您的脚本可以决定最小帧数[例如,在10帧内有两个I图片,您真的不想在那里切块]。
另一种方法是使用gstreamer的multisfilesink并将模式设置为关键帧[但这样会在每个关键帧处切块,因此可能不理想]。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接