feat(main.py): add synchronous video rendering method for overwrite operations

This commit is contained in:
2025-09-08 00:32:48 +02:00
parent 0dbf82f76b
commit 1da8efc528

View File

@@ -1494,6 +1494,13 @@ class VideoEditor:
else: else:
return self._render_video_threaded(output_path) return self._render_video_threaded(output_path)
def render_video_sync(self, output_path: str):
"""Render video synchronously (for overwrite operations)"""
if self.is_image_mode:
return self._render_image(output_path)
else:
return self._render_video_sync(output_path)
def _render_video_threaded(self, output_path: str): def _render_video_threaded(self, output_path: str):
"""Start video rendering in a separate thread""" """Start video rendering in a separate thread"""
# Check if already rendering # Check if already rendering
@@ -1516,6 +1523,109 @@ class VideoEditor:
print("You can continue editing while rendering. Press 'X' to cancel.") print("You can continue editing while rendering. Press 'X' to cancel.")
return True return True
def _render_video_sync(self, output_path: str):
"""Render video synchronously (for overwrite operations)"""
render_cap = None
try:
if not output_path.endswith(".mp4"):
output_path += ".mp4"
start_time = time.time()
print("Rendering video synchronously...")
# Create a separate VideoCapture for the render thread to avoid thread safety issues
render_cap = cv2.VideoCapture(str(self.video_path))
if not render_cap.isOpened():
print("Could not open video for rendering!")
return False
# 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:
print("Invalid cut range!")
return False
# 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)
# Use mp4v codec (most compatible with MP4)
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
out = cv2.VideoWriter(
output_path, fourcc, self.fps, (output_width, output_height)
)
if not out.isOpened():
print("Could not open video writer!")
return False
# Simple sequential processing
total_output_frames = end_frame - start_frame + 1
frames_written = 0
for frame_idx in range(start_frame, end_frame + 1):
# Read frame using the separate VideoCapture
render_cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
ret, frame = render_cap.read()
if not ret:
break
# Process and write frame directly
processed_frame = self._process_frame_for_render(
frame, output_width, output_height
)
if processed_frame is not None:
out.write(processed_frame)
frames_written += 1
out.release()
# Ensure the video writer is completely closed and file handles are freed
del out
time.sleep(0.1) # Small delay to ensure file is unlocked
total_time = time.time() - start_time
avg_fps = frames_written / total_time if total_time > 0 else 0
print(f"Video rendered successfully to {output_path}")
print(f"Rendered {frames_written} frames in {total_time:.2f}s (avg {avg_fps:.1f} FPS)")
return True
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"
print(f"Render error: {error_msg}")
return False
finally:
# Always clean up the render VideoCapture
if render_cap:
render_cap.release()
def _render_video_worker(self, output_path: str): def _render_video_worker(self, output_path: str):
"""Worker method that runs in the render thread""" """Worker method that runs in the render thread"""
render_cap = None render_cap = None
@@ -2007,7 +2117,11 @@ class VideoEditor:
success = self.render_video(output_path) success = self.render_video(output_path)
elif key == 13: # Enter elif key == 13: # Enter
# Only overwrite if file already contains "_edited_" in name # Only overwrite if file already contains "_edited_" in name
print(f"DEBUG: Checking if '{self.video_path.stem}' contains '_edited_'")
if "_edited_" in self.video_path.stem: if "_edited_" in self.video_path.stem:
print("DEBUG: File contains '_edited_', proceeding with overwrite")
print(f"DEBUG: Original file path: {self.video_path}")
print(f"DEBUG: Original file exists: {self.video_path.exists()}")
output_path = str(self.video_path) output_path = str(self.video_path)
# If we're overwriting the same file, use a temporary file first # If we're overwriting the same file, use a temporary file first
@@ -2016,8 +2130,9 @@ class VideoEditor:
temp_fd, temp_path = tempfile.mkstemp(suffix=self.video_path.suffix, dir=temp_dir) temp_fd, temp_path = tempfile.mkstemp(suffix=self.video_path.suffix, dir=temp_dir)
os.close(temp_fd) # Close the file descriptor, we just need the path os.close(temp_fd) # Close the file descriptor, we just need the path
print(f"DEBUG: Created temp file: {temp_path}")
print("Rendering to temporary file first...") print("Rendering to temporary file first...")
success = self.render_video(temp_path) success = self.render_video_sync(temp_path)
if success: if success:
print("Replacing original file...") print("Replacing original file...")
@@ -2027,7 +2142,16 @@ class VideoEditor:
# Replace the original file with the temporary file # Replace the original file with the temporary file
import shutil import shutil
print(f"DEBUG: Moving {temp_path} to {self.video_path}")
try:
shutil.move(temp_path, str(self.video_path)) shutil.move(temp_path, str(self.video_path))
print("DEBUG: File move successful")
except Exception as e:
print(f"DEBUG: File move failed: {e}")
# Try to clean up temp file
if os.path.exists(temp_path):
os.remove(temp_path)
raise
# Small delay to ensure file system operations are complete # Small delay to ensure file system operations are complete
time.sleep(0.1) time.sleep(0.1)
@@ -2040,10 +2164,12 @@ class VideoEditor:
print(f"Warning: Could not reload file after overwrite: {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.") print("The file was saved successfully, but you may need to restart the editor to continue editing it.")
else: else:
print("DEBUG: Rendering failed")
# Clean up temp file if rendering failed # Clean up temp file if rendering failed
if os.path.exists(temp_path): if os.path.exists(temp_path):
os.remove(temp_path) os.remove(temp_path)
else: else:
print(f"DEBUG: File '{self.video_path.stem}' does not contain '_edited_'")
print("Enter key only overwrites files with '_edited_' in the name. Use 'n' to create new files.") print("Enter key only overwrites files with '_edited_' in the name. Use 'n' to create new files.")
elif key == ord("t"): elif key == ord("t"):
# Marker looping only for videos # Marker looping only for videos