feat(main.py): implement I-frame seeking for improved performance and add display interval for seeking

This commit is contained in:
2025-08-18 16:41:54 +02:00
parent 3900d1cb11
commit e43865a7c8
2 changed files with 116 additions and 65 deletions

179
main.py
View File

@@ -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)
# 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()
# Load the frame we just seeked to and display it immediately
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,