Advertisement
jikamens

gallery-video.py

Oct 28th, 2020
386
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 7.36 KB | None | 0 0
  1. #!/usr/bin/env python3
  2.  
  3. # Takes a bunch of videos on the command line, converts them into a single
  4. # video with all of the input videos shrunk into it.
  5. #
  6. # By Jonathan Kamens <jik@kamens.us>. Let me know if you're using it!
  7.  
  8. import argparse
  9. import math
  10. import os
  11. import re
  12. import subprocess
  13. import sys
  14. from tempfile import NamedTemporaryFile
  15.  
  16.  
  17. class Video(object):
  18.     def __init__(self, spec):
  19.         # Format filename:[start_seconds]:[end_seconds]
  20.         pieces = spec.split(':')
  21.         self.filename = pieces[0]
  22.  
  23.         probe_output = subprocess.check_output(('ffprobe', self.filename),
  24.                                                stderr=subprocess.STDOUT).\
  25.             decode('us-ascii')
  26.  
  27.         match = re.search(r'Duration: (\d+):(\d+):([\d.]+)', probe_output)
  28.         hours = int(match.group(1))
  29.         minutes = int(match.group(2))
  30.         seconds = float(match.group(3))
  31.         self.length = seconds + (minutes + hours * 60) * 60
  32.  
  33.         self.start_at = 0
  34.         self.end_at = self.length
  35.         if len(pieces) > 1:
  36.             self.start_at = float(pieces[1]) if pieces[1] else 0
  37.             if len(pieces) > 2:
  38.                 self.end_at = float(pieces[2]) if pieces[2] else self.length
  39.  
  40.         match = re.search(r'Stream .*, (\d+)x(\d+)', probe_output)
  41.         self.width = int(match.group(1))
  42.         self.height = int(match.group(2))
  43.  
  44.     def __str__(self):
  45.         return('<{} {}x{} length={} start_at={} end_at={}>'.format(
  46.             self.filename, self.width, self.height, self.length,
  47.             self.start_at, self.end_at))
  48.  
  49.     def __repr__(self):
  50.         return self.__str__()
  51.  
  52.  
  53. def parse_args():
  54.     parser = argparse.ArgumentParser(
  55.         description='Merge several videos into a gallery view video',
  56.         epilog='Takes two or more videos as command-line arguments and '
  57.         'produces a video as output that stacks all the input videos into a '
  58.         'grid, sort of like the "Gallery View" in Zoom. The start and stop '
  59.         'position in fractional seconds can optionally be specified for each '
  60.         'video.\n\n'
  61.         'Requires ffmpeg and the ImageMagick "convert" command.')
  62.     parser.add_argument('--output-file', metavar='FILENAME', required=True,
  63.                         help='Output file')
  64.     parser.add_argument('--force', action='store_true', help='Overwrite '
  65.                         'output file if it exists')
  66.     parser.add_argument('--width', metavar='PIXELS', default=1280, type=int,
  67.                         help='Output width (default 1280)')
  68.     parser.add_argument('--height', metavar='PIXELS', default=720, type=int,
  69.                         help='Output height (default 720)')
  70.     parser.add_argument('--border-width', metavar='PIXELS', dest='border',
  71.                         default=25, type=int, help='Border width (default 25)')
  72.     parser.add_argument('video', nargs='+', type=Video, help='Input video '
  73.                         '(filename[:start_at[:end_at]])')
  74.     args = parser.parse_args()
  75.     if os.path.exists(args.output_file) and not args.force:
  76.         sys.exit('Will not overwrite {} unless --force is specified.'.format(
  77.             args.output_file))
  78.     return args
  79.  
  80.  
  81. def main():
  82.     args = parse_args()
  83.     if len(args.video) == 1:
  84.         sys.exit('This script does not make sense with just one video!')
  85.  
  86.     # Figure out how long the video is going to be and which video's audio is
  87.     # the shortest.
  88.     output_length = args.video[0].end_at - args.video[0].start_at
  89.     shortest_video = args.video[0]
  90.     shortest_length = output_length
  91.     for video in args.video[1:]:
  92.         video_length = video.end_at - video.start_at
  93.         if video_length > output_length:
  94.             output_length = video_length
  95.         if video_length < shortest_length:
  96.             shortest_video = video
  97.     print('Output video will be {} seconds long.'.format(output_length))
  98.     print('Audio will end after {} ({} seconds).'.format(
  99.         shortest_video.filename, shortest_length))
  100.  
  101.     # Figure out grid size.
  102.     grid_size = math.ceil(math.sqrt(len(args.video)))
  103.     print('Output video will be {}x{}'.format(grid_size, grid_size))
  104.  
  105.     # Figure out the maximum dimensions of each video.
  106.     max_width = int((args.width - args.border * (grid_size + 1)) / grid_size)
  107.     max_height = int((args.height - args.border * (grid_size + 1)) / grid_size)
  108.     print('Maximum dimensions of embedded videos will be {}x{}.'.format(
  109.         max_width, max_height))
  110.  
  111.     # Calculate scaling for each video.
  112.     for video in args.video:
  113.         if video.width > max_width or video.height > max_height:
  114.             if video.width / max_width > video.height / max_height:
  115.                 scale_width = max_width
  116.                 scale_height = int(scale_width / video.width *
  117.                                    video.height)
  118.                 video.scale = '{}:-1'.format(scale_width)
  119.             else:
  120.                 scale_height = max_height
  121.                 scale_width = int(scale_height / video.height *
  122.                                   video.width)
  123.                 video.scale = '-1:{}'.format(scale_height)
  124.             print('Scaling {} to {}x{}'.format(
  125.                 video.filename, scale_width, scale_height))
  126.  
  127.     with NamedTemporaryFile(suffix='.png') as background_png, \
  128.          NamedTemporaryFile(suffix='.mp4') as background_mp4:
  129.         subprocess.run(('convert', '-size',
  130.                         '{}x{}'.format(args.width, args.height),
  131.                         'xc:black', background_png.name))
  132.  
  133.         # Create background video.
  134.         subprocess.run(('ffmpeg', '-loglevel', 'fatal', '-y', '-loop', '1',
  135.                         '-t', str(output_length), '-i', background_png.name,
  136.                         '-pix_fmt', 'yuv420p', background_mp4.name))
  137.  
  138.         # Construct the ffmpeg command.
  139.         cmd = ['ffmpeg', '-y', '-loglevel', 'fatal', '-i', background_mp4.name]
  140.         for video in args.video:
  141.             cmd.extend(('-ss', str(video.start_at), '-to', str(video.end_at),
  142.                         '-i', video.filename))
  143.         column = 0
  144.         row = 0
  145.         filters = [
  146.             ''.join(['[{}]'.format(n+1) for n in range(len(args.video))]) +
  147.             'amix=inputs={}'.format(len(args.video))
  148.         ]
  149.  
  150.         h_border = int((args.width - max_width * grid_size) / (grid_size + 1))
  151.         v_border = int((args.height - max_height * grid_size) /
  152.                        (grid_size + 1))
  153.  
  154.         for i in range(len(args.video)):
  155.             video = args.video[i]
  156.             cur = i + 1
  157.             filters.append('[{}]scale={}[{}s]'.format(cur, video.scale, cur))
  158.             if i:
  159.                 prev = '[{}o]'.format(i)
  160.             else:
  161.                 prev = '[0]'
  162.             filters.append('{}[{}s]overlay={}:{}[{}o]'.format(
  163.                 prev, cur, h_border + column * (max_width + h_border),
  164.                 v_border + row * (max_height + v_border), cur))
  165.             column += 1
  166.             if column == grid_size:
  167.                 column = 0
  168.                 row += 1
  169.         # Get rid of the output for the last overlay, so it goes into the
  170.         # output video.
  171.         filters[-1] = re.sub(r'\[\d+o\]$', '', filters[-1])
  172.  
  173.         cmd.extend(('-filter_complex', ';'.join(filters), args.output_file))
  174.         print('Processing video.')
  175.         subprocess.run(cmd)
  176.         print('Done.')
  177.  
  178.  
  179. if __name__ == '__main__':
  180.     main()
  181.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement