Skip to content

Instantly share code, notes, and snippets.

@wanderingstan
Last active November 17, 2020 20:27
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save wanderingstan/5ec0a6802d87f3934796 to your computer and use it in GitHub Desktop.
Save wanderingstan/5ec0a6802d87f3934796 to your computer and use it in GitHub Desktop.
Create grid of movies using ffpeg
# Create a movie which is a grid of other movies.
# Based on: https://trac.ffmpeg.org/wiki/Create%20a%20mosaic%20out%20of%20several%20input%20videos
# With help from: http://superuser.com/questions/991714/select-audio-from-second-input-file-for-ffmpeg-movie-overlay/991747#991747
#
# Example:
# python make_grid.py -x 4 -y 4 -o 4x4.mov Breezeblocks*.mov
#
# Will take up to 16 (4x4) movies with name staring with "Breezeblocks" and
# create a movie grid of them playing simultaneously, saved as 4x4.mov.
#
# Old demo: https://www.youtube.com/watch?v=e6bhi60xOig
#
# Sound is taken from the longest movie.
import os
import argparse
import glob
# Parse arguments
parser = argparse.ArgumentParser(description='Create grid of movies.')
parser.add_argument('movies', metavar='movie_file', nargs='+',
help='Movies for the grid.')
parser.add_argument('-y', dest='grid_count_y', type=int, default=2,
help='Movies across y axis')
parser.add_argument('-x', dest='grid_count_x', type=int, default=2,
help='Movies across x axis')
parser.add_argument('-mw', dest='movie_width', type=int, default=340,
help='Width of a movie in pixels.')
parser.add_argument('-mh', dest='movie_height', type=int, default=340,
help='Width of a movie in pixels.')
parser.add_argument('-d', '--dry', dest='dry', action='store_true',
help='Do a dry run; just output the ffmpeg command')
parser.add_argument('-o', dest='output_movie', default="out.mov",
help='Output movie file.')
parser.add_argument('-t', dest='movie_duration', type=float, default=0,
help='Movie duration in seconds. Default is max movie length')
args = parser.parse_args()
# movies = [
# "Breezeblocks-easy-1.mov",
# "Breezeblocks-easy-2.mov",
# "Breezeblocks-easy-3.mov",
# "Breezeblocks-hard-1.mov",
# "Breezeblocks-hard-2.mov",
# "Breezeblocks-hard-3.mov",
# ]
grid_count_x = args.grid_count_x
grid_count_y = args.grid_count_y
grid_total = grid_count_x * grid_count_y
output_movie = args.output_movie
movie_width = args.movie_width
movie_height = args.movie_height
movie_scale = "%sx%s" % (movie_width, movie_height)
grid_movie_width = movie_width * grid_count_x
grid_movie_height = movie_height * grid_count_y
grid_movie_size = "%sx%s" % (grid_movie_width, grid_movie_height)
# Get movie files, expanding bash patterns, only up to maximum size of grid
movies = reduce(lambda x, y: x+y, map(glob.glob, args.movies))[:grid_total]
if len(movies) == 0:
print "No movies found."
exit()
logest_movie_index = -1
if args.movie_duration == 0:
# Find longest movie length
longest = 0
for i, movie in enumerate(movies):
length = float(
os.popen("ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 '%s'" % movie)
.read()
)
if length > longest:
longest = length
logest_movie_index = i
max_duration = longest
movie_duration_command = ""
else:
movie_duration_command = " -t %f" % (args.movie_duration)
max_duration = args.movie_duration
# For half speed
# [0:v]setpts=0.5*PTS[s0];
# [1:v]setpts=0.5*PTS[s1];
# [2:v]setpts=0.5*PTS[s2];
# [3:v]setpts=0.5*PTS[s3];
# -i %(movie1)s
# -i %(movie2)s
# -i %(movie3)s
# -i %(movie4)s
movie_input_command = "\n ".join(map( lambda x: "-i '%s'" % x, movies ))
# [0:v] setpts=PTS-STARTPTS, scale=%(movie_scale)s [upperleft];
# [1:v] setpts=PTS-STARTPTS, scale=%(movie_scale)s [upperright];
# [2:v] setpts=PTS-STARTPTS, scale=%(movie_scale)s [lowerleft];
# [3:v] setpts=PTS-STARTPTS, scale=%(movie_scale)s [lowerright];
movie_setup_command = "".join([
"\n [%d:v] setpts=PTS-STARTPTS, scale=%s [m%dx%d];" %
(i, movie_scale, i % grid_count_x, i / grid_count_x) for i,x in enumerate(movies)])
movie_overlay_command = "".join([
"\n [b%d][m%dx%d] overlay=x=%d:y=%d [b%d];" %
(i, i % grid_count_x, i / grid_count_x, (i % grid_count_x) * movie_width, (i / grid_count_x) * movie_height, i+1)
for i,x in enumerate(movies)])
# Remove last label for end of processing
# movie_overlay_command = movie_overlay_command.replace("[b%s];" % len(movies),"")
trim_command = (
"\n [b%s] trim=duration=%f [out]" % (len(movies), max_duration)
# "\n join=map=1.0-0;" + #% (logest_movie_index) +
# "\n [%d:a] atrim=duration=%f" % (logest_movie_index, max_duration)
)
audio_route_command = '\n -map "[out]" -map %d:a' % (logest_movie_index)
command = """
ffmpeg
%(movie_duration_command)s
%(movie_input_command)s
-filter_complex "
nullsrc=size=%(grid_movie_size)s [b0];
%(movie_setup_command)s
%(movie_overlay_command)s
%(trim_command)s
"
%(audio_route_command)s
-c:v libx264 %(output_movie)s
""" % locals()
print (command)
if args.dry:
exit()
out = os.popen(command.replace("\n"," ")).read()
print(out)
@wanderingstan
Copy link
Author

Audio not working right. See this Stack Overflow question.

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