Compare commits
4 Commits
df103e4070
...
0a73926427
Author | SHA1 | Date | |
---|---|---|---|
0a73926427 | |||
007e371db6 | |||
6c8a5dad8e | |||
6e9bb9ad8e |
107
croppa/main.py
107
croppa/main.py
@@ -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)
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user