feat(main.py): add synchronous video rendering method for overwrite operations
This commit is contained in:
128
croppa/main.py
128
croppa/main.py
@@ -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
|
||||||
|
Reference in New Issue
Block a user