From 1da8efc528600d0a0c0a37170718e4c81ac0d2e5 Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Mon, 8 Sep 2025 00:32:48 +0200 Subject: [PATCH] feat(main.py): add synchronous video rendering method for overwrite operations --- croppa/main.py | 130 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 2 deletions(-) diff --git a/croppa/main.py b/croppa/main.py index 33f74f2..4f7bf87 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -1494,6 +1494,13 @@ class VideoEditor: else: 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): """Start video rendering in a separate thread""" # Check if already rendering @@ -1516,6 +1523,109 @@ class VideoEditor: print("You can continue editing while rendering. Press 'X' to cancel.") 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): """Worker method that runs in the render thread""" render_cap = None @@ -2007,7 +2117,11 @@ class VideoEditor: success = self.render_video(output_path) elif key == 13: # Enter # 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: + 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) # 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) 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...") - success = self.render_video(temp_path) + success = self.render_video_sync(temp_path) if success: print("Replacing original file...") @@ -2027,7 +2142,16 @@ class VideoEditor: # Replace the original file with the temporary file import shutil - shutil.move(temp_path, str(self.video_path)) + print(f"DEBUG: Moving {temp_path} to {self.video_path}") + try: + 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 time.sleep(0.1) @@ -2040,10 +2164,12 @@ class VideoEditor: 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.") else: + print("DEBUG: Rendering failed") # Clean up temp file if rendering failed if os.path.exists(temp_path): os.remove(temp_path) 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.") elif key == ord("t"): # Marker looping only for videos