How can I remove multiple segments from a video using FFmpeg?

I am trying to delete a few sections of a video using FFmpeg.

For example, imagine if you recorded a show on television and wanted to cut out the commercials. This is simple with a GUI video-editor; you just mark the beginning and ending of each clip to be removed, and select delete. I am trying to do the same thing from the command-line with FFmpeg.

I know how to cut a single segment to a new video like so:

ffmpeg -i input.avi -ss 00:00:20 -t 00:00:05 -map 0 -codec copy output.avi

This cuts a five-second clip and saves it as a new video file, but how can I do the opposite and save the whole video without the specified clip, and how I can I specify multiple clips to be removed?

For example, if my video could be represented by ABCDEFG, I would like to create a new one that would consist of ACDFG.


Solution 1:

Well, you still can use the trim filter for that. Here is an example, lets assume that you want to cut out three segments at first and end of the video as well as in the middle:

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

What I did here? I trimmed first 30 sec, 40-50 sec and 80 sec to end, and then combined them into stream out1 with the concat filter, leaving 30-40 sec (10 sec) and 50-80 sec (30 sec).

About setpts: we need this because trim does not modify picture display time, and when we cut out 10 sec decoder counter does not see any frames for this 10 sec.

If you want to have audio too, You have to do the same for audio streams. So the command should be:

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

Solution 2:

I can never get ptQa's solution to work, mostly because I can never figure out what the errors from the filters mean or how to fix them. My solution seems a little clunkier because it can leave behind a mess, but if you're throwing it into a script, the clean up can be automated. I also like this approach because if something goes wrong on step 4, you end up with completed steps 1-3 so recovering from errors is a little more efficient.

The basic strategy is using -t and -ss to get videos of each segment you want, then join together all the parts for your final version.

Say you have 6 segments ABCDEF each 5 seconds long and you want A (0-5 seconds), C (10-15 seconds) and E (20-25 seconds) you'd do this:

ffmpeg -i abcdef.tvshow -t 5 a.tvshow -ss 10 -t 5 c.tvshow -ss 20 -t 5 e.tvshow

or

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

That will make files a.tvshow, c.tvshow and e.tvshow. The -t says how long each clip is, so if c is 30 seconds long you could pass in 30 or 0:00:30. The -ss option says how far to skip into the source video, so it's always relative to the start of the file.

Then once you have a bunch of video files I make a file ace-files.txt like this:

file 'a.tvshow'
file 'c.tvshow'
file 'e.tvshow'

Note the "file" at the beginning and the escaped file name after that.

Then the command:

ffmpeg -f concat -i ace-files.txt -c copy ace.tvshow

That concats all the files in abe-files.txt together, copying their audio and video codecs and makes a file ace.tvshow which should just be sections a, c and e. Then just remember to delete ace-files.txt, a.tvshow, c.tvshow and e.tvshow.

Disclaimer: I have no idea how (in)efficient this is compared to the other approaches in terms of ffmpeg but for my purposes it works better. Hope it helps someone.

Solution 3:

For those having trouble following ptQa's approach, there's a slightly more streamlined way to go about it. Rather than concat each step of the way, just do them all at the end.

For each input, define a A/V pair:

//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];

Define as many pairs as you need, then concat them all in one pass, where n=total input count.

[0v][0a][1v][1a][2v][2a]concat=n=3:v=1:a=1[outv][outa] -map [outv] -map [outa] out.mp4

This can easily be constructed in a loop.

A complete command that uses 2 inputs might look like this:

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

Solution 4:

This will do it:

 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

This command will discard the segment from 20 seconds to 25 seconds and keep everything else. Instead of an exclusion list, you can also do an inclusion list like this:

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

This will keep 3 segments of the original video (0-20 seconds, 790-810 seconds, and 1440-end seconds), while discarding anything else.

The key parts (of the second example, since it is more complicated):

  • -vf is the option for a video filter expression. We use the select filter, which will take an expression, evaluate it for each input frame, and keep the frame if the expression is 1, or discard it if the expression is 0. (There is actually a bit more nuance to this, but it isn't really relevant to your problem. Look here if you want to know the full story.)

  • lt(t,20) is the first part of the select filter expression. lt stands for less-than and t is the timestamp of the frame in seconds that is evaluated, so this will evaluate to 1 for any frame with t<20s

  • between(t,790,810) will evaluate to 1 for any frame between 790s and 810s

  • gt(t, 1440) (greater-than) will become 1 for any frame after 1440s

  • We then add all the conditions, to logically or them.

  • setpts=N/FRAME_RATE/TB is needed to provide us with the t value that we use in the logical expression. ffmpeg seems to think in samples/frames here, not in seconds, so we use this expression to convert to seconds.

  • -af is the option for an audio filter expression. The audio filter works analogous to the video filter, except that to convert to seconds, we use the sample rate SR of the audio, and not the FRAME_RATE of the video.

You can go crazy with the filter expression and add new segments by adding a between(t,...,...) term and combine terms as you need. The exclusion list you wanted simply utilizes the fact that we can invert our filter expression by subtracting it from 1, so everything discarded will now be kept, and vice versa.

Ensure that you have the same select filter for audio and video, or they will get out of sync!

Note that you won't be able to use -codec copy with this solution, since copy tells ffmpeg to not decode the video. Since the video filter is evaluated for decoded frames, we must decode first.