feat(main.py): implement auto-repeat seeking for video editor with configurable delays and rates
This commit is contained in:
116
croppa/main.py
116
croppa/main.py
@@ -18,6 +18,13 @@ class VideoEditor:
|
||||
SPEED_INCREMENT = 0.2
|
||||
MIN_PLAYBACK_SPEED = 0.1
|
||||
MAX_PLAYBACK_SPEED = 10.0
|
||||
|
||||
# Auto-repeat seeking configuration
|
||||
INITIAL_REPEAT_DELAY = 0.5 # seconds before auto-repeat starts
|
||||
REPEAT_RATE_SLOW = 0.15 # seconds between seeks when holding key
|
||||
REPEAT_RATE_FAST = 0.05 # seconds between seeks after holding for a while
|
||||
FAST_SEEK_THRESHOLD = 2.0 # seconds before switching to fast repeat
|
||||
DISPLAY_UPDATE_THROTTLE = 0.033 # minimum time between display updates (30 FPS)
|
||||
|
||||
# Timeline configuration
|
||||
TIMELINE_HEIGHT = 60
|
||||
@@ -89,6 +96,14 @@ class VideoEditor:
|
||||
self.current_seek_key = None
|
||||
self.key_first_press_time = 0
|
||||
self.last_seek_time = 0
|
||||
|
||||
# Auto-repeat seeking state
|
||||
self.auto_repeat_active = False
|
||||
self.auto_repeat_key = None
|
||||
self.auto_repeat_direction = 0
|
||||
self.auto_repeat_shift = False
|
||||
self.auto_repeat_ctrl = False
|
||||
self.last_display_update = 0
|
||||
|
||||
# Crop settings
|
||||
self.crop_rect = None # (x, y, width, height)
|
||||
@@ -499,6 +514,74 @@ class VideoEditor:
|
||||
|
||||
self.seek_video(frames)
|
||||
|
||||
def start_auto_repeat_seek(self, key: int, direction: int, shift_pressed: bool, ctrl_pressed: bool):
|
||||
"""Start auto-repeat seeking for a held key"""
|
||||
if self.is_image_mode:
|
||||
return
|
||||
|
||||
# If the same key is already being auto-repeated, don't restart
|
||||
if (self.auto_repeat_active and
|
||||
self.auto_repeat_key == key and
|
||||
self.auto_repeat_direction == direction and
|
||||
self.auto_repeat_shift == shift_pressed and
|
||||
self.auto_repeat_ctrl == ctrl_pressed):
|
||||
return
|
||||
|
||||
self.auto_repeat_active = True
|
||||
self.auto_repeat_key = key
|
||||
self.auto_repeat_direction = direction
|
||||
self.auto_repeat_shift = shift_pressed
|
||||
self.auto_repeat_ctrl = ctrl_pressed
|
||||
self.key_first_press_time = time.time()
|
||||
self.last_seek_time = 0
|
||||
|
||||
# Do initial seek immediately
|
||||
self.seek_video_with_modifier(direction, shift_pressed, ctrl_pressed)
|
||||
|
||||
def stop_auto_repeat_seek(self):
|
||||
"""Stop auto-repeat seeking"""
|
||||
self.auto_repeat_active = False
|
||||
self.auto_repeat_key = None
|
||||
self.auto_repeat_direction = 0
|
||||
self.auto_repeat_shift = False
|
||||
self.auto_repeat_ctrl = False
|
||||
|
||||
def update_auto_repeat_seek(self):
|
||||
"""Update auto-repeat seeking if active"""
|
||||
if not self.auto_repeat_active or self.is_image_mode:
|
||||
return
|
||||
|
||||
current_time = time.time()
|
||||
time_since_first_press = current_time - self.key_first_press_time
|
||||
|
||||
# Determine repeat rate based on how long the key has been held
|
||||
if time_since_first_press < self.INITIAL_REPEAT_DELAY:
|
||||
# Still in initial delay period
|
||||
return
|
||||
elif time_since_first_press < self.FAST_SEEK_THRESHOLD:
|
||||
# Slow repeat rate
|
||||
repeat_rate = self.REPEAT_RATE_SLOW
|
||||
else:
|
||||
# Fast repeat rate
|
||||
repeat_rate = self.REPEAT_RATE_FAST
|
||||
|
||||
# Check if enough time has passed for the next seek
|
||||
if current_time - self.last_seek_time >= repeat_rate:
|
||||
self.seek_video_with_modifier(
|
||||
self.auto_repeat_direction,
|
||||
self.auto_repeat_shift,
|
||||
self.auto_repeat_ctrl
|
||||
)
|
||||
self.last_seek_time = current_time
|
||||
|
||||
def should_update_display(self) -> bool:
|
||||
"""Check if display should be updated based on throttling"""
|
||||
current_time = time.time()
|
||||
if current_time - self.last_display_update >= self.DISPLAY_UPDATE_THROTTLE:
|
||||
self.last_display_update = current_time
|
||||
return True
|
||||
return False
|
||||
|
||||
def seek_to_frame(self, frame_number: int):
|
||||
"""Seek to specific frame"""
|
||||
self.current_frame = max(0, min(frame_number, self.total_frames - 1))
|
||||
@@ -1723,11 +1806,24 @@ class VideoEditor:
|
||||
self.load_current_frame()
|
||||
|
||||
while True:
|
||||
# Only update display if needed
|
||||
self.display_current_frame()
|
||||
# Update auto-repeat seeking if active
|
||||
self.update_auto_repeat_seek()
|
||||
|
||||
# Only update display if needed and throttled
|
||||
if self.should_update_display():
|
||||
self.display_current_frame()
|
||||
|
||||
delay = self.calculate_frame_delay() if self.is_playing else 30
|
||||
key = cv2.waitKey(delay) & 0xFF
|
||||
|
||||
# Handle auto-repeat timeout - only stop if no key is pressed for a longer period
|
||||
if key == 255 and self.auto_repeat_active: # 255 means no key pressed
|
||||
# Check if enough time has passed since last key press
|
||||
if time.time() - self.last_seek_time > 0.3: # 300ms timeout (increased)
|
||||
self.stop_auto_repeat_seek()
|
||||
elif key != 255 and self.auto_repeat_active:
|
||||
# A key is pressed, update the last seek time to prevent timeout
|
||||
self.last_seek_time = time.time()
|
||||
|
||||
# Get modifier key states
|
||||
window_title = "Image Editor" if self.is_image_mode else "Video Editor"
|
||||
@@ -1736,10 +1832,12 @@ class VideoEditor:
|
||||
# We'll handle this through special key combinations
|
||||
|
||||
if key == ord("q") or key == 27: # ESC
|
||||
self.stop_auto_repeat_seek()
|
||||
break
|
||||
elif key == ord(" "):
|
||||
# Don't allow play/pause for images
|
||||
if not self.is_image_mode:
|
||||
self.stop_auto_repeat_seek() # Stop seeking when toggling play/pause
|
||||
self.is_playing = not self.is_playing
|
||||
self.save_state()
|
||||
elif key == ord("a") or key == ord("A"):
|
||||
@@ -1747,27 +1845,25 @@ class VideoEditor:
|
||||
if not self.is_image_mode:
|
||||
# Check if it's uppercase A (Shift+A)
|
||||
if key == ord("A"):
|
||||
self.seek_video_with_modifier(
|
||||
-1, True, False
|
||||
) # Shift+A: -10 frames
|
||||
self.start_auto_repeat_seek(key, -1, True, False) # Shift+A: -10 frames
|
||||
else:
|
||||
self.seek_video_with_modifier(-1, False, False) # A: -1 frame
|
||||
self.start_auto_repeat_seek(key, -1, False, False) # A: -1 frame
|
||||
elif key == ord("d") or key == ord("D"):
|
||||
# Seeking only for videos
|
||||
if not self.is_image_mode:
|
||||
# Check if it's uppercase D (Shift+D)
|
||||
if key == ord("D"):
|
||||
self.seek_video_with_modifier(1, True, False) # Shift+D: +10 frames
|
||||
self.start_auto_repeat_seek(key, 1, True, False) # Shift+D: +10 frames
|
||||
else:
|
||||
self.seek_video_with_modifier(1, False, False) # D: +1 frame
|
||||
self.start_auto_repeat_seek(key, 1, False, False) # D: +1 frame
|
||||
elif key == 1: # Ctrl+A
|
||||
# Seeking only for videos
|
||||
if not self.is_image_mode:
|
||||
self.seek_video_with_modifier(-1, False, True) # Ctrl+A: -60 frames
|
||||
self.start_auto_repeat_seek(key, -1, False, True) # Ctrl+A: -60 frames
|
||||
elif key == 4: # Ctrl+D
|
||||
# Seeking only for videos
|
||||
if not self.is_image_mode:
|
||||
self.seek_video_with_modifier(1, False, True) # Ctrl+D: +60 frames
|
||||
self.start_auto_repeat_seek(key, 1, False, True) # Ctrl+D: +60 frames
|
||||
elif key == ord("-") or key == ord("_"):
|
||||
self.rotate_clockwise()
|
||||
print(f"Rotated to {self.rotation_angle}°")
|
||||
|
Reference in New Issue
Block a user