Last active
November 17, 2020 20:27
-
-
Save wanderingstan/5ec0a6802d87f3934796 to your computer and use it in GitHub Desktop.
Create grid of movies using ffpeg
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Audio not working right. See this Stack Overflow question.