Improve robustness: add watchdog timers, error handling, and logging to prevent player freeze

This commit is contained in:
2025-08-29 13:23:40 +03:00
parent 8d69a737f9
commit 2a564f5e84
18 changed files with 3303 additions and 73 deletions

View File

@@ -98,6 +98,8 @@ class SimpleTkPlayer:
pass
def show_controls(self):
if not hasattr(self, 'controls_win') or self.controls_win is None or not self.controls_win.winfo_exists():
self.create_controls()
self.controls_win.deiconify()
self.controls_win.lift()
self.show_mouse()
@@ -162,73 +164,141 @@ class SimpleTkPlayer:
self.show_current_media()
self.root.after(100, self.next_media_loop)
def show_video(self, file_path, on_end=None):
if hasattr(self, 'vlc_player') and self.vlc_player:
self.vlc_player.stop()
if not hasattr(self, 'video_canvas'):
self.video_canvas = tk.Canvas(self.root, bg='black', highlightthickness=0)
def show_video(self, file_path, on_end=None, duration=None):
try:
print(f"[PLAYER] Attempting to play video: {file_path}")
if hasattr(self, 'vlc_player') and self.vlc_player:
self.vlc_player.stop()
if not hasattr(self, 'video_canvas'):
self.video_canvas = tk.Canvas(self.root, bg='black', highlightthickness=0)
self.video_canvas.pack(fill=tk.BOTH, expand=True)
self.label.pack_forget()
self.video_canvas.pack(fill=tk.BOTH, expand=True)
self.label.pack_forget()
self.video_canvas.pack(fill=tk.BOTH, expand=True)
self.root.attributes('-fullscreen', True)
self.root.update_idletasks()
self.vlc_instance = vlc.Instance('--vout=x11')
self.vlc_player = self.vlc_instance.media_player_new()
self.vlc_player.set_mrl(file_path)
self.vlc_player.set_fullscreen(True)
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
self.vlc_player.play()
def check_end():
if self.vlc_player.get_state() == vlc.State.Ended:
self.root.attributes('-fullscreen', True)
self.root.update_idletasks()
self.vlc_instance = vlc.Instance('--vout=x11')
self.vlc_player = self.vlc_instance.media_player_new()
self.vlc_player.set_mrl(file_path)
self.vlc_player.set_fullscreen(True)
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
self.vlc_player.play()
# Watchdog timer: fallback if video doesn't end
def watchdog():
print(f"[WATCHDOG] Video watchdog triggered for {file_path}")
self.vlc_player.stop()
self.video_canvas.pack_forget()
self.label.pack(fill=tk.BOTH, expand=True)
if on_end:
on_end()
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'):
self.root.after_cancel(self.video_watchdog)
self.vlc_player.stop()
self.video_canvas.pack_forget()
self.label.pack(fill=tk.BOTH, expand=True)
if on_end:
on_end()
if duration is not None:
self.root.after(int(duration * 1000), finish_video)
else:
self.root.after(200, check_end)
check_end()
def check_end():
try:
if self.vlc_player.get_state() == vlc.State.Ended:
finish_video()
elif self.vlc_player.get_state() == vlc.State.Error:
print(f"[VLC] Error state detected for {file_path}")
finish_video()
else:
self.root.after(200, check_end)
except Exception as e:
print(f"[VLC] Exception in check_end: {e}")
finish_video()
check_end()
except Exception as e:
print(f"[VLC] Error playing video {file_path}: {e}")
if on_end:
on_end()
def show_current_media(self):
self.root.attributes('-fullscreen', True)
self.root.update_idletasks()
if not self.playlist:
print("[PLAYER] Playlist is empty. No media to show.")
self.label.config(text="No media available", fg='white', font=('Arial', 32))
# Try to reload playlist after 10 seconds
self.root.after(10000, self.reload_playlist_and_continue)
return
media = self.playlist[self.current_index]
file_path = os.path.join(MEDIA_DATA_PATH, media['file_name'])
ext = file_path.lower()
duration = media.get('duration', None)
if not os.path.isfile(file_path):
print(f"[PLAYER] File missing: {file_path}. Skipping to next.")
self.next_media()
return
if ext.endswith(('.mp4', '.avi', '.mov', '.mkv')):
self.show_video(file_path, on_end=self.next_media)
self.show_video(file_path, on_end=self.next_media, duration=duration)
elif ext.endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')):
self.show_image_via_vlc(file_path, media.get('duration', 10), on_end=self.next_media)
self.show_image_via_vlc(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')
self.root.after(2000, self.next_media)
def reload_playlist_and_continue(self):
print("[PLAYER] Attempting to reload playlist...")
new_playlist = load_latest_playlist()
if new_playlist:
self.playlist = new_playlist
self.current_index = 0
print("[PLAYER] Playlist reloaded. Continuing playback.")
self.show_current_media()
else:
print("[PLAYER] Still no playlist. Will retry.")
self.root.after(10000, self.reload_playlist_and_continue)
def show_image_via_vlc(self, file_path, duration, on_end=None):
# Use VLC to show image for a set duration
if hasattr(self, 'vlc_player') and self.vlc_player:
self.vlc_player.stop()
if not hasattr(self, 'video_canvas'):
self.video_canvas = tk.Canvas(self.root, bg='black', highlightthickness=0)
try:
print(f"[PLAYER] Attempting to show image: {file_path}")
if hasattr(self, 'vlc_player') and self.vlc_player:
self.vlc_player.stop()
if not hasattr(self, 'video_canvas'):
self.video_canvas = tk.Canvas(self.root, bg='black', highlightthickness=0)
self.video_canvas.pack(fill=tk.BOTH, expand=True)
self.label.pack_forget()
self.video_canvas.pack(fill=tk.BOTH, expand=True)
self.label.pack_forget()
self.video_canvas.pack(fill=tk.BOTH, expand=True)
self.root.attributes('-fullscreen', True)
self.root.update_idletasks()
self.vlc_instance = vlc.Instance('--vout=x11')
self.vlc_player = self.vlc_instance.media_player_new()
self.vlc_player.set_mrl(file_path)
self.vlc_player.set_fullscreen(True)
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
self.vlc_player.play()
# Schedule stop and next after duration
def finish_image():
self.vlc_player.stop()
self.video_canvas.pack_forget()
self.label.pack(fill=tk.BOTH, expand=True)
self.root.attributes('-fullscreen', True)
self.root.update_idletasks()
self.vlc_instance = vlc.Instance('--vout=x11')
self.vlc_player = self.vlc_instance.media_player_new()
self.vlc_player.set_mrl(file_path)
self.vlc_player.set_fullscreen(True)
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
self.vlc_player.play()
# Watchdog timer: fallback if image doesn't advance
def watchdog():
print(f"[WATCHDOG] Image watchdog triggered for {file_path}")
self.vlc_player.stop()
self.video_canvas.pack_forget()
self.label.pack(fill=tk.BOTH, expand=True)
if on_end:
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)
if on_end:
on_end()
self.root.after(int(duration * 1000), finish_image)
except Exception as e:
print(f"[VLC] Error showing image {file_path}: {e}")
if on_end:
on_end()
self.root.after(int(duration * 1000), finish_image)
def next_media(self):
self.current_index = (self.current_index + 1) % len(self.playlist)
@@ -241,20 +311,49 @@ class SimpleTkPlayer:
self.show_current_media()
def exit_app(self):
# Signal the update thread to stop if stop_event is present
# 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
# Unbind all events to prevent callbacks after destroy
try:
self.root.unbind('<Motion>')
self.root.unbind('<Button-1>')
self.root.unbind('<Configure>')
except Exception:
pass
# Attempt to destroy all Toplevel windows before root
try:
# Withdraw controls_win if it exists
if hasattr(self, 'controls_win') and self.controls_win:
self.controls_win.destroy()
except:
pass
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
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:
widget.destroy()
except Exception:
pass
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)
try:
self.root.destroy()
except:
pass
if self.root.winfo_exists():
self.root.destroy()
except Exception as e:
print(f"[EXIT] Error destroying root: {e}")
def open_settings(self):
if self.paused is not True:
@@ -285,6 +384,9 @@ class SimpleTkPlayer:
# Restore and recreate controls overlay
self.root.deiconify()
self.create_controls()
# Re-bind mouse and button events to new controls
self.root.bind('<Motion>', self.on_activity)
self.root.bind('<Button-1>', self.on_activity)
self.show_controls()
if hasattr(self, 'vlc_player') and self.vlc_player:
try:
@@ -308,4 +410,14 @@ def load_latest_playlist():
latest_file = files[0]
with open(os.path.join(PLAYLIST_DIR, latest_file), 'r') as f:
data = json.load(f)
return data.get('playlist', [])
playlist = data.get('playlist', [])
# Validate playlist: skip missing or unsupported files
valid_exts = ('.mp4', '.avi', '.mov', '.mkv', '.jpg', '.jpeg', '.png', '.bmp', '.gif')
valid_playlist = []
for item in playlist:
file_path = os.path.join(MEDIA_DATA_PATH, item.get('file_name', ''))
if os.path.isfile(file_path) and file_path.lower().endswith(valid_exts):
valid_playlist.append(item)
else:
print(f"[PLAYLIST] Skipping missing or unsupported file: {item.get('file_name')}")
return valid_playlist