From 02d13b2eaa4fc84faf9b6b7c1fe2fa3e96feabc1 Mon Sep 17 00:00:00 2001 From: Scheianu Ionut Date: Thu, 4 Sep 2025 16:32:48 +0300 Subject: [PATCH] Enhanced player stability and added exit confirmation - Fixed VLC display errors by implementing PIL fallback for images - Added comprehensive timer management with individual error handling - Implemented watchdog timers to prevent freezing during media transitions - Enhanced exit functionality with quickconnect code confirmation dialog - Improved settings window behavior with proper modal focus - Added transition protection to prevent rapid media cycling - Enhanced error handling throughout the application - Fixed controls window cleanup and destruction process --- signage_player/appsettings.py | 20 ++ signage_player/player.py | 610 ++++++++++++++++++++++++++++++---- 2 files changed, 557 insertions(+), 73 deletions(-) diff --git a/signage_player/appsettings.py b/signage_player/appsettings.py index e4aea6a..862ae58 100644 --- a/signage_player/appsettings.py +++ b/signage_player/appsettings.py @@ -12,13 +12,33 @@ class AppSettingsWindow(tk.Tk): self.geometry('440x600') # Increased height for better button visibility self.resizable(False, False) self.config(bg='#23272e') + + # Ensure window appears on top and gets focus self.attributes('-topmost', True) + self.lift() self.focus_force() + self.grab_set() # Make window modal + + # Center the window on screen + self.center_window() + self.fields = {} self.load_config() self.style = ttk.Style(self) self.set_styles() self.create_widgets() + + # Ensure focus after widgets are created + self.after(100, self.focus_force) + + def center_window(self): + """Center the settings window on the screen""" + self.update_idletasks() + width = self.winfo_width() + height = self.winfo_height() + x = (self.winfo_screenwidth() // 2) - (width // 2) + y = (self.winfo_screenheight() // 2) - (height // 2) + self.geometry(f'{width}x{height}+{x}+{y}') def set_styles(self): self.style.theme_use('clam') diff --git a/signage_player/player.py b/signage_player/player.py index dece49a..9125d89 100644 --- a/signage_player/player.py +++ b/signage_player/player.py @@ -17,6 +17,14 @@ class SimpleTkPlayer: self.paused = False self.pause_timer = None self.is_transitioning = False # Flag to prevent rapid cycling + self.is_exiting = False # Flag to prevent operations during exit + + # Initialize all timer variables to None + self.hide_controls_timer = None + self.video_watchdog = None + self.image_watchdog = None + self.image_timer = None + self.label = tk.Label(root, bg='black') self.label.pack(fill=tk.BOTH, expand=True) self.create_controls() @@ -107,16 +115,25 @@ class SimpleTkPlayer: self.schedule_hide_controls() def hide_controls(self): - self.controls_win.withdraw() - self.hide_mouse() + try: + if hasattr(self, 'controls_win') and self.controls_win and self.controls_win.winfo_exists(): + self.controls_win.withdraw() + self.hide_mouse() + except Exception as e: + print(f"[CONTROLS] Error hiding controls: {e}") def schedule_hide_controls(self): - if hasattr(self, 'hide_controls_timer') and self.hide_controls_timer: - self.root.after_cancel(self.hide_controls_timer) - self.hide_controls_timer = self.root.after(5000, self.hide_controls) + try: + if hasattr(self, 'hide_controls_timer') and self.hide_controls_timer: + self.root.after_cancel(self.hide_controls_timer) + self.hide_controls_timer = self.root.after(5000, self.hide_controls) + except Exception as e: + print(f"[CONTROLS] Error scheduling hide controls: {e}") + self.hide_controls_timer = None def on_activity(self, event=None): - self.show_controls() + if not self.is_exiting: + self.show_controls() def prev_media(self): self.current_index = (self.current_index - 1) % len(self.playlist) @@ -203,9 +220,11 @@ class SimpleTkPlayer: max_duration = duration if duration is not None else 60 # fallback max 60s self.video_watchdog = self.root.after(int(max_duration * 1200), watchdog) def finish_video(): - if hasattr(self, 'video_watchdog'): + if hasattr(self, 'video_watchdog') and self.video_watchdog: self.root.after_cancel(self.video_watchdog) - self.vlc_player.stop() + self.video_watchdog = None + if hasattr(self, 'vlc_player') and self.vlc_player: + self.vlc_player.stop() self.video_canvas.pack_forget() self.label.pack(fill=tk.BOTH, expand=True) if on_end: @@ -253,11 +272,6 @@ class SimpleTkPlayer: elif ext.endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')): # Use PIL for images instead of VLC to avoid display issues self.show_image_via_pil(file_path, duration if duration is not None else 10, on_end=self.next_media) - try: - self.show_image_via_vlc(file_path, duration if duration is not None else 10, on_end=self.next_media) - except Exception as e: - print(f"[PLAYER] VLC image display failed: {e}. Trying PIL fallback.") - self.show_image_via_pil(file_path, duration if duration is not None else 10, on_end=self.next_media) else: print(f"[PLAYER] Unsupported file type: {media['file_name']}") self.label.config(text=f"Unsupported: {media['file_name']}", fg='yellow') @@ -308,6 +322,8 @@ class SimpleTkPlayer: self.label.config(image="", text="") if hasattr(self, 'photo'): del self.photo # Free memory + if hasattr(self, 'image_timer'): + self.image_timer = None # Clear timer reference if on_end: on_end() @@ -395,11 +411,21 @@ class SimpleTkPlayer: on_end() self.image_watchdog = self.root.after(int(duration * 1200), watchdog) def finish_image(): - if hasattr(self, 'image_watchdog'): - self.root.after_cancel(self.image_watchdog) - self.vlc_player.stop() - self.video_canvas.pack_forget() - self.label.pack(fill=tk.BOTH, expand=True) + try: + if hasattr(self, 'image_watchdog') and self.image_watchdog: + self.root.after_cancel(self.image_watchdog) + self.image_watchdog = None + except Exception as e: + print(f"[VLC] Error canceling image_watchdog: {e}") + + try: + if hasattr(self, 'vlc_player') and self.vlc_player: + self.vlc_player.stop() + self.video_canvas.pack_forget() + self.label.pack(fill=tk.BOTH, expand=True) + except Exception as e: + print(f"[VLC] Error in finish_image cleanup: {e}") + if on_end: on_end() self.root.after(int(duration * 1000), finish_image) @@ -417,13 +443,17 @@ class SimpleTkPlayer: self.current_index = (self.current_index + 1) % len(self.playlist) print(f"[PLAYER] Moving to next media: index {self.current_index}") - # Clear any existing timers - if hasattr(self, 'video_watchdog'): - self.root.after_cancel(self.video_watchdog) - if hasattr(self, 'image_watchdog'): - self.root.after_cancel(self.image_watchdog) - if hasattr(self, 'image_timer'): - self.root.after_cancel(self.image_timer) + # Clear any existing timers safely + timer_names = ['video_watchdog', 'image_watchdog', 'image_timer'] + for timer_name in timer_names: + try: + if hasattr(self, timer_name): + timer_id = getattr(self, timer_name) + if timer_id is not None: + self.root.after_cancel(timer_id) + setattr(self, timer_name, None) + except Exception as e: + print(f"[PLAYER] Error canceling {timer_name} during transition: {e}") self.show_current_media() # Reset transition flag after a brief delay @@ -434,11 +464,291 @@ class SimpleTkPlayer: pass def exit_app(self): + """Show exit confirmation dialog instead of immediate exit""" + print("[EXIT] Exit button pressed, showing confirmation dialog...") + self.show_exit_confirmation() + + def show_exit_confirmation(self): + """Show exit confirmation popup with quickconnect code verification""" + print("[EXIT] Opening exit confirmation window...") + + # Pause playback if not already paused + if not self.paused: + self.paused = True + self.pause_btn.config(text='▶ Resume') + # Explicitly pause VLC video if playing + if hasattr(self, 'vlc_player') and self.vlc_player: + try: + self.vlc_player.pause() + print("[EXIT] VLC player paused") + except Exception as e: + print(f"[EXIT] Error pausing VLC: {e}") + + # Hide controls immediately and prevent them from reappearing + print("[EXIT] Hiding controls for confirmation dialog...") + try: + # Cancel any pending control show/hide timers + if hasattr(self, 'hide_controls_timer') and self.hide_controls_timer: + self.root.after_cancel(self.hide_controls_timer) + self.hide_controls_timer = None + + # Hide controls window + if hasattr(self, 'controls_win') and self.controls_win and self.controls_win.winfo_exists(): + self.controls_win.withdraw() + print("[EXIT] Controls hidden successfully") + + # Temporarily unbind mouse events to prevent controls from showing + self.root.unbind('') + self.root.unbind('') + print("[EXIT] Mouse events unbound") + + except Exception as e: + print(f"[EXIT] Error hiding controls: {e}") + + # Create exit confirmation window + self.create_exit_confirmation_window() + + def create_exit_confirmation_window(self): + """Create the exit confirmation popup window""" + try: + # Create confirmation window + self.exit_window = tk.Toplevel(self.root) + self.exit_window.title('Exit Confirmation') + self.exit_window.geometry('400x250') + self.exit_window.resizable(False, False) + self.exit_window.config(bg='#2c3e50') + + # Make window modal and on top + self.exit_window.attributes('-topmost', True) + self.exit_window.lift() + self.exit_window.focus_force() + self.exit_window.grab_set() + + # Center the window + self.center_exit_window() + + # Create content + self.create_exit_window_content() + + # Bind window close event + self.exit_window.protocol('WM_DELETE_WINDOW', self.cancel_exit) + + print("[EXIT] Exit confirmation window created") + + except Exception as e: + print(f"[EXIT] Error creating confirmation window: {e}") + # If window creation fails, restore normal functionality + self.cancel_exit() + + def center_exit_window(self): + """Center the exit confirmation window on screen""" + self.exit_window.update_idletasks() + width = self.exit_window.winfo_width() + height = self.exit_window.winfo_height() + x = (self.exit_window.winfo_screenwidth() // 2) - (width // 2) + y = (self.exit_window.winfo_screenheight() // 2) - (height // 2) + self.exit_window.geometry(f'{width}x{height}+{x}+{y}') + + def create_exit_window_content(self): + """Create the content for the exit confirmation window""" + # Title + title_label = tk.Label( + self.exit_window, + text='⚠️ Exit Application', + font=('Arial', 16, 'bold'), + fg='#e74c3c', + bg='#2c3e50' + ) + title_label.pack(pady=(20, 10)) + + # Instructions + instruction_label = tk.Label( + self.exit_window, + text='Enter the quickconnect code to exit:', + font=('Arial', 12), + fg='#ecf0f1', + bg='#2c3e50' + ) + instruction_label.pack(pady=(0, 15)) + + # Code entry + self.code_var = tk.StringVar() + self.code_entry = tk.Entry( + self.exit_window, + textvariable=self.code_var, + font=('Arial', 14), + width=15, + justify='center', + show='*' # Hide the input like a password + ) + self.code_entry.pack(pady=(0, 20)) + self.code_entry.focus_set() + + # Bind Enter key to confirm + self.code_entry.bind('', lambda e: self.verify_exit_code()) + + # Buttons frame + buttons_frame = tk.Frame(self.exit_window, bg='#2c3e50') + buttons_frame.pack(pady=(0, 20)) + + # Cancel button + cancel_btn = tk.Button( + buttons_frame, + text='Cancel', + font=('Arial', 12, 'bold'), + bg='#95a5a6', + fg='white', + padx=20, + pady=8, + command=self.cancel_exit + ) + cancel_btn.pack(side=tk.LEFT, padx=(0, 10)) + + # Confirm button + confirm_btn = tk.Button( + buttons_frame, + text='Exit App', + font=('Arial', 12, 'bold'), + bg='#e74c3c', + fg='white', + padx=20, + pady=8, + command=self.verify_exit_code + ) + confirm_btn.pack(side=tk.LEFT, padx=(10, 0)) + + def verify_exit_code(self): + """Verify the entered code and proceed with exit if correct""" + try: + entered_code = self.code_var.get().strip() + + # Load the quickconnect code from config + with open(CONFIG_PATH, 'r') as f: + config = json.load(f) + quickconnect_code = config.get('quickconnect_key', '') + + print(f"[EXIT] Verifying exit code...") + + if entered_code == quickconnect_code: + print("[EXIT] Correct code entered, proceeding with exit...") + # Close confirmation window + self.exit_window.destroy() + # Proceed with actual exit + self.perform_actual_exit() + else: + print("[EXIT] Incorrect code entered") + # Show error and clear entry + self.show_exit_error() + + except Exception as e: + print(f"[EXIT] Error verifying code: {e}") + self.show_exit_error() + + def show_exit_error(self): + """Show error message for incorrect code""" + # Clear the entry + self.code_var.set('') + + # Temporarily change entry background to indicate error + original_bg = self.code_entry.cget('bg') + self.code_entry.config(bg='#e74c3c') + + # Reset background after 1 second + self.exit_window.after(1000, lambda: self.code_entry.config(bg=original_bg)) + + # Focus back to entry + self.code_entry.focus_set() + + def cancel_exit(self): + """Cancel exit and restore normal functionality""" + print("[EXIT] Exit cancelled, restoring normal functionality...") + + try: + # Close confirmation window if it exists + if hasattr(self, 'exit_window') and self.exit_window: + self.exit_window.destroy() + del self.exit_window + except Exception as e: + print(f"[EXIT] Error closing confirmation window: {e}") + + # Restore normal functionality (similar to settings restoration) + self.restore_after_exit_cancel() + + def restore_after_exit_cancel(self): + """Restore normal player functionality after exit cancellation""" + if self.is_exiting: + return # Don't restore if we're actually exiting + + try: + print("[EXIT] Restoring player functionality after exit cancellation...") + + # Resume playback + if self.paused: + self.resume_play() + print("[EXIT] Playback resumed") + + # Re-bind mouse and button events + self.root.bind('', self.on_activity) + self.root.bind('', self.on_activity) + print("[EXIT] Mouse events re-bound") + + # Restore controls if needed + if not hasattr(self, 'controls_win') or self.controls_win is None or not self.controls_win.winfo_exists(): + self.create_controls() + print("[EXIT] Controls recreated") + + # Force focus back to main window + self.root.focus_force() + self.root.lift() + print("[EXIT] Focus restored to main window") + + # Resume VLC if it was paused + if hasattr(self, 'vlc_player') and self.vlc_player: + try: + if self.vlc_player.get_state() == vlc.State.Paused: + self.vlc_player.play() + print("[EXIT] VLC playback resumed") + except Exception as e: + print(f"[EXIT] Error resuming VLC: {e}") + + print("[EXIT] Player functionality fully restored after exit cancellation") + + except Exception as e: + print(f"[EXIT] Error restoring functionality: {e}") + + def perform_actual_exit(self): + """Perform the actual application exit after code verification""" + print("[EXIT] Starting application exit...") + self.is_exiting = True # Set exit flag to prevent further operations + # Signal all threads and flags to stop if hasattr(self, 'stop_event') and self.stop_event: self.stop_event.set() if hasattr(self, 'app_running') and self.app_running: self.app_running[0] = False + + # Cancel all pending timers first + timer_names = ['hide_controls_timer', 'video_watchdog', 'image_watchdog', 'image_timer', 'pause_timer'] + for timer_name in timer_names: + try: + if hasattr(self, timer_name): + timer_id = getattr(self, timer_name) + if timer_id is not None: + self.root.after_cancel(timer_id) + print(f"[EXIT] Canceled {timer_name}") + setattr(self, timer_name, None) + except Exception as e: + print(f"[EXIT] Error canceling {timer_name}: {e}") + # Continue with other timers even if one fails + + # Stop VLC player + try: + if hasattr(self, 'vlc_player') and self.vlc_player: + self.vlc_player.stop() + self.vlc_player = None + except Exception as e: + print(f"[EXIT] Error stopping VLC: {e}") + # Unbind all events to prevent callbacks after destroy try: self.root.unbind('') @@ -446,80 +756,234 @@ class SimpleTkPlayer: self.root.unbind('') except Exception: pass - # Attempt to destroy all Toplevel windows before root + + # Force hide and destroy controls immediately try: - # Withdraw controls_win if it exists + print("[EXIT] Destroying controls...") + + # First, try to hide and destroy the main controls window if hasattr(self, 'controls_win') and self.controls_win: - if self.controls_win.winfo_exists(): - self.controls_win.withdraw() - # Destroy controls_win if it exists (this will also destroy controls_frame) - if hasattr(self, 'controls_win') and self.controls_win: - if self.controls_win.winfo_exists(): - self.controls_win.destroy() - self.controls_win = None - # Fallback: destroy controls_frame if it somehow still exists + try: + if self.controls_win.winfo_exists(): + self.controls_win.withdraw() + self.controls_win.destroy() + self.controls_win = None + print("[EXIT] Main controls window destroyed") + except Exception as e: + print(f"[EXIT] Error destroying main controls_win: {e}") + + # Destroy controls_frame separately if it exists if hasattr(self, 'controls_frame') and self.controls_frame: - if self.controls_frame.winfo_exists(): - self.controls_frame.destroy() - self.controls_frame = None - # Fallback: destroy any remaining Toplevels in the app - for widget in self.root.winfo_children(): - if isinstance(widget, tk.Toplevel): + try: + if self.controls_frame.winfo_exists(): + self.controls_frame.destroy() + self.controls_frame = None + print("[EXIT] Controls frame destroyed") + except Exception as e: + print(f"[EXIT] Error destroying controls_frame: {e}") + + # Destroy all button references + button_names = ['prev_btn', 'pause_btn', 'next_btn', 'settings_btn', 'exit_btn'] + for btn_name in button_names: + try: + if hasattr(self, btn_name): + btn = getattr(self, btn_name) + if btn and btn.winfo_exists(): + btn.destroy() + setattr(self, btn_name, None) + except Exception as e: + print(f"[EXIT] Error destroying {btn_name}: {e}") + + # Destroy any remaining Toplevel windows (more aggressive search) + try: + # Get all toplevel windows including potential orphans + all_toplevels = [] + for widget in list(self.root.winfo_children()): + if isinstance(widget, tk.Toplevel): + all_toplevels.append(widget) + + # Also check if there are any floating Toplevel references + if hasattr(tk, '_default_root') and tk._default_root: + for child in tk._default_root.winfo_children(): + if isinstance(child, tk.Toplevel) and child not in all_toplevels: + all_toplevels.append(child) + + print(f"[EXIT] Found {len(all_toplevels)} toplevel windows to destroy") + + for widget in all_toplevels: try: - widget.destroy() - except Exception: - pass + if widget.winfo_exists(): + widget.withdraw() + widget.quit() if hasattr(widget, 'quit') else None + widget.destroy() + print(f"[EXIT] Destroyed toplevel widget: {type(widget).__name__}") + except Exception as e: + print(f"[EXIT] Error destroying toplevel {widget}: {e}") + + except Exception as e: + print(f"[EXIT] Error in comprehensive toplevel cleanup: {e}") + except Exception as e: - print(f"[EXIT] Error destroying controls_win/frame/toplevels: {e}") - # Destroy any other Toplevels if needed (add here if you have more) + print(f"[EXIT] Error in controls cleanup: {e}") + + # Final step: destroy root window try: - if self.root.winfo_exists(): - self.root.destroy() + print("[EXIT] Destroying main window...") + + # Force update to process any pending widget operations + self.root.update_idletasks() + + # One final check for any remaining Toplevel windows + try: + remaining_toplevels = [w for w in self.root.winfo_children() if isinstance(w, tk.Toplevel)] + if remaining_toplevels: + print(f"[EXIT] Found {len(remaining_toplevels)} remaining toplevel windows, destroying...") + for widget in remaining_toplevels: + try: + widget.withdraw() + widget.destroy() + print(f"[EXIT] Destroyed remaining toplevel: {widget}") + except Exception as e: + print(f"[EXIT] Error destroying remaining toplevel: {e}") + except Exception as e: + print(f"[EXIT] Error checking for remaining toplevels: {e}") + + # Force another update + self.root.update_idletasks() + + # Exit the main loop and destroy + self.root.quit() # Exit mainloop first + self.root.update() # Process the quit + self.root.destroy() + print("[EXIT] Application exit complete") + except Exception as e: print(f"[EXIT] Error destroying root: {e}") + # Force exit using system methods + try: + import os + os._exit(0) + except Exception: + import sys + sys.exit(0) def open_settings(self): - if self.paused is not True: + print("[SETTINGS] Opening settings window...") + + # Pause playback if not already paused + if not self.paused: self.paused = True self.pause_btn.config(text='▶ Resume') # Explicitly pause VLC video if playing if hasattr(self, 'vlc_player') and self.vlc_player: try: self.vlc_player.pause() - except Exception: - pass - # Destroy controls overlay so settings window is always interactive - if hasattr(self, 'controls_win') and self.controls_win: - self.controls_win.destroy() - self.controls_win = None + print("[SETTINGS] VLC player paused") + except Exception as e: + print(f"[SETTINGS] Error pausing VLC: {e}") + + # Hide controls immediately and prevent them from reappearing + print("[SETTINGS] Hiding controls...") + try: + # Cancel any pending control show/hide timers + if hasattr(self, 'hide_controls_timer'): + self.root.after_cancel(self.hide_controls_timer) + + # Hide controls window + if hasattr(self, 'controls_win') and self.controls_win and self.controls_win.winfo_exists(): + self.controls_win.withdraw() + print("[SETTINGS] Controls hidden successfully") + + # Temporarily unbind mouse events to prevent controls from showing + self.root.unbind('') + self.root.unbind('') + print("[SETTINGS] Mouse events unbound") + + except Exception as e: + print(f"[SETTINGS] Error hiding controls: {e}") + + # Launch settings window settings_path = os.path.join(os.path.dirname(__file__), 'appsettings.py') - # Open settings in a new process so it doesn't block the main player - proc = subprocess.Popen([sys.executable, settings_path], close_fds=True) - # Give the window manager a moment to focus the new window - self.root.after(300, lambda: self.root.focus_force()) - # Wait for the settings window to close, then resume - self.root.after(1000, lambda: self.check_settings_closed(proc)) + try: + # Open settings in a new process + proc = subprocess.Popen([sys.executable, settings_path], close_fds=True) + print(f"[SETTINGS] Settings process started with PID: {proc.pid}") + + # Start monitoring the settings window + self.root.after(500, lambda: self.check_settings_closed(proc)) + + except Exception as e: + print(f"[SETTINGS] Error opening settings: {e}") + # Restore functionality if settings failed to open + self.restore_after_settings() def check_settings_closed(self, proc): + if self.is_exiting: + print("[SETTINGS] Exit in progress, stopping settings monitoring") + return + if proc.poll() is not None: - # Resume playback and unpause VLC if needed - self.resume_play() - # Restore and recreate controls overlay - self.root.deiconify() - self.create_controls() - # Re-bind mouse and button events to new controls + print("[SETTINGS] Settings window closed, restoring player functionality...") + self.restore_after_settings() + else: + # Continue checking every second + self.root.after(1000, lambda: self.check_settings_closed(proc)) + + def restore_after_settings(self): + """Restore normal player functionality after settings window closes""" + if self.is_exiting: + print("[SETTINGS] Exit in progress, skipping settings restoration") + return + + try: + print("[SETTINGS] Restoring player functionality...") + + # Resume playback + if self.paused: + self.resume_play() + print("[SETTINGS] Playback resumed") + + # Re-bind mouse and button events self.root.bind('', self.on_activity) self.root.bind('', self.on_activity) - self.show_controls() + print("[SETTINGS] Mouse events re-bound") + + # Ensure controls are properly destroyed before recreating + if hasattr(self, 'controls_win') and self.controls_win: + try: + if self.controls_win.winfo_exists(): + self.controls_win.destroy() + self.controls_win = None + except Exception as e: + print(f"[SETTINGS] Error destroying old controls: {e}") + + # Recreate controls + self.create_controls() + print("[SETTINGS] Controls recreated") + + # Ensure exit button is properly configured after recreation + if hasattr(self, 'exit_btn') and self.exit_btn: + self.exit_btn.config(command=self.exit_app) + print("[SETTINGS] Exit button command restored") + + # Force focus back to main window + self.root.focus_force() + self.root.lift() + print("[SETTINGS] Focus restored to main window") + + # Resume VLC if it was paused if hasattr(self, 'vlc_player') and self.vlc_player: try: - # Only resume if it was paused by us if self.vlc_player.get_state() == vlc.State.Paused: self.vlc_player.play() - except Exception: - pass - else: - self.root.after(1000, lambda: self.check_settings_closed(proc)) + print("[SETTINGS] VLC playback resumed") + except Exception as e: + print(f"[SETTINGS] Error resuming VLC: {e}") + + print("[SETTINGS] Player functionality fully restored") + + except Exception as e: + print(f"[SETTINGS] Error restoring functionality: {e}") def main_start(self): self.play_intro_video()