Skip to content

Instantly share code, notes, and snippets.

@jannisteunissen
Created October 6, 2025 08:22
Show Gist options
  • Select an option

  • Save jannisteunissen/f8efd9ad6edb25710d6d49c38ba3d8cf to your computer and use it in GitHub Desktop.

Select an option

Save jannisteunissen/f8efd9ad6edb25710d6d49c38ba3d8cf to your computer and use it in GitHub Desktop.
Combine multiple videos using ffmpeg concat demuxer
#!/usr/bin/env python3
# Author: Jannis Teunissen
# Inspired by https://gist.github.com/tom-huntington/1323af4b991ca34ef4a46b3e44532ada
import subprocess
from os.path import basename, splitext
from tempfile import NamedTemporaryFile
import argparse
def get_video_metadata(videos):
"""Get ffmpeg metadata for videos
:param videos: list of videos
:returns: metadata as a string
"""
chapters = []
for i, video in enumerate(videos):
command = ["ffprobe", "-v", "quiet", "-of", "csv=p=0",
"-show_entries", "format=duration", video]
output = subprocess.run(command, capture_output=True, text=True)
stdout = output.stdout.strip()
duration_us = int(stdout.replace(".", ""))
title = splitext(basename(video))[0]
chapters.append({"duration": duration_us,
"title": title})
end = 0
for i in range(len(videos)):
chapters[i]["start"] = end + 1
end = chapters[i]["start"] + chapters[i]["duration"]
chapters[i]["end"] = end
metadata = ";FFMETADATA1\n"
for chapter in chapters:
metadata += f"""
[CHAPTER]
TIMEBASE=1/1000000
START={chapter["start"]}
END={chapter["end"]}
title={chapter["title"]}
"""
return metadata
def combine_videos(videos, output_file):
"""Combine several videos into one, including metadata
:param videos: list of videos
:param output_file: single output file
:returns: noting
"""
metadata = get_video_metadata(videos)
# With Python 3.12, delete_on_close can be used instead
with NamedTemporaryFile(mode='wt', delete=False) as tmp_file_list, \
NamedTemporaryFile(mode='wt', delete=False) as tmp_metadata:
# Escape single quotes in filenames according to
# https://www.ffmpeg.org/ffmpeg-utils.html#Quoting-and-escaping
filelist = ["file '{}'".format(fname.replace("'", r"'\''"))
for fname in videos]
# Write list of files
tmp_file_list.write('\n'.join(filelist) + '\n')
tmp_file_list.close()
# Write metadata
tmp_metadata.write(metadata)
tmp_metadata.close()
command = ["ffmpeg", "-hide_banner", "-y",
"-v", "quiet",
"-f", "concat",
"-safe", "0",
"-i", tmp_file_list.name,
"-i", tmp_metadata.name,
"-map_metadata", "1",
"-codec", "copy",
output_file]
output = subprocess.run(command, capture_output=True, text=True)
if output.stderr:
raise RuntimeError(output.stderr)
if __name__ == '__main__':
p = argparse.ArgumentParser(
description='Combine multiple videos using ffmpeg concat demuxer')
p.add_argument('-i', required=True, type=str, nargs='+',
help='Input videos')
p.add_argument('-o', required=True, type=str,
help='Output video. Will be overwritten if it exists.')
args = p.parse_args()
combine_videos(args.i, args.o)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment