#!/usr/bin/env python3 import os import re import subprocess import tempfile import sys from datetime import datetime, timedelta from argparse import ArgumentParser from typing import List input_fmt = '%Y-%m-%d-%H:%M:%S.%f' output_fmt = '%Y-%m-%d-%H:%M:%S' # declare types File = dict FileList = List[File] def get_files(source_directory: str) -> FileList: files = [] for f in os.listdir(source_directory): # 2022-06-15-00:02:40.187877.jpg m = re.match(r'(^\d{4}-\d{2}-\d{2}-\d{2}:\d{2}:\d{2}\.\d{1,6})\.jpg$', f) if not m: continue files.append({ 'filename': os.path.join(source_directory, f), 'time': datetime.strptime(m.group(1), input_fmt), }) files.sort(key=lambda f: f['time'].timestamp()) return files def group_files(files: FileList, timedelta_val: int) -> List[FileList]: groups = [] group_idx = None for file in files: if group_idx is None or \ not groups[group_idx] or \ file['time'] - groups[group_idx][-1]['time'] <= timedelta(seconds=timedelta_val): if group_idx is None: groups.append([]) group_idx = 0 else: group_idx += 1 groups.append([]) groups[group_idx].append(file) return groups def merge(groups: List[FileList], output_directory: str, delete_source_files=False, cedrus=False, rotate=0) -> None: for g in groups: success = False fd = tempfile.NamedTemporaryFile(delete=False) try: n = len(g) last_frame_dur = 0 for i in range(n): file = g[i] fd.write(f'file \'{file["filename"]}\'\n'.encode()) if i < n-1: last_frame_dur = g[i+1]['time'].timestamp()-file['time'].timestamp() fd.write(f'duration {last_frame_dur}\n'.encode()) fd.close() print(f'temp concat file: {fd.name}') start = g[0]['time'].strftime(output_fmt) stop = g[-1]['time'].strftime(output_fmt) fn = f'{start}_{stop}_merged.mp4' output = os.path.join(output_directory, fn) if cedrus: ffmpeg = '/home/user/.local/bin/ffmpeg-cedrus' env = dict(os.environ) env['LD_LIBRARY_PATH'] = '/usr/local/lib' args = ['-c:v', 'cedrus264', '-pix_fmt', 'nv12'] else: ffmpeg = 'ffmpeg' env = {} args = ['-c:v', 'libx264', '-preset', 'veryslow', '-crf', '34', # '-vb', '448k', '-pix_fmt', 'yuv420p', '-movflags', '+faststart' # '-filter:v', 'fps=2' ] if rotate != 0: args.extend(['-map_metadata', '0', '-metadata:s:v', f'rotate="{rotate}"']) if rotate != 0: args.extend(['-map_metadata', '0', '-metadata:s:v', f'rotate="{rotate}"', '-codec', 'copy']) cmd = [ffmpeg, '-y', '-f', 'concat', '-safe', '0', '-i', fd.name, '-map_metadata', '-1', *args, output] p = subprocess.run(cmd, capture_output=False, env=env) if p.returncode != 0: print(f'error: ffmpeg returned {p.returncode}') else: success = True finally: os.unlink(fd.name) if success and delete_source_files: for file in g: os.unlink(file['filename']) def print_groups(groups): for g in groups: g1 = g[0] g2 = g[len(g)-1] print(str(g1['time'])+' .. '+str(g2['time'])) if __name__ == '__main__': parser = ArgumentParser() parser.add_argument('--input-directory', '-i', type=str, required=True, help='Directory with files') parser.add_argument('--output-directory', '-o', type=str, required=True, help='Output directory') parser.add_argument('--timedelta', type=int, default=10) parser.add_argument('-D', '--delete-source-files', action='store_true') parser.add_argument('--cedrus', action='store_true') parser.add_argument('--rotate', type=int, choices=(90, 180, 270), default=0) # parser.add_argument('--vbr', action='store_true', # help='Re-encode using VBR (-q:a 4)') arg = parser.parse_args() # if arg.cedrus and not os.getegid() == 0: # raise RuntimeError("Must be run as root.") files = get_files(os.path.realpath(arg.input_directory)) if not len(files): print(f"No jpeg files found in {arg.input_directory}.") sys.exit() groups = group_files(files, timedelta_val=arg.timedelta) # print_groups(groups) merge(groups, os.path.realpath(arg.output_directory), delete_source_files=arg.delete_source_files, cedrus=arg.cedrus, rotate=arg.rotate)