Compare commits

...

2 Commits

Author SHA1 Message Date
e97ce026da Enhance VideoEditor with tracking-adjusted cropping and improved transformation logic
This commit introduces a new method, _get_tracking_adjusted_crop, to calculate crop rectangles that center on tracked points when motion tracking is enabled. The transform_video_to_screen and transform_screen_to_video methods are refactored to utilize this new method, streamlining the handling of crop, rotation, and zoom transformations. These changes improve the accuracy and efficiency of video rendering and editing processes, enhancing the overall user experience.
2025-09-16 17:49:50 +02:00
cacaa5f2ac 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.
2025-09-16 17:38:39 +02:00
3 changed files with 415 additions and 299 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:
@@ -1757,6 +1764,28 @@ class VideoEditor:
self.feedback_message = message
self.feedback_message_time = time.time()
def _get_tracking_adjusted_crop(self) -> Tuple[int, int, int, int]:
"""Return crop rect adjusted to center on tracked point when enabled, clamped to frame."""
if self.crop_rect:
base_x, base_y, w, h = map(int, self.crop_rect)
else:
base_x, base_y, w, h = 0, 0, int(self.frame_width), int(self.frame_height)
x, y = base_x, base_y
if self.motion_tracker.tracking_enabled:
pos = self.motion_tracker.get_interpolated_position(self.current_frame)
if pos:
tx, ty = pos
x = int(tx - w // 2)
y = int(ty - h // 2)
# clamp
x = max(0, min(x, int(self.frame_width) - 1))
y = max(0, min(y, int(self.frame_height) - 1))
w = min(int(w), int(self.frame_width) - x)
h = min(int(h), int(self.frame_height) - y)
return (x, y, w, h)
def transform_video_to_screen(
self, video_x: int, video_y: int
) -> Optional[Tuple[int, int]]:
@@ -1764,93 +1793,47 @@ class VideoEditor:
if self.current_display_frame is None:
return None
# Get the original frame dimensions
original_height, original_width = self.current_display_frame.shape[:2]
available_height = self.window_height - (
0 if self.is_image_mode else self.TIMELINE_HEIGHT
)
angle = int(self.rotation_angle) % 360
zoom = float(self.zoom_factor)
crop_x, crop_y, crop_w, crop_h = self._get_tracking_adjusted_crop()
# Step 1: Apply crop (subtract crop offset, including motion tracking offset)
display_x = video_x
display_y = video_y
if self.crop_rect:
crop_x, crop_y, crop_w, crop_h = self.crop_rect
# translate to crop
display_x = float(video_x - crop_x)
display_y = float(video_y - crop_y)
# Apply motion tracking offset if enabled
if self.motion_tracker.tracking_enabled:
current_pos = self.motion_tracker.get_interpolated_position(
self.current_frame
)
if current_pos:
# Move crop center to tracked point (same logic as in apply_crop_zoom_and_rotation)
tracked_x, tracked_y = current_pos
new_x = int(tracked_x - crop_w // 2)
new_y = int(tracked_y - crop_h // 2)
crop_x, crop_y = new_x, new_y
display_x -= crop_x
display_y -= crop_y
# Step 2: Apply rotation
if self.rotation_angle != 0:
if self.crop_rect:
crop_w, crop_h = int(self.crop_rect[2]), int(self.crop_rect[3])
else:
crop_w, crop_h = original_width, original_height
if self.rotation_angle == 90:
# 90° clockwise rotation: (x,y) -> (y, crop_w-x)
new_x = display_y
new_y = crop_w - display_x
elif self.rotation_angle == 180:
# 180° rotation: (x,y) -> (crop_w-x, crop_h-y)
new_x = crop_w - display_x
new_y = crop_h - display_y
elif self.rotation_angle == 270:
# 270° clockwise rotation: (x,y) -> (crop_h-y, x)
new_x = crop_h - display_y
new_y = display_x
else:
new_x, new_y = display_x, display_y
display_x, display_y = new_x, new_y
# Step 3: Apply zoom
if self.zoom_factor != 1.0:
display_x *= self.zoom_factor
display_y *= self.zoom_factor
# Step 4: Apply display offset (panning when zoomed)
display_x += self.display_offset[0]
display_y += self.display_offset[1]
# Step 5: Scale to fit window
if self.crop_rect:
crop_w, crop_h = int(self.crop_rect[2]), int(self.crop_rect[3])
# rotate within crop space
if angle == 90:
new_x = display_y
new_y = float(crop_w) - display_x
elif angle == 180:
new_x = float(crop_w) - display_x
new_y = float(crop_h) - display_y
elif angle == 270:
new_x = float(crop_h) - display_y
new_y = display_x
else:
crop_w, crop_h = original_width, original_height
new_x, new_y = display_x, display_y
display_x, display_y = new_x, new_y
# Apply zoom factor to dimensions
if self.zoom_factor != 1.0:
crop_w = int(crop_w * self.zoom_factor)
crop_h = int(crop_h * self.zoom_factor)
# zoom and pan
display_x *= zoom
display_y *= zoom
display_x += float(self.display_offset[0])
display_y += float(self.display_offset[1])
# Calculate scale to fit window
scale_x = self.window_width / crop_w
scale_y = available_height / crop_h
scale = min(scale_x, scale_y)
# fit to window
available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT)
rot_w = float(crop_h) if angle in (90, 270) else float(crop_w)
rot_h = float(crop_w) if angle in (90, 270) else float(crop_h)
rot_w *= zoom
rot_h *= zoom
scale = min(self.window_width / max(1.0, rot_w), available_height / max(1.0, rot_h))
start_x = (self.window_width - rot_w * scale) / 2.0
start_y = (available_height - rot_h * scale) / 2.0
# Center the scaled content
scaled_w = crop_w * scale
scaled_h = crop_h * scale
start_x = (self.window_width - scaled_w) // 2
start_y = (available_height - scaled_h) // 2
# Apply final scaling and centering
screen_x = start_x + display_x * scale
screen_y = start_y + display_y * scale
return (int(screen_x), int(screen_y))
return (int(round(screen_x)), int(round(screen_y)))
def transform_screen_to_video(
self, screen_x: int, screen_y: int
@@ -1859,93 +1842,44 @@ class VideoEditor:
if self.current_display_frame is None:
return None
# Get the original frame dimensions
original_height, original_width = self.current_display_frame.shape[:2]
available_height = self.window_height - (
0 if self.is_image_mode else self.TIMELINE_HEIGHT
)
angle = int(self.rotation_angle) % 360
zoom = float(self.zoom_factor)
crop_x, crop_y, crop_w, crop_h = self._get_tracking_adjusted_crop()
available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT)
rot_w = float(crop_h) if angle in (90, 270) else float(crop_w)
rot_h = float(crop_w) if angle in (90, 270) else float(crop_h)
rot_w *= zoom
rot_h *= zoom
scale = min(self.window_width / max(1.0, rot_w), available_height / max(1.0, rot_h))
start_x = (self.window_width - rot_w * scale) / 2.0
start_y = (available_height - rot_h * scale) / 2.0
# Step 1: Reverse scaling and centering
if self.crop_rect:
crop_w, crop_h = int(self.crop_rect[2]), int(self.crop_rect[3])
# reverse fit
display_x = (float(screen_x) - start_x) / scale
display_y = (float(screen_y) - start_y) / scale
# reverse pan
display_x -= float(self.display_offset[0])
display_y -= float(self.display_offset[1])
# reverse zoom
if zoom != 1.0:
display_x /= zoom
display_y /= zoom
# reverse rotation
if angle == 90:
new_x = float(crop_w) - display_y
new_y = display_x
elif angle == 180:
new_x = float(crop_w) - display_x
new_y = float(crop_h) - display_y
elif angle == 270:
new_x = display_y
new_y = float(crop_h) - display_x
else:
crop_w, crop_h = original_width, original_height
new_x, new_y = display_x, display_y
# Apply zoom factor to dimensions
if self.zoom_factor != 1.0:
crop_w = int(crop_w * self.zoom_factor)
crop_h = int(crop_h * self.zoom_factor)
# Calculate scale to fit window
scale_x = self.window_width / crop_w
scale_y = available_height / crop_h
scale = min(scale_x, scale_y)
# Center the scaled content
scaled_w = crop_w * scale
scaled_h = crop_h * scale
start_x = (self.window_width - scaled_w) // 2
start_y = (available_height - scaled_h) // 2
# Reverse scaling and centering
display_x = (screen_x - start_x) / scale
display_y = (screen_y - start_y) / scale
# Step 2: Reverse display offset (panning when zoomed)
display_x -= self.display_offset[0]
display_y -= self.display_offset[1]
# Step 3: Reverse zoom
if self.zoom_factor != 1.0:
display_x /= self.zoom_factor
display_y /= self.zoom_factor
# Step 4: Reverse rotation
if self.rotation_angle != 0:
if self.crop_rect:
crop_w, crop_h = int(self.crop_rect[2]), int(self.crop_rect[3])
else:
crop_w, crop_h = original_width, original_height
if self.rotation_angle == 90:
# Reverse 90° clockwise rotation: (y, crop_w-x) -> (x,y)
new_x = crop_w - display_y
new_y = display_x
elif self.rotation_angle == 180:
# Reverse 180° rotation: (crop_w-x, crop_h-y) -> (x,y)
new_x = crop_w - display_x
new_y = crop_h - display_y
elif self.rotation_angle == 270:
# Reverse 270° clockwise rotation: (crop_h-y, x) -> (x,y)
new_x = display_y
new_y = crop_h - display_x
else:
new_x, new_y = display_x, display_y
display_x, display_y = new_x, new_y
# Step 5: Reverse crop (add crop offset, including motion tracking offset)
video_x = display_x
video_y = display_y
if self.crop_rect:
crop_x, crop_y, crop_w, crop_h = self.crop_rect
# Apply motion tracking offset if enabled
if self.motion_tracker.tracking_enabled:
current_pos = self.motion_tracker.get_interpolated_position(
self.current_frame
)
if current_pos:
# Move crop center to tracked point (same logic as in apply_crop_zoom_and_rotation)
tracked_x, tracked_y = current_pos
new_x = int(tracked_x - crop_w // 2)
new_y = int(tracked_y - crop_h // 2)
crop_x, crop_y = new_x, new_y
video_x += crop_x
video_y += crop_y
return (int(video_x), int(video_y))
video_x = new_x + float(crop_x)
video_y = new_y + float(crop_y)
return (int(round(video_x)), int(round(video_y)))
def transform_point_for_display(
self, video_x: int, video_y: int
@@ -2313,145 +2247,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"""

View File

@@ -9,5 +9,16 @@ dependencies = [
"numpy>=1.24.0"
]
[tool.setuptools]
py-modules = [
"main",
"editor",
"capture",
"tracking",
"utils",
"project_view",
"rendering"
]
[project.scripts]
croppa = "croppa.main:main"
croppa = "main:main"

303
croppa/rendering.py Normal file
View File

@@ -0,0 +1,303 @@
import os
import time
import subprocess
import threading
import queue
import tempfile
from typing import Optional
import cv2
def start_render_thread(editor, output_path: str) -> bool:
if editor.render_thread and editor.render_thread.is_alive():
print("Render already in progress! Use 'X' to cancel first.")
return False
editor.render_cancelled = False
editor.render_thread = threading.Thread(target=_render_worker, args=(editor, output_path), daemon=True)
editor.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
def _render_worker(editor, output_path: str) -> bool:
try:
if not output_path.endswith(".mp4"):
output_path += ".mp4"
start_frame = editor.cut_start_frame if editor.cut_start_frame is not None else 0
end_frame = editor.cut_end_frame if editor.cut_end_frame is not None else editor.total_frames - 1
if start_frame >= end_frame:
editor.render_progress_queue.put(("error", "Invalid cut range!", 1.0, 0.0))
return False
editor.render_progress_queue.put(("progress", "Calculating output dimensions...", 0.05, 0.0))
if editor.crop_rect:
crop_width = int(editor.crop_rect[2])
crop_height = int(editor.crop_rect[3])
else:
crop_width = editor.frame_width
crop_height = editor.frame_height
if editor.rotation_angle in (90, 270):
output_width = int(crop_height * editor.zoom_factor)
output_height = int(crop_width * editor.zoom_factor)
else:
output_width = int(crop_width * editor.zoom_factor)
output_height = int(crop_height * editor.zoom_factor)
output_width -= output_width % 2
output_height -= output_height % 2
editor.render_progress_queue.put(("progress", "Setting up FFmpeg encoder...", 0.1, 0.0))
print(f"Output dimensions: {output_width}x{output_height}")
print(f"Zoom factor: {editor.zoom_factor}")
print(f"Crop dimensions: {crop_width}x{crop_height}")
print("Using FFmpeg for encoding with OpenCV transformations...")
return _render_with_ffmpeg_pipe(editor, output_path, start_frame, end_frame, output_width, output_height)
except Exception as e:
error_msg = str(e)
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"
editor.render_progress_queue.put(("error", f"Render error: {error_msg}", 1.0, 0.0))
print(f"Render error: {error_msg}")
return False
def pump_progress(editor):
try:
while True:
update_type, text, progress, fps = editor.render_progress_queue.get_nowait()
if update_type == "init":
editor.show_progress_bar(text)
elif update_type == "progress":
editor.update_progress_bar(progress, text, fps)
elif update_type == "complete":
editor.update_progress_bar(progress, text, fps)
if hasattr(editor, "overwrite_temp_path") and editor.overwrite_temp_path:
_handle_overwrite_completion(editor)
elif update_type == "error":
editor.update_progress_bar(progress, text, fps)
editor.show_feedback_message(f"ERROR: {text}")
elif update_type == "cancelled":
editor.hide_progress_bar()
editor.show_feedback_message("Render cancelled")
except queue.Empty:
pass
def _handle_overwrite_completion(editor):
try:
print("Replacing original file...")
if hasattr(editor, "cap") and editor.cap:
editor.cap.release()
import shutil
print(f"DEBUG: Moving {editor.overwrite_temp_path} to {editor.overwrite_target_path}")
try:
shutil.move(editor.overwrite_temp_path, editor.overwrite_target_path)
print("DEBUG: File move successful")
except Exception as e:
print(f"DEBUG: File move failed: {e}")
if os.path.exists(editor.overwrite_temp_path):
os.remove(editor.overwrite_temp_path)
raise
time.sleep(0.1)
try:
editor._load_video(editor.video_path)
editor.load_current_frame()
print("File reloaded successfully")
except Exception as e:
print(f"Warning: Could not reload file after overwrite: {e}")
print("The file was saved successfully, but you may need to restart the editor to continue editing it.")
except Exception as e:
print(f"Error during file overwrite: {e}")
finally:
editor.overwrite_temp_path = None
editor.overwrite_target_path = None
def request_cancel(editor) -> bool:
if editor.render_thread and editor.render_thread.is_alive():
editor.render_cancelled = True
print("Render cancellation requested...")
return True
return False
def is_rendering(editor) -> bool:
return editor.render_thread and editor.render_thread.is_alive()
def cleanup_thread(editor):
if editor.render_thread and editor.render_thread.is_alive():
editor.render_cancelled = True
if editor.ffmpeg_process:
try:
editor.ffmpeg_process.terminate()
editor.ffmpeg_process.wait(timeout=1.0)
except Exception:
try:
editor.ffmpeg_process.kill()
except Exception:
pass
editor.ffmpeg_process = None
editor.render_thread.join(timeout=2.0)
if editor.render_thread.is_alive():
print("Warning: Render thread did not finish gracefully")
editor.render_thread = None
editor.render_cancelled = False
def _process_frame_for_render(editor, frame, output_width: int, output_height: int, frame_number: Optional[int] = None):
try:
if editor.crop_rect:
x, y, w, h = map(int, editor.crop_rect)
if editor.motion_tracker.tracking_enabled and frame_number is not None:
current_pos = editor.motion_tracker.get_interpolated_position(frame_number)
if current_pos:
tracked_x, tracked_y = current_pos
new_x = int(tracked_x - w // 2)
new_y = int(tracked_y - h // 2)
x, y = new_x, new_y
h_frame, w_frame = frame.shape[:2]
x = max(0, min(x, w_frame - 1))
y = max(0, min(y, h_frame - 1))
w = min(w, w_frame - x)
h = min(h, h_frame - y)
if w > 0 and h > 0:
frame = frame[y:y + h, x:x + w]
else:
return None
frame = editor.apply_brightness_contrast(frame)
if editor.rotation_angle != 0:
frame = editor.apply_rotation(frame)
if editor.zoom_factor != 1.0:
height, width = frame.shape[:2]
zoomed_width = int(width * editor.zoom_factor)
zoomed_height = int(height * editor.zoom_factor)
if zoomed_width == output_width and zoomed_height == output_height:
frame = cv2.resize(frame, (zoomed_width, zoomed_height), interpolation=cv2.INTER_LINEAR)
else:
frame = cv2.resize(frame, (output_width, output_height), interpolation=cv2.INTER_LINEAR)
else:
if frame.shape[1] != output_width or frame.shape[0] != output_height:
frame = cv2.resize(frame, (output_width, output_height), interpolation=cv2.INTER_LINEAR)
return frame
except Exception as e:
print(f"Error processing frame: {e}")
return None
def _render_with_ffmpeg_pipe(editor, output_path: str, start_frame: int, end_frame: int, output_width: int, output_height: int):
try:
try:
test_result = subprocess.run(["ffmpeg", "-version"], capture_output=True, text=True, timeout=10)
if test_result.returncode != 0:
print(f"FFmpeg test failed with return code {test_result.returncode}")
print(f"FFmpeg stderr: {test_result.stderr}")
editor.render_progress_queue.put(("error", "FFmpeg is not working properly", 1.0, 0.0))
return False
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired) as e:
error_msg = f"FFmpeg not found or not working: {e}"
print(error_msg)
editor.render_progress_queue.put(("error", error_msg, 1.0, 0.0))
return False
editor.render_progress_queue.put(("progress", "Starting encoder...", 0.0, 0.0))
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.raw')
temp_file.close()
ffmpeg_cmd = [
'ffmpeg', '-y',
'-f', 'rawvideo',
'-s', f'{output_width}x{output_height}',
'-pix_fmt', 'bgr24',
'-r', str(editor.fps),
'-i', temp_file.name,
'-c:v', 'libx264',
'-preset', 'fast',
'-crf', '18',
'-pix_fmt', 'yuv420p',
output_path
]
editor.temp_file_name = temp_file.name
render_cap = cv2.VideoCapture(str(editor.video_path))
render_cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
total_frames = end_frame - start_frame + 1
frames_written = 0
start_time = time.time()
last_progress_update = 0
editor.render_progress_queue.put(("progress", f"Processing {total_frames} frames...", 0.1, 0.0))
with open(editor.temp_file_name, 'wb') as tf:
for i in range(total_frames):
if editor.render_cancelled:
render_cap.release()
editor.render_progress_queue.put(("cancelled", "Render cancelled", 0.0, 0.0))
return False
ret, frame = render_cap.read()
if not ret:
break
processed = _process_frame_for_render(editor, frame, output_width, output_height, start_frame + i)
if processed is not None:
if i == 0:
print(f"Processed frame dimensions: {processed.shape[1]}x{processed.shape[0]}")
print(f"Expected dimensions: {output_width}x{output_height}")
tf.write(processed.tobytes())
frames_written += 1
current_time = time.time()
progress = 0.1 + (0.8 * (i + 1) / total_frames)
if current_time - last_progress_update > 0.5:
elapsed = current_time - start_time
fps_rate = frames_written / elapsed if elapsed > 0 else 0
editor.render_progress_queue.put(("progress", f"Processed {i+1}/{total_frames} frames", progress, fps_rate))
last_progress_update = current_time
render_cap.release()
editor.render_progress_queue.put(("progress", "Encoding...", 0.9, 0.0))
result = subprocess.run(
ffmpeg_cmd,
capture_output=True,
text=True,
timeout=300,
creationflags=(subprocess.CREATE_NO_WINDOW if hasattr(subprocess, 'CREATE_NO_WINDOW') else 0)
)
if os.path.exists(editor.temp_file_name):
try:
os.unlink(editor.temp_file_name)
except OSError:
pass
if result.returncode == 0:
total_time = time.time() - start_time
avg_fps = frames_written / total_time if total_time > 0 else 0
editor.render_progress_queue.put(("complete", f"Rendered {frames_written} frames", 1.0, avg_fps))
print(f"Successfully rendered {frames_written} frames (avg {avg_fps:.1f} FPS)")
return True
else:
error_details = result.stderr if result.stderr else "No error details available"
print(f"Encoding failed with return code {result.returncode}")
print(f"Error: {error_details}")
editor.render_progress_queue.put(("error", f"Encoding failed: {error_details}", 1.0, 0.0))
return False
except Exception as e:
error_msg = str(e)
print(f"Rendering exception: {error_msg}")
print(f"Exception type: {type(e).__name__}")
if "Errno 22" in error_msg or "invalid argument" in error_msg.lower():
error_msg = "File system error - try using a different output path"
elif "BrokenPipeError" in error_msg:
error_msg = "Process terminated unexpectedly"
elif "FileNotFoundError" in error_msg or "ffmpeg" in error_msg.lower():
error_msg = "FFmpeg not found - please install FFmpeg and ensure it's in your PATH"
editor.render_progress_queue.put(("error", f"Rendering failed: {error_msg}", 1.0, 0.0))
return False