Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env python3
- # Takes a bunch of videos on the command line, converts them into a single
- # video with all of the input videos shrunk into it.
- #
- # By Jonathan Kamens <jik@kamens.us>. Let me know if you're using it!
- import argparse
- import math
- import os
- import re
- import subprocess
- import sys
- from tempfile import NamedTemporaryFile
- class Video(object):
- def __init__(self, spec):
- # Format filename:[start_seconds]:[end_seconds]
- pieces = spec.split(':')
- self.filename = pieces[0]
- probe_output = subprocess.check_output(('ffprobe', self.filename),
- stderr=subprocess.STDOUT).\
- decode('us-ascii')
- match = re.search(r'Duration: (\d+):(\d+):([\d.]+)', probe_output)
- hours = int(match.group(1))
- minutes = int(match.group(2))
- seconds = float(match.group(3))
- self.length = seconds + (minutes + hours * 60) * 60
- self.start_at = 0
- self.end_at = self.length
- if len(pieces) > 1:
- self.start_at = float(pieces[1]) if pieces[1] else 0
- if len(pieces) > 2:
- self.end_at = float(pieces[2]) if pieces[2] else self.length
- match = re.search(r'Stream .*, (\d+)x(\d+)', probe_output)
- self.width = int(match.group(1))
- self.height = int(match.group(2))
- def __str__(self):
- return('<{} {}x{} length={} start_at={} end_at={}>'.format(
- self.filename, self.width, self.height, self.length,
- self.start_at, self.end_at))
- def __repr__(self):
- return self.__str__()
- def parse_args():
- parser = argparse.ArgumentParser(
- description='Merge several videos into a gallery view video',
- epilog='Takes two or more videos as command-line arguments and '
- 'produces a video as output that stacks all the input videos into a '
- 'grid, sort of like the "Gallery View" in Zoom. The start and stop '
- 'position in fractional seconds can optionally be specified for each '
- 'video.\n\n'
- 'Requires ffmpeg and the ImageMagick "convert" command.')
- parser.add_argument('--output-file', metavar='FILENAME', required=True,
- help='Output file')
- parser.add_argument('--force', action='store_true', help='Overwrite '
- 'output file if it exists')
- parser.add_argument('--width', metavar='PIXELS', default=1280, type=int,
- help='Output width (default 1280)')
- parser.add_argument('--height', metavar='PIXELS', default=720, type=int,
- help='Output height (default 720)')
- parser.add_argument('--border-width', metavar='PIXELS', dest='border',
- default=25, type=int, help='Border width (default 25)')
- parser.add_argument('video', nargs='+', type=Video, help='Input video '
- '(filename[:start_at[:end_at]])')
- args = parser.parse_args()
- if os.path.exists(args.output_file) and not args.force:
- sys.exit('Will not overwrite {} unless --force is specified.'.format(
- args.output_file))
- return args
- def main():
- args = parse_args()
- if len(args.video) == 1:
- sys.exit('This script does not make sense with just one video!')
- # Figure out how long the video is going to be and which video's audio is
- # the shortest.
- output_length = args.video[0].end_at - args.video[0].start_at
- shortest_video = args.video[0]
- shortest_length = output_length
- for video in args.video[1:]:
- video_length = video.end_at - video.start_at
- if video_length > output_length:
- output_length = video_length
- if video_length < shortest_length:
- shortest_video = video
- print('Output video will be {} seconds long.'.format(output_length))
- print('Audio will end after {} ({} seconds).'.format(
- shortest_video.filename, shortest_length))
- # Figure out grid size.
- grid_size = math.ceil(math.sqrt(len(args.video)))
- print('Output video will be {}x{}'.format(grid_size, grid_size))
- # Figure out the maximum dimensions of each video.
- max_width = int((args.width - args.border * (grid_size + 1)) / grid_size)
- max_height = int((args.height - args.border * (grid_size + 1)) / grid_size)
- print('Maximum dimensions of embedded videos will be {}x{}.'.format(
- max_width, max_height))
- # Calculate scaling for each video.
- for video in args.video:
- if video.width > max_width or video.height > max_height:
- if video.width / max_width > video.height / max_height:
- scale_width = max_width
- scale_height = int(scale_width / video.width *
- video.height)
- video.scale = '{}:-1'.format(scale_width)
- else:
- scale_height = max_height
- scale_width = int(scale_height / video.height *
- video.width)
- video.scale = '-1:{}'.format(scale_height)
- print('Scaling {} to {}x{}'.format(
- video.filename, scale_width, scale_height))
- with NamedTemporaryFile(suffix='.png') as background_png, \
- NamedTemporaryFile(suffix='.mp4') as background_mp4:
- subprocess.run(('convert', '-size',
- '{}x{}'.format(args.width, args.height),
- 'xc:black', background_png.name))
- # Create background video.
- subprocess.run(('ffmpeg', '-loglevel', 'fatal', '-y', '-loop', '1',
- '-t', str(output_length), '-i', background_png.name,
- '-pix_fmt', 'yuv420p', background_mp4.name))
- # Construct the ffmpeg command.
- cmd = ['ffmpeg', '-y', '-loglevel', 'fatal', '-i', background_mp4.name]
- for video in args.video:
- cmd.extend(('-ss', str(video.start_at), '-to', str(video.end_at),
- '-i', video.filename))
- column = 0
- row = 0
- filters = [
- ''.join(['[{}]'.format(n+1) for n in range(len(args.video))]) +
- 'amix=inputs={}'.format(len(args.video))
- ]
- h_border = int((args.width - max_width * grid_size) / (grid_size + 1))
- v_border = int((args.height - max_height * grid_size) /
- (grid_size + 1))
- for i in range(len(args.video)):
- video = args.video[i]
- cur = i + 1
- filters.append('[{}]scale={}[{}s]'.format(cur, video.scale, cur))
- if i:
- prev = '[{}o]'.format(i)
- else:
- prev = '[0]'
- filters.append('{}[{}s]overlay={}:{}[{}o]'.format(
- prev, cur, h_border + column * (max_width + h_border),
- v_border + row * (max_height + v_border), cur))
- column += 1
- if column == grid_size:
- column = 0
- row += 1
- # Get rid of the output for the last overlay, so it goes into the
- # output video.
- filters[-1] = re.sub(r'\[\d+o\]$', '', filters[-1])
- cmd.extend(('-filter_complex', ';'.join(filters), args.output_file))
- print('Processing video.')
- subprocess.run(cmd)
- print('Done.')
- if __name__ == '__main__':
- main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement