From 85768f6323a30db83476dda8284bfd7935ee3911 Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Mon, 18 Aug 2025 16:48:13 +0200 Subject: [PATCH] refactor(main.py): simplify seeking logic and improve key repeat handling --- main.py | 147 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 81 insertions(+), 66 deletions(-) diff --git a/main.py b/main.py index d3c4d20..57b1318 100644 --- a/main.py +++ b/main.py @@ -13,8 +13,9 @@ class MediaGrader: # Configuration constants DEFAULT_FPS = 30 BASE_FRAME_DELAY_MS = 33 # ~30 FPS - KEY_REPEAT_THRESHOLD_SEC = 0.5 # Faster detection for repeat + KEY_REPEAT_RATE_SEC = 0.5 # How often to process key repeats (10 times per second) FAST_SEEK_ACTIVATION_TIME = 0.5 # How long to hold before fast seek + FRAME_RENDER_TIME_MS = 50 # Time to let frames render between seeks WINDOW_MAX_WIDTH = 1200 WINDOW_MAX_HEIGHT = 800 WINDOW_MAX_SCALE_UP = 2.0 @@ -40,10 +41,11 @@ class MediaGrader: self.current_frame = 0 self.total_frames = 0 - # Key repeat tracking - self.last_key_time = 0 - self.last_key = None + # Key repeat tracking with rate limiting + self.last_seek_time = 0 + self.current_seek_key = None self.key_first_press_time = 0 + self.is_seeking = False # Seeking modes self.fine_seek_frames = 1 # Frame-by-frame @@ -90,7 +92,7 @@ class MediaGrader: def is_video(self, file_path: Path) -> bool: """Check if file is a video""" - return file_path.suffix.lower() in [".mp4", ".avi", ".mov", ".mkv", ".gif"] + return file_path.suffix.lower() in self.extensions def calculate_frame_delay(self) -> int: """Calculate frame delay in milliseconds based on playback speed""" @@ -136,7 +138,7 @@ class MediaGrader: if self.is_video(self.media_files[self.current_index]): if not self.current_cap: return False - + # Read frame at current position ret, frame = self.current_cap.read() if ret: @@ -219,12 +221,12 @@ class MediaGrader: # Skip frames for high-speed playback frames_to_skip = self.calculate_frames_to_skip() - + for _ in range(frames_to_skip + 1): # +1 to advance at least one frame ret, frame = self.current_cap.read() if not ret: return False - + self.current_display_frame = frame self.current_frame = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES)) return True @@ -305,67 +307,76 @@ class MediaGrader: f"Seeking from {self.current_frame} to {target_frame} (delta: {frames_delta})" ) - # Use I-frame seeking for smoother performance - if abs(frames_delta) > 5 or self.snap_to_iframe: - self.seek_to_iframe(target_frame) - else: - # For small seeks, use direct frame seeking - self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame) - self.load_current_frame() + # Use simple direct seeking - let OpenCV handle it + self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame) + self.load_current_frame() print(f"Seeked to frame {self.current_frame}") - def handle_seeking_key(self, key: int) -> bool: - """Handle seeking keys with different granularities. Returns True if key was handled.""" + def process_seek_key(self, key: int) -> bool: + """Process seeking keys with proper rate limiting""" current_time = time.time() - - # Determine seek amount based on key and timing + + # Check if this is a seek key + seek_direction = 0 seek_amount = 0 - is_arrow_key = False - - # Try different arrow key detection methods - if key == ord("a"): # Left arrow (various systems) - is_arrow_key = True - direction = -1 - elif key == ord("d"): # Right arrow (various systems) - is_arrow_key = True - direction = 1 - elif key == ord(","): # Comma - fine seek backward + + if key == ord("a"): # Seek backward + seek_direction = -1 + elif key == ord("d"): # Seek forward + seek_direction = 1 + elif key == ord(","): # Fine seek backward seek_amount = -self.fine_seek_frames - elif key == ord("."): # Period - fine seek forward + elif key == ord("."): # Fine seek forward seek_amount = self.fine_seek_frames else: + # Not a seek key, reset seeking state + if self.current_seek_key is not None: + self.current_seek_key = None + self.is_seeking = False + print("Seek key released") return False - if is_arrow_key: - # Track key press timing for fast seek detection - if self.last_key != key: - # New key press - self.key_first_press_time = current_time - self.last_key = key - seek_amount = direction * self.coarse_seek_frames - else: - # Repeated key press - time_held = current_time - self.key_first_press_time - time_since_last = current_time - self.last_key_time - - print( - f"Key held for {time_held:.2f}s, since last: {time_since_last:.2f}s" - ) - - if time_held > self.FAST_SEEK_ACTIVATION_TIME: - # Fast seek mode - seek_amount = direction * self.fast_seek_frames - print(f"FAST SEEK: {seek_amount} frames") - else: - # Normal seek - seek_amount = direction * self.coarse_seek_frames - + # Handle fine seeking (comma/period) - always immediate if seek_amount != 0: self.seek_video(seek_amount) - self.last_key_time = current_time return True + # Handle arrow key seeking with rate limiting + if seek_direction != 0: + # Check if we should process this key press + if self.current_seek_key != key: + # New key press + self.current_seek_key = key + self.key_first_press_time = current_time + self.last_seek_time = current_time + self.is_seeking = True + + # Immediate first seek + seek_amount = seek_direction * self.coarse_seek_frames + self.seek_video(seek_amount) + print(f"Started seeking {seek_direction}") + return True + + elif self.is_seeking: + # Continuing to hold the same key + time_since_last_seek = current_time - self.last_seek_time + time_held = current_time - self.key_first_press_time + + # Only seek if enough time has passed (rate limiting) + if time_since_last_seek >= self.KEY_REPEAT_RATE_SEC: + self.last_seek_time = current_time + + # Determine seek amount based on how long key has been held + if time_held > self.FAST_SEEK_ACTIVATION_TIME: + seek_amount = seek_direction * self.fast_seek_frames + print(f"FAST SEEK: {seek_amount} frames") + else: + seek_amount = seek_direction * self.coarse_seek_frames + + self.seek_video(seek_amount) + return True + return False def grade_media(self, grade: int): @@ -423,6 +434,7 @@ class MediaGrader: print(" N: Next file") print(" P: Previous file") print(" Q/ESC: Quit") + print(f" Seek repeat rate: {1/self.KEY_REPEAT_RATE_SEC:.1f} seeks/second") cv2.namedWindow("Media Grader", cv2.WINDOW_NORMAL) @@ -443,7 +455,11 @@ class MediaGrader: # Calculate appropriate delay if self.is_video(current_file): - delay = self.calculate_frame_delay() + if self.is_seeking: + # Shorter delay when seeking to be more responsive + delay = self.FRAME_RENDER_TIME_MS + else: + delay = self.calculate_frame_delay() else: delay = self.IMAGE_DISPLAY_DELAY_MS @@ -464,14 +480,14 @@ class MediaGrader: self.playback_speed - self.SPEED_INCREMENT, ) print(f"Speed: {self.playback_speed:.1f}x") - elif key == ord("w"): + elif key == ord("s"): self.playback_speed = min( self.MAX_PLAYBACK_SPEED, self.playback_speed + self.SPEED_INCREMENT, ) print(f"Speed: {self.playback_speed:.1f}x") - elif self.handle_seeking_key(key): - # Seeking was handled and frame was updated + elif self.process_seek_key(key): + # Seeking was handled pass elif key == ord("n"): # Next file break @@ -484,13 +500,12 @@ class MediaGrader: return break elif key == 255: # No key pressed - # Reset key tracking if no key is pressed - if self.last_key is not None: - self.last_key = None - print("Key released") - - # Advance frame only if playing (and it's a video) - if self.is_playing and self.is_video(current_file): + # Continue seeking if we're in seeking mode + if self.is_seeking and self.current_seek_key is not None: + self.process_seek_key(self.current_seek_key) + + # Advance frame only if playing (and it's a video) and not seeking + if self.is_playing and self.is_video(current_file) and not self.is_seeking: if not self.advance_frame(): # End of video break