Skip to content

Instantly share code, notes, and snippets.

@royshil
Last active April 28, 2024 17:12
Show Gist options
  • Save royshil/369e175960718b5a03e40f279b131788 to your computer and use it in GitHub Desktop.
Save royshil/369e175960718b5a03e40f279b131788 to your computer and use it in GitHub Desktop.
A video concatenation tool based on FFMPEG with crossfade between the segments (with the `xfade` filter)
#!/usr/local/bin/python3
import argparse
import subprocess
import itertools
parser = argparse.ArgumentParser(description='Concatenate videos with FFMPEG, add "xfade" between segments.')
parser.add_argument('--segments_file', '-f', metavar='Segments file', type=str, nargs=1,
help='Segments text file for concatenating. e.g. "segments.txt"')
parser.add_argument('--output', '-o', dest='output_filename', type=str,
default='ffmpeg_concat_fade_out.mp4',
help='output filename to provide to ffmpeg. default="ffmpeg_concat_fade_out.mp4"')
parser.add_argument('segments', nargs='+')
args = parser.parse_args()
if args.segments_file:
with open(args.segments_file[0], 'r') as seg_file:
# cut the `file '` prefix and `'` postfix
segments = [line[6:-2] for line in seg_file.readlines() if len(line.strip()) > 0 and line[0] != "#"]
else:
segments = args.segments
# Get the lengths of the videos in seconds
file_lengths = [
float(subprocess.run(['/usr/local/bin/ffprobe',
'-v', 'error',
'-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1',
f],
capture_output=True).stdout.splitlines()[0])
for f in segments
]
width = int(subprocess.run(['/usr/local/bin/ffprobe', '-v', 'error', '-select_streams', 'v', '-show_entries',
'stream=width', '-of', 'default=nw=1:nk=1', segments[0]],
capture_output=True).stdout.splitlines()[0])
height = int(subprocess.run(['/usr/local/bin/ffprobe', '-v', 'error', '-select_streams', 'v', '-show_entries',
'stream=height', '-of', 'default=nw=1:nk=1', segments[0]],
capture_output=True).stdout.splitlines()[0])
# File inputs from the list
files_input = [['-i', f] for f in segments]
# Prepare the filter graph
video_fades = ""
audio_fades = ""
last_fade_output = "0v"
last_audio_output = "0:a"
video_length = 0
normalizer = ""
scaler_default = f",scale=w={width}:h={height}:force_original_aspect_ratio=1,pad={width}:{height}:(ow-iw)/2:(oh-ih)/2"
for i in range(len(segments)):
# Videos normalizer
scaler = scaler_default if i > 0 else ""
normalizer += f"[{i}:v]settb=AVTB,setsar=sar=1,fps=30{scaler}[{i}v];"
if i == 0:
continue
# Video graph: chain the xfade operator together
video_length += file_lengths[i - 1] - 0.25
next_fade_output = "v%d%d" % (i - 1, i)
video_fades += "[%s][%dv]xfade=duration=0.5:offset=%.3f[%s];" % \
(last_fade_output, i, video_length - 1, next_fade_output)
last_fade_output = next_fade_output
# Audio graph:
next_audio_output = "a%d%d" % (i - 1, i)
audio_fades += f"[{last_audio_output}][{i}:a]acrossfade=d=1[{next_audio_output}];"
last_audio_output = next_audio_output
video_fades += f"[{last_fade_output}]format=pix_fmts=yuv420p[final];"
# Assemble the FFMPEG command arguments
ffmpeg_args = ['/usr/local/bin/ffmpeg',
*itertools.chain(*files_input),
'-filter_complex', normalizer + video_fades + audio_fades[:-1],
'-map', '[final]',
'-map', f"[{last_audio_output}]",
'-y',
args.output_filename]
print(" ".join(ffmpeg_args))
# Run FFMPEG
subprocess.run(ffmpeg_args)
@royshil
Copy link
Author

royshil commented Oct 28, 2021

Running:

$ ffmpeg_concat_xfade.py video1.mp4 video2.mp4 video3.mp4

The script will handle videos of different sizes or frame-rates as well.

@federicopalumbo
Copy link

Hi please help me where to change duration and offset, let's says duration=4 offset=2

thanks

@royshil
Copy link
Author

royshil commented Mar 14, 2022

Hi please help me where to change duration and offset, let's says duration=4 offset=2

thanks

This is where you determine duration: https://gist.github.com/royshil/369e175960718b5a03e40f279b131788#file-ffmpeg_concat_xfade-py-L65 (line 65 - it's currently 0.5)
and the offset is calculated video_length - 1 in line 66, just replace 1 to be a different number

@federicopalumbo
Copy link

federicopalumbo commented Mar 14, 2022 via email

@royshil
Copy link
Author

royshil commented Mar 16, 2022

Do I need to change the offset and the acrossfade duration?

yes you should double the number from xfade into acrossfade=d=NN on line 71

@Dex1975
Copy link

Dex1975 commented Jul 19, 2022

What is the usage of a segment file? Is it for passing multiple video locations in a single file?

@federicopalumbo
Copy link

federicopalumbo commented Oct 11, 2022 via email

@Clemweb
Copy link

Clemweb commented Nov 5, 2022

@federicopalumbo can you give me the full corrected script please ? because the one above doesn't seems to work, with 9 videos to concats I have a big delay between video and audio on the last videos...
Thanks

@SaltFishBoi
Copy link

@federicopalumbo can you give me the full corrected script please ? because the one above doesn't seems to work, with 9 videos to concats I have a big delay between video and audio on the last videos... Thanks

Little difficult to paste all the code onto the comment section. But I made 3 simple changes to made it works better:

1. add a fade duration constant in the beginning.

#constants
FADE_TIME=1

2. apply the constant under video graph section.

#Video graph: chain the xfade operator together
video_offset += file_lengths[i - 1] - FADE_TIME
next_fade_output = "v%d%d" % (i - 1, i)
video_fades += "[%s][%dv]xfade=duration=%d:offset=%.5f[%s];" % (last_fade_output, i, FADE_TIME, video_offset, next_fade_output)
last_fade_output = next_fade_output

3. apply constant at the audio graph as well.

#Audio graph:
next_audio_output = "a%d%d" % (i - 1, i)
audio_fades += f"[{last_audio_output}][{i}:a]acrossfade=d=%d[{next_audio_output}];" % (FADE_TIME)
last_audio_output = next_audio_output

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment