Fucking clean shit up
This commit is contained in:
382
main.py
382
main.py
@@ -13,22 +13,19 @@ from typing import List
|
|||||||
|
|
||||||
|
|
||||||
class MediaGrader:
|
class MediaGrader:
|
||||||
# Configuration constants
|
BASE_FRAME_DELAY_MS = 16
|
||||||
BASE_FRAME_DELAY_MS = 16 # ~30 FPS
|
KEY_REPEAT_RATE_SEC = 0.5
|
||||||
KEY_REPEAT_RATE_SEC = 0.5 # How often to process key repeats
|
FAST_SEEK_ACTIVATION_TIME = 2.0
|
||||||
FAST_SEEK_ACTIVATION_TIME = 2.0 # How long to hold before fast seek
|
FRAME_RENDER_TIME_MS = 50
|
||||||
FRAME_RENDER_TIME_MS = 50 # Time to let frames render between seeks
|
|
||||||
SPEED_INCREMENT = 0.2
|
SPEED_INCREMENT = 0.2
|
||||||
MIN_PLAYBACK_SPEED = 0.1
|
MIN_PLAYBACK_SPEED = 0.1
|
||||||
MAX_PLAYBACK_SPEED = 100.0
|
MAX_PLAYBACK_SPEED = 100.0
|
||||||
FAST_SEEK_MULTIPLIER = 60
|
FAST_SEEK_MULTIPLIER = 60
|
||||||
IMAGE_DISPLAY_DELAY_MS = 100
|
IMAGE_DISPLAY_DELAY_MS = 100
|
||||||
|
|
||||||
# Monitor dimensions for full-screen sizing
|
|
||||||
MONITOR_WIDTH = 2560
|
MONITOR_WIDTH = 2560
|
||||||
MONITOR_HEIGHT = 1440
|
MONITOR_HEIGHT = 1440
|
||||||
|
|
||||||
# Timeline configuration
|
|
||||||
TIMELINE_HEIGHT = 60
|
TIMELINE_HEIGHT = 60
|
||||||
TIMELINE_MARGIN = 20
|
TIMELINE_MARGIN = 20
|
||||||
TIMELINE_BAR_HEIGHT = 12
|
TIMELINE_BAR_HEIGHT = 12
|
||||||
@@ -38,13 +35,11 @@ class MediaGrader:
|
|||||||
TIMELINE_COLOR_HANDLE = (255, 255, 255)
|
TIMELINE_COLOR_HANDLE = (255, 255, 255)
|
||||||
TIMELINE_COLOR_BORDER = (200, 200, 200)
|
TIMELINE_COLOR_BORDER = (200, 200, 200)
|
||||||
|
|
||||||
# Seek modifiers for A/D keys
|
SHIFT_SEEK_MULTIPLIER = 5
|
||||||
SHIFT_SEEK_MULTIPLIER = 5 # SHIFT + A/D multiplier
|
CTRL_SEEK_MULTIPLIER = 10
|
||||||
CTRL_SEEK_MULTIPLIER = 10 # CTRL + A/D multiplier
|
|
||||||
|
|
||||||
# Multi-segment mode configuration
|
SEGMENT_COUNT = 16
|
||||||
SEGMENT_COUNT = 16 # Number of video segments (4x4 grid)
|
SEGMENT_OVERLAP_PERCENT = 10
|
||||||
SEGMENT_OVERLAP_PERCENT = 10 # Percentage overlap between segments
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, directory: str, seek_frames: int = 30, snap_to_iframe: bool = False
|
self, directory: str, seek_frames: int = 30, snap_to_iframe: bool = False
|
||||||
@@ -59,37 +54,26 @@ class MediaGrader:
|
|||||||
self.current_frame = 0
|
self.current_frame = 0
|
||||||
self.total_frames = 0
|
self.total_frames = 0
|
||||||
|
|
||||||
# Multi-segment mode state
|
|
||||||
self.multi_segment_mode = False
|
self.multi_segment_mode = False
|
||||||
self.segment_count = self.SEGMENT_COUNT # Use the class constant
|
self.segment_count = self.SEGMENT_COUNT
|
||||||
self.segment_overlap_percent = self.SEGMENT_OVERLAP_PERCENT # Use the class constant
|
self.segment_overlap_percent = self.SEGMENT_OVERLAP_PERCENT
|
||||||
self.segment_caps = [] # List of VideoCapture objects for each segment
|
self.segment_caps = []
|
||||||
self.segment_frames = [] # List of current frames for each segment
|
self.segment_frames = []
|
||||||
self.segment_positions = [] # List of frame positions for each segment
|
self.segment_positions = []
|
||||||
|
|
||||||
# Timeline visibility state
|
|
||||||
self.timeline_visible = True
|
self.timeline_visible = True
|
||||||
|
|
||||||
# Improved frame cache for performance
|
|
||||||
self.frame_cache = {} # Dict[frame_number: frame_data]
|
|
||||||
self.cache_size_limit = 200 # Increased cache size
|
|
||||||
self.cache_lock = threading.Lock() # Thread safety for cache
|
|
||||||
|
|
||||||
# Key repeat tracking with rate limiting
|
|
||||||
self.last_seek_time = 0
|
self.last_seek_time = 0
|
||||||
self.current_seek_key = None
|
self.current_seek_key = None
|
||||||
self.key_first_press_time = 0
|
self.key_first_press_time = 0
|
||||||
self.is_seeking = False
|
self.is_seeking = False
|
||||||
|
|
||||||
# Seeking modes
|
self.fine_seek_frames = 1
|
||||||
self.fine_seek_frames = 1 # Frame-by-frame
|
self.coarse_seek_frames = self.seek_frames
|
||||||
self.coarse_seek_frames = self.seek_frames # User-configurable
|
|
||||||
self.fast_seek_frames = self.seek_frames * self.FAST_SEEK_MULTIPLIER
|
self.fast_seek_frames = self.seek_frames * self.FAST_SEEK_MULTIPLIER
|
||||||
|
|
||||||
# Current frame cache for display
|
|
||||||
self.current_display_frame = None
|
self.current_display_frame = None
|
||||||
|
|
||||||
# Supported media extensions
|
|
||||||
self.extensions = [
|
self.extensions = [
|
||||||
".png",
|
".png",
|
||||||
".jpg",
|
".jpg",
|
||||||
@@ -101,27 +85,20 @@ class MediaGrader:
|
|||||||
".mkv",
|
".mkv",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Mouse interaction for timeline
|
|
||||||
self.mouse_dragging = False
|
self.mouse_dragging = False
|
||||||
self.timeline_rect = None
|
self.timeline_rect = None
|
||||||
self.window_width = 800
|
self.window_width = 800
|
||||||
self.window_height = 600
|
self.window_height = 600
|
||||||
|
|
||||||
# Undo functionality
|
self.undo_history = []
|
||||||
self.undo_history = [] # List of (source_path, destination_path, original_index) tuples
|
|
||||||
|
|
||||||
# Watch tracking for "good look" feature
|
self.watched_regions = {}
|
||||||
self.watched_regions = {} # Dict[file_path: List[Tuple[start_frame, end_frame]]]
|
self.current_watch_start = None
|
||||||
self.current_watch_start = None # Frame where current viewing session started
|
self.last_frame_position = 0
|
||||||
self.last_frame_position = 0 # Track last known frame position
|
|
||||||
|
|
||||||
# Bisection navigation tracking
|
self.last_jump_position = {}
|
||||||
self.last_jump_position = {} # Dict[file_path: last_frame] for bisection reference
|
self.jump_history = {}
|
||||||
|
|
||||||
# Jump history for H key (undo jump)
|
|
||||||
self.jump_history = {} # Dict[file_path: List[frame_positions]] for jump undo
|
|
||||||
|
|
||||||
# Performance optimization: Thread pool for parallel operations
|
|
||||||
self.thread_pool = ThreadPoolExecutor(max_workers=4)
|
self.thread_pool = ThreadPoolExecutor(max_workers=4)
|
||||||
|
|
||||||
def display_with_aspect_ratio(self, frame):
|
def display_with_aspect_ratio(self, frame):
|
||||||
@@ -384,11 +361,7 @@ class MediaGrader:
|
|||||||
self.jump_history[current_file].append(self.current_frame)
|
self.jump_history[current_file].append(self.current_frame)
|
||||||
|
|
||||||
# Jump to the target frame
|
# Jump to the target frame
|
||||||
if self.multi_segment_mode:
|
if not self.multi_segment_mode:
|
||||||
# In multi-segment mode, reposition all segments relative to the jump target
|
|
||||||
self.reposition_segments_around_frame(target_frame)
|
|
||||||
else:
|
|
||||||
# In single mode, just jump the main capture
|
|
||||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
|
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
|
||||||
self.load_current_frame()
|
self.load_current_frame()
|
||||||
|
|
||||||
@@ -560,27 +533,26 @@ class MediaGrader:
|
|||||||
print("Preloading entire video into memory...")
|
print("Preloading entire video into memory...")
|
||||||
preload_start = time.time()
|
preload_start = time.time()
|
||||||
|
|
||||||
self.video_frame_cache = [] # Array to hold all frames
|
self.video_frame_cache = []
|
||||||
|
|
||||||
if self.current_cap and self.current_cap.isOpened():
|
if self.current_cap and self.current_cap.isOpened():
|
||||||
# Reset to beginning
|
|
||||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||||
|
|
||||||
|
# Pre-allocate list for better performance
|
||||||
|
self.video_frame_cache = [None] * safe_frame_count
|
||||||
|
|
||||||
frame_count = 0
|
frame_count = 0
|
||||||
while frame_count < safe_frame_count:
|
while frame_count < safe_frame_count:
|
||||||
ret, frame = self.current_cap.read()
|
ret, frame = self.current_cap.read()
|
||||||
if ret and frame is not None:
|
if ret and frame is not None:
|
||||||
self.video_frame_cache.append(frame.copy())
|
# Direct assignment - no copy() needed yet
|
||||||
|
self.video_frame_cache[frame_count] = frame
|
||||||
frame_count += 1
|
frame_count += 1
|
||||||
|
|
||||||
# Show progress for large videos
|
|
||||||
if frame_count % 50 == 0:
|
|
||||||
print(f"Preloaded {frame_count}/{safe_frame_count} frames...")
|
|
||||||
else:
|
else:
|
||||||
print(f"Reached end of video at frame {frame_count}")
|
# Truncate list to actual size
|
||||||
|
self.video_frame_cache = self.video_frame_cache[:frame_count]
|
||||||
break
|
break
|
||||||
|
|
||||||
# Reset main capture to original position
|
|
||||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, self.current_frame)
|
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, self.current_frame)
|
||||||
|
|
||||||
preload_time = (time.time() - preload_start) * 1000
|
preload_time = (time.time() - preload_start) * 1000
|
||||||
@@ -605,63 +577,6 @@ class MediaGrader:
|
|||||||
successful_segments = sum(1 for frame in self.segment_frames if frame is not None)
|
successful_segments = sum(1 for frame in self.segment_frames if frame is not None)
|
||||||
print(f"Successfully preloaded video with {successful_segments}/{self.segment_count} active segments")
|
print(f"Successfully preloaded video with {successful_segments}/{self.segment_count} active segments")
|
||||||
|
|
||||||
def _create_segment_parallel(self, segment_index: int, file_path: str, start_frame: int):
|
|
||||||
"""Create a single segment capture and load its initial frame (runs in thread)"""
|
|
||||||
try:
|
|
||||||
# Create optimized capture for this segment
|
|
||||||
cap = None
|
|
||||||
backends_to_try = []
|
|
||||||
if hasattr(cv2, 'CAP_FFMPEG'):
|
|
||||||
backends_to_try.append(cv2.CAP_FFMPEG)
|
|
||||||
if hasattr(cv2, 'CAP_DSHOW'):
|
|
||||||
backends_to_try.append(cv2.CAP_DSHOW)
|
|
||||||
backends_to_try.append(cv2.CAP_ANY)
|
|
||||||
|
|
||||||
for backend in backends_to_try:
|
|
||||||
try:
|
|
||||||
cap = cv2.VideoCapture(file_path, backend)
|
|
||||||
if cap.isOpened():
|
|
||||||
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
|
||||||
break
|
|
||||||
cap.release()
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not cap or not cap.isOpened():
|
|
||||||
cap = cv2.VideoCapture(file_path) # Fallback
|
|
||||||
|
|
||||||
if not cap.isOpened():
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
# Try seeking to the requested frame
|
|
||||||
cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
|
|
||||||
ret, frame = cap.read()
|
|
||||||
|
|
||||||
if ret and frame is not None:
|
|
||||||
# Reset to start position for future reads
|
|
||||||
cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
|
|
||||||
return cap, frame.copy()
|
|
||||||
else:
|
|
||||||
# Frame doesn't exist - try seeking to a safer position
|
|
||||||
# Try progressively earlier frames
|
|
||||||
for fallback_frame in [start_frame // 2, start_frame // 4, 0]:
|
|
||||||
cap.set(cv2.CAP_PROP_POS_FRAMES, fallback_frame)
|
|
||||||
ret, frame = cap.read()
|
|
||||||
if ret and frame is not None:
|
|
||||||
print(f"Segment {segment_index}: fell back from frame {start_frame} to {fallback_frame}")
|
|
||||||
cap.set(cv2.CAP_PROP_POS_FRAMES, fallback_frame)
|
|
||||||
return cap, frame.copy()
|
|
||||||
|
|
||||||
# If all fallbacks failed, give up
|
|
||||||
cap.release()
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error creating segment {segment_index}: {e}")
|
|
||||||
if cap:
|
|
||||||
cap.release()
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
def cleanup_segment_captures(self):
|
def cleanup_segment_captures(self):
|
||||||
"""Clean up all segment video captures and preloaded cache"""
|
"""Clean up all segment video captures and preloaded cache"""
|
||||||
for cap in self.segment_caps:
|
for cap in self.segment_caps:
|
||||||
@@ -671,230 +586,24 @@ class MediaGrader:
|
|||||||
self.segment_frames = []
|
self.segment_frames = []
|
||||||
self.segment_positions = []
|
self.segment_positions = []
|
||||||
if hasattr(self, 'video_frame_cache'):
|
if hasattr(self, 'video_frame_cache'):
|
||||||
self.video_frame_cache = [] # Clear preloaded video cache
|
self.video_frame_cache = []
|
||||||
if hasattr(self, 'segment_current_frames'):
|
if hasattr(self, 'segment_current_frames'):
|
||||||
self.segment_current_frames = [] # Clear frame tracking
|
self.segment_current_frames = []
|
||||||
# Clear frame cache
|
|
||||||
self.frame_cache.clear()
|
|
||||||
|
|
||||||
def get_cached_frame(self, frame_number: int):
|
|
||||||
"""Get frame from cache or load it if not cached"""
|
|
||||||
# Check cache first (thread-safe)
|
|
||||||
with self.cache_lock:
|
|
||||||
if frame_number in self.frame_cache:
|
|
||||||
return self.frame_cache[frame_number].copy() # Return a copy to avoid modification
|
|
||||||
|
|
||||||
# Load frame outside of lock to avoid blocking other threads
|
|
||||||
frame = None
|
|
||||||
if self.current_cap:
|
|
||||||
# Create a temporary capture to avoid interfering with main playback
|
|
||||||
current_file = self.media_files[self.current_index]
|
|
||||||
|
|
||||||
# Use optimized backend for temporary capture too
|
|
||||||
temp_cap = None
|
|
||||||
backends_to_try = []
|
|
||||||
if hasattr(cv2, 'CAP_FFMPEG'): # FFmpeg - best for video files
|
|
||||||
backends_to_try.append(cv2.CAP_FFMPEG)
|
|
||||||
if hasattr(cv2, 'CAP_DSHOW'): # DirectShow - fallback
|
|
||||||
backends_to_try.append(cv2.CAP_DSHOW)
|
|
||||||
backends_to_try.append(cv2.CAP_ANY) # Final fallback
|
|
||||||
|
|
||||||
for backend in backends_to_try:
|
|
||||||
try:
|
|
||||||
temp_cap = cv2.VideoCapture(str(current_file), backend)
|
|
||||||
if temp_cap.isOpened():
|
|
||||||
temp_cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
|
||||||
break
|
|
||||||
temp_cap.release()
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not temp_cap:
|
|
||||||
temp_cap = cv2.VideoCapture(str(current_file)) # Fallback
|
|
||||||
|
|
||||||
if temp_cap.isOpened():
|
|
||||||
temp_cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
|
|
||||||
ret, frame = temp_cap.read()
|
|
||||||
temp_cap.release()
|
|
||||||
|
|
||||||
if ret and frame is not None:
|
|
||||||
# Cache the frame (with size limit) - thread-safe
|
|
||||||
with self.cache_lock:
|
|
||||||
if len(self.frame_cache) >= self.cache_size_limit:
|
|
||||||
# Remove oldest cached frames (remove multiple at once for efficiency)
|
|
||||||
keys_to_remove = sorted(self.frame_cache.keys())[:len(self.frame_cache) // 4]
|
|
||||||
for key in keys_to_remove:
|
|
||||||
del self.frame_cache[key]
|
|
||||||
|
|
||||||
self.frame_cache[frame_number] = frame.copy()
|
|
||||||
return frame
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_segment_capture(self, segment_index):
|
|
||||||
"""Get or create a capture for a specific segment (lazy loading)"""
|
|
||||||
if segment_index >= len(self.segment_caps) or self.segment_caps[segment_index] is None:
|
|
||||||
if segment_index < len(self.segment_caps):
|
|
||||||
# Create capture on demand with optimized backend
|
|
||||||
current_file = self.media_files[self.current_index]
|
|
||||||
|
|
||||||
# Use optimized backend for segment capture
|
|
||||||
cap = None
|
|
||||||
backends_to_try = []
|
|
||||||
if hasattr(cv2, 'CAP_FFMPEG'): # FFmpeg - best for video files
|
|
||||||
backends_to_try.append(cv2.CAP_FFMPEG)
|
|
||||||
if hasattr(cv2, 'CAP_DSHOW'): # DirectShow - fallback
|
|
||||||
backends_to_try.append(cv2.CAP_DSHOW)
|
|
||||||
backends_to_try.append(cv2.CAP_ANY) # Final fallback
|
|
||||||
|
|
||||||
for backend in backends_to_try:
|
|
||||||
try:
|
|
||||||
cap = cv2.VideoCapture(str(current_file), backend)
|
|
||||||
if cap.isOpened():
|
|
||||||
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
|
||||||
break
|
|
||||||
cap.release()
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not cap:
|
|
||||||
cap = cv2.VideoCapture(str(current_file)) # Fallback
|
|
||||||
|
|
||||||
if cap.isOpened():
|
|
||||||
cap.set(cv2.CAP_PROP_POS_FRAMES, self.segment_positions[segment_index])
|
|
||||||
self.segment_caps[segment_index] = cap
|
|
||||||
return cap
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
return None
|
|
||||||
return self.segment_caps[segment_index]
|
|
||||||
|
|
||||||
def update_segment_frame_parallel(self, segment_index):
|
|
||||||
"""Update a single segment frame"""
|
|
||||||
try:
|
|
||||||
cap = self.get_segment_capture(segment_index)
|
|
||||||
if cap and cap.isOpened():
|
|
||||||
ret, frame = cap.read()
|
|
||||||
if ret:
|
|
||||||
return segment_index, frame
|
|
||||||
else:
|
|
||||||
# Loop back to segment start when reaching end
|
|
||||||
cap.set(cv2.CAP_PROP_POS_FRAMES, self.segment_positions[segment_index])
|
|
||||||
ret, frame = cap.read()
|
|
||||||
if ret:
|
|
||||||
return segment_index, frame
|
|
||||||
else:
|
|
||||||
return segment_index, None
|
|
||||||
return segment_index, None
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error updating segment {segment_index}: {e}")
|
|
||||||
return segment_index, None
|
|
||||||
|
|
||||||
def update_segment_frames(self):
|
def update_segment_frames(self):
|
||||||
"""Update frames for segments using the preloaded video array - smooth playback!"""
|
"""Update frames for segments using the preloaded video array - smooth playback!"""
|
||||||
if not self.multi_segment_mode or not self.segment_frames or not hasattr(self, 'video_frame_cache'):
|
if not self.multi_segment_mode or not self.segment_frames or not hasattr(self, 'video_frame_cache'):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Each segment advances through the video at its own pace
|
|
||||||
for i in range(len(self.segment_frames)):
|
for i in range(len(self.segment_frames)):
|
||||||
if self.segment_frames[i] is not None and self.video_frame_cache:
|
if self.segment_frames[i] is not None and self.video_frame_cache:
|
||||||
# Advance this segment's current frame
|
|
||||||
self.segment_current_frames[i] += 1
|
self.segment_current_frames[i] += 1
|
||||||
|
|
||||||
# Loop back to start if we reach the end
|
|
||||||
if self.segment_current_frames[i] >= len(self.video_frame_cache):
|
if self.segment_current_frames[i] >= len(self.video_frame_cache):
|
||||||
self.segment_current_frames[i] = 0
|
self.segment_current_frames[i] = 0
|
||||||
|
|
||||||
# Update the segment frame from the cache
|
|
||||||
self.segment_frames[i] = self.video_frame_cache[self.segment_current_frames[i]].copy()
|
self.segment_frames[i] = self.video_frame_cache[self.segment_current_frames[i]].copy()
|
||||||
|
|
||||||
def reposition_segments_around_frame(self, center_frame: int):
|
|
||||||
"""Reposition all segments around a center frame while maintaining spacing"""
|
|
||||||
if not self.multi_segment_mode or not self.segment_caps:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Calculate new segment positions around the center frame
|
|
||||||
# Keep the same relative spacing but center around the new frame
|
|
||||||
segment_spacing = self.total_frames // (self.segment_count + 1)
|
|
||||||
|
|
||||||
new_positions = []
|
|
||||||
for i in range(self.segment_count):
|
|
||||||
# Spread segments around center_frame
|
|
||||||
offset = (i - (self.segment_count - 1) / 2) * segment_spacing
|
|
||||||
new_frame = int(center_frame + offset)
|
|
||||||
new_frame = max(0, min(new_frame, self.total_frames - 1))
|
|
||||||
new_positions.append(new_frame)
|
|
||||||
|
|
||||||
# Update segment positions and seek all captures
|
|
||||||
self.segment_positions = new_positions
|
|
||||||
|
|
||||||
for i, cap in enumerate(self.segment_caps):
|
|
||||||
if cap and cap.isOpened():
|
|
||||||
cap.set(cv2.CAP_PROP_POS_FRAMES, self.segment_positions[i])
|
|
||||||
|
|
||||||
# Load new frame
|
|
||||||
ret, frame = cap.read()
|
|
||||||
if ret:
|
|
||||||
self.segment_frames[i] = frame
|
|
||||||
# Reset position for next read
|
|
||||||
cap.set(cv2.CAP_PROP_POS_FRAMES, self.segment_positions[i])
|
|
||||||
|
|
||||||
def seek_segment_parallel(self, segment_index, frames_delta):
|
|
||||||
"""Seek a single segment by the specified number of frames"""
|
|
||||||
try:
|
|
||||||
if segment_index >= len(self.segment_positions):
|
|
||||||
return segment_index, None
|
|
||||||
|
|
||||||
cap = self.get_segment_capture(segment_index)
|
|
||||||
if cap and cap.isOpened():
|
|
||||||
current_frame = int(cap.get(cv2.CAP_PROP_POS_FRAMES))
|
|
||||||
segment_start = self.segment_positions[segment_index]
|
|
||||||
segment_duration = self.total_frames // self.segment_count
|
|
||||||
segment_end = min(self.total_frames - 1, segment_start + segment_duration)
|
|
||||||
|
|
||||||
target_frame = max(segment_start, min(current_frame + frames_delta, segment_end))
|
|
||||||
|
|
||||||
# Try cache first for better performance
|
|
||||||
cached_frame = self.get_cached_frame(target_frame)
|
|
||||||
if cached_frame is not None:
|
|
||||||
cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
|
|
||||||
return segment_index, cached_frame
|
|
||||||
else:
|
|
||||||
# Fall back to normal seeking
|
|
||||||
cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
|
|
||||||
ret, frame = cap.read()
|
|
||||||
if ret:
|
|
||||||
return segment_index, frame
|
|
||||||
else:
|
|
||||||
return segment_index, None
|
|
||||||
return segment_index, None
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error seeking segment {segment_index}: {e}")
|
|
||||||
return segment_index, None
|
|
||||||
|
|
||||||
def seek_all_segments(self, frames_delta: int):
|
|
||||||
"""Seek all segments by the specified number of frames with parallel processing"""
|
|
||||||
if not self.multi_segment_mode or not self.segment_frames:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Only seek segments that have valid frames loaded
|
|
||||||
active_segments = [i for i, frame in enumerate(self.segment_frames) if frame is not None]
|
|
||||||
|
|
||||||
if not active_segments:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Use parallel processing for seeking
|
|
||||||
futures = []
|
|
||||||
for i in active_segments:
|
|
||||||
future = self.thread_pool.submit(self.seek_segment_parallel, i, frames_delta)
|
|
||||||
futures.append(future)
|
|
||||||
|
|
||||||
# Collect results
|
|
||||||
for future in futures:
|
|
||||||
segment_index, frame = future.result()
|
|
||||||
if frame is not None:
|
|
||||||
self.segment_frames[segment_index] = frame
|
|
||||||
|
|
||||||
def display_current_frame(self):
|
def display_current_frame(self):
|
||||||
"""Display the current cached frame with overlays"""
|
"""Display the current cached frame with overlays"""
|
||||||
if self.multi_segment_mode:
|
if self.multi_segment_mode:
|
||||||
@@ -1176,7 +885,6 @@ class MediaGrader:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.multi_segment_mode:
|
if self.multi_segment_mode:
|
||||||
# Update all segment frames
|
|
||||||
self.update_segment_frames()
|
self.update_segment_frames()
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@@ -1185,9 +893,8 @@ class MediaGrader:
|
|||||||
for _ in range(frames_to_skip + 1):
|
for _ in range(frames_to_skip + 1):
|
||||||
ret, frame = self.current_cap.read()
|
ret, frame = self.current_cap.read()
|
||||||
if not ret:
|
if not ret:
|
||||||
# Hit actual end of video - check if frame count was wrong
|
|
||||||
actual_frame = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
|
actual_frame = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
|
||||||
if actual_frame < self.total_frames - 5: # Allow some tolerance
|
if actual_frame < self.total_frames - 5:
|
||||||
print(f"Frame count mismatch! Reported: {self.total_frames}, Actual: {actual_frame}")
|
print(f"Frame count mismatch! Reported: {self.total_frames}, Actual: {actual_frame}")
|
||||||
self.total_frames = actual_frame
|
self.total_frames = actual_frame
|
||||||
return False
|
return False
|
||||||
@@ -1195,7 +902,6 @@ class MediaGrader:
|
|||||||
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))
|
||||||
|
|
||||||
# Update watch tracking
|
|
||||||
self.update_watch_tracking()
|
self.update_watch_tracking()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -1206,17 +912,17 @@ class MediaGrader:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.multi_segment_mode:
|
if self.multi_segment_mode:
|
||||||
self.seek_all_segments(frames_delta)
|
return
|
||||||
else:
|
|
||||||
if not self.current_cap:
|
if not self.current_cap:
|
||||||
return
|
return
|
||||||
|
|
||||||
target_frame = max(
|
target_frame = max(
|
||||||
0, min(self.current_frame + frames_delta, self.total_frames - 1)
|
0, min(self.current_frame + frames_delta, self.total_frames - 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
|
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
|
||||||
self.load_current_frame()
|
self.load_current_frame()
|
||||||
|
|
||||||
def process_seek_key(self, key: int) -> bool:
|
def process_seek_key(self, key: int) -> bool:
|
||||||
"""Process seeking keys with proper rate limiting"""
|
"""Process seeking keys with proper rate limiting"""
|
||||||
@@ -1455,7 +1161,7 @@ class MediaGrader:
|
|||||||
self.playback_speed + self.SPEED_INCREMENT,
|
self.playback_speed + self.SPEED_INCREMENT,
|
||||||
)
|
)
|
||||||
elif self.process_seek_key(key):
|
elif self.process_seek_key(key):
|
||||||
pass
|
continue
|
||||||
elif key == ord("n"):
|
elif key == ord("n"):
|
||||||
break
|
break
|
||||||
elif key == ord("p"):
|
elif key == ord("p"):
|
||||||
|
Reference in New Issue
Block a user