diff --git a/croppa/main.py b/croppa/main.py index cf36c6f..4d985d1 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -6,9 +6,6 @@ import numpy as np from pathlib import Path from typing import Optional, Tuple, List import time -import threading -from concurrent.futures import ThreadPoolExecutor -from queue import Queue class VideoEditor: @@ -547,96 +544,45 @@ class VideoEditor: print("Error: Could not open video writer!") return False + # Simple sequential processing - the I/O is the bottleneck anyway total_output_frames = end_frame - start_frame + 1 - frames_written = 0 last_progress_update = 0 - # Batch processing configuration - batch_size = min(50, max(10, total_output_frames // 20)) # Adaptive batch size - frame_queue = Queue(maxsize=batch_size * 2) - processed_queue = Queue(maxsize=batch_size * 2) - - def frame_reader(): - """Background thread for reading frames""" - try: - for frame_idx in range(start_frame, end_frame + 1): - self.cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx) - ret, frame = self.cap.read() - if ret: - frame_queue.put((frame_idx, frame)) - else: - break - finally: - frame_queue.put(None) # Signal end of frames - - def frame_processor(): - """Background thread for processing frames""" - try: - while True: - item = frame_queue.get() - if item is None: # End signal - break - - frame_idx, frame = item - processed_frame = self._process_frame_for_render(frame, output_width, output_height) - - if processed_frame is not None: - processed_queue.put((frame_idx, processed_frame)) - - frame_queue.task_done() - finally: - processed_queue.put(None) # Signal end of processing - - # Start background threads - reader_thread = threading.Thread(target=frame_reader, daemon=True) - processor_thread = threading.Thread(target=frame_processor, daemon=True) - - reader_thread.start() - processor_thread.start() - - # Main thread writes frames in order - expected_frame = start_frame - frame_buffer = {} # Buffer for out-of-order frames - - while frames_written < total_output_frames: - item = processed_queue.get() - if item is None: # End signal + for frame_idx in range(start_frame, end_frame + 1): + # Read frame + self.cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx) + ret, frame = self.cap.read() + + if not ret: break - frame_idx, processed_frame = item - frame_buffer[frame_idx] = processed_frame + # Process and write frame directly (minimize memory copies) + processed_frame = self._process_frame_for_render(frame, output_width, output_height) - # Write frames in order - while expected_frame in frame_buffer: - out.write(frame_buffer.pop(expected_frame)) - frames_written += 1 - expected_frame += 1 + if processed_frame is not None: + out.write(processed_frame) - # Progress update (throttled to avoid too frequent updates) + frames_written = frame_idx - start_frame + 1 + + # Throttled progress update current_time = time.time() - if current_time - last_progress_update > 0.5: # Update every 0.5 seconds + if current_time - last_progress_update > 0.5: progress = frames_written / total_output_frames * 100 elapsed = current_time - start_time - if frames_written > 0: - eta = (elapsed / frames_written) * (total_output_frames - frames_written) - fps_rate = frames_written / elapsed - print(f"Progress: {progress:.1f}% | {frames_written}/{total_output_frames} | " - f"FPS: {fps_rate:.1f} | ETA: {eta:.1f}s\r", end="") + fps_rate = frames_written / elapsed + eta = (elapsed / frames_written) * (total_output_frames - frames_written) + print(f"Progress: {progress:.1f}% | {frames_written}/{total_output_frames} | " + f"FPS: {fps_rate:.1f} | ETA: {eta:.1f}s\r", end="") last_progress_update = current_time - - processed_queue.task_done() - - # Wait for threads to complete - reader_thread.join() - processor_thread.join() out.release() total_time = time.time() - start_time - avg_fps = frames_written / total_time if total_time > 0 else 0 + total_frames_written = end_frame - start_frame + 1 + avg_fps = total_frames_written / total_time if total_time > 0 else 0 print(f"\nVideo rendered successfully to {output_path}") - print(f"Rendered {frames_written} frames in {total_time:.2f}s (avg {avg_fps:.1f} FPS)") + print(f"Rendered {total_frames_written} frames in {total_time:.2f}s (avg {avg_fps:.1f} FPS)") return True def _process_frame_for_render(self, frame, output_width: int, output_height: int):