Compare commits

...

4 Commits

View File

@@ -180,9 +180,32 @@ class VideoEditor:
self.cap.release()
self.video_path = video_path
self.cap = cv2.VideoCapture(str(self.video_path))
if not self.cap.isOpened():
# Try different backends for better performance
# Order of preference: DirectShow (Windows), FFmpeg, any available
backends_to_try = []
if hasattr(cv2, 'CAP_DSHOW'): # Windows DirectShow
backends_to_try.append(cv2.CAP_DSHOW)
if hasattr(cv2, 'CAP_FFMPEG'): # FFmpeg
backends_to_try.append(cv2.CAP_FFMPEG)
backends_to_try.append(cv2.CAP_ANY) # Fallback
self.cap = None
for backend in backends_to_try:
try:
self.cap = cv2.VideoCapture(str(self.video_path), backend)
if self.cap.isOpened():
# Optimize buffer settings for better performance
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Minimize buffer to reduce latency
# Try to set hardware acceleration if available
if hasattr(cv2, 'CAP_PROP_HW_ACCELERATION'):
self.cap.set(cv2.CAP_PROP_HW_ACCELERATION, cv2.VIDEO_ACCELERATION_ANY)
break
self.cap.release()
except:
continue
if not self.cap or not self.cap.isOpened():
raise ValueError(f"Could not open video file: {video_path}")
# Video properties
@@ -191,6 +214,13 @@ class VideoEditor:
self.frame_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
self.frame_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# Get codec information for debugging
fourcc = int(self.cap.get(cv2.CAP_PROP_FOURCC))
codec = "".join([chr((fourcc >> 8 * i) & 0xFF) for i in range(4)])
# Get backend information
backend = self.cap.getBackendName()
# Reset playback state for new video
self.current_frame = 0
self.is_playing = False
@@ -212,6 +242,16 @@ class VideoEditor:
print(
f"Loaded video: {self.video_path.name} ({self.current_video_index + 1}/{len(self.video_files)})"
)
print(f" Codec: {codec} | Backend: {backend} | Resolution: {self.frame_width}x{self.frame_height}")
print(f" FPS: {self.fps:.2f} | Frames: {self.total_frames} | Duration: {self.total_frames/self.fps:.1f}s")
# Performance warning for known problematic cases
if codec in ['H264', 'H.264', 'AVC1', 'avc1'] and self.total_frames > 10000:
print(f" Warning: Large H.264 video detected - seeking may be slow")
if self.frame_width * self.frame_height > 1920 * 1080:
print(f" Warning: High resolution video - decoding may be slow")
if self.fps > 60:
print(f" Warning: High framerate video - may impact playback smoothness")
def switch_to_video(self, index: int):
"""Switch to a specific video by index"""
@@ -271,15 +311,46 @@ class VideoEditor:
self.load_current_frame()
def advance_frame(self) -> bool:
"""Advance to next frame"""
"""Advance to next frame - optimized to avoid seeking, handles playback speed"""
if not self.is_playing:
return True
self.current_frame += 1
if self.current_frame >= self.total_frames:
self.current_frame = 0 # Loop
# Calculate how many frames to advance based on speed
# For speeds > 1.0, we skip frames. For speeds < 1.0, we delay in main loop
frames_to_advance = max(1, int(self.playback_speed))
new_frame = self.current_frame + frames_to_advance
if new_frame >= self.total_frames:
new_frame = 0 # Loop - this will require a seek
self.current_frame = new_frame
return self.load_current_frame()
return self.load_current_frame()
# For sequential playback at normal speed, just read the next frame without seeking
if frames_to_advance == 1:
ret, frame = self.cap.read()
if ret:
self.current_frame = new_frame
self.current_display_frame = frame
return True
else:
# If sequential read failed, we've hit the actual end of video
# Update total_frames to the actual count and loop
print(f"Reached actual end of video at frame {self.current_frame} (reported: {self.total_frames})")
self.total_frames = self.current_frame
self.current_frame = 0 # Loop back to start
return self.load_current_frame()
else:
# For speed > 1.0, we need to seek to skip frames
self.current_frame = new_frame
success = self.load_current_frame()
if not success:
# Hit actual end of video
print(f"Reached actual end of video at frame {self.current_frame} (reported: {self.total_frames})")
self.total_frames = self.current_frame
self.current_frame = 0 # Loop back to start
return self.load_current_frame()
return success
def apply_crop_zoom_and_rotation(self, frame):
"""Apply current crop, zoom, rotation, and brightness/contrast settings to frame"""
@@ -291,6 +362,7 @@ class VideoEditor:
# Apply brightness/contrast first (to original frame for best quality)
processed_frame = self.apply_brightness_contrast(processed_frame)
# Apply crop
if self.crop_rect:
x, y, w, h = self.crop_rect
@@ -1245,6 +1317,7 @@ class VideoEditor:
self.load_current_frame()
while True:
# Only update display if needed
self.display_current_frame()
delay = self.calculate_frame_delay() if self.is_playing else 30
@@ -1355,7 +1428,6 @@ class VideoEditor:
print(f"Contracted crop from left by {self.crop_size_step}px")
# Auto advance frame when playing
if self.is_playing:
self.advance_frame()
@@ -1372,17 +1444,28 @@ def main():
"video", help="Path to video file or directory containing videos"
)
args = parser.parse_args()
try:
args = parser.parse_args()
except SystemExit as e:
# If launched from context menu without arguments, this might fail
input(f"Argument parsing failed. Press Enter to exit...")
return
if not os.path.exists(args.video):
print(f"Error: {args.video} does not exist")
error_msg = f"Error: {args.video} does not exist"
print(error_msg)
input("Press Enter to exit...") # Keep window open in context menu
sys.exit(1)
try:
editor = VideoEditor(args.video)
editor.run()
except Exception as e:
print(f"Error: {e}")
error_msg = f"Error initializing video editor: {e}"
print(error_msg)
import traceback
traceback.print_exc() # Full error trace for debugging
input("Press Enter to exit...") # Keep window open in context menu
sys.exit(1)