feat(main.py): implement I-frame seeking for improved performance and add display interval for seeking
This commit is contained in:
179
main.py
179
main.py
@@ -13,7 +13,7 @@ class MediaGrader:
|
||||
# Configuration constants
|
||||
DEFAULT_FPS = 30
|
||||
BASE_FRAME_DELAY_MS = 33 # ~30 FPS
|
||||
KEY_REPEAT_THRESHOLD_SEC = 0.2 # Faster detection for repeat
|
||||
KEY_REPEAT_THRESHOLD_SEC = 0.5 # Faster detection for repeat
|
||||
FAST_SEEK_ACTIVATION_TIME = 0.5 # How long to hold before fast seek
|
||||
WINDOW_MAX_WIDTH = 1200
|
||||
WINDOW_MAX_HEIGHT = 800
|
||||
@@ -24,6 +24,7 @@ class MediaGrader:
|
||||
FAST_SEEK_MULTIPLIER = 5
|
||||
IFRAME_SNAP_INTERVAL = 30
|
||||
IMAGE_DISPLAY_DELAY_MS = 100
|
||||
SEEK_DISPLAY_INTERVAL = 10 # Update display every N frames during seeking
|
||||
|
||||
def __init__(
|
||||
self, directory: str, seek_frames: int = 30, snap_to_iframe: bool = False
|
||||
@@ -51,6 +52,7 @@ class MediaGrader:
|
||||
|
||||
# Current frame cache for display
|
||||
self.current_display_frame = None
|
||||
self.window_resized = False
|
||||
|
||||
# Supported media extensions
|
||||
self.extensions = [
|
||||
@@ -126,6 +128,7 @@ class MediaGrader:
|
||||
|
||||
# Load initial frame
|
||||
self.load_current_frame()
|
||||
self.window_resized = False
|
||||
return True
|
||||
|
||||
def load_current_frame(self):
|
||||
@@ -149,6 +152,63 @@ class MediaGrader:
|
||||
return True
|
||||
return False
|
||||
|
||||
def display_current_frame(self):
|
||||
"""Display the current cached frame with overlays"""
|
||||
if self.current_display_frame is None:
|
||||
return
|
||||
|
||||
frame = self.current_display_frame.copy()
|
||||
|
||||
# Auto-resize window on first frame
|
||||
if not self.window_resized:
|
||||
self.auto_resize_window(frame)
|
||||
self.window_resized = True
|
||||
|
||||
# Add info overlay
|
||||
current_file = self.media_files[self.current_index]
|
||||
info_text = f"Speed: {self.playback_speed:.1f}x | Frame: {self.current_frame}/{self.total_frames} | File: {self.current_index + 1}/{len(self.media_files)} | {'Playing' if self.is_playing else 'PAUSED'}"
|
||||
help_text = "Seek: A/D (hold=FAST) ,. (fine) | W/S speed | 1-5 grade | Space pause | Q quit"
|
||||
|
||||
# White background for text visibility
|
||||
cv2.putText(
|
||||
frame,
|
||||
info_text,
|
||||
(10, 30),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.7,
|
||||
(255, 255, 255),
|
||||
2,
|
||||
)
|
||||
cv2.putText(
|
||||
frame,
|
||||
info_text,
|
||||
(10, 30),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.7,
|
||||
(0, 0, 0),
|
||||
1,
|
||||
)
|
||||
cv2.putText(
|
||||
frame,
|
||||
help_text,
|
||||
(10, 60),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.5,
|
||||
(255, 255, 255),
|
||||
2,
|
||||
)
|
||||
cv2.putText(
|
||||
frame,
|
||||
help_text,
|
||||
(10, 60),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.5,
|
||||
(0, 0, 0),
|
||||
1,
|
||||
)
|
||||
|
||||
cv2.imshow("Media Grader", frame)
|
||||
|
||||
def advance_frame(self):
|
||||
"""Advance to next frame(s) based on playback speed"""
|
||||
if (
|
||||
@@ -191,6 +251,45 @@ class MediaGrader:
|
||||
|
||||
cv2.resizeWindow("Media Grader", new_width, new_height)
|
||||
|
||||
def seek_to_iframe(self, target_frame):
|
||||
"""Seek to the nearest I-frame at or before target_frame"""
|
||||
if not self.current_cap:
|
||||
return False
|
||||
|
||||
# For more reliable seeking, always snap to I-frames
|
||||
iframe_frame = (
|
||||
target_frame // self.IFRAME_SNAP_INTERVAL
|
||||
) * self.IFRAME_SNAP_INTERVAL
|
||||
iframe_frame = max(0, min(iframe_frame, self.total_frames - 1))
|
||||
|
||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, iframe_frame)
|
||||
|
||||
# If we need to get closer to target, read frames sequentially
|
||||
current_pos = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
|
||||
frames_to_read = target_frame - current_pos
|
||||
|
||||
if frames_to_read > 0 and frames_to_read < 60: # Only if it's reasonable
|
||||
for i in range(frames_to_read):
|
||||
ret, frame = self.current_cap.read()
|
||||
if not ret:
|
||||
break
|
||||
# Update display every few frames during seeking
|
||||
if i % self.SEEK_DISPLAY_INTERVAL == 0:
|
||||
self.current_display_frame = frame
|
||||
self.current_frame = int(
|
||||
self.current_cap.get(cv2.CAP_PROP_POS_FRAMES)
|
||||
)
|
||||
self.display_current_frame()
|
||||
cv2.waitKey(1) # Process display events
|
||||
else:
|
||||
# For large seeks, just go to the I-frame
|
||||
ret, frame = self.current_cap.read()
|
||||
if ret:
|
||||
self.current_display_frame = frame
|
||||
|
||||
self.current_frame = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
|
||||
return True
|
||||
|
||||
def seek_video(self, frames_delta: int):
|
||||
"""Seek video by specified number of frames"""
|
||||
if not self.current_cap or not self.is_video(
|
||||
@@ -198,19 +297,23 @@ class MediaGrader:
|
||||
):
|
||||
return
|
||||
|
||||
new_frame = max(
|
||||
target_frame = max(
|
||||
0, min(self.current_frame + frames_delta, self.total_frames - 1)
|
||||
)
|
||||
|
||||
if self.snap_to_iframe and frames_delta < 0:
|
||||
# Find previous I-frame (approximation)
|
||||
new_frame = max(0, new_frame - (new_frame % self.IFRAME_SNAP_INTERVAL))
|
||||
print(
|
||||
f"Seeking from {self.current_frame} to {target_frame} (delta: {frames_delta})"
|
||||
)
|
||||
|
||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, new_frame)
|
||||
|
||||
# Load the frame we just seeked to and display it immediately
|
||||
# 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()
|
||||
print(f"Seeked by {frames_delta} frames to frame {new_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."""
|
||||
@@ -334,61 +437,9 @@ class MediaGrader:
|
||||
window_title = f"Media Grader - {current_file.name} ({self.current_index + 1}/{len(self.media_files)})"
|
||||
cv2.setWindowTitle("Media Grader", window_title)
|
||||
|
||||
window_resized = False
|
||||
|
||||
while True:
|
||||
# Always display the current cached frame
|
||||
if self.current_display_frame is not None:
|
||||
frame = self.current_display_frame.copy()
|
||||
|
||||
# Auto-resize window on first frame
|
||||
if not window_resized:
|
||||
self.auto_resize_window(frame)
|
||||
window_resized = True
|
||||
|
||||
# Add info overlay
|
||||
info_text = f"Speed: {self.playback_speed:.1f}x | Frame: {self.current_frame}/{self.total_frames} | File: {self.current_index + 1}/{len(self.media_files)} | {'Playing' if self.is_playing else 'PAUSED'}"
|
||||
help_text = "Seek: A/D (hold=FAST) ,. (fine) | W/S speed | 1-5 grade | Space pause | Q quit"
|
||||
|
||||
# White background for text visibility
|
||||
cv2.putText(
|
||||
frame,
|
||||
info_text,
|
||||
(10, 30),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.7,
|
||||
(255, 255, 255),
|
||||
2,
|
||||
)
|
||||
cv2.putText(
|
||||
frame,
|
||||
info_text,
|
||||
(10, 30),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.7,
|
||||
(0, 0, 0),
|
||||
1,
|
||||
)
|
||||
cv2.putText(
|
||||
frame,
|
||||
help_text,
|
||||
(10, 60),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.5,
|
||||
(255, 255, 255),
|
||||
2,
|
||||
)
|
||||
cv2.putText(
|
||||
frame,
|
||||
help_text,
|
||||
(10, 60),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.5,
|
||||
(0, 0, 0),
|
||||
1,
|
||||
)
|
||||
|
||||
cv2.imshow("Media Grader", frame)
|
||||
self.display_current_frame()
|
||||
|
||||
# Calculate appropriate delay
|
||||
if self.is_video(current_file):
|
||||
@@ -407,13 +458,13 @@ class MediaGrader:
|
||||
elif key == ord(" "): # Space - pause/play
|
||||
self.is_playing = not self.is_playing
|
||||
print(f"{'Playing' if self.is_playing else 'Paused'}")
|
||||
elif key == ord("s"): # W - decrease speed
|
||||
elif key == ord("w"):
|
||||
self.playback_speed = max(
|
||||
self.MIN_PLAYBACK_SPEED,
|
||||
self.playback_speed - self.SPEED_INCREMENT,
|
||||
)
|
||||
print(f"Speed: {self.playback_speed:.1f}x")
|
||||
elif key == ord("w"): # S - increase speed
|
||||
elif key == ord("w"):
|
||||
self.playback_speed = min(
|
||||
self.MAX_PLAYBACK_SPEED,
|
||||
self.playback_speed + self.SPEED_INCREMENT,
|
||||
|
Reference in New Issue
Block a user