如何使用 FFmpeg 从视频中删除多个片段?

Mat*_*ias 80 mp4 ffmpeg video-editing

我正在尝试使用 FFmpeg 删除视频的几个部分。

例如,想象一下,如果您在电视上录制了一个节目并想剪掉广告。这对于 GUI 视频编辑器来说很简单;您只需标记要删除的每个剪辑的开头和结尾,然后选择删除。我正在尝试使用 FFmpeg 从命令行执行相同的操作。

我知道如何将单个片段剪切为视频,如下所示:

ffmpeg -i input.avi -ss 00:00:20 -t 00:00:05 -map 0 -codec copy output.avi
Run Code Online (Sandbox Code Playgroud)

这会剪切一个 5 秒的剪辑并将其另存为新的视频文件,但是我如何做相反的事情并在没有指定剪辑的情况下保存整个视频,以及如何指定要删除的多个剪辑?

例如,如果我的视频可以用 ABCDEFG 表示,我想创建一个由 ACDFG 组成的新视频。

ptQ*_*tQa 68

好吧,您仍然可以trim为此使用过滤器。这是一个示例,假设您要剪切 30-40 秒(10 秒)和 50-80 秒(30 秒)的片段:

ffmpeg -i in.ts -filter_complex \
"[0:v]trim=duration=30[a]; \
 [0:v]trim=start=40:end=50,setpts=PTS-STARTPTS[b]; \
 [a][b]concat[c]; \
 [0:v]trim=start=80,setpts=PTS-STARTPTS[d]; \
 [c][d]concat[out1]" -map [out1] out.ts
Run Code Online (Sandbox Code Playgroud)

我在这里做了什么?我先修剪 30 秒、40-50 秒和 80 秒以结束,然后将它们out1concat过滤器组合成流。

关于setpts:我们需要这个,因为trim不会修改图片显示时间,当我们剪掉10秒解码器计数器在这10秒内看不到任何帧。

如果你也想拥有音频,你必须对音频流做同样的事情。所以命令应该是:

ffmpeg -i utv.ts -filter_complex \
"[0:v]trim=duration=30[av];[0:a]atrim=duration=30[aa];\
 [0:v]trim=start=40:end=50,setpts=PTS-STARTPTS[bv];\
 [0:a]atrim=start=40:end=50,asetpts=PTS-STARTPTS[ba];\
 [av][bv]concat[cv];[aa][ba]concat=v=0:a=1[ca];\
 [0:v]trim=start=80,setpts=PTS-STARTPTS[dv];\
 [0:a]atrim=start=80,asetpts=PTS-STARTPTS[da];\
 [cv][dv]concat[outv];[ca][da]concat=v=0:a=1[outa]" -map [outv] -map [outa] out.ts
Run Code Online (Sandbox Code Playgroud)

  • +1 这确实应该被选为答案。它更适合“多段”,并且无需一个接一个地执行多个命令。(除非另一种方式更高效) (4认同)
  • 根据 ffmpeg doc,您应该在单个过滤器中连接视频和音频以避免同步问题。IE。`[av][bv]concat[cv];[aa][ba]concat=v=0:a=1[ca]` 应该是 `[av][aa][bv][ba]concat=a= 1[cv][ca]` 与第二个 concat 相同。 (3认同)
  • 这是非常不人道的。 (3认同)
  • 在我的 zsh 上,我必须引用结果流。即 ```-map '[outv]' -map '[outa]'``` (3认同)
  • 当您跳帧时,您将如何维护字幕数据? (2认同)

xba*_*esx 35

我永远无法让 ptQa 的解决方案起作用,主要是因为我永远无法弄清楚过滤器中的错误意味着什么或如何修复它们。我的解决方案似乎有点笨拙,因为它可能会留下一团糟,但是如果您将其放入脚本中,则可以自动进行清理。我也喜欢这种方法,因为如果第 4 步出现问题,您最终会完成第 1-3 步,因此从错误中恢复会更有效一些。

基本策略是使用-t-ss获取您想要的每个片段的视频,然后将所有部分连接在一起以获得最终版本。

假设您有 6 个每 5 秒长的 ABCDEF 段,并且您想要 A(0-5 秒)、C(10-15 秒)和 E(20-25 秒),您可以这样做:

ffmpeg -i abcdef.tvshow -t 5 a.tvshow -ss 10 -t 5 c.tvshow -ss 20 -t 5 e.tvshow
Run Code Online (Sandbox Code Playgroud)

或者

ffmpeg -i abcdef.tvshow -t 0:00:05 a.tvshow -ss 0:00:10 -t 0:00:05 c.tvshow -ss 0:00:20 -t 0:00:05 e.tvshow
Run Code Online (Sandbox Code Playgroud)

这将使文件 a.tvshow、c.tvshow 和 e.tvshow。该-t说每个剪辑有多长,所以如果c是30秒长,你可以通过在30 〇时00分30秒。该-ss选项表示跳入源视频的距离,因此它始终相对于文件的开头。

然后一旦你有了一堆视频文件,我就会制作一个ace-files.txt这样的文件:

file 'a.tvshow'
file 'c.tvshow'
file 'e.tvshow'
Run Code Online (Sandbox Code Playgroud)

请注意开头的“文件”和之后的转义文件名。

然后命令:

ffmpeg -f concat -i ace-files.txt -c copy ace.tvshow
Run Code Online (Sandbox Code Playgroud)

这将所有文件abe-files.txt连接在一起,复制它们的音频和视频编解码器,并生成一个文件ace.tvshow,该文件应该只是部分 a、c 和 e。然后只记得删除ace-files.txt, a.tvshow,c.tvshowe.tvshow

免责声明:我不知道这与其他方法相比如何(低)效率,ffmpeg但就我的目的而言,它效果更好。希望它可以帮助某人。

  • 这个对我这样的新手来说很容易理解,谢谢 (8认同)
  • 请注意,如果您使用 bash,则可以避免创建文件(在您的示例中为 `ace-files.txt`):`ffmpeg -f concat -i <(printf "file '%s'\n" $( pwd)/prefix_*.tvshow) -c 复制 output.tvshow` (7认同)
  • 谢谢,@bufh,我想知道如何做到这一点!只是一个可能对其他人有帮助的说明:正如@pkSML 解释的 [下面](https://superuser.com/questions/681885/how-can-i-remove-multiple-segments-from-a-video-using-ffmpeg# comment2240760_719610),如果使用绝对文件路径,您还需要在 ffmpeg 命令中添加“-safe 0”,以使其工作。或者,您可以删除 bash 子命令的 `$(pwd)/` 部分。 (2认同)

小智 16

对于那些在遵循 ptQa 方法时遇到问题的人,有一种稍微简化的方法来解决它。与其把每一步都串联起来,不如把它们都放在最后。

对于每个输入,定义一个 A/V 对:

//Input1:
[0:v]trim=start=10:end=20,setpts=PTS-STARTPTS,format=yuv420p[0v];
[0:a]atrim=start=10:end=20,asetpts=PTS-STARTPTS[0a];
//Input2:
[0:v]trim=start=30:end=40,setpts=PTS-STARTPTS,format=yuv420p[1v];
[0:a]atrim=start=30:end=40,asetpts=PTS-STARTPTS[1a];
//Input3:
[0:v]trim=start=30:end=40,setpts=PTS-STARTPTS,format=yuv420p[2v];
[0:a]atrim=start=30:end=40,asetpts=PTS-STARTPTS[2a];
Run Code Online (Sandbox Code Playgroud)

根据需要定义尽可能多的对,然后将它们全部连接一次,其中 n = 总输入计数。

[0v][0a][1v][1a][2v][2a]concat=n=3:v=1:a=1[outv][outa] -map [outv] -map [outa] out.mp4
Run Code Online (Sandbox Code Playgroud)

这可以很容易地构建在一个循环中。

使用 2 个输入的完整命令可能如下所示:

ffmpeg -i in.mp4 -filter_complex 
[0:v]trim=start=10.0:end=15.0,setpts=PTS-STARTPTS,format=yuv420p[0v];
[0:a]atrim=start=10.0:end=15.0,asetpts=PTS-STARTPTS[0a];
[0:v]trim=start=65.0:end=70.0,setpts=PTS-STARTPTS,format=yuv420p[1v];
[0:a]atrim=start=65.0:end=70.0,asetpts=PTS-STARTPTS[1a];[0v][0a][1v]
[1a]concat=n=2:v=1:a=1[outv][outa] -map [outv] -map [outa] out.mp4
Run Code Online (Sandbox Code Playgroud)

  • 简单、直观、美观。就像一个神奇的咒语一样起作用。 (3认同)
  • 谢谢。我能够根据您的回答创建一个工作命令。我发现@ptQa 的回答有点令人困惑。 (2认同)

ria*_*noc 6

我制作了一个脚本来加速编辑录制的电视节目。该脚本会询问您要保留的段的开始和结束时间,并将它们拆分为文件。它为您提供选项,您可以:

  • 取一个或多个片段。
  • 您可以将这些段组合成一个结果文件。
  • 加入后,您可以保留或删除零件文件。
  • 您可以保留原始文件或用新文件替换它。

它的操作视频:这里

让我知道你的想法。

 #!/bin/bash
/bin/date >>segmenter.log

function segment (){
while true; do
    echo "Would you like to cut out a segment ?"
    echo -e "1) Yes\n2) No\n3) Quit"
    read CHOICE
    if [ "$CHOICE" == "3" ]; then
        exit
    elif [ "$CHOICE" == "2" ]; then
        clear
        break
    elif [ "$CHOICE" == "1" ]; then
        clear
        ((segments++))
        echo "What time does segment $segments start ?"
        read SEGSTART
        clear
        echo -e "Segment $segments start set to $SEGSTART\n"  
        echo "What time does segment $segments end ?"
        read SEGEND
        clear
        echo -e "Segment $segments end set to $SEGEND\n"
        break
    else
        clear
        echo -e "Bad option"
        segment "$segments"
    fi
done
if [ "$CHOICE" == "1" ]; then
    echo "Cutting file $file video segment $segments starting at $SEGSTART and ending at $SEGEND"
    ffmpeg -i "$file" -ss $SEGSTART -to  $SEGEND -map 0:0 -map 0:1 -c:a copy -c:v copy  "$filename-part$segments.$extension"  >> segmenter.log 2>&1
    clear
    echo -e "Cut file $filename-part$segments.$extension starting at $SEGSTART and ending at $SEGEND\n"                             
    segment "$segments"
fi
}

file="$1"
filename="${file%.*}"
extension="${file##*.}"
clear
segments=0
segment "$segments"
clear
if (("$segments"==1)); then
mv $filename"-part1."$extension "$filename-segmented.$extension"
elif (("$segments">1)); then
echo "Would you like to join the segments into one file ?"      
       OPTIONS="Yes No Quit"
       select opt in $OPTIONS; do
       clear
        if [ "$opt" == "Quit" ]; then
            exit
        elif [ "$opt" == "Yes" ]; then
            clear
            echo "Joining segments"
            ffmpeg -f concat -i <(for f in $filename"-part"*$extension;         do echo "file '$(pwd)/$f'"; done) -c:a copy -c:v copy "$filename-segmented.$extension" >>         segmenter.log 2>&1
            clear
            echo "Would you like to delete the part files ?"
            select opt in $OPTIONS; do
            clear
            if [ "$opt" == "Quit" ]; then
                exit
            elif [ "$opt" == "Yes" ]; then
                for f in $filename"-part"*$extension; do rm $f; done
                break
            elif [ "$opt" == "No" ]; then
                break
            else
                clear
                echo -e "Bad option\n"
            fi
            done
            break
        clear
        elif [ "$opt" == "No" ]; then
            exit
        else
            clear
            echo -e "Bad option\n"
        fi
    done
fi
echo "Would you like to replace the original file with the result of your changes ?"
OPTIONS="Yes No Quit"
select opt in $OPTIONS; do
    clear
    if [ "$opt" == "Quit" ]; then
        exit
    elif [ "$opt" == "Yes" ]; then
        rm $file
        mv "$filename-segmented.$extension" $file
        break
    elif [ "$opt" == "No" ]; then
        break
    else
        clear
        echo -e "Bad option\n"
    fi
done
Run Code Online (Sandbox Code Playgroud)

  • 这个剧本很棒!由于您使用绝对路径进行连接,因此只需进行一项修改:添加`safe=0`,即`ffmpeg -f concat -safe 0 -i ...` 参见 https://ffmpeg.org/ffmpeg-all.html#选项-36 (2认同)

det*_*tic 6

转换ABCDEFGACDFG本质上是从视频中剪切三个片段,然后连接这三个片段。如果从同一视频中剪切所有片段,并且无需更改视频的编码(分辨率、比特率、帧速率等),则不需要重新编码,并且会降低有损格式的质量。然而,如果不重新编码,剪切位置将不是精确的,而是最接近指定时间的关键帧。

下面的示例假设vidfull.mp4ABCDEFG,其中每个字母代表一个五分钟长的片段。

无需重新编码,该过程几乎是瞬时的,但需要显式地进行切割,在其中创建临时文件,并且可以使用串联协议(即文件级串联)和容器.ts作为中间格式来完成连接,它支持文件级别的串联:

ffmpeg \
  -to 5:0 -i vidfull.mp4 \
  -ss 10:0 -to 20:0 -i vidfull.mp4 \
  -ss 25:0 -i vidfull.mp4 \
  -map 0:v -map 0:a -c copy part1.ts \
  -map 1:v -map 1:a -c copy part2.ts \
  -map 2:v -map 2:a -c copy part3.ts
ffmpeg -i 'concat:part1.ts|part2.ts|part3.ts' -c copy vidcut.ts
ffmpeg -i vidcut.ts -c copy vidcut.mp4
rm -f part{1..3}.ts vidcut.ts
Run Code Online (Sandbox Code Playgroud)

或使用串联脚本解复用器:

ffmpeg \
  -to 5:0 -i vidfull.mp4 \
  -ss 10:0 -to 20:0 -i vidfull.mp4 \
  -ss 25:0 -i vidfull.mp4 \
  -map 0:v -map 0:a -c copy -avoid_negative_ts make_non_negative part1.mp4 \
  -map 1:v -map 1:a -c copy -avoid_negative_ts make_non_negative part2.mp4 \
  -map 2:v -map 2:a -c copy -avoid_negative_ts make_non_negative part3.mp4
cat <<- eof > script.txt
    ffconcat version 1.0
    file '/path to/part1.mp4'
    file '/path to/part2.mp4'
    file '/path to/part3.mp4'
eof
ffmpeg -f concat -safe 0 -i script.txt -c copy vidcut.mp4
rm -f part{1..3}.mp4 script.txt
Run Code Online (Sandbox Code Playgroud)

通过重新编码,该过程很慢,但切割和连接是通过单个命令完成的,方法是在寻找不同位置后多次使用相同的视频作为复杂串联过滤器的输入:

ffmpeg \
  -to 5:0 -i vidfull.mp4 \
  -ss 10:0 -to 20:0 -i vidfull.mp4 \
  -ss 25:0 -i vidfull.mp4 \
  -lavfi concat=n=3:a=1 vidcut.mp4
Run Code Online (Sandbox Code Playgroud)

无需将输入或输出 pad 标记到 concat 过滤器,因为过滤器链中没有除 concat 之外的任何其他过滤器来使用特定 pad 中的特定流,不会从输出中排除任何流,并且所有流来自未标记的 pad 会自动添加到第一个输出文件中(输出格式需要支持所有流类型以避免致命错误)。并且,在 concat 过滤器的选项中,是输入段的数量,因为默认情况下n需要设置为, 是输出音频流的数量,因为默认情况下需要设置为。32a10


小智 5

这将做到:

 ffmpeg -i input.avi -vf "select='1-between(t,20,25)', setpts=N/FRAME_RATE/TB" -af "aselect='1-between(t,20,25)', asetpts=N/SR/TB" output.avi
Run Code Online (Sandbox Code Playgroud)

此命令将丢弃 20 秒到 25 秒的段并保留其他所有内容。除了排除列表,您还可以执行包含列表,如下所示:

ffmpeg -i input.avi -vf "select='lt(t,20)+between(t,790,810)+gt(t,1440)', setpts=N/FRAME_RATE/TB" -af "aselect='lt(t,20)+between(t,790,810)+gt(t,1440)', asetpts=N/SR/TB" output.avi
Run Code Online (Sandbox Code Playgroud)

这将保留原始视频的 3 个片段(0-20 秒、790-810 秒和 1440 结束秒),同时丢弃其他任何内容。

关键部分(第二个例子的,因为它更复杂):

  • -vf是视频过滤器表达式的选项。我们使用 select 过滤器,它将接受一个表达式,为每个输入帧评估它,如果表达式为 1,则保留该帧,如果表达式为 0,则丢弃它。(实际上这有一点细微差别,但是它与您的问题并不真正相关。如果您想了解完整故事,请看这里。)

  • lt(t,20)是选择过滤器表达式的第一部分。lt代表小于t,是被评估的帧的时间戳,以秒为单位,因此对于任何具有 t<20s 的帧,这将评估为 1

  • between(t,790,810) 对于 790s 和 810s 之间的任何帧,将评估为 1

  • gt(t, 1440) (大于)将在 1440 秒后的任何帧变为 1

  • 然后我们将所有条件添加到逻辑或它们。

  • setpts=N/FRAME_RATE/TB需要为我们提供我们t在逻辑表达式中使用的值。ffmpeg 似乎在这里以样本/帧为单位思考,而不是以秒为单位,因此我们使用此表达式转换为秒。

  • -af是音频过滤器表达式的选项。音频过滤器的工作原理类似于视频过滤器,不同之处在于转换为秒时,我们使用SR音频的采样率,而不是FRAME_RATE视频的采样率。

您可以疯狂地使用过滤器表达式并通过添加between(t,...,...)术语和根据需要组合术语来添加新段。您想要的排除列表只是利用了这样一个事实,即我们可以通过从 1 中减去过滤器表达式来反转过滤器表达式,因此现在将保留丢弃的所有内容,反之亦然。

确保您为音频和视频选择相同的过滤器,否则它们将不同步!

请注意,您将无法使用-codec copy此解决方案,因为它copy告诉 ffmpeg 不要解码视频。由于视频过滤器是针对解码帧进行评估的,因此我们必须先进行解码。


Mat*_*ias 2

虽然 ptQa 提供的答案似乎有效,但我开发了另一个解决方案,事实证明该解决方案工作正常。

本质上,我所做的就是为原始视频的每个部分剪切一个视频,并将其包含在我的结果中。后来,我将它们与此处解释的 Concat Demuxer 连接起来。

结果与我第一次尝试的结果相同,但出现了同步问题。我添加的是生成不同视频时的命令-avoid_male_ts 1 。有了这个解决方案,同步问题就消失了。