Skip to content

Instantly share code, notes, and snippets.

@kylemcdonald
Created September 5, 2025 22:58
Show Gist options
  • Select an option

  • Save kylemcdonald/d7fa3adf1cda4546818fe797925d3e53 to your computer and use it in GitHub Desktop.

Select an option

Save kylemcdonald/d7fa3adf1cda4546818fe797925d3e53 to your computer and use it in GitHub Desktop.
Process MP4 files: trim and concatenate using ffmpeg.
#!/usr/bin/env python3
import os
import sys
import argparse
import subprocess
import glob
from pathlib import Path
import natsort
def list_mp4_files(directory):
"""List all MP4 files in the specified directory in natural sorted order."""
mp4_files = []
for filename in os.listdir(directory):
if filename.lower().endswith('.mp4'):
mp4_files.append(os.path.join(directory, filename))
return natsort.natsorted(mp4_files)
def create_trim_directory():
"""Create the trim directory if it doesn't exist."""
trim_dir = Path("trim")
trim_dir.mkdir(exist_ok=True)
return trim_dir
def trim_video(input_file, output_file, duration=5):
"""Trim a video to the specified duration using ffmpeg."""
cmd = [
"ffmpeg", "-i", input_file,
"-t", str(duration),
"-c:v", "libx265",
"-c:a", "aac",
"-preset", "medium",
"-crf", "28",
"-pix_fmt", "yuv420p10le",
"-tag:v", "hvc1",
"-movflags", "+faststart",
"-vsync", "1",
"-y", output_file
]
try:
subprocess.run(cmd, check=True, capture_output=True)
return True
except subprocess.CalledProcessError as e:
print(f"Error trimming {input_file}: {e}")
return False
def create_concat_file(trimmed_files, concat_file_path):
"""Create a concat file for ffmpeg."""
with open(concat_file_path, 'w') as f:
for file_path in trimmed_files:
f.write(f"file '{file_path}'\n")
def concatenate_videos(trimmed_files, output_file):
"""Concatenate all trimmed videos using ffmpeg."""
concat_file = "concat_list.txt"
create_concat_file(trimmed_files, concat_file)
cmd = [
"ffmpeg", "-f", "concat",
"-safe", "0",
"-i", concat_file,
"-c:v", "libx265",
"-c:a", "aac",
"-preset", "medium",
"-crf", "28",
"-pix_fmt", "yuv420p10le",
"-tag:v", "hvc1",
"-movflags", "+faststart",
"-vsync", "1",
"-y", output_file
]
try:
subprocess.run(cmd, check=True, capture_output=True)
# Clean up the concat file
os.remove(concat_file)
return True
except subprocess.CalledProcessError as e:
print(f"Error concatenating videos: {e}")
return False
def main():
parser = argparse.ArgumentParser(description="Process MP4 files: trim and concatenate")
parser.add_argument("directory", nargs="?", help="Directory containing MP4 files (not required with --combine-only)")
parser.add_argument("-d", "--duration", type=int, default=4,
help="Duration to trim videos to (default: 4 seconds)")
parser.add_argument("-o", "--output", default="combined_video.mp4",
help="Output filename for concatenated video (default: combined_video.mp4)")
parser.add_argument("-m", "--max-files", type=int, default=None,
help="Maximum number of files to process (default: all files)")
parser.add_argument("--combine-only", action="store_true",
help="Only combine videos from trim folder, skip trimming step")
args = parser.parse_args()
if args.combine_only:
# Only combine videos from trim folder
trim_dir = Path("trim")
if not trim_dir.exists():
print("Error: trim directory does not exist. Run without --combine-only first.")
sys.exit(1)
# List trimmed MP4 files
trimmed_files = list_mp4_files("trim")
if not trimmed_files:
print("No trimmed MP4 files found in trim directory")
sys.exit(1)
print(f"Found {len(trimmed_files)} trimmed videos to combine:")
for file in trimmed_files:
print(f" - {os.path.basename(file)}")
# Concatenate videos
print(f"\nConcatenating {len(trimmed_files)} trimmed videos...")
if concatenate_videos(trimmed_files, args.output):
print(f"✓ Successfully created combined video: {args.output}")
else:
print("✗ Failed to concatenate videos.")
sys.exit(1)
return
# Check if directory is provided
if not args.directory:
print("Error: Directory argument is required when not using --combine-only")
sys.exit(1)
# Check if directory exists
if not os.path.isdir(args.directory):
print(f"Error: Directory '{args.directory}' does not exist.")
sys.exit(1)
# List MP4 files
mp4_files = list_mp4_files(args.directory)
if not mp4_files:
print(f"No MP4 files found in directory '{args.directory}'")
sys.exit(1)
# Limit the number of files to process
if args.max_files and len(mp4_files) > args.max_files:
mp4_files = mp4_files[:args.max_files]
print(f"Limiting to first {args.max_files} files (use -m to change)")
print(f"Found {len(mp4_files)} MP4 files to process:")
for file in mp4_files:
print(f" - {os.path.basename(file)}")
# Create trim directory
trim_dir = create_trim_directory()
print(f"\nCreated trim directory: {trim_dir}")
# Trim videos
trimmed_files = []
print(f"\nTrimming videos to {args.duration} seconds...")
for mp4_file in mp4_files:
filename = os.path.basename(mp4_file)
name_without_ext = os.path.splitext(filename)[0]
trimmed_filename = f"{name_without_ext}_trimmed.mp4"
trimmed_path = trim_dir / trimmed_filename
print(f" Trimming {filename}...")
if trim_video(mp4_file, str(trimmed_path), args.duration):
trimmed_files.append(str(trimmed_path))
print(f" ✓ Saved to {trimmed_filename}")
else:
print(f" ✗ Failed to trim {filename}")
if not trimmed_files:
print("No videos were successfully trimmed.")
sys.exit(1)
# Concatenate videos
print(f"\nConcatenating {len(trimmed_files)} trimmed videos...")
if concatenate_videos(trimmed_files, args.output):
print(f"✓ Successfully created combined video: {args.output}")
else:
print("✗ Failed to concatenate videos.")
sys.exit(1)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment