#!/usr/bin/env python3 """Combine multiple video files into one file using FFmpeg. Example: python helper_files/combine_videos.py output.mp4 video1.mp4 video2.mp4 video3.mp4 python helper_files/combine_videos.py --output merged.mp4 --inputs video1.mp4 video2.mp4 """ import argparse import os import subprocess import sys from pathlib import Path def build_concat_list(input_files, list_path): with open(list_path, "w", encoding="utf-8") as f: for path in input_files: f.write(f"file '{os.path.abspath(path)}'\n") def run_ffmpeg(cmd): print("Running:", " ".join(cmd)) result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: print(result.stdout) print(result.stderr) raise RuntimeError(f"FFmpeg failed with exit code {result.returncode}") print("✅ Finished successfully.") def concat_videos_copy(input_files, output_path): list_path = Path(output_path).with_suffix(".concat.txt") build_concat_list(input_files, list_path) cmd = [ "ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", str(list_path), "-c", "copy", str(output_path), ] run_ffmpeg(cmd) list_path.unlink(missing_ok=True) def concat_videos_reencode(input_files, output_path): inputs = [] filter_parts = [] audio_parts = [] for i, video_path in enumerate(input_files): inputs.extend(["-i", str(video_path)]) # Use semicolons to separate different input streams in the filter graph filter_parts.append(f"[{i}:v]scale=trunc(iw/2)*2:trunc(ih/2)*2,setpts=PTS-STARTPTS[v{i}]") audio_parts.append(f"[{i}:a]aresample=async=1[a{i}]") # Correctly separate individual stream filters with semicolons filter_complex = f"{';'.join(filter_parts)};{';'.join(audio_parts)};" filter_complex += "".join(f"[v{i}][a{i}]" for i in range(len(input_files))) filter_complex += f"concat=n={len(input_files)}:v=1:a=1[outv][outa]" cmd = [ "ffmpeg", "-y", *inputs, "-filter_complex", filter_complex, "-map", "[outv]", "-map", "[outa]", "-c:v", "libx264", "-crf", "18", # High quality video "-preset", "slow", # High efficiency "-c:a", "aac", "-b:a", "256k", # High quality audio "-movflags", "+faststart", str(output_path), ] run_ffmpeg(cmd) def merge_videos(input_files, output_path): input_files = [str(Path(p).expanduser().resolve()) for p in input_files] output_path = str(Path(output_path).expanduser().resolve()) if len(input_files) < 2: raise ValueError("You need at least 2 video files to merge.") for path in input_files: if not os.path.exists(path): raise FileNotFoundError(f"File not found: {path}") output_dir = Path(output_path).parent output_dir.mkdir(parents=True, exist_ok=True) try: concat_videos_copy(input_files, output_path) except Exception as exc: print(f"Fast copy method failed: {exc}") print("Retrying with re-encode fallback...") concat_videos_reencode(input_files, output_path) def main(): parser = argparse.ArgumentParser(description="Join two or more video files together with FFmpeg.") parser.add_argument("output_file", help="Path to the merged output video") parser.add_argument("input_files", nargs="+", help="Two or more videos to concatenate") args = parser.parse_args() try: merge_videos(args.input_files, args.output_file) print(f"Merged video saved to: {args.output_file}") except Exception as exc: print(f"Error: {exc}") sys.exit(1) if __name__ == "__main__": main()