import os import time import subprocess import threading import queue import tempfile from typing import Optional import cv2 def start_render_thread(editor, output_path: str) -> bool: if editor.render_thread and editor.render_thread.is_alive(): print("Render already in progress! Use 'X' to cancel first.") return False editor.render_cancelled = False editor.render_thread = threading.Thread(target=_render_worker, args=(editor, output_path), daemon=True) editor.render_thread.start() print(f"Started rendering to {output_path} in background thread...") print("You can continue editing while rendering. Press 'X' to cancel.") return True def _render_worker(editor, output_path: str) -> bool: try: if not output_path.endswith(".mp4"): output_path += ".mp4" start_frame = editor.cut_start_frame if editor.cut_start_frame is not None else 0 end_frame = editor.cut_end_frame if editor.cut_end_frame is not None else editor.total_frames - 1 if start_frame >= end_frame: editor.render_progress_queue.put(("error", "Invalid cut range!", 1.0, 0.0)) return False editor.render_progress_queue.put(("progress", "Calculating output dimensions...", 0.05, 0.0)) if editor.crop_rect: crop_width = int(editor.crop_rect[2]) crop_height = int(editor.crop_rect[3]) else: crop_width = editor.frame_width crop_height = editor.frame_height if editor.rotation_angle in (90, 270): output_width = int(crop_height * editor.zoom_factor) output_height = int(crop_width * editor.zoom_factor) else: output_width = int(crop_width * editor.zoom_factor) output_height = int(crop_height * editor.zoom_factor) output_width -= output_width % 2 output_height -= output_height % 2 editor.render_progress_queue.put(("progress", "Setting up FFmpeg encoder...", 0.1, 0.0)) print(f"Output dimensions: {output_width}x{output_height}") print(f"Zoom factor: {editor.zoom_factor}") print(f"Crop dimensions: {crop_width}x{crop_height}") print("Using FFmpeg for encoding with OpenCV transformations...") return _render_with_ffmpeg_pipe(editor, output_path, start_frame, end_frame, output_width, output_height) except Exception as e: error_msg = str(e) if "async_lock" in error_msg or "pthread_frame" in error_msg: error_msg = "FFmpeg threading error - try restarting the application" elif "Assertion" in error_msg: error_msg = "Video codec error - the video file may be corrupted or incompatible" editor.render_progress_queue.put(("error", f"Render error: {error_msg}", 1.0, 0.0)) print(f"Render error: {error_msg}") return False def pump_progress(editor): try: while True: update_type, text, progress, fps = editor.render_progress_queue.get_nowait() if update_type == "init": editor.show_progress_bar(text) elif update_type == "progress": editor.update_progress_bar(progress, text, fps) elif update_type == "complete": editor.update_progress_bar(progress, text, fps) if hasattr(editor, "overwrite_temp_path") and editor.overwrite_temp_path: _handle_overwrite_completion(editor) elif update_type == "error": editor.update_progress_bar(progress, text, fps) editor.show_feedback_message(f"ERROR: {text}") elif update_type == "cancelled": editor.hide_progress_bar() editor.show_feedback_message("Render cancelled") except queue.Empty: pass def _handle_overwrite_completion(editor): try: print("Replacing original file...") if hasattr(editor, "cap") and editor.cap: editor.cap.release() import shutil print(f"DEBUG: Moving {editor.overwrite_temp_path} to {editor.overwrite_target_path}") try: shutil.move(editor.overwrite_temp_path, editor.overwrite_target_path) print("DEBUG: File move successful") except Exception as e: print(f"DEBUG: File move failed: {e}") if os.path.exists(editor.overwrite_temp_path): os.remove(editor.overwrite_temp_path) raise time.sleep(0.1) try: editor._load_video(editor.video_path) editor.load_current_frame() print("File reloaded successfully") except Exception as e: print(f"Warning: Could not reload file after overwrite: {e}") print("The file was saved successfully, but you may need to restart the editor to continue editing it.") except Exception as e: print(f"Error during file overwrite: {e}") finally: editor.overwrite_temp_path = None editor.overwrite_target_path = None def request_cancel(editor) -> bool: if editor.render_thread and editor.render_thread.is_alive(): editor.render_cancelled = True print("Render cancellation requested...") return True return False def is_rendering(editor) -> bool: return editor.render_thread and editor.render_thread.is_alive() def cleanup_thread(editor): if editor.render_thread and editor.render_thread.is_alive(): editor.render_cancelled = True if editor.ffmpeg_process: try: editor.ffmpeg_process.terminate() editor.ffmpeg_process.wait(timeout=1.0) except Exception: try: editor.ffmpeg_process.kill() except Exception: pass editor.ffmpeg_process = None editor.render_thread.join(timeout=2.0) if editor.render_thread.is_alive(): print("Warning: Render thread did not finish gracefully") editor.render_thread = None editor.render_cancelled = False def _process_frame_for_render(editor, frame, output_width: int, output_height: int, frame_number: Optional[int] = None): try: if editor.crop_rect: x, y, w, h = map(int, editor.crop_rect) if editor.motion_tracker.tracking_enabled and frame_number is not None: current_pos = editor.motion_tracker.get_interpolated_position(frame_number) if current_pos: tracked_x, tracked_y = current_pos new_x = int(tracked_x - w // 2) new_y = int(tracked_y - h // 2) x, y = new_x, new_y h_frame, w_frame = frame.shape[:2] x = max(0, min(x, w_frame - 1)) y = max(0, min(y, h_frame - 1)) w = min(w, w_frame - x) h = min(h, h_frame - y) if w > 0 and h > 0: frame = frame[y:y + h, x:x + w] else: return None frame = editor.apply_brightness_contrast(frame) if editor.rotation_angle != 0: frame = editor.apply_rotation(frame) if editor.zoom_factor != 1.0: height, width = frame.shape[:2] zoomed_width = int(width * editor.zoom_factor) zoomed_height = int(height * editor.zoom_factor) if zoomed_width == output_width and zoomed_height == output_height: frame = cv2.resize(frame, (zoomed_width, zoomed_height), interpolation=cv2.INTER_LINEAR) else: frame = cv2.resize(frame, (output_width, output_height), interpolation=cv2.INTER_LINEAR) else: if frame.shape[1] != output_width or frame.shape[0] != output_height: frame = cv2.resize(frame, (output_width, output_height), interpolation=cv2.INTER_LINEAR) return frame except Exception as e: print(f"Error processing frame: {e}") return None def _render_with_ffmpeg_pipe(editor, output_path: str, start_frame: int, end_frame: int, output_width: int, output_height: int): try: try: test_result = subprocess.run(["ffmpeg", "-version"], capture_output=True, text=True, timeout=10) if test_result.returncode != 0: print(f"FFmpeg test failed with return code {test_result.returncode}") print(f"FFmpeg stderr: {test_result.stderr}") editor.render_progress_queue.put(("error", "FFmpeg is not working properly", 1.0, 0.0)) return False except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired) as e: error_msg = f"FFmpeg not found or not working: {e}" print(error_msg) editor.render_progress_queue.put(("error", error_msg, 1.0, 0.0)) return False editor.render_progress_queue.put(("progress", "Starting encoder...", 0.0, 0.0)) temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.raw') temp_file.close() ffmpeg_cmd = [ 'ffmpeg', '-y', '-f', 'rawvideo', '-s', f'{output_width}x{output_height}', '-pix_fmt', 'bgr24', '-r', str(editor.fps), '-i', temp_file.name, '-c:v', 'libx264', '-preset', 'fast', '-crf', '18', '-pix_fmt', 'yuv420p', output_path ] editor.temp_file_name = temp_file.name render_cap = cv2.VideoCapture(str(editor.video_path)) render_cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame) total_frames = end_frame - start_frame + 1 frames_written = 0 start_time = time.time() last_progress_update = 0 editor.render_progress_queue.put(("progress", f"Processing {total_frames} frames...", 0.1, 0.0)) with open(editor.temp_file_name, 'wb') as tf: for i in range(total_frames): if editor.render_cancelled: render_cap.release() editor.render_progress_queue.put(("cancelled", "Render cancelled", 0.0, 0.0)) return False ret, frame = render_cap.read() if not ret: break processed = _process_frame_for_render(editor, frame, output_width, output_height, start_frame + i) if processed is not None: if i == 0: print(f"Processed frame dimensions: {processed.shape[1]}x{processed.shape[0]}") print(f"Expected dimensions: {output_width}x{output_height}") tf.write(processed.tobytes()) frames_written += 1 current_time = time.time() progress = 0.1 + (0.8 * (i + 1) / total_frames) if current_time - last_progress_update > 0.5: elapsed = current_time - start_time fps_rate = frames_written / elapsed if elapsed > 0 else 0 editor.render_progress_queue.put(("progress", f"Processed {i+1}/{total_frames} frames", progress, fps_rate)) last_progress_update = current_time render_cap.release() editor.render_progress_queue.put(("progress", "Encoding...", 0.9, 0.0)) result = subprocess.run( ffmpeg_cmd, capture_output=True, text=True, timeout=300, creationflags=(subprocess.CREATE_NO_WINDOW if hasattr(subprocess, 'CREATE_NO_WINDOW') else 0) ) if os.path.exists(editor.temp_file_name): try: os.unlink(editor.temp_file_name) except OSError: pass if result.returncode == 0: total_time = time.time() - start_time avg_fps = frames_written / total_time if total_time > 0 else 0 editor.render_progress_queue.put(("complete", f"Rendered {frames_written} frames", 1.0, avg_fps)) print(f"Successfully rendered {frames_written} frames (avg {avg_fps:.1f} FPS)") return True else: error_details = result.stderr if result.stderr else "No error details available" print(f"Encoding failed with return code {result.returncode}") print(f"Error: {error_details}") editor.render_progress_queue.put(("error", f"Encoding failed: {error_details}", 1.0, 0.0)) return False except Exception as e: error_msg = str(e) print(f"Rendering exception: {error_msg}") print(f"Exception type: {type(e).__name__}") if "Errno 22" in error_msg or "invalid argument" in error_msg.lower(): error_msg = "File system error - try using a different output path" elif "BrokenPipeError" in error_msg: error_msg = "Process terminated unexpectedly" elif "FileNotFoundError" in error_msg or "ffmpeg" in error_msg.lower(): error_msg = "FFmpeg not found - please install FFmpeg and ensure it's in your PATH" editor.render_progress_queue.put(("error", f"Rendering failed: {error_msg}", 1.0, 0.0)) return False