Refactor video rendering functionality in VideoEditor and introduce rendering module

This commit refactors the video rendering process in the VideoEditor class by delegating rendering tasks to a new rendering module. The _render_video_threaded and _render_video_worker methods are replaced with start_render_thread and pump_progress functions, enhancing code organization and maintainability. Additionally, the pyproject.toml file is updated to include the new rendering module, ensuring proper module recognition. This restructuring improves the overall clarity and efficiency of the video rendering workflow.
This commit is contained in:
2025-09-16 17:38:39 +02:00
parent 099d551e1d
commit cacaa5f2ac
3 changed files with 325 additions and 136 deletions

View File

@@ -14,6 +14,13 @@ from croppa.capture import Cv2BufferedCap
from croppa.tracking import MotionTracker
from croppa.utils import get_active_window_title
from croppa.project_view import ProjectView
from croppa.rendering import (
start_render_thread,
pump_progress,
request_cancel,
is_rendering as rendering_is_rendering,
cleanup_thread,
)
class VideoEditor:
@@ -2313,145 +2320,13 @@ class VideoEditor:
return self._render_video_threaded(output_path)
def _render_video_threaded(self, output_path: str):
"""Start video rendering in a separate thread"""
# Check if already rendering
if self.render_thread and self.render_thread.is_alive():
print("Render already in progress! Use 'X' to cancel first.")
return False
# Reset render state
self.render_cancelled = False
# Start render thread
self.render_thread = threading.Thread(
target=self._render_video_worker, args=(output_path,), daemon=True
)
self.render_thread.start()
print(f"Started rendering to {output_path} in background thread...")
print("You can continue editing while rendering. Press 'X' to cancel.")
return True
return start_render_thread(self, output_path)
def _render_video_worker(self, output_path: str):
"""Worker method that runs in the render thread"""
render_cap = None
try:
if not output_path.endswith(".mp4"):
output_path += ".mp4"
start_time = time.time()
# Send progress update to main thread
self.render_progress_queue.put(("init", "Initializing render...", 0.0, 0.0))
# No need to create VideoCapture since we use FFmpeg directly
# Determine frame range
start_frame = (
self.cut_start_frame if self.cut_start_frame is not None else 0
)
end_frame = (
self.cut_end_frame
if self.cut_end_frame is not None
else self.total_frames - 1
)
if start_frame >= end_frame:
self.render_progress_queue.put(
("error", "Invalid cut range!", 1.0, 0.0)
)
return False
# Send progress update
self.render_progress_queue.put(
("progress", "Calculating output dimensions...", 0.05, 0.0)
)
# Calculate output dimensions (accounting for rotation)
if self.crop_rect:
crop_width = int(self.crop_rect[2])
crop_height = int(self.crop_rect[3])
else:
crop_width = self.frame_width
crop_height = self.frame_height
# Swap dimensions if rotation is 90 or 270 degrees
if self.rotation_angle == 90 or self.rotation_angle == 270:
output_width = int(crop_height * self.zoom_factor)
output_height = int(crop_width * self.zoom_factor)
else:
output_width = int(crop_width * self.zoom_factor)
output_height = int(crop_height * self.zoom_factor)
# Ensure dimensions are divisible by 2 for H.264 encoding
output_width = output_width - (output_width % 2)
output_height = output_height - (output_height % 2)
# Send progress update
self.render_progress_queue.put(
("progress", "Setting up FFmpeg encoder...", 0.1, 0.0)
)
# Debug output dimensions
print(f"Output dimensions: {output_width}x{output_height}")
print(f"Zoom factor: {self.zoom_factor}")
print(f"Crop dimensions: {crop_width}x{crop_height}")
# Skip all the OpenCV codec bullshit and go straight to FFmpeg
print("Using FFmpeg for encoding with OpenCV transformations...")
return self._render_with_ffmpeg_pipe(
output_path, start_frame, end_frame, output_width, output_height
)
except Exception as e:
error_msg = str(e)
# Handle specific FFmpeg threading errors
if "async_lock" in error_msg or "pthread_frame" in error_msg:
error_msg = "FFmpeg threading error - try restarting the application"
elif "Assertion" in error_msg:
error_msg = "Video codec error - the video file may be corrupted or incompatible"
self.render_progress_queue.put(
("error", f"Render error: {error_msg}", 1.0, 0.0)
)
print(f"Render error: {error_msg}")
return False
finally:
# No cleanup needed since we don't create VideoCapture
pass
raise NotImplementedError
def update_render_progress(self):
"""Process progress updates from the render thread"""
try:
while True:
# Non-blocking get from queue
update_type, text, progress, fps = (
self.render_progress_queue.get_nowait()
)
if update_type == "init":
self.show_progress_bar(text)
elif update_type == "progress":
self.update_progress_bar(progress, text, fps)
elif update_type == "complete":
self.update_progress_bar(progress, text, fps)
# Handle file overwrite if this was an overwrite operation
if (
hasattr(self, "overwrite_temp_path")
and self.overwrite_temp_path
):
self._handle_overwrite_completion()
elif update_type == "error":
self.update_progress_bar(progress, text, fps)
# Also show error as feedback message for better visibility
self.show_feedback_message(f"ERROR: {text}")
elif update_type == "cancelled":
self.hide_progress_bar()
self.show_feedback_message("Render cancelled")
except queue.Empty:
# No more updates in queue
pass
pump_progress(self)
def _handle_overwrite_completion(self):
"""Handle file replacement after successful render"""