Compare commits
2 Commits
099d551e1d
...
e97ce026da
Author | SHA1 | Date | |
---|---|---|---|
e97ce026da | |||
cacaa5f2ac |
398
croppa/editor.py
398
croppa/editor.py
@@ -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"""
|
||||
|
@@ -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
303
croppa/rendering.py
Normal 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
|
Reference in New Issue
Block a user