diff --git a/.gitignore b/.gitignore index 2fc5174..5d1d88b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ outputs/ .DS_Store Examples/ env/ - +ideas/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/experimental/convertersink.py b/experimental/convertersink.py new file mode 100644 index 0000000..d486b27 --- /dev/null +++ b/experimental/convertersink.py @@ -0,0 +1,315 @@ +#!/usr/bin/env python3 +import argparse, os, shlex, subprocess, sys +from pathlib import Path + +# ------------------------------ +# Helpers +# ------------------------------ +def run(cmd): + print(">>", " ".join(shlex.quote(c) for c in cmd)) + proc = subprocess.run(cmd) + if proc.returncode != 0: + sys.exit(proc.returncode) + +def ext_lower(path): + return Path(path).suffix.lower().lstrip(".") + +def guess_container(output_path, fmt_override=None): + if fmt_override: + return fmt_override.lower() + ext = ext_lower(output_path) + # Map common aliases/extensions to FFmpeg muxer names + alias = { + "mp4":"mp4", "m4v":"mp4", "mov":"mov", "qt":"mov", "mkv":"matroska", + "webm":"webm", "avi":"avi", "wmv":"asf", "asf":"asf", + "flv":"flv", "ts":"mpegts", "m2ts":"mpegts", "mpeg":"mpeg", "mpg":"mpeg", + "mxf":"mxf", "wav":"wav", "mp3":"mp3", "m4a":"ipod", "aac":"adts", "ogg":"ogg", + "oga":"ogg", "opus":"ogg", "flac":"flac", "alac":"ipod", + "gif":"gif", + # image sequences + "jpg":"image2", "jpeg":"image2", "png":"image2", "tif":"image2", "tiff":"image2", "bmp":"image2", "exr":"image2" + } + return alias.get(ext, ext or "mp4") + +def is_audio_only(fmt): + return fmt in {"mp3","ipod","adts","ogg","flac","wav"} + +def default_codecs_for_container(mux): + """ + Conservative, broadly compatible defaults. + Overridden by profiles or explicit --vcodec/--acodec. + """ + if mux in ("mp4", "mov", "matroska"): + return ("libx264", "aac") + if mux == "webm": + return ("libvpx-vp9", "libopus") + if mux == "gif": + return (None, None) # handled via palette method + if mux in ("mp3","adts","ogg","flac","wav","ipod"): + return (None, None) # audio-only handled separately + if mux == "mxf": + # Default to XDCAM HD 50 unless profile overrides + return ("mpeg2video", "pcm_s16le") + if mux == "mpegts": + return ("libx264", "aac") + if mux == "avi": + # Legacy default: MPEG-4 ASP + MP3 + return ("mpeg4", "mp3") + return ("libx264", "aac") + +# ------------------------------ +# Profiles (curated, battle-tested) +# ------------------------------ +PROFILES = { + # Web/General delivery + "mp4_h264": lambda: [ + "-c:v","libx264","-preset","veryfast","-crf","18", + "-pix_fmt","yuv420p", + "-movflags","+faststart", + "-c:a","aac","-b:a","192k","-ac","2" + ], + "mp4_hevc": lambda: [ + "-c:v","libx265","-preset","medium","-crf","22", + "-tag:v","hvc1", + "-pix_fmt","yuv420p", + "-movflags","+faststart", + "-c:a","aac","-b:a","192k","-ac","2" + ], + "mp4_av1": lambda: [ + "-c:v","libaom-av1","-crf","28","-b:v","0", + "-pix_fmt","yuv420p", + "-movflags","+faststart", + "-c:a","aac","-b:a","192k","-ac","2" + ], + + # WebM + "webm_vp9": lambda: [ + "-c:v","libvpx-vp9","-b:v","0","-crf","30", + "-row-mt","1", + "-pix_fmt","yuv420p", + "-c:a","libopus","-b:a","160k","-ac","2" + ], + "webm_av1": lambda: [ + "-c:v","libaom-av1","-crf","32","-b:v","0", + "-pix_fmt","yuv420p", + "-c:a","libopus","-b:a","160k","-ac","2" + ], + + # Professional mezzanine + "prores_422": lambda: [ + "-c:v","prores_ks","-profile:v","2", + "-pix_fmt","yuv422p10le", + "-c:a","pcm_s16le" + ], + "prores_4444": lambda: [ + "-c:v","prores_ks","-profile:v","4", + "-pix_fmt","yuva444p10le", + "-c:a","pcm_s24le" + ], + "dnxhr_hq": lambda: [ + "-c:v","dnxhd","-profile:v","dnxhr_hq", + "-pix_fmt","yuv422p", + "-c:a","pcm_s16le" + ], + + # Broadcast MXF (OP1a) + "mxf_xdcamhd50_1080i59": lambda: [ + "-c:v","mpeg2video","-b:v","50M","-minrate","50M","-maxrate","50M","-bufsize","17825792", + "-r","30000/1001","-flags","+ildct+ilme","-top","1", + "-pix_fmt","yuv422p", + "-c:a","pcm_s24le","-ar","48000","-ac","2", + "-f","mxf" + ], + + # Archival lossless + "lossless_ffv1": lambda: [ + "-c:v","ffv1","-level","3","-g","1","-slicecrc","1", + "-c:a","pcm_s24le" + ], + + # GIF + "gif": lambda: ["-filter_complex","[0:v]fps=15,scale=iw:-2:flags=lanczos"], + + # Image sequences + "png_seq": lambda: ["-c:v","png"], + "jpg_seq": lambda: ["-qscale:v","2"], + + # Audio-only + "mp3_320": lambda: ["-vn","-c:a","libmp3lame","-b:a","320k"], + "aac_192": lambda: ["-vn","-c:a","aac","-b:a","192k"], + "flac": lambda: ["-vn","-c:a","flac"], + + # --- NEW: AVI speed-focused presets --- + # Very fast encode, big files; great for quick turnarounds or NLE ingest + "avi_mjpeg_fast": lambda: ["-c:v","mjpeg","-q:v","3","-c:a","pcm_s16le"], + # Legacy-compatible, faster-than-default MPEG-4 ASP + "avi_mpeg4_fast": lambda: ["-c:v","mpeg4","-qscale:v","3","-bf","0","-mbd","0","-c:a","mp3","-b:a","192k"] +} + +# ------------------------------ +# Command builder +# ------------------------------ +def build_ffmpeg_command(args): + in_path = args.input + out_path = args.output + + if not out_path: + base = str(Path(in_path).with_suffix("")) + ext = f".{args.format}" if args.format else ".mp4" + out_path = base + ext + + mux = guess_container(out_path, args.format) + common = ["ffmpeg", "-hide_banner", "-y" if args.force else "-n", "-i", in_path] + + # Stream copy? + if args.copy: + return common + ["-c","copy", out_path] + + # Explicit codecs (can be overridden by profile if repeated later) + vcodec = args.vcodec + acodec = args.acodec + + # Apply profile if chosen + profile_args = [] + if args.profile: + prof = PROFILES.get(args.profile) + if not prof: + sys.exit(f"Unknown profile: {args.profile}") + profile_args = prof() + + # If no explicit profile and no explicit codecs, choose container defaults + if not args.profile: + dv, da = default_codecs_for_container(mux) + if not vcodec and dv: vcodec = dv + if not acodec and da: acodec = da + + cmd = list(common) + + # Special path: GIF palette method + if mux == "gif" or args.profile == "gif": + vf = [] + if args.vf: vf.append(args.vf) + else: vf.append("fps=15,scale=iw:-2:flags=lanczos") + palette_chain = f"[0:v]{','.join(vf)},palettegen[p];[0:v]{','.join(vf)}[v];[v][p]paletteuse" + gif_cmd = list(common) + ["-filter_complex", palette_chain, "-gifflags", "+transdiff", out_path] + return gif_cmd + + # Video settings + if vcodec: + cmd += ["-c:v", vcodec] + if args.crf is not None: + cmd += ["-crf", str(args.crf)] + if args.bitrate: + cmd += ["-b:v", args.bitrate] + if args.maxrate: + cmd += ["-maxrate", args.maxrate] + if args.bufsize: + cmd += ["-bufsize", args.bufsize] + if args.preset: + cmd += ["-preset", args.preset] + if args.profile_v: + cmd += ["-profile:v", args.profile_v] + if args.level: + cmd += ["-level", args.level] + if args.pix_fmt: + cmd += ["-pix_fmt", args.pix_fmt] + if args.gop: + cmd += ["-g", str(args.gop)] + if args.r: + cmd += ["-r", args.r] + if args.vf: + cmd += ["-vf", args.vf] + if args.tagv: + cmd += ["-tag:v", args.tagv] + + # Audio settings + if acodec: + cmd += ["-c:a", acodec] + if args.abitrate: + cmd += ["-b:a", args.abitrate] + if args.ac: + cmd += ["-ac", str(args.ac)] + if args.ar: + cmd += ["-ar", args.ar] + + # Container niceties + if mux == "mp4" and "+faststart" not in " ".join(profile_args): + cmd += ["-movflags", "+faststart"] + + # Append profile args last so they can set container-specific flags + if profile_args: + cmd += profile_args + + # --- NEW: Sensible AVI defaults when user didn't specify quality flags or a profile --- + if mux == "avi" and not args.profile: + # If using MPEG-4 ASP and no quality flags were set, favor speed with decent visual quality + have_quality_flag = any(x in (args.crf, args.bitrate, args.maxrate, args.bufsize) for x in [args.crf, args.bitrate, args.maxrate, args.bufsize]) + if (vcodec or "").lower() == "mpeg4" and not have_quality_flag: + # Avoid B-frames for speed; use qscale for constant quality + cmd += ["-qscale:v","3","-bf","0"] + # If user picked MJPEG explicitly without profile and no quality, set q:v + if (vcodec or "").lower() == "mjpeg" and not have_quality_flag: + cmd += ["-q:v","3"] + # Reasonable audio default if none provided + if not acodec: + cmd += ["-c:a","mp3","-b:a","192k"] + + # Raw passthrough flags after '--' + if args.ffmpeg_args: + cmd += args.ffmpeg_args + + cmd += [out_path] + return cmd + +# ------------------------------ +# CLI +# ------------------------------ +def parse_args(): + p = argparse.ArgumentParser( + description="Black-box FFmpeg converter: any format in → any format out.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + # Globals / required + p.add_argument("-i","--input", required=True, help="Input media file") + p.add_argument("-o","--output", help="Output path (extension chooses container unless --format)") + p.add_argument("-F","--force", action="store_true", help="Overwrite output file") + + # Container/format control + p.add_argument("--format", help="Force container/muxer (e.g., mp4, mov, webm, matroska, mxf, gif, image2, avi)") + p.add_argument("--profile", choices=sorted(PROFILES.keys()), help="Apply a curated preset") + + # Codecs & quality + p.add_argument("--vcodec", help="Video codec (e.g., libx264, libx265, libaom-av1, prores_ks, dnxhd, mpeg2video, mpeg4, mjpeg)") + p.add_argument("--acodec", help="Audio codec (e.g., aac, libopus, libmp3lame, mp3, pcm_s16le)") + p.add_argument("--crf", type=int, help="Constant Rate Factor (quality target for many codecs)") + p.add_argument("--bitrate", help="Video bitrate (e.g., 5M)") + p.add_argument("--maxrate", help="Video maxrate") + p.add_argument("--bufsize", help="Video VBV buffer size") + p.add_argument("--preset", help="Codec speed/efficiency preset (varies by codec)") + p.add_argument("--profile-v", dest="profile_v", help="Video codec profile (e.g., high, main, baseline; or prores profile index)") + p.add_argument("--level", help="Video level (e.g., 4.1)") + p.add_argument("--pix-fmt", dest="pix_fmt", help="Pixel format (e.g., yuv420p, yuv422p10le, yuva444p10le)") + p.add_argument("--gop", type=int, help="GOP/keyframe interval (frames)") + p.add_argument("-r", help="Output frame rate (e.g., 30000/1001, 25, 24)") + p.add_argument("--vf", help="Video filtergraph") + p.add_argument("--tagv", help="Force video fourcc/tag (e.g., hvc1)") + + # Audio + p.add_argument("--abitrate", help="Audio bitrate (e.g., 192k)") + p.add_argument("--ac", type=int, help="Audio channels") + p.add_argument("--ar", help="Audio sample rate (e.g., 48000)") + + # Stream copy + p.add_argument("--copy", action="store_true", help="Stream copy all streams when compatible (no re-encode)") + + # Pass-through extra ffmpeg args after -- + p.add_argument("ffmpeg_args", nargs=argparse.REMAINDER, help="Raw args after -- go straight to ffmpeg") + return p.parse_args() + +def main(): + args = parse_args() + cmd = build_ffmpeg_command(args) + run(cmd) + +if __name__ == "__main__": + main() diff --git a/experimental/convertersink.sh b/experimental/convertersink.sh new file mode 100644 index 0000000..030c096 --- /dev/null +++ b/experimental/convertersink.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# run_examples.sh +# Batch conversions using convertersink.py, pausing 2s between each job. + +set -e + +# General H.264 MP4 (web-friendly) +python3 convertersink.py -i bbb.mov -o out.mp4 --profile mp4_h264 -F +sleep 2 + +# HEVC in MP4 (Apple-friendly tag) +# python3 convertersink.py -i bbb.mkv -o out_hevc.mp4 --profile mp4_hevc -F +sleep 2 + +# AV1 in MP4 (tiny files, slow encode) +python3 convertersink.py -i bbb.mov -o out_av1.mp4 --profile mp4_av1 -F +sleep 2 + +# WebM VP9 + Opus +python3 convertersink.py -i bbb.mp4 -o out.webm --profile webm_vp9 -F +sleep 2 + +# WebM AV1 + Opus +python3 convertersink.py -i bbb.mp4 -o out_av1.webm --profile webm_av1 -F +sleep 2 + +# ProRes 422 in MOV (mezzanine) +python3 convertersink.py -i bbb.mp4 -o out_prores.mov --profile prores_422 -F +sleep 2 + +# ProRes 4444 in MOV (alpha support) +python3 convertersink.py -i in_prores4444_src.mov -o out_4444.mov --profile prores_4444 -F +sleep 2 + +# DNxHR HQ in MOV +python3 convertersink.py -i bbb.mp4 -o out_dnxhr.mov --profile dnxhr_hq -F +sleep 2 + +# MXF XDCAM HD 50 (1080i59.94 broadcast) +python3 convertersink.py -i bbb.mov -o out_xdcamhd50.mxf --profile mxf_xdcamhd50_1080i59 -F +sleep 2 + +# Lossless archival (FFV1 in MKV) +python3 convertersink.py -i bbb.mov -o archive.mkv --profile lossless_ffv1 -F +sleep 2 + +# High-quality GIF +python3 convertersink.py -i bbb.mp4 -o out.gif --profile gif -F +sleep 2 + +# Image sequence (PNG) +python3 convertersink.py -i bbb.mp4 -o frames_%05d.png --format image2 --profile png_seq -F +sleep 2 + +# Image sequence (JPEG, quality ~2) +python3 convertersink.py -i bbb.mp4 -o frames_%05d.jpg --format image2 --profile jpg_seq -F +sleep 2 + +# Audio-only (MP3 320k) +python3 convertersink.py -i bbb.mov -o track.mp3 --profile mp3_320 -F +sleep 2 + +# Audio-only (AAC 192k in M4A) +python3 convertersink.py -i bbb.mov -o track.m4a --profile aac_192 -F +sleep 2 + +# Quick stream-copy (no re-encode) when compatible +python3 convertersink.py -i bbb.mp4 -o just_rewrap.mkv --copy -F +sleep 2 + +# Custom overrides (manual control) +python3 convertersink.py -i bbb.mov -o out_custom.mp4 --vcodec libx264 --crf 20 --preset medium \ + --pix-fmt yuv420p --acodec aac --abitrate 160k --ac 2 --vf "scale=1280:-2" -F +sleep 2 + +# Pass raw ffmpeg flags after `--` +python3 convertersink.py -i bbb.mov -o out_muxqueue.mp4 --profile mp4_h264 -- --max_muxing_queue_size 9999 +sleep 2 + +# --- New fast AVI presets --- +# Fast AVI via MJPEG (very fast encode; large output) +python3 convertersink.py -i bbb.mp4 -o fast_review.avi --profile avi_mjpeg_fast -F +sleep 2 + +# Faster legacy AVI via MPEG-4 ASP +python3 convertersink.py -i bbb.mp4 -o legacy_fast.avi --profile avi_mpeg4_fast -F +sleep 2