Created
September 5, 2025 22:58
-
-
Save kylemcdonald/d7fa3adf1cda4546818fe797925d3e53 to your computer and use it in GitHub Desktop.
Process MP4 files: trim and concatenate using ffmpeg.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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