refactor(main.py): simplify seeking logic and improve key repeat handling
This commit is contained in:
147
main.py
147
main.py
@@ -13,8 +13,9 @@ class MediaGrader:
|
|||||||
# Configuration constants
|
# Configuration constants
|
||||||
DEFAULT_FPS = 30
|
DEFAULT_FPS = 30
|
||||||
BASE_FRAME_DELAY_MS = 33 # ~30 FPS
|
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
|
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_WIDTH = 1200
|
||||||
WINDOW_MAX_HEIGHT = 800
|
WINDOW_MAX_HEIGHT = 800
|
||||||
WINDOW_MAX_SCALE_UP = 2.0
|
WINDOW_MAX_SCALE_UP = 2.0
|
||||||
@@ -40,10 +41,11 @@ class MediaGrader:
|
|||||||
self.current_frame = 0
|
self.current_frame = 0
|
||||||
self.total_frames = 0
|
self.total_frames = 0
|
||||||
|
|
||||||
# Key repeat tracking
|
# Key repeat tracking with rate limiting
|
||||||
self.last_key_time = 0
|
self.last_seek_time = 0
|
||||||
self.last_key = None
|
self.current_seek_key = None
|
||||||
self.key_first_press_time = 0
|
self.key_first_press_time = 0
|
||||||
|
self.is_seeking = False
|
||||||
|
|
||||||
# Seeking modes
|
# Seeking modes
|
||||||
self.fine_seek_frames = 1 # Frame-by-frame
|
self.fine_seek_frames = 1 # Frame-by-frame
|
||||||
@@ -90,7 +92,7 @@ class MediaGrader:
|
|||||||
|
|
||||||
def is_video(self, file_path: Path) -> bool:
|
def is_video(self, file_path: Path) -> bool:
|
||||||
"""Check if file is a video"""
|
"""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:
|
def calculate_frame_delay(self) -> int:
|
||||||
"""Calculate frame delay in milliseconds based on playback speed"""
|
"""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 self.is_video(self.media_files[self.current_index]):
|
||||||
if not self.current_cap:
|
if not self.current_cap:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Read frame at current position
|
# Read frame at current position
|
||||||
ret, frame = self.current_cap.read()
|
ret, frame = self.current_cap.read()
|
||||||
if ret:
|
if ret:
|
||||||
@@ -219,12 +221,12 @@ class MediaGrader:
|
|||||||
|
|
||||||
# Skip frames for high-speed playback
|
# Skip frames for high-speed playback
|
||||||
frames_to_skip = self.calculate_frames_to_skip()
|
frames_to_skip = self.calculate_frames_to_skip()
|
||||||
|
|
||||||
for _ in range(frames_to_skip + 1): # +1 to advance at least one frame
|
for _ in range(frames_to_skip + 1): # +1 to advance at least one frame
|
||||||
ret, frame = self.current_cap.read()
|
ret, frame = self.current_cap.read()
|
||||||
if not ret:
|
if not ret:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.current_display_frame = frame
|
self.current_display_frame = frame
|
||||||
self.current_frame = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
|
self.current_frame = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
|
||||||
return True
|
return True
|
||||||
@@ -305,67 +307,76 @@ class MediaGrader:
|
|||||||
f"Seeking from {self.current_frame} to {target_frame} (delta: {frames_delta})"
|
f"Seeking from {self.current_frame} to {target_frame} (delta: {frames_delta})"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use I-frame seeking for smoother performance
|
# Use simple direct seeking - let OpenCV handle it
|
||||||
if abs(frames_delta) > 5 or self.snap_to_iframe:
|
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
|
||||||
self.seek_to_iframe(target_frame)
|
self.load_current_frame()
|
||||||
else:
|
|
||||||
# For small seeks, use direct frame seeking
|
|
||||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
|
|
||||||
self.load_current_frame()
|
|
||||||
|
|
||||||
print(f"Seeked to frame {self.current_frame}")
|
print(f"Seeked to frame {self.current_frame}")
|
||||||
|
|
||||||
def handle_seeking_key(self, key: int) -> bool:
|
def process_seek_key(self, key: int) -> bool:
|
||||||
"""Handle seeking keys with different granularities. Returns True if key was handled."""
|
"""Process seeking keys with proper rate limiting"""
|
||||||
current_time = time.time()
|
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
|
seek_amount = 0
|
||||||
is_arrow_key = False
|
|
||||||
|
if key == ord("a"): # Seek backward
|
||||||
# Try different arrow key detection methods
|
seek_direction = -1
|
||||||
if key == ord("a"): # Left arrow (various systems)
|
elif key == ord("d"): # Seek forward
|
||||||
is_arrow_key = True
|
seek_direction = 1
|
||||||
direction = -1
|
elif key == ord(","): # Fine seek backward
|
||||||
elif key == ord("d"): # Right arrow (various systems)
|
|
||||||
is_arrow_key = True
|
|
||||||
direction = 1
|
|
||||||
elif key == ord(","): # Comma - fine seek backward
|
|
||||||
seek_amount = -self.fine_seek_frames
|
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
|
seek_amount = self.fine_seek_frames
|
||||||
else:
|
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
|
return False
|
||||||
|
|
||||||
if is_arrow_key:
|
# Handle fine seeking (comma/period) - always immediate
|
||||||
# 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
|
|
||||||
|
|
||||||
if seek_amount != 0:
|
if seek_amount != 0:
|
||||||
self.seek_video(seek_amount)
|
self.seek_video(seek_amount)
|
||||||
self.last_key_time = current_time
|
|
||||||
return True
|
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
|
return False
|
||||||
|
|
||||||
def grade_media(self, grade: int):
|
def grade_media(self, grade: int):
|
||||||
@@ -423,6 +434,7 @@ class MediaGrader:
|
|||||||
print(" N: Next file")
|
print(" N: Next file")
|
||||||
print(" P: Previous file")
|
print(" P: Previous file")
|
||||||
print(" Q/ESC: Quit")
|
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)
|
cv2.namedWindow("Media Grader", cv2.WINDOW_NORMAL)
|
||||||
|
|
||||||
@@ -443,7 +455,11 @@ class MediaGrader:
|
|||||||
|
|
||||||
# Calculate appropriate delay
|
# Calculate appropriate delay
|
||||||
if self.is_video(current_file):
|
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:
|
else:
|
||||||
delay = self.IMAGE_DISPLAY_DELAY_MS
|
delay = self.IMAGE_DISPLAY_DELAY_MS
|
||||||
|
|
||||||
@@ -464,14 +480,14 @@ class MediaGrader:
|
|||||||
self.playback_speed - self.SPEED_INCREMENT,
|
self.playback_speed - self.SPEED_INCREMENT,
|
||||||
)
|
)
|
||||||
print(f"Speed: {self.playback_speed:.1f}x")
|
print(f"Speed: {self.playback_speed:.1f}x")
|
||||||
elif key == ord("w"):
|
elif key == ord("s"):
|
||||||
self.playback_speed = min(
|
self.playback_speed = min(
|
||||||
self.MAX_PLAYBACK_SPEED,
|
self.MAX_PLAYBACK_SPEED,
|
||||||
self.playback_speed + self.SPEED_INCREMENT,
|
self.playback_speed + self.SPEED_INCREMENT,
|
||||||
)
|
)
|
||||||
print(f"Speed: {self.playback_speed:.1f}x")
|
print(f"Speed: {self.playback_speed:.1f}x")
|
||||||
elif self.handle_seeking_key(key):
|
elif self.process_seek_key(key):
|
||||||
# Seeking was handled and frame was updated
|
# Seeking was handled
|
||||||
pass
|
pass
|
||||||
elif key == ord("n"): # Next file
|
elif key == ord("n"): # Next file
|
||||||
break
|
break
|
||||||
@@ -484,13 +500,12 @@ class MediaGrader:
|
|||||||
return
|
return
|
||||||
break
|
break
|
||||||
elif key == 255: # No key pressed
|
elif key == 255: # No key pressed
|
||||||
# Reset key tracking if no key is pressed
|
# Continue seeking if we're in seeking mode
|
||||||
if self.last_key is not None:
|
if self.is_seeking and self.current_seek_key is not None:
|
||||||
self.last_key = None
|
self.process_seek_key(self.current_seek_key)
|
||||||
print("Key released")
|
|
||||||
|
# Advance frame only if playing (and it's a video) and not seeking
|
||||||
# Advance frame only if playing (and it's a video)
|
if self.is_playing and self.is_video(current_file) and not self.is_seeking:
|
||||||
if self.is_playing and self.is_video(current_file):
|
|
||||||
if not self.advance_frame():
|
if not self.advance_frame():
|
||||||
# End of video
|
# End of video
|
||||||
break
|
break
|
||||||
|
Reference in New Issue
Block a user