Created
October 27, 2025 04:00
-
-
Save Ryan-Haines/81829db1a92f3fe0bec932a43b9f7be6 to your computer and use it in GitHub Desktop.
Script to flip all videos in a folder by 180 degrees, using ffmpeg with vulkan APIs
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 subprocess, sys, shutil | |
| from pathlib import Path | |
| # --- Config ------------------------------------------------------------- | |
| # Will try all methods until one works: vulkan -> cuda -> opencl | |
| # Encoder: h264_nvenc is fast and widely compatible on NVIDIA. | |
| # You can switch to "hevc_nvenc" or "av1_nvenc" if you prefer. | |
| VIDEO_ENCODER = "h264_nvenc" | |
| # Quality/Speed trade-offs (NVENC). Match original quality. | |
| NVENC_ARGS = [ | |
| "-preset", "p5", # p1 slowest/best -> p7 fastest | |
| "-cq", "23", # 23 is more reasonable (lower = higher quality) | |
| "-b:v", "0" # Let CQ control quality | |
| ] | |
| # File types to process | |
| EXTS = {".mp4", ".mov", ".mkv", ".avi", ".webm", ".m4v", ".mts", ".m2ts", ".ts", ".wmv"} | |
| # ----------------------------------------------------------------------- | |
| def have(cmd: str) -> bool: | |
| return shutil.which(cmd) is not None | |
| if not have("ffmpeg"): | |
| sys.exit("ffmpeg not found on PATH") | |
| if not have("ffprobe"): | |
| print("Warning: ffprobe not found; proceeding without it.", file=sys.stderr) | |
| def out_name(p: Path) -> Path: | |
| return p.with_name(p.stem + "_flipped" + p.suffix) | |
| def get_video_bitrate(inp: Path) -> str: | |
| """Get the original video bitrate to match it""" | |
| try: | |
| cmd = [ | |
| "ffprobe", "-v", "error", "-select_streams", "v:0", | |
| "-show_entries", "stream=bit_rate", "-of", "default=noprint_wrappers=1:nokey=1", | |
| str(inp) | |
| ] | |
| result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) | |
| if result.returncode == 0 and result.stdout.strip(): | |
| bitrate = int(result.stdout.strip()) | |
| return str(bitrate) | |
| except: | |
| pass | |
| return None | |
| def get_all_methods(): | |
| """Return all methods to try in order of preference""" | |
| return [ | |
| { | |
| "name": "Vulkan GPU", | |
| "vf": "hwupload,hflip_vulkan,vflip_vulkan,hwdownload,format=yuv420p", | |
| "hwargs": ["-init_hw_device", "vulkan"], | |
| "vcodec": VIDEO_ENCODER | |
| }, | |
| { | |
| "name": "CUDA GPU", | |
| "vf": "hwupload_cuda,hflip_cuda,vflip_cuda,hwdownload,format=yuv420p", | |
| "hwargs": ["-hwaccel", "cuda"], | |
| "vcodec": VIDEO_ENCODER | |
| }, | |
| { | |
| "name": "OpenCL GPU", | |
| "vf": "format=nv12,hwupload,hflip_opencl,vflip_opencl,hwdownload,format=yuv420p", | |
| "hwargs": ["-init_hw_device", "opencl"], | |
| "vcodec": VIDEO_ENCODER | |
| }, | |
| ] | |
| def flip_file(inp: Path): | |
| outp = out_name(inp) | |
| if outp.exists(): | |
| print(f"SKIP (exists): {outp.name}") | |
| return True | |
| # Get original bitrate to match it | |
| original_bitrate = get_video_bitrate(inp) | |
| methods = get_all_methods() | |
| for method in methods: | |
| print(f"Trying: {method['name']}") | |
| # Build encoding args - use original bitrate if available | |
| if original_bitrate and "nvenc" in method["vcodec"]: | |
| encoding_args = ["-b:v", original_bitrate, "-maxrate", original_bitrate, "-bufsize", str(int(original_bitrate) * 2)] | |
| else: | |
| encoding_args = NVENC_ARGS | |
| cmd = [ | |
| "ffmpeg", | |
| "-y", | |
| "-hide_banner", | |
| "-loglevel", "error", | |
| *method["hwargs"], | |
| "-i", str(inp), | |
| "-map", "0", | |
| "-vf", method["vf"], | |
| "-c:v", method["vcodec"], | |
| *encoding_args, | |
| "-c:a", "copy", | |
| "-c:s", "copy", | |
| "-movflags", "+faststart", | |
| str(outp) | |
| ] | |
| result = subprocess.run(cmd, capture_output=True, text=True) | |
| if result.returncode == 0: | |
| print(f"✓ Success with {method['name']}") | |
| return True | |
| else: | |
| print(f"✗ {method['name']} failed") | |
| if outp.exists(): | |
| outp.unlink() | |
| print(f"❌ All methods failed for {inp.name}") | |
| return False | |
| def main(): | |
| print(f"Auto-trying all methods | Encoder: {VIDEO_ENCODER}") | |
| vids = [p for p in Path(".").iterdir() if p.is_file() and p.suffix.lower() in EXTS] | |
| if not vids: | |
| print("No videos found in current folder.") | |
| return | |
| print(f"Found {len(vids)} video(s) to process\n") | |
| failures = 0 | |
| for i, p in enumerate(vids, 1): | |
| print(f"[{i}/{len(vids)}] {p.name}") | |
| if not flip_file(p): | |
| failures += 1 | |
| print() | |
| print("=" * 50) | |
| print(f"Processed: {len(vids)} | Success: {len(vids) - failures} | Failed: {failures}") | |
| if failures: | |
| sys.exit(f"{failures} file(s) failed.") | |
| print("✓ All done!") | |
| if __name__ == "__main__": | |
| main() |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Took some videos with a gopro and only realized several days later that they were all upside down! This script flips them with a minimal quality loss.