Compare commits
4 Commits
b7e4fac9e7
...
0fb591d0b3
Author | SHA1 | Date | |
---|---|---|---|
0fb591d0b3 | |||
709e637e88 | |||
252cda9ad3 | |||
d29d45d4fd |
100
croppa/main.py
100
croppa/main.py
@@ -19,7 +19,7 @@ class VideoEditor:
|
||||
MAX_PLAYBACK_SPEED = 10.0
|
||||
|
||||
# Seek multiplier configuration
|
||||
SEEK_MULTIPLIER_INCREMENT = 1.0
|
||||
SEEK_MULTIPLIER_INCREMENT = 2.0
|
||||
MIN_SEEK_MULTIPLIER = 1.0
|
||||
MAX_SEEK_MULTIPLIER = 100.0
|
||||
|
||||
@@ -116,10 +116,6 @@ class VideoEditor:
|
||||
self.brightness = 0 # -100 to 100
|
||||
self.contrast = 1.0 # 0.1 to 3.0
|
||||
|
||||
# Cut points
|
||||
self.cut_start_frame = None
|
||||
self.cut_end_frame = None
|
||||
|
||||
# Marker looping state
|
||||
self.looping_between_markers = False
|
||||
|
||||
@@ -150,8 +146,11 @@ class VideoEditor:
|
||||
def _get_state_file_path(self) -> Path:
|
||||
"""Get the state file path for the current media file"""
|
||||
if not hasattr(self, 'video_path') or not self.video_path:
|
||||
print("DEBUG: No video_path available for state file")
|
||||
return None
|
||||
return self.video_path.with_suffix('.json')
|
||||
state_path = self.video_path.with_suffix('.json')
|
||||
print(f"DEBUG: State file path would be: {state_path}")
|
||||
return state_path
|
||||
|
||||
def save_state(self):
|
||||
"""Save current editor state to JSON file"""
|
||||
@@ -190,13 +189,20 @@ class VideoEditor:
|
||||
def load_state(self) -> bool:
|
||||
"""Load editor state from JSON file"""
|
||||
state_file = self._get_state_file_path()
|
||||
if not state_file or not state_file.exists():
|
||||
if not state_file:
|
||||
print("No state file path available")
|
||||
return False
|
||||
if not state_file.exists():
|
||||
print(f"State file does not exist: {state_file}")
|
||||
return False
|
||||
|
||||
print(f"Loading state from: {state_file}")
|
||||
try:
|
||||
with open(state_file, 'r') as f:
|
||||
state = json.load(f)
|
||||
|
||||
print(f"State file contents: {state}")
|
||||
|
||||
# Restore state values
|
||||
if 'current_frame' in state:
|
||||
self.current_frame = state['current_frame']
|
||||
@@ -214,8 +220,16 @@ class VideoEditor:
|
||||
self.contrast = state['contrast']
|
||||
if 'cut_start_frame' in state:
|
||||
self.cut_start_frame = state['cut_start_frame']
|
||||
print(f"Restored cut_start_frame: {self.cut_start_frame}")
|
||||
if 'cut_end_frame' in state:
|
||||
self.cut_end_frame = state['cut_end_frame']
|
||||
print(f"Restored cut_end_frame: {self.cut_end_frame}")
|
||||
|
||||
# Calculate and show marker positions on timeline
|
||||
if self.cut_start_frame is not None and self.cut_end_frame is not None:
|
||||
start_progress = self.cut_start_frame / max(1, self.total_frames - 1)
|
||||
end_progress = self.cut_end_frame / max(1, self.total_frames - 1)
|
||||
print(f"Markers will be drawn at: Start {start_progress:.4f} ({self.cut_start_frame}/{self.total_frames}), End {end_progress:.4f} ({self.cut_end_frame}/{self.total_frames})")
|
||||
if 'looping_between_markers' in state:
|
||||
self.looping_between_markers = state['looping_between_markers']
|
||||
if 'display_offset' in state:
|
||||
@@ -460,6 +474,12 @@ class VideoEditor:
|
||||
# Try to load saved state for this media file
|
||||
if self.load_state():
|
||||
print("Loaded saved state for this media file")
|
||||
if self.cut_start_frame is not None:
|
||||
print(f" Cut start frame: {self.cut_start_frame}")
|
||||
if self.cut_end_frame is not None:
|
||||
print(f" Cut end frame: {self.cut_end_frame}")
|
||||
else:
|
||||
print("No saved state found for this media file")
|
||||
|
||||
def switch_to_video(self, index: int):
|
||||
"""Switch to a specific video by index"""
|
||||
@@ -1500,7 +1520,7 @@ class VideoEditor:
|
||||
"""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!")
|
||||
print("Render already in progress! Use 'X' to cancel first.")
|
||||
return False
|
||||
|
||||
# Reset render state
|
||||
@@ -1515,10 +1535,12 @@ class VideoEditor:
|
||||
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
|
||||
|
||||
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"
|
||||
@@ -1528,6 +1550,12 @@ class VideoEditor:
|
||||
# Send progress update to main thread
|
||||
self.render_progress_queue.put(("init", "Initializing render...", 0.0, 0.0))
|
||||
|
||||
# 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():
|
||||
self.render_progress_queue.put(("error", "Could not open video for rendering!", 1.0, 0.0))
|
||||
return False
|
||||
|
||||
# Determine frame range
|
||||
start_frame = self.cut_start_frame if self.cut_start_frame is not None else 0
|
||||
end_frame = (
|
||||
@@ -1583,9 +1611,9 @@ class VideoEditor:
|
||||
self.render_progress_queue.put(("cancelled", "Render cancelled", 0.0, 0.0))
|
||||
return False
|
||||
|
||||
# Read frame
|
||||
self.cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
|
||||
ret, frame = self.cap.read()
|
||||
# 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
|
||||
@@ -1634,9 +1662,20 @@ class VideoEditor:
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.render_progress_queue.put(("error", f"Render error: {str(e)}", 1.0, 0.0))
|
||||
print(f"Render error: {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:
|
||||
# Always clean up the render VideoCapture
|
||||
if render_cap:
|
||||
render_cap.release()
|
||||
|
||||
def update_render_progress(self):
|
||||
"""Process progress updates from the render thread"""
|
||||
@@ -1792,7 +1831,8 @@ class VideoEditor:
|
||||
if len(self.video_files) > 1:
|
||||
print(" N: Next file")
|
||||
print(" n: Previous file")
|
||||
print(" Enter: Save image")
|
||||
print(" Enter: Save image (overwrites if '_edited_' in name)")
|
||||
print(" Shift+Enter: Save image as _edited_edited")
|
||||
print(" Q/ESC: Quit")
|
||||
print()
|
||||
else:
|
||||
@@ -1823,7 +1863,8 @@ class VideoEditor:
|
||||
if len(self.video_files) > 1:
|
||||
print(" N: Next video")
|
||||
print(" n: Previous video")
|
||||
print(" Enter: Render video")
|
||||
print(" Enter: Render video (overwrites if '_edited_' in name)")
|
||||
print(" Shift+Enter: Render video as _edited_edited")
|
||||
print(" X: Cancel render")
|
||||
print(" Q/ESC: Quit")
|
||||
print()
|
||||
@@ -1968,11 +2009,11 @@ class VideoEditor:
|
||||
if len(self.video_files) > 1:
|
||||
self.next_video()
|
||||
elif key == 13: # Enter
|
||||
output_name = self._get_next_edited_filename(self.video_path)
|
||||
output_path = str(self.video_path.parent / output_name)
|
||||
|
||||
# If we're overwriting the same file, use a temporary file first
|
||||
if output_name == self.video_path.name:
|
||||
# Only overwrite if file already contains "_edited_" in name
|
||||
if "_edited_" in self.video_path.stem:
|
||||
output_path = str(self.video_path)
|
||||
|
||||
# If we're overwriting the same file, use a temporary file first
|
||||
import tempfile
|
||||
temp_dir = self.video_path.parent
|
||||
temp_fd, temp_path = tempfile.mkstemp(suffix=self.video_path.suffix, dir=temp_dir)
|
||||
@@ -2006,8 +2047,23 @@ class VideoEditor:
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
else:
|
||||
# Normal case - render to new file
|
||||
success = self.render_video(output_path)
|
||||
print("Enter key only overwrites files with '_edited_' in the name. Use Shift+Enter to create new files.")
|
||||
elif key == ord("\r"): # Shift+Enter (carriage return)
|
||||
# Create _edited_edited file
|
||||
directory = self.video_path.parent
|
||||
base_name = self.video_path.stem
|
||||
extension = self.video_path.suffix
|
||||
|
||||
# Create _edited_edited filename
|
||||
if "_edited_" in base_name:
|
||||
# If already edited, create _edited_edited
|
||||
new_name = f"{base_name}_edited{extension}"
|
||||
else:
|
||||
# If not edited, create _edited_edited
|
||||
new_name = f"{base_name}_edited_edited{extension}"
|
||||
|
||||
output_path = str(directory / new_name)
|
||||
success = self.render_video(output_path)
|
||||
elif key == ord("t"):
|
||||
# Marker looping only for videos
|
||||
if not self.is_image_mode:
|
||||
|
Reference in New Issue
Block a user