130 lines
3.8 KiB
Python
130 lines
3.8 KiB
Python
#!/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)])
|
|
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}]")
|
|
|
|
concat_filter = ",".join(filter_parts + audio_parts)
|
|
map_parts = []
|
|
for i in range(len(input_files)):
|
|
map_parts.append(f"[v{i}]")
|
|
map_parts.append(f"[a{i}]")
|
|
|
|
# Join the video and audio streams in order and concatenate them.
|
|
# This works well when the clips are similar enough in format.
|
|
filter_complex = f"{','.join(filter_parts)};{';'.join(audio_parts)};"
|
|
filter_complex += "".join(f"[{i}:v][{i}:a]" 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",
|
|
"-c:a",
|
|
"aac",
|
|
"-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()
|