diff --git a/scripts/process-videos.py b/scripts/process-videos.py index 9200194..123a26a 100755 --- a/scripts/process-videos.py +++ b/scripts/process-videos.py @@ -3,6 +3,7 @@ import argparse import fcntl import subprocess +import platform import os import sys from concurrent.futures import ThreadPoolExecutor @@ -175,6 +176,9 @@ def __init__(self, prefix: str): self.state = self._load_state() # Register cleanup on program exit atexit.register(self.cleanup_temp_dir) + # Get decoder and encoder codecs + self.decoder, self.encoder = self.get_ffmpeg_codecs() + print(f"INFO: using decoder={self.decoder}, encoder={self.encoder}") @classmethod def get_last_run(cls) -> Optional[Tuple[str, ProcessingState]]: @@ -338,10 +342,13 @@ def create_ffmpeg_command( cmd.extend( [ + "-c:v", + self.decoder, "-i", input_file, + # Encoder "-c:v", - "libx264", # H.264 video codec for compatibility + self.encoder, "-c:a", "aac", # AAC audio codec for compatibility "-pix_fmt", @@ -376,11 +383,11 @@ def create_settling_period_video( ) try: - subprocess.run(cmd) + self.run_ffmpeg_with_output(cmd, "setting") self.state.stage = ProcessingStage.SETTLING_CREATED self._save_state() print(f"✓ Finished creating settling period video: {output_file}") - except Exception as e: + except Exception: # Save state even on failure self._save_state() raise @@ -504,6 +511,23 @@ def prepare_video_segments( # Return all segment files in correct order return [s.output_file for s in self.state.segments.values()] + def get_ffmpeg_codecs(self): + """Return tuple of (decoder, encoder) codecs""" + try: + result = subprocess.run( + ["ffmpeg", "-hide_banner", "-encoders"], + capture_output=True, + text=True, + check=True, + ) + + # Prefer hardware accelerated on MacOS + if platform.system() == "Darwin" and "h264_videotoolbox" in result.stdout: + return ("h264", "h264_videotoolbox") + return ("h264", "libx264") # software fallback + except subprocess.SubprocessError: + return ("h264", "libx264") + def merge_video_segments(self, segment_files: List[str], output_file: str) -> None: """Merge all video segments into final output""" if self.state.stage >= ProcessingStage.MERGED: @@ -535,20 +559,22 @@ def merge_video_segments(self, segment_files: List[str], output_file: str) -> No "concat", "-safe", "0", + "-c:v", + self.decoder, "-i", concat_file, "-c:v", - "libx264", + self.encoder, "-c:a", "aac", "-pix_fmt", "yuv420p", output_file, ] - result = subprocess.run(merge_cmd, capture_output=True, text=True) - # If the first attempt fails, try alternative method - if result.returncode != 0: + try: + self.run_ffmpeg_with_output(merge_cmd, "merge") + except subprocess.CalledProcessError: print("First merge attempt failed, trying alternative method...") # Create intermediate list for complex filter @@ -577,7 +603,7 @@ def merge_video_segments(self, segment_files: List[str], output_file: str) -> No "-map", "[outa]", "-c:v", - "libx264", + self.encoder, "-c:a", "aac", "-pix_fmt", @@ -586,7 +612,7 @@ def merge_video_segments(self, segment_files: List[str], output_file: str) -> No ] ) - subprocess.run(alternative_cmd, check=True) + self.run_ffmpeg_with_output(alternative_cmd, "merge") self.state.merged_output = output_file self.state.stage = ProcessingStage.MERGED @@ -594,7 +620,6 @@ def merge_video_segments(self, segment_files: List[str], output_file: str) -> No except subprocess.CalledProcessError as e: print(f"Error during video merge: {e}") - print(f"ffmpeg output: {e.output}") raise finally: # Ensure concat file is removed diff --git a/src/clip_manager.py b/src/clip_manager.py index 846e1cb..bed28d7 100644 --- a/src/clip_manager.py +++ b/src/clip_manager.py @@ -5,10 +5,10 @@ utility functions. """ - import os import shutil import subprocess +import platform from multiprocessing import Process, Queue, Event from queue import Empty @@ -110,14 +110,38 @@ def __init__(self, logger, output_dir): self.output_dir = output_dir self.clip_queue = Queue() self.stop_event = Event() + + # Get the best codecs for this system + self.decoder, self.encoder = self.get_ffmpeg_codecs() + self.logger.info(f"Using decoder={self.decoder}, encoder={self.encoder}") + self.clip_process = Process( - target=self.create_clip_process, args=(self.clip_queue, self.stop_event) + target=self.create_clip_process, + args=(self.clip_queue, self.stop_event, self.decoder, self.encoder), ) self.clip_process.start() + self.clip_count = 0 self.bbox_count = 0 - def create_clip_process(self, queue, stop_event): + def get_ffmpeg_codecs(self): + """Return tuple of (decoder, encoder) codecs""" + try: + result = subprocess.run( + ["ffmpeg", "-hide_banner", "-encoders"], + capture_output=True, + text=True, + check=True, + ) + + # Prefer hardware accelerated encoding on MacOS + if platform.system() == "Darwin" and "h264_videotoolbox" in result.stdout: + return ("h264", "h264_videotoolbox") # Use h264 decoder with hw encoder + return ("h264", "libx264") # software fallback + except subprocess.SubprocessError: + return ("h264", "libx264") + + def create_clip_process(self, queue, stop_event, decoder, encoder): """ Create a clip process using ffmpeg. It consumes clip requests from a queue. @@ -157,10 +181,12 @@ def create_clip_process(self, queue, stop_event): str(clip_start_time), "-t", str(clip_duration), + "-c:v", + decoder, "-i", video_path, "-c:v", - "libx264", # use H.264 for video codec to maximize compatibility + encoder, "-c:a", "aac", # use Advanced Audio Coding (AAC) for audio compatibility "-pix_fmt", # use YUV planar color space with 4:2:0 chroma subsampling (QuickTime)