Improve robustness: add watchdog timers, error handling, and logging to prevent player freeze
This commit is contained in:
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Core requirements for tkinter_player signage app
|
||||||
|
requests
|
||||||
|
bcrypt
|
||||||
|
python-vlc
|
||||||
|
pyautogui
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
|
|
||||||
# Change to the tkinter app src directory
|
# Change to the tkinter app src directory
|
||||||
cd tkinter_app/src
|
cd signage_player/src
|
||||||
|
|
||||||
# Run the main application with full error output
|
# Run the main application with full error output
|
||||||
python main.py
|
python main.py
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -12,6 +12,8 @@ class AppSettingsWindow(tk.Tk):
|
|||||||
self.geometry('440x600') # Increased height for better button visibility
|
self.geometry('440x600') # Increased height for better button visibility
|
||||||
self.resizable(False, False)
|
self.resizable(False, False)
|
||||||
self.config(bg='#23272e')
|
self.config(bg='#23272e')
|
||||||
|
self.attributes('-topmost', True)
|
||||||
|
self.focus_force()
|
||||||
self.fields = {}
|
self.fields = {}
|
||||||
self.load_config()
|
self.load_config()
|
||||||
self.style = ttk.Style(self)
|
self.style = ttk.Style(self)
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ def main():
|
|||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
app_running[0] = False
|
app_running[0] = False
|
||||||
update_thread.join()
|
# Do not join the update_thread; let it exit as a daemon
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"screen_orientation": "Landscape",
|
"screen_orientation": "Landscape",
|
||||||
"screen_name": "tv-terasa",
|
"screen_name": "tv-terasa",
|
||||||
"quickconnect_key": "8887779",
|
"quickconnect_key": "8887779",
|
||||||
"server_ip": "192.168.1.22",
|
"server_ip": "10.232.7.231",
|
||||||
"port": "80",
|
"port": "80",
|
||||||
"screen_w": "1920",
|
"screen_w": "1920",
|
||||||
"screen_h": "1080",
|
"screen_h": "1080",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
353
signage_player/player copy vineri.py
Normal file
353
signage_player/player copy vineri.py
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import tkinter as tk
|
||||||
|
import vlc
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'main_data', 'app_config.txt')
|
||||||
|
PLAYLIST_DIR = os.path.join(os.path.dirname(__file__), 'static_data', 'playlist')
|
||||||
|
MEDIA_DATA_PATH = os.path.join(os.path.dirname(__file__), 'static_data', 'media')
|
||||||
|
|
||||||
|
class SimpleTkPlayer:
|
||||||
|
def __init__(self, root, playlist):
|
||||||
|
self.root = root
|
||||||
|
self.playlist = playlist
|
||||||
|
self.current_index = 0
|
||||||
|
self.paused = False
|
||||||
|
self.pause_timer = None
|
||||||
|
self.label = tk.Label(root, bg='black')
|
||||||
|
self.label.pack(fill=tk.BOTH, expand=True)
|
||||||
|
self.create_controls()
|
||||||
|
self.hide_controls()
|
||||||
|
self.root.bind('<Motion>', self.on_activity)
|
||||||
|
self.root.bind('<Button-1>', self.on_activity)
|
||||||
|
self.root.after(100, self.ensure_fullscreen)
|
||||||
|
self.root.after(200, self.hide_mouse)
|
||||||
|
self.root.after(300, self.move_mouse_to_corner)
|
||||||
|
self.root.protocol('WM_DELETE_WINDOW', self.exit_app)
|
||||||
|
|
||||||
|
def ensure_fullscreen(self):
|
||||||
|
self.root.attributes('-fullscreen', True)
|
||||||
|
self.root.update_idletasks()
|
||||||
|
|
||||||
|
def create_controls(self):
|
||||||
|
# Create a transparent, borderless top-level window for controls
|
||||||
|
self.controls_win = tk.Toplevel(self.root)
|
||||||
|
self.controls_win.overrideredirect(True)
|
||||||
|
self.controls_win.attributes('-topmost', True)
|
||||||
|
self.controls_win.attributes('-alpha', 0.92)
|
||||||
|
self.controls_win.configure(bg='')
|
||||||
|
# Place the window at the bottom right
|
||||||
|
def place_controls():
|
||||||
|
self.controls_win.update_idletasks()
|
||||||
|
w = self.controls_win.winfo_reqwidth()
|
||||||
|
h = self.controls_win.winfo_reqheight()
|
||||||
|
sw = self.root.winfo_screenwidth()
|
||||||
|
sh = self.root.winfo_screenheight()
|
||||||
|
x = sw - w - 30
|
||||||
|
y = sh - h - 30
|
||||||
|
self.controls_win.geometry(f'+{x}+{y}')
|
||||||
|
self.controls_frame = tk.Frame(self.controls_win, bg='#222', bd=2, relief='ridge')
|
||||||
|
self.controls_frame.pack()
|
||||||
|
btn_style = {
|
||||||
|
'bg': '#333',
|
||||||
|
'fg': 'white',
|
||||||
|
'activebackground': '#555',
|
||||||
|
'activeforeground': '#00e6e6',
|
||||||
|
'font': ('Arial', 16, 'bold'),
|
||||||
|
'bd': 0,
|
||||||
|
'highlightthickness': 0,
|
||||||
|
'relief': 'flat',
|
||||||
|
'cursor': 'hand2',
|
||||||
|
'padx': 10,
|
||||||
|
'pady': 6
|
||||||
|
}
|
||||||
|
self.prev_btn = tk.Button(self.controls_frame, text='⏮ Prev', command=self.prev_media, **btn_style)
|
||||||
|
self.prev_btn.grid(row=0, column=0, padx=4)
|
||||||
|
self.pause_btn = tk.Button(self.controls_frame, text='⏸ Pause', command=self.toggle_pause, **btn_style)
|
||||||
|
self.pause_btn.grid(row=0, column=1, padx=4)
|
||||||
|
self.next_btn = tk.Button(self.controls_frame, text='Next ⏭', command=self.next_media, **btn_style)
|
||||||
|
self.next_btn.grid(row=0, column=2, padx=4)
|
||||||
|
self.settings_btn = tk.Button(self.controls_frame, text='⚙ Settings', command=self.open_settings, **btn_style)
|
||||||
|
self.settings_btn.grid(row=0, column=3, padx=4)
|
||||||
|
self.exit_btn = tk.Button(self.controls_frame, text='⏻ Exit', command=self.exit_app, **btn_style)
|
||||||
|
self.exit_btn.grid(row=0, column=4, padx=4)
|
||||||
|
self.exit_btn.config(fg='#ff4d4d')
|
||||||
|
self.controls_win.withdraw()
|
||||||
|
self.controls_win.after(200, place_controls)
|
||||||
|
self.root.bind('<Configure>', lambda e: self.controls_win.after(200, place_controls))
|
||||||
|
|
||||||
|
def hide_mouse(self):
|
||||||
|
self.root.config(cursor='none')
|
||||||
|
if hasattr(self, 'controls_win'):
|
||||||
|
self.controls_win.config(cursor='none')
|
||||||
|
|
||||||
|
def show_mouse(self):
|
||||||
|
self.root.config(cursor='arrow')
|
||||||
|
if hasattr(self, 'controls_win'):
|
||||||
|
self.controls_win.config(cursor='arrow')
|
||||||
|
|
||||||
|
def move_mouse_to_corner(self):
|
||||||
|
try:
|
||||||
|
import pyautogui
|
||||||
|
sw = self.root.winfo_screenwidth()
|
||||||
|
sh = self.root.winfo_screenheight()
|
||||||
|
pyautogui.moveTo(sw-2, sh-2)
|
||||||
|
except Exception:
|
||||||
|
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()
|
||||||
|
self.schedule_hide_controls()
|
||||||
|
|
||||||
|
def hide_controls(self):
|
||||||
|
self.controls_win.withdraw()
|
||||||
|
self.hide_mouse()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def on_activity(self, event=None):
|
||||||
|
self.show_controls()
|
||||||
|
|
||||||
|
def prev_media(self):
|
||||||
|
self.current_index = (self.current_index - 1) % len(self.playlist)
|
||||||
|
self.show_current_media()
|
||||||
|
|
||||||
|
def next_media(self):
|
||||||
|
# If at the last media, update playlist before looping
|
||||||
|
if self.current_index == len(self.playlist) - 1:
|
||||||
|
self.update_playlist_from_server()
|
||||||
|
self.current_index = 0
|
||||||
|
self.show_current_media()
|
||||||
|
else:
|
||||||
|
self.current_index = (self.current_index + 1) % len(self.playlist)
|
||||||
|
self.show_current_media()
|
||||||
|
|
||||||
|
def update_playlist_from_server(self):
|
||||||
|
# Dummy implementation: replace with your actual update logic
|
||||||
|
# For example, call a function to fetch and reload the playlist
|
||||||
|
print("[INFO] Updating playlist from server...")
|
||||||
|
# You can import and call your real update function here
|
||||||
|
# Example: self.playlist = get_latest_playlist()
|
||||||
|
|
||||||
|
def toggle_pause(self):
|
||||||
|
if not self.paused:
|
||||||
|
self.paused = True
|
||||||
|
self.pause_btn.config(text='▶ Resume')
|
||||||
|
self.pause_timer = self.root.after(30000, self.resume_play)
|
||||||
|
else:
|
||||||
|
self.resume_play()
|
||||||
|
|
||||||
|
def resume_play(self):
|
||||||
|
self.paused = False
|
||||||
|
self.pause_btn.config(text='⏸ Pause')
|
||||||
|
if self.pause_timer:
|
||||||
|
self.root.after_cancel(self.pause_timer)
|
||||||
|
self.pause_timer = None
|
||||||
|
|
||||||
|
def play_intro_video(self):
|
||||||
|
intro_path = os.path.join(os.path.dirname(__file__), 'main_data', 'intro1.mp4')
|
||||||
|
if os.path.exists(intro_path):
|
||||||
|
self.show_video(intro_path, on_end=self.after_intro)
|
||||||
|
else:
|
||||||
|
self.after_intro()
|
||||||
|
|
||||||
|
def after_intro(self):
|
||||||
|
self.show_current_media()
|
||||||
|
self.root.after(100, self.next_media_loop)
|
||||||
|
|
||||||
|
def show_video(self, file_path, on_end=None, duration=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)
|
||||||
|
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()
|
||||||
|
# Force video to play for the specified duration
|
||||||
|
def finish_video():
|
||||||
|
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:
|
||||||
|
def check_end():
|
||||||
|
if self.vlc_player.get_state() == vlc.State.Ended:
|
||||||
|
finish_video()
|
||||||
|
else:
|
||||||
|
self.root.after(200, check_end)
|
||||||
|
check_end()
|
||||||
|
|
||||||
|
def show_current_media(self):
|
||||||
|
self.root.attributes('-fullscreen', True)
|
||||||
|
self.root.update_idletasks()
|
||||||
|
if not self.playlist:
|
||||||
|
self.label.config(text="No media available", fg='white', font=('Arial', 32))
|
||||||
|
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 ext.endswith(('.mp4', '.avi', '.mov', '.mkv')):
|
||||||
|
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, duration if duration is not None else 10, on_end=self.next_media)
|
||||||
|
else:
|
||||||
|
self.label.config(text=f"Unsupported: {media['file_name']}", fg='yellow')
|
||||||
|
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
self.show_current_media()
|
||||||
|
|
||||||
|
def next_media_loop(self):
|
||||||
|
if not self.playlist or self.paused:
|
||||||
|
self.root.after(1000, self.next_media_loop)
|
||||||
|
return
|
||||||
|
self.show_current_media()
|
||||||
|
|
||||||
|
def exit_app(self):
|
||||||
|
# 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:
|
||||||
|
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:
|
||||||
|
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:
|
||||||
|
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
|
||||||
|
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))
|
||||||
|
|
||||||
|
def check_settings_closed(self, proc):
|
||||||
|
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
|
||||||
|
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:
|
||||||
|
# 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))
|
||||||
|
|
||||||
|
def main_start(self):
|
||||||
|
self.play_intro_video()
|
||||||
|
|
||||||
|
def load_latest_playlist():
|
||||||
|
files = [f for f in os.listdir(PLAYLIST_DIR) if f.startswith('server_playlist_v') and f.endswith('.json')]
|
||||||
|
if not files:
|
||||||
|
return []
|
||||||
|
# Sort by version number descending
|
||||||
|
files.sort(key=lambda x: int(x.split('_v')[-1].split('.json')[0]), reverse=True)
|
||||||
|
latest_file = files[0]
|
||||||
|
with open(os.path.join(PLAYLIST_DIR, latest_file), 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return data.get('playlist', [])
|
||||||
@@ -98,6 +98,8 @@ class SimpleTkPlayer:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def show_controls(self):
|
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.deiconify()
|
||||||
self.controls_win.lift()
|
self.controls_win.lift()
|
||||||
self.show_mouse()
|
self.show_mouse()
|
||||||
@@ -162,7 +164,9 @@ class SimpleTkPlayer:
|
|||||||
self.show_current_media()
|
self.show_current_media()
|
||||||
self.root.after(100, self.next_media_loop)
|
self.root.after(100, self.next_media_loop)
|
||||||
|
|
||||||
def show_video(self, file_path, on_end=None):
|
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:
|
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||||
self.vlc_player.stop()
|
self.vlc_player.stop()
|
||||||
if not hasattr(self, 'video_canvas'):
|
if not hasattr(self, 'video_canvas'):
|
||||||
@@ -178,34 +182,86 @@ class SimpleTkPlayer:
|
|||||||
self.vlc_player.set_fullscreen(True)
|
self.vlc_player.set_fullscreen(True)
|
||||||
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
|
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
|
||||||
self.vlc_player.play()
|
self.vlc_player.play()
|
||||||
def check_end():
|
# Watchdog timer: fallback if video doesn't end
|
||||||
if self.vlc_player.get_state() == vlc.State.Ended:
|
def watchdog():
|
||||||
|
print(f"[WATCHDOG] Video watchdog triggered for {file_path}")
|
||||||
|
self.vlc_player.stop()
|
||||||
self.video_canvas.pack_forget()
|
self.video_canvas.pack_forget()
|
||||||
self.label.pack(fill=tk.BOTH, expand=True)
|
self.label.pack(fill=tk.BOTH, expand=True)
|
||||||
if on_end:
|
if on_end:
|
||||||
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:
|
||||||
|
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:
|
else:
|
||||||
self.root.after(200, check_end)
|
self.root.after(200, check_end)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[VLC] Exception in check_end: {e}")
|
||||||
|
finish_video()
|
||||||
check_end()
|
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):
|
def show_current_media(self):
|
||||||
self.root.attributes('-fullscreen', True)
|
self.root.attributes('-fullscreen', True)
|
||||||
self.root.update_idletasks()
|
self.root.update_idletasks()
|
||||||
if not self.playlist:
|
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))
|
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
|
return
|
||||||
media = self.playlist[self.current_index]
|
media = self.playlist[self.current_index]
|
||||||
file_path = os.path.join(MEDIA_DATA_PATH, media['file_name'])
|
file_path = os.path.join(MEDIA_DATA_PATH, media['file_name'])
|
||||||
ext = file_path.lower()
|
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')):
|
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')):
|
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:
|
else:
|
||||||
|
print(f"[PLAYER] Unsupported file type: {media['file_name']}")
|
||||||
self.label.config(text=f"Unsupported: {media['file_name']}", fg='yellow')
|
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):
|
def show_image_via_vlc(self, file_path, duration, on_end=None):
|
||||||
# Use VLC to show image for a set duration
|
try:
|
||||||
|
print(f"[PLAYER] Attempting to show image: {file_path}")
|
||||||
if hasattr(self, 'vlc_player') and self.vlc_player:
|
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||||
self.vlc_player.stop()
|
self.vlc_player.stop()
|
||||||
if not hasattr(self, 'video_canvas'):
|
if not hasattr(self, 'video_canvas'):
|
||||||
@@ -221,14 +277,28 @@ class SimpleTkPlayer:
|
|||||||
self.vlc_player.set_fullscreen(True)
|
self.vlc_player.set_fullscreen(True)
|
||||||
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
|
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
|
||||||
self.vlc_player.play()
|
self.vlc_player.play()
|
||||||
# Schedule stop and next after duration
|
# 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():
|
def finish_image():
|
||||||
|
if hasattr(self, 'image_watchdog'):
|
||||||
|
self.root.after_cancel(self.image_watchdog)
|
||||||
self.vlc_player.stop()
|
self.vlc_player.stop()
|
||||||
self.video_canvas.pack_forget()
|
self.video_canvas.pack_forget()
|
||||||
self.label.pack(fill=tk.BOTH, expand=True)
|
self.label.pack(fill=tk.BOTH, expand=True)
|
||||||
if on_end:
|
if on_end:
|
||||||
on_end()
|
on_end()
|
||||||
self.root.after(int(duration * 1000), finish_image)
|
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()
|
||||||
|
|
||||||
def next_media(self):
|
def next_media(self):
|
||||||
self.current_index = (self.current_index + 1) % len(self.playlist)
|
self.current_index = (self.current_index + 1) % len(self.playlist)
|
||||||
@@ -241,20 +311,49 @@ class SimpleTkPlayer:
|
|||||||
self.show_current_media()
|
self.show_current_media()
|
||||||
|
|
||||||
def exit_app(self):
|
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:
|
if hasattr(self, 'stop_event') and self.stop_event:
|
||||||
self.stop_event.set()
|
self.stop_event.set()
|
||||||
if hasattr(self, 'app_running') and self.app_running:
|
if hasattr(self, 'app_running') and self.app_running:
|
||||||
self.app_running[0] = False
|
self.app_running[0] = False
|
||||||
|
# Unbind all events to prevent callbacks after destroy
|
||||||
try:
|
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:
|
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.destroy()
|
||||||
except:
|
self.controls_win = None
|
||||||
pass
|
# 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:
|
try:
|
||||||
self.root.destroy()
|
widget.destroy()
|
||||||
except:
|
except Exception:
|
||||||
pass
|
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:
|
||||||
|
if self.root.winfo_exists():
|
||||||
|
self.root.destroy()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[EXIT] Error destroying root: {e}")
|
||||||
|
|
||||||
def open_settings(self):
|
def open_settings(self):
|
||||||
if self.paused is not True:
|
if self.paused is not True:
|
||||||
@@ -285,6 +384,9 @@ class SimpleTkPlayer:
|
|||||||
# Restore and recreate controls overlay
|
# Restore and recreate controls overlay
|
||||||
self.root.deiconify()
|
self.root.deiconify()
|
||||||
self.create_controls()
|
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()
|
self.show_controls()
|
||||||
if hasattr(self, 'vlc_player') and self.vlc_player:
|
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||||
try:
|
try:
|
||||||
@@ -308,4 +410,14 @@ def load_latest_playlist():
|
|||||||
latest_file = files[0]
|
latest_file = files[0]
|
||||||
with open(os.path.join(PLAYLIST_DIR, latest_file), 'r') as f:
|
with open(os.path.join(PLAYLIST_DIR, latest_file), 'r') as f:
|
||||||
data = json.load(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
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 523 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 537 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.2 MiB |
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"playlist": [
|
|
||||||
{
|
|
||||||
"file_name": "big-buck-bunny-1080p-60fps-30sec.mp4",
|
|
||||||
"url": "media/big-buck-bunny-1080p-60fps-30sec.mp4",
|
|
||||||
"duration": 30
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file_name": "demo2.jpeg",
|
|
||||||
"url": "media/demo2.jpeg",
|
|
||||||
"duration": 50
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file_name": "call-of-duty-black-3840x2160-23674.jpg",
|
|
||||||
"url": "media/call-of-duty-black-3840x2160-23674.jpg",
|
|
||||||
"duration": 30
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"version": 4
|
|
||||||
}
|
|
||||||
20
signage_player/static_data/playlist/server_playlist_v6.json
Normal file
20
signage_player/static_data/playlist/server_playlist_v6.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"playlist": [
|
||||||
|
{
|
||||||
|
"file_name": "HARTING_Safety_day_informare_2_page_003.jpg",
|
||||||
|
"url": "media/HARTING_Safety_day_informare_2_page_003.jpg",
|
||||||
|
"duration": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "big-buck-bunny-1080p-60fps-30sec.mp4",
|
||||||
|
"url": "media/big-buck-bunny-1080p-60fps-30sec.mp4",
|
||||||
|
"duration": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "one-piece-season-2-5120x2880-23673.jpg",
|
||||||
|
"url": "media/one-piece-season-2-5120x2880-23673.jpg",
|
||||||
|
"duration": 30
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": 6
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user