Compare commits

...

4 Commits

View File

@@ -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)
# 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
if output_name == self.video_path.name:
import tempfile
temp_dir = self.video_path.parent
temp_fd, temp_path = tempfile.mkstemp(suffix=self.video_path.suffix, dir=temp_dir)
@@ -2006,7 +2047,22 @@ class VideoEditor:
if os.path.exists(temp_path):
os.remove(temp_path)
else:
# Normal case - render to new file
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