From dbefc5b3595c75762bcd142bfcf0782baf2e2759 Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Thu, 4 Sep 2025 15:46:45 +0200 Subject: [PATCH] Add a render progress bar --- croppa/main.py | 154 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 150 insertions(+), 4 deletions(-) diff --git a/croppa/main.py b/croppa/main.py index b622ead..2acc5ae 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -28,6 +28,16 @@ class VideoEditor: TIMELINE_COLOR_BORDER = (200, 200, 200) TIMELINE_COLOR_CUT_POINT = (255, 0, 0) + # Progress bar configuration + PROGRESS_BAR_HEIGHT = 30 + PROGRESS_BAR_MARGIN_PERCENT = 5 # 5% margin on each side + PROGRESS_BAR_TOP_MARGIN = 20 # Fixed top margin + PROGRESS_BAR_FADE_DURATION = 3.0 # seconds to fade out after completion + PROGRESS_BAR_COLOR_BG = (50, 50, 50) + PROGRESS_BAR_COLOR_FILL = (0, 255, 0) # Green when complete + PROGRESS_BAR_COLOR_PROGRESS = (0, 120, 255) # Blue during progress + PROGRESS_BAR_COLOR_BORDER = (200, 200, 200) + # Zoom and crop settings MIN_ZOOM = 0.1 MAX_ZOOM = 10.0 @@ -93,6 +103,14 @@ class VideoEditor: # Display offset for panning when zoomed self.display_offset = [0, 0] + + # Progress bar state + self.progress_bar_visible = False + self.progress_bar_progress = 0.0 # 0.0 to 1.0 + self.progress_bar_complete = False + self.progress_bar_complete_time = None + self.progress_bar_text = "" + self.progress_bar_fps = 0.0 # Current rendering FPS def _get_video_files_from_directory(self, directory: Path) -> List[Path]: """Get all video files from a directory, sorted by name""" @@ -296,6 +314,104 @@ class VideoEditor: """Adjust contrast by delta (0.1 to 3.0)""" self.contrast = max(0.1, min(3.0, self.contrast + delta)) + def show_progress_bar(self, text: str = "Processing..."): + """Show progress bar with given text""" + self.progress_bar_visible = True + self.progress_bar_progress = 0.0 + self.progress_bar_complete = False + self.progress_bar_complete_time = None + self.progress_bar_text = text + + def update_progress_bar(self, progress: float, text: str = None, fps: float = None): + """Update progress bar progress (0.0 to 1.0) and optionally text and FPS""" + if self.progress_bar_visible: + self.progress_bar_progress = max(0.0, min(1.0, progress)) + if text is not None: + self.progress_bar_text = text + if fps is not None: + self.progress_bar_fps = fps + + # Mark as complete when reaching 100% + if self.progress_bar_progress >= 1.0 and not self.progress_bar_complete: + self.progress_bar_complete = True + self.progress_bar_complete_time = time.time() + + def hide_progress_bar(self): + """Hide progress bar""" + self.progress_bar_visible = False + self.progress_bar_complete = False + self.progress_bar_complete_time = None + self.progress_bar_fps = 0.0 + + def draw_progress_bar(self, frame): + """Draw progress bar on frame if visible - positioned at top with full width""" + if not self.progress_bar_visible: + return + + # Check if we should fade out + if self.progress_bar_complete and self.progress_bar_complete_time: + elapsed = time.time() - self.progress_bar_complete_time + if elapsed > self.PROGRESS_BAR_FADE_DURATION: + self.hide_progress_bar() + return + + # Calculate fade alpha (1.0 at start, 0.0 at end) + fade_alpha = max(0.0, 1.0 - (elapsed / self.PROGRESS_BAR_FADE_DURATION)) + else: + fade_alpha = 1.0 + + height, width = frame.shape[:2] + + # Calculate progress bar position (top of frame with 5% margins) + margin_width = int(width * self.PROGRESS_BAR_MARGIN_PERCENT / 100) + bar_width = width - (2 * margin_width) + bar_x = margin_width + bar_y = self.PROGRESS_BAR_TOP_MARGIN + + # Apply fade alpha to colors + bg_color = tuple(int(c * fade_alpha) for c in self.PROGRESS_BAR_COLOR_BG) + border_color = tuple(int(c * fade_alpha) for c in self.PROGRESS_BAR_COLOR_BORDER) + + if self.progress_bar_complete: + fill_color = tuple(int(c * fade_alpha) for c in self.PROGRESS_BAR_COLOR_FILL) + else: + fill_color = tuple(int(c * fade_alpha) for c in self.PROGRESS_BAR_COLOR_PROGRESS) + + # Draw background + cv2.rectangle(frame, (bar_x, bar_y), (bar_x + bar_width, bar_y + self.PROGRESS_BAR_HEIGHT), bg_color, -1) + + # Draw progress fill + fill_width = int(bar_width * self.progress_bar_progress) + if fill_width > 0: + cv2.rectangle(frame, (bar_x, bar_y), (bar_x + fill_width, bar_y + self.PROGRESS_BAR_HEIGHT), fill_color, -1) + + # Draw border + cv2.rectangle(frame, (bar_x, bar_y), (bar_x + bar_width, bar_y + self.PROGRESS_BAR_HEIGHT), border_color, 2) + + # Draw progress percentage on the left + percentage_text = f"{self.progress_bar_progress * 100:.1f}%" + text_color = tuple(int(255 * fade_alpha) for _ in range(3)) + cv2.putText(frame, percentage_text, (bar_x + 12, bar_y + 22), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 4) + cv2.putText(frame, percentage_text, (bar_x + 10, bar_y + 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, text_color, 2) + + # Draw FPS on the right if available + if self.progress_bar_fps > 0: + fps_text = f"{self.progress_bar_fps:.1f} FPS" + fps_text_size = cv2.getTextSize(fps_text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)[0] + fps_x = bar_x + bar_width - fps_text_size[0] - 10 + cv2.putText(frame, fps_text, (fps_x + 2, bar_y + 22), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 4) + cv2.putText(frame, fps_text, (fps_x, bar_y + 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, text_color, 2) + + # Draw main text in center + if self.progress_bar_text: + text_size = cv2.getTextSize(self.progress_bar_text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)[0] + text_x = bar_x + (bar_width - text_size[0]) // 2 + text_y = bar_y + 20 + + # Draw text shadow for better visibility + cv2.putText(frame, self.progress_bar_text, (text_x + 2, text_y + 2), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 4) + cv2.putText(frame, self.progress_bar_text, (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, text_color, 2) + def draw_timeline(self, frame): """Draw timeline at the bottom of the frame""" height, width = frame.shape[:2] @@ -582,6 +698,9 @@ class VideoEditor: # Draw timeline self.draw_timeline(canvas) + # Draw progress bar (if visible) + self.draw_progress_bar(canvas) + cv2.imshow("Video Editor", canvas) def mouse_callback(self, event, x, y, flags, param): @@ -746,6 +865,9 @@ class VideoEditor: print(f"Rendering video to {output_path}...") start_time = time.time() + # Show progress bar + self.show_progress_bar("Initializing render...") + # Determine frame range start_frame = self.cut_start_frame if self.cut_start_frame is not None else 0 end_frame = ( @@ -756,8 +878,12 @@ class VideoEditor: if start_frame >= end_frame: print("Invalid cut range!") + self.update_progress_bar(1.0, "Error: Invalid cut range!") return False + # Update progress for initialization + self.update_progress_bar(0.05, "Calculating output dimensions...") + # Calculate output dimensions (accounting for rotation) if self.crop_rect: crop_width = int(self.crop_rect[2]) @@ -774,6 +900,9 @@ class VideoEditor: output_width = int(crop_width * self.zoom_factor) output_height = int(crop_height * self.zoom_factor) + # Update progress for video writer setup + self.update_progress_bar(0.1, "Setting up video writer...") + # Use mp4v codec (most compatible with MP4) fourcc = cv2.VideoWriter_fourcc(*"mp4v") out = cv2.VideoWriter( @@ -782,11 +911,13 @@ class VideoEditor: if not out.isOpened(): print("Error: Could not open video writer!") + self.update_progress_bar(1.0, "Error: Could not open video writer!") return False # Simple sequential processing - the I/O is the bottleneck anyway total_output_frames = end_frame - start_frame + 1 last_progress_update = 0 + last_display_update = 0 for frame_idx in range(start_frame, end_frame + 1): # Read frame @@ -805,29 +936,44 @@ class VideoEditor: out.write(processed_frame) frames_written = frame_idx - start_frame + 1 - - # Throttled progress update current_time = time.time() + + # Update progress bar (10% to 95% of progress reserved for frame processing) + progress = 0.1 + (0.85 * (frames_written / total_output_frames)) + + # Throttled progress update if current_time - last_progress_update > 0.5: - progress = frames_written / total_output_frames * 100 elapsed = current_time - start_time fps_rate = frames_written / elapsed eta = (elapsed / frames_written) * ( total_output_frames - frames_written ) + + progress_text = f"Rendering {frames_written}/{total_output_frames} frames (ETA: {eta:.1f}s)" + self.update_progress_bar(progress, progress_text, fps_rate) + print( - f"Progress: {progress:.1f}% | {frames_written}/{total_output_frames} | " + f"Progress: {progress*100:.1f}% | {frames_written}/{total_output_frames} | " f"FPS: {fps_rate:.1f} | ETA: {eta:.1f}s\r", end="", ) last_progress_update = current_time + # Update display more frequently to show progress bar + if current_time - last_display_update > 0.1: # Update display every 100ms + self.display_current_frame() + cv2.waitKey(1) # Allow OpenCV to process events + last_display_update = current_time + out.release() total_time = time.time() - start_time total_frames_written = end_frame - start_frame + 1 avg_fps = total_frames_written / total_time if total_time > 0 else 0 + # Complete the progress bar + self.update_progress_bar(1.0, f"Complete! Rendered {total_frames_written} frames in {total_time:.1f}s", avg_fps) + print(f"\nVideo rendered successfully to {output_path}") print( f"Rendered {total_frames_written} frames in {total_time:.2f}s (avg {avg_fps:.1f} FPS)"