Skip to content

Commit

Permalink
Merge pull request #32 from humphrem/humphd/prefer-hardware-accel
Browse files Browse the repository at this point in the history
Prefer hardware acceleration when possible
  • Loading branch information
humphrem authored Dec 2, 2024
2 parents 5a9ea84 + 90f7e0d commit c3feeeb
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 22 deletions.
71 changes: 53 additions & 18 deletions scripts/process-videos.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import argparse
import fcntl
import subprocess
import platform
import os
import sys
from concurrent.futures import ThreadPoolExecutor
Expand Down Expand Up @@ -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]]:
Expand Down Expand Up @@ -287,23 +291,33 @@ def run_ffmpeg_with_output(self, cmd: List[str], prefix: str = "") -> None:
stderr=subprocess.PIPE,
universal_newlines=True,
bufsize=1,
env={**os.environ, "PYTHONUNBUFFERED": "1"},
)

def handle_output(stream, prefix):
for line in stream:
# Skip empty lines
if line.strip():
# Add prefix to each line for identification
print(f"{prefix}{line}", end="", flush=True)
def handle_output(stream, prefix, stream_name):
try:
while True:
line = stream.readline()
if not line:
print(f"{prefix}[{stream_name}] stream ended")
break
if line.strip():
print(f"{prefix}{line}", end="", flush=True)
except Exception as e:
print(f"{prefix}Error in {stream_name} handler: {str(e)}")

# Create threads to handle stdout and stderr
from threading import Thread

stdout_thread = Thread(
target=handle_output, args=(process.stdout, f"[{prefix}] ")
target=handle_output,
args=(process.stdout, f"[{prefix}] ", "stdout"),
daemon=True,
)
stderr_thread = Thread(
target=handle_output, args=(process.stderr, f"[{prefix}] ")
target=handle_output,
args=(process.stderr, f"[{prefix}] ", "stderr"),
daemon=True,
)

# Start threads
Expand Down Expand Up @@ -338,10 +352,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",
Expand Down Expand Up @@ -376,11 +393,11 @@ def create_settling_period_video(
)

try:
subprocess.run(cmd)
self.run_ffmpeg_with_output(cmd, "settling")
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
Expand Down Expand Up @@ -504,6 +521,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:
Expand Down Expand Up @@ -535,20 +569,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
Expand Down Expand Up @@ -577,7 +613,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",
Expand All @@ -586,15 +622,14 @@ 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
self._save_state()

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
Expand Down
34 changes: 30 additions & 4 deletions src/clip_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
utility functions.
"""


import os
import shutil
import subprocess
import platform
from multiprocessing import Process, Queue, Event
from queue import Empty

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit c3feeeb

Please sign in to comment.