final touches
This commit is contained in:
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+17
-1
@@ -10,11 +10,27 @@ import sys
|
|||||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
|
||||||
# Import the player module from player_app.py
|
from logging_config import Logger
|
||||||
from player_app import SimpleMediaPlayerApp
|
from player_app import SimpleMediaPlayerApp
|
||||||
|
from splash_screen import SplashScreen
|
||||||
|
from playlist_manager import PlaylistManager
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
|
# Play splash screen first, then start player
|
||||||
|
playlist_manager = PlaylistManager()
|
||||||
|
playlist_manager.fetch_playlist() # Start fetching playlist in parallel with splash
|
||||||
|
intro_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'intro1.mp4')
|
||||||
|
Logger.info(f"[MAIN] About to show splash screen: {intro_path}")
|
||||||
|
def start_player():
|
||||||
|
Logger.info("[MAIN] Splash finished, waiting for playlist...")
|
||||||
|
playlist = playlist_manager.wait_for_playlist()
|
||||||
|
Logger.info(f"[MAIN] Playlist loaded: {playlist}")
|
||||||
player = SimpleMediaPlayerApp(root)
|
player = SimpleMediaPlayerApp(root)
|
||||||
|
player.playlist = playlist or []
|
||||||
player.run()
|
player.run()
|
||||||
|
splash = SplashScreen(root, intro_path, duration=10)
|
||||||
|
splash.show(on_finish=start_player)
|
||||||
|
Logger.info("[MAIN] splash.show() called, entering mainloop...")
|
||||||
|
root.mainloop()
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import threading
|
||||||
|
import os
|
||||||
|
from logging_config import Logger
|
||||||
|
from PIL import Image, ImageTk
|
||||||
|
|
||||||
|
class MediaPlaybackController:
|
||||||
|
def __init__(self, app, ui):
|
||||||
|
self.app = app # Reference to SimpleMediaPlayerApp
|
||||||
|
self.ui = ui # Reference to PlayerUI
|
||||||
|
self.auto_advance_timer = None
|
||||||
|
self.current_index = 0
|
||||||
|
self.playlist = []
|
||||||
|
self.scaling_mode = 'fit'
|
||||||
|
|
||||||
|
def set_playlist(self, playlist):
|
||||||
|
self.playlist = playlist
|
||||||
|
self.current_index = 0
|
||||||
|
|
||||||
|
def play_current_media(self):
|
||||||
|
if not self.playlist or self.current_index >= len(self.playlist):
|
||||||
|
self.show_no_content_message()
|
||||||
|
return
|
||||||
|
media = self.playlist[self.current_index]
|
||||||
|
file_path = media.get('url', '')
|
||||||
|
file_name = media.get('file_name', '')
|
||||||
|
duration = media.get('duration', 10)
|
||||||
|
if file_path.startswith('static/resurse/'):
|
||||||
|
absolute_path = os.path.join(os.path.dirname(__file__), file_path)
|
||||||
|
file_path = absolute_path
|
||||||
|
Logger.info(f"Playing media: {file_name} from {file_path}")
|
||||||
|
self.log_event(file_name, "STARTED")
|
||||||
|
self.cancel_timers()
|
||||||
|
if file_path.startswith('text://'):
|
||||||
|
self.show_text_content(file_path[7:], duration)
|
||||||
|
elif file_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')):
|
||||||
|
self.play_video(file_path)
|
||||||
|
elif os.path.exists(file_path) and file_path.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp')):
|
||||||
|
self.show_image(file_path, duration)
|
||||||
|
else:
|
||||||
|
Logger.error(f"Unsupported or missing media: {file_path}")
|
||||||
|
self.ui.status_label.config(text=f"Missing or unsupported media:\n{file_name}")
|
||||||
|
self.auto_advance_timer = self.ui.root.after(5000, self.next_media)
|
||||||
|
|
||||||
|
def play_video(self, file_path):
|
||||||
|
self.ui.status_label.place_forget()
|
||||||
|
def run_vlc_subprocess():
|
||||||
|
import subprocess
|
||||||
|
try:
|
||||||
|
Logger.info(f"Starting system VLC subprocess for video: {file_path}")
|
||||||
|
vlc_cmd = [
|
||||||
|
'cvlc',
|
||||||
|
'--fullscreen',
|
||||||
|
'--no-osd',
|
||||||
|
'--no-video-title-show',
|
||||||
|
'--play-and-exit',
|
||||||
|
'--quiet',
|
||||||
|
file_path
|
||||||
|
]
|
||||||
|
proc = subprocess.Popen(vlc_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
proc.wait()
|
||||||
|
Logger.info(f"VLC subprocess finished: {file_path}")
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"VLC subprocess error: {e}")
|
||||||
|
finally:
|
||||||
|
self.ui.root.after_idle(lambda: setattr(self, 'auto_advance_timer', self.ui.root.after(1000, self.next_media)))
|
||||||
|
threading.Thread(target=run_vlc_subprocess, daemon=True).start()
|
||||||
|
|
||||||
|
def show_image(self, file_path, duration):
|
||||||
|
try:
|
||||||
|
self.ui.status_label.place_forget()
|
||||||
|
self.ui.status_label.config(text="")
|
||||||
|
img = Image.open(file_path)
|
||||||
|
original_size = img.size
|
||||||
|
screen_width = self.ui.root.winfo_width()
|
||||||
|
screen_height = self.ui.root.winfo_height()
|
||||||
|
if screen_width <= 1 or screen_height <= 1:
|
||||||
|
screen_width = 1920
|
||||||
|
screen_height = 1080
|
||||||
|
final_img, offset = self.ui.scale_image_to_screen(img, screen_width, screen_height, self.scaling_mode)
|
||||||
|
photo = ImageTk.PhotoImage(final_img)
|
||||||
|
self.ui.image_label.config(image=photo)
|
||||||
|
self.ui.image_label.image = photo
|
||||||
|
Logger.info(f"Successfully displayed image: {os.path.basename(file_path)} "
|
||||||
|
f"(Original: {original_size}, Screen: {screen_width}x{screen_height}, "
|
||||||
|
f"Mode: {self.scaling_mode}, Offset: {offset})")
|
||||||
|
self.auto_advance_timer = self.ui.root.after(
|
||||||
|
int(duration * 1000),
|
||||||
|
self.next_media
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"Failed to show image {file_path}: {e}")
|
||||||
|
self.ui.image_label.config(image='')
|
||||||
|
self.ui.status_label.config(text=f"Image Error:\n{os.path.basename(file_path)}\n{str(e)}")
|
||||||
|
self.ui.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
||||||
|
self.auto_advance_timer = self.ui.root.after(5000, self.next_media)
|
||||||
|
|
||||||
|
def show_text_content(self, text, duration):
|
||||||
|
self.ui.image_label.config(image='')
|
||||||
|
self.ui.status_label.config(text=text)
|
||||||
|
self.auto_advance_timer = self.ui.root.after(
|
||||||
|
int(duration * 1000),
|
||||||
|
self.next_media
|
||||||
|
)
|
||||||
|
|
||||||
|
def next_media(self):
|
||||||
|
self.cancel_timers()
|
||||||
|
if not self.playlist:
|
||||||
|
return
|
||||||
|
self.current_index = (self.current_index + 1) % len(self.playlist)
|
||||||
|
self.play_current_media()
|
||||||
|
|
||||||
|
def previous_media(self):
|
||||||
|
self.cancel_timers()
|
||||||
|
if not self.playlist:
|
||||||
|
return
|
||||||
|
self.current_index = (self.current_index - 1) % len(self.playlist)
|
||||||
|
self.play_current_media()
|
||||||
|
|
||||||
|
def toggle_play_pause(self):
|
||||||
|
self.app.is_paused = not self.app.is_paused
|
||||||
|
if self.app.is_paused:
|
||||||
|
self.ui.play_pause_btn.config(text="▶ Play")
|
||||||
|
self.cancel_timers()
|
||||||
|
else:
|
||||||
|
self.ui.play_pause_btn.config(text="⏸ Pause")
|
||||||
|
self.play_current_media()
|
||||||
|
Logger.info(f"Media {'paused' if self.app.is_paused else 'resumed'}")
|
||||||
|
|
||||||
|
def cancel_timers(self):
|
||||||
|
if self.auto_advance_timer:
|
||||||
|
self.ui.root.after_cancel(self.auto_advance_timer)
|
||||||
|
self.auto_advance_timer = None
|
||||||
|
|
||||||
|
def show_no_content_message(self):
|
||||||
|
self.ui.status_label.config(text="No content available.")
|
||||||
|
|
||||||
|
def log_event(self, file_name, event):
|
||||||
|
import datetime
|
||||||
|
try:
|
||||||
|
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
log_message = f"{timestamp} - {event}: {file_name}\n"
|
||||||
|
log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'log.txt')
|
||||||
|
with open(log_file, 'a') as f:
|
||||||
|
f.write(log_message)
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"Failed to log event: {e}")
|
||||||
+107
-429
@@ -13,7 +13,12 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import requests # Required for server communication
|
import requests # Required for server communication
|
||||||
import queue
|
import queue
|
||||||
|
from media_playback_controller import MediaPlaybackController
|
||||||
|
try:
|
||||||
import vlc # For video playback with hardware acceleration
|
import vlc # For video playback with hardware acceleration
|
||||||
|
except Exception as e:
|
||||||
|
vlc = None
|
||||||
|
print(f"WARNING: VLC not available: {e}. Video playback may be limited.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PIL import Image, ImageTk
|
from PIL import Image, ImageTk
|
||||||
@@ -30,6 +35,8 @@ from python_functions import (
|
|||||||
from logging_config import Logger
|
from logging_config import Logger
|
||||||
from virtual_keyboard import VirtualKeyboard, TouchOptimizedEntry, TouchOptimizedButton
|
from virtual_keyboard import VirtualKeyboard, TouchOptimizedEntry, TouchOptimizedButton
|
||||||
from settings_screen import SettingsWindow
|
from settings_screen import SettingsWindow
|
||||||
|
from splash_screen import SplashScreen
|
||||||
|
from player_ui import PlayerUI
|
||||||
|
|
||||||
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'app_config.txt')
|
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'app_config.txt')
|
||||||
|
|
||||||
@@ -43,29 +50,86 @@ class SimpleMediaPlayerApp:
|
|||||||
self.playlist = []
|
self.playlist = []
|
||||||
self.current_index = 0
|
self.current_index = 0
|
||||||
self.scaling_mode = 'fit'
|
self.scaling_mode = 'fit'
|
||||||
self.auto_advance_timer = None
|
|
||||||
self.hide_controls_timer = None
|
self.hide_controls_timer = None
|
||||||
self.control_frame = None
|
|
||||||
self.settings_window = None
|
self.settings_window = None
|
||||||
self.content_frame = None
|
# UI extraction
|
||||||
self.image_label = None
|
self.ui = PlayerUI(root, control_callbacks={
|
||||||
self.status_label = None
|
'prev': self.previous_media,
|
||||||
self.play_pause_btn = None
|
'play_pause': self.toggle_play_pause,
|
||||||
self.prev_btn = None
|
'next': self.next_media,
|
||||||
self.next_btn = None
|
'settings': self.open_settings,
|
||||||
self.exit_btn = None
|
'exit': self.show_exit_dialog
|
||||||
self.settings_btn = None
|
})
|
||||||
|
self.content_frame = self.ui.content_frame
|
||||||
|
self.image_label = self.ui.image_label
|
||||||
|
self.status_label = self.ui.status_label
|
||||||
|
self.play_pause_btn = self.ui.play_pause_btn
|
||||||
|
self.prev_btn = self.ui.prev_btn
|
||||||
|
self.next_btn = self.ui.next_btn
|
||||||
|
self.exit_btn = self.ui.exit_btn
|
||||||
|
self.settings_btn = self.ui.settings_btn
|
||||||
|
self.playback = MediaPlaybackController(self, self.ui)
|
||||||
self.setup_window()
|
self.setup_window()
|
||||||
self.setup_ui()
|
self.start_player_logic()
|
||||||
# Automatically check for new server playlist on startup
|
|
||||||
try:
|
|
||||||
from python_functions import check_for_new_server_playlist
|
|
||||||
check_for_new_server_playlist()
|
|
||||||
except Exception as e:
|
|
||||||
Logger.error(f"Failed to check for new server playlist: {e}")
|
|
||||||
self.initialize_playlist_from_server()
|
|
||||||
self.start_periodic_checks()
|
|
||||||
|
|
||||||
|
def start_player_logic(self):
|
||||||
|
# Start playlist/server thread
|
||||||
|
self._playlist_ready = threading.Event()
|
||||||
|
self._playlist_data = None
|
||||||
|
def fetch_playlist_logic():
|
||||||
|
fallback_playlist = None
|
||||||
|
try:
|
||||||
|
local_playlist_data = load_local_playlist()
|
||||||
|
fallback_playlist = local_playlist_data.get('playlist', [])
|
||||||
|
if fallback_playlist:
|
||||||
|
Logger.info(f"Found fallback playlist with {len(fallback_playlist)} items")
|
||||||
|
except Exception as e:
|
||||||
|
Logger.warning(f"No fallback playlist available: {e}")
|
||||||
|
config = load_config()
|
||||||
|
server = config.get("server_ip", "")
|
||||||
|
host = config.get("screen_name", "")
|
||||||
|
quick = config.get("quickconnect_key", "")
|
||||||
|
port = config.get("port", "")
|
||||||
|
Logger.info(f"Initializing with settings: server={server}, host={host}, port={port}")
|
||||||
|
playlist = None
|
||||||
|
# Try to fetch from server
|
||||||
|
if server and host and quick and port:
|
||||||
|
try:
|
||||||
|
Logger.info("Attempting to connect to server...")
|
||||||
|
server_playlist_data = fetch_server_playlist()
|
||||||
|
server_playlist = server_playlist_data.get('playlist', [])
|
||||||
|
server_version = server_playlist_data.get('version', 0)
|
||||||
|
if server_playlist:
|
||||||
|
Logger.info(f"Server playlist found with {len(server_playlist)} items, version {server_version}")
|
||||||
|
download_media_files(server_playlist, server_version)
|
||||||
|
update_config_playlist_version(server_version)
|
||||||
|
local_playlist_data = load_local_playlist()
|
||||||
|
playlist = local_playlist_data.get('playlist', [])
|
||||||
|
if playlist:
|
||||||
|
Logger.info(f"Successfully loaded {len(playlist)} items from server")
|
||||||
|
else:
|
||||||
|
Logger.warning("Server playlist was empty, falling back to local playlist")
|
||||||
|
else:
|
||||||
|
Logger.warning("Server returned empty playlist, falling back to local playlist")
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"Failed to fetch playlist from server: {e}, using fallback playlist")
|
||||||
|
if not playlist:
|
||||||
|
playlist = fallback_playlist
|
||||||
|
if not playlist:
|
||||||
|
playlist = None
|
||||||
|
self._playlist_data = playlist
|
||||||
|
self._playlist_ready.set()
|
||||||
|
threading.Thread(target=fetch_playlist_logic, daemon=True).start()
|
||||||
|
self._playlist_ready.wait()
|
||||||
|
playlist = self._playlist_data
|
||||||
|
if playlist and len(playlist) > 0:
|
||||||
|
self.playlist = playlist
|
||||||
|
Logger.info(f"Loaded playlist with {len(self.playlist)} items")
|
||||||
|
self.play_current_media()
|
||||||
|
else:
|
||||||
|
Logger.warning("No playlist available, loading demo content")
|
||||||
|
self.load_demo_or_local_playlist()
|
||||||
|
self.start_periodic_checks()
|
||||||
|
|
||||||
def setup_window(self):
|
def setup_window(self):
|
||||||
self.root.title("Simple Signage Player")
|
self.root.title("Simple Signage Player")
|
||||||
@@ -85,433 +149,37 @@ class SimpleMediaPlayerApp:
|
|||||||
self.root.bind('<Motion>', self.on_mouse_motion)
|
self.root.bind('<Motion>', self.on_mouse_motion)
|
||||||
self.root.focus_set()
|
self.root.focus_set()
|
||||||
|
|
||||||
def setup_ui(self):
|
|
||||||
self.content_frame = tk.Frame(self.root, bg='black')
|
|
||||||
self.content_frame.pack(fill=tk.BOTH, expand=True)
|
|
||||||
self.image_label = tk.Label(self.content_frame, bg='black')
|
|
||||||
self.image_label.pack(fill=tk.BOTH, expand=True)
|
|
||||||
self.status_label = tk.Label(
|
|
||||||
self.content_frame,
|
|
||||||
bg='black',
|
|
||||||
fg='white',
|
|
||||||
font=('Arial', 24),
|
|
||||||
text=""
|
|
||||||
)
|
|
||||||
self.create_control_panel()
|
|
||||||
self.show_controls()
|
|
||||||
self.schedule_hide_controls()
|
|
||||||
|
|
||||||
# --- FULL METHOD BODIES FROM tkinter_simple_player_old.py BELOW ---
|
|
||||||
def create_control_panel(self):
|
|
||||||
"""Create touch-optimized control panel with larger buttons"""
|
|
||||||
self.control_frame = tk.Frame(
|
|
||||||
self.root,
|
|
||||||
bg='#1a1a1a',
|
|
||||||
bd=2,
|
|
||||||
relief=tk.RAISED,
|
|
||||||
padx=15,
|
|
||||||
pady=15
|
|
||||||
)
|
|
||||||
self.control_frame.place(relx=0.98, rely=0.98, anchor='se')
|
|
||||||
button_config = {
|
|
||||||
'bg': '#333333',
|
|
||||||
'fg': 'white',
|
|
||||||
'activebackground': '#555555',
|
|
||||||
'activeforeground': 'white',
|
|
||||||
'relief': tk.FLAT,
|
|
||||||
'borderwidth': 0,
|
|
||||||
'width': 10,
|
|
||||||
'height': 3,
|
|
||||||
'font': ('Segoe UI', 10, 'bold'),
|
|
||||||
'cursor': 'hand2'
|
|
||||||
}
|
|
||||||
self.prev_btn = tk.Button(
|
|
||||||
self.control_frame,
|
|
||||||
text="⏮ Prev",
|
|
||||||
command=self.previous_media,
|
|
||||||
**button_config
|
|
||||||
)
|
|
||||||
self.prev_btn.grid(row=0, column=0, padx=5)
|
|
||||||
self.play_pause_btn = tk.Button(
|
|
||||||
self.control_frame,
|
|
||||||
text="⏸ Pause" if not self.is_paused else "▶ Play",
|
|
||||||
command=self.toggle_play_pause,
|
|
||||||
bg='#27ae60',
|
|
||||||
activebackground='#35d974',
|
|
||||||
**{k: v for k, v in button_config.items() if k not in ['bg', 'activebackground']}
|
|
||||||
)
|
|
||||||
self.play_pause_btn.grid(row=0, column=1, padx=5)
|
|
||||||
self.next_btn = tk.Button(
|
|
||||||
self.control_frame,
|
|
||||||
text="Next ⏭",
|
|
||||||
command=self.next_media,
|
|
||||||
**button_config
|
|
||||||
)
|
|
||||||
self.next_btn.grid(row=0, column=2, padx=5)
|
|
||||||
self.settings_btn = tk.Button(
|
|
||||||
self.control_frame,
|
|
||||||
text="⚙️ Settings",
|
|
||||||
command=self.open_settings,
|
|
||||||
bg='#9b59b6',
|
|
||||||
activebackground='#bb8fce',
|
|
||||||
**{k: v for k, v in button_config.items() if k not in ['bg', 'activebackground']}
|
|
||||||
)
|
|
||||||
self.settings_btn.grid(row=0, column=3, padx=5)
|
|
||||||
self.exit_btn = tk.Button(
|
|
||||||
self.control_frame,
|
|
||||||
text="❌ EXIT",
|
|
||||||
command=self.show_exit_dialog,
|
|
||||||
bg='#e74c3c',
|
|
||||||
fg='white',
|
|
||||||
activebackground='#ec7063',
|
|
||||||
activeforeground='white',
|
|
||||||
relief=tk.FLAT,
|
|
||||||
borderwidth=0,
|
|
||||||
width=8,
|
|
||||||
height=3,
|
|
||||||
font=('Segoe UI', 10, 'bold'),
|
|
||||||
cursor='hand2'
|
|
||||||
)
|
|
||||||
self.exit_btn.grid(row=0, column=4, padx=5)
|
|
||||||
for button in [self.prev_btn, self.play_pause_btn, self.next_btn, self.settings_btn, self.exit_btn]:
|
|
||||||
self.add_touch_feedback_to_control_button(button)
|
|
||||||
|
|
||||||
def scale_image_to_screen(self, img, screen_width, screen_height, mode='fit'):
|
|
||||||
img_width, img_height = img.size
|
|
||||||
if mode == 'stretch':
|
|
||||||
return img.resize((screen_width, screen_height), Image.LANCZOS), (0, 0)
|
|
||||||
elif mode == 'fill':
|
|
||||||
screen_ratio = screen_width / screen_height
|
|
||||||
img_ratio = img_width / img_height
|
|
||||||
if img_ratio > screen_ratio:
|
|
||||||
new_height = screen_height
|
|
||||||
new_width = int(screen_height * img_ratio)
|
|
||||||
x_offset = (screen_width - new_width) // 2
|
|
||||||
y_offset = 0
|
|
||||||
else:
|
|
||||||
new_width = screen_width
|
|
||||||
new_height = int(screen_width / img_ratio)
|
|
||||||
x_offset = 0
|
|
||||||
y_offset = (screen_height - new_height) // 2
|
|
||||||
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
|
|
||||||
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
|
|
||||||
if new_width > screen_width:
|
|
||||||
crop_x = (new_width - screen_width) // 2
|
|
||||||
img_resized = img_resized.crop((crop_x, 0, crop_x + screen_width, new_height))
|
|
||||||
x_offset = 0
|
|
||||||
if new_height > screen_height:
|
|
||||||
crop_y = (new_height - screen_height) // 2
|
|
||||||
img_resized = img_resized.crop((0, crop_y, new_width, crop_y + screen_height))
|
|
||||||
y_offset = 0
|
|
||||||
final_img.paste(img_resized, (x_offset, y_offset))
|
|
||||||
return final_img, (x_offset, y_offset)
|
|
||||||
else:
|
|
||||||
screen_ratio = screen_width / screen_height
|
|
||||||
img_ratio = img_width / img_height
|
|
||||||
if img_ratio > screen_ratio:
|
|
||||||
new_width = screen_width
|
|
||||||
new_height = int(screen_width / img_ratio)
|
|
||||||
else:
|
|
||||||
new_height = screen_height
|
|
||||||
new_width = int(screen_height * img_ratio)
|
|
||||||
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
|
|
||||||
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
|
|
||||||
x_offset = (screen_width - new_width) // 2
|
|
||||||
y_offset = (screen_height - new_height) // 2
|
|
||||||
final_img.paste(img_resized, (x_offset, y_offset))
|
|
||||||
return final_img, (x_offset, y_offset)
|
|
||||||
|
|
||||||
def add_touch_feedback_to_control_button(self, button):
|
|
||||||
original_bg = button.cget('bg')
|
|
||||||
def on_press(e):
|
|
||||||
button.configure(relief=tk.SUNKEN)
|
|
||||||
def on_release(e):
|
|
||||||
button.configure(relief=tk.FLAT)
|
|
||||||
def on_enter(e):
|
|
||||||
button.configure(relief=tk.RAISED)
|
|
||||||
def on_leave(e):
|
|
||||||
button.configure(relief=tk.FLAT)
|
|
||||||
button.bind("<Button-1>", on_press)
|
|
||||||
button.bind("<ButtonRelease-1>", on_release)
|
|
||||||
button.bind("<Enter>", on_enter)
|
|
||||||
button.bind("<Leave>", on_leave)
|
|
||||||
|
|
||||||
def initialize_playlist_from_server(self):
|
|
||||||
fallback_playlist = None
|
|
||||||
try:
|
|
||||||
local_playlist_data = load_local_playlist()
|
|
||||||
fallback_playlist = local_playlist_data.get('playlist', [])
|
|
||||||
if fallback_playlist:
|
|
||||||
Logger.info(f"Found fallback playlist with {len(fallback_playlist)} items")
|
|
||||||
except Exception as e:
|
|
||||||
Logger.warning(f"No fallback playlist available: {e}")
|
|
||||||
self.status_label.config(text="Connecting to server...\nPlease wait")
|
|
||||||
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
|
||||||
self.root.update()
|
|
||||||
config = load_config()
|
|
||||||
server = config.get("server_ip", "")
|
|
||||||
host = config.get("screen_name", "")
|
|
||||||
quick = config.get("quickconnect_key", "")
|
|
||||||
port = config.get("port", "")
|
|
||||||
Logger.info(f"Initializing with settings: server={server}, host={host}, port={port}")
|
|
||||||
if not server or not host or not quick or not port:
|
|
||||||
Logger.warning("Missing server configuration, using fallback playlist")
|
|
||||||
self.status_label.place_forget()
|
|
||||||
self.load_fallback_playlist(fallback_playlist)
|
|
||||||
return
|
|
||||||
server_connection_successful = False
|
|
||||||
try:
|
|
||||||
Logger.info("Attempting to connect to server...")
|
|
||||||
self.status_label.config(text="Connecting to server...\nAttempting connection")
|
|
||||||
self.root.update()
|
|
||||||
server_playlist_data = fetch_server_playlist()
|
|
||||||
server_playlist = server_playlist_data.get('playlist', [])
|
|
||||||
server_version = server_playlist_data.get('version', 0)
|
|
||||||
if server_playlist:
|
|
||||||
Logger.info(f"Server playlist found with {len(server_playlist)} items, version {server_version}")
|
|
||||||
server_connection_successful = True
|
|
||||||
self.status_label.config(text="Downloading media files...\nPlease wait")
|
|
||||||
self.root.update()
|
|
||||||
download_media_files(server_playlist, server_version)
|
|
||||||
update_config_playlist_version(server_version)
|
|
||||||
local_playlist_data = load_local_playlist()
|
|
||||||
self.playlist = local_playlist_data.get('playlist', [])
|
|
||||||
if self.playlist:
|
|
||||||
Logger.info(f"Successfully loaded {len(self.playlist)} items from server")
|
|
||||||
self.status_label.place_forget()
|
|
||||||
self.play_current_media()
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
Logger.warning("Server playlist was empty, falling back to local playlist")
|
|
||||||
else:
|
|
||||||
Logger.warning("Server returned empty playlist, falling back to local playlist")
|
|
||||||
except requests.exceptions.ConnectTimeout:
|
|
||||||
Logger.error("Server connection timeout, using fallback playlist")
|
|
||||||
except requests.exceptions.ConnectionError:
|
|
||||||
Logger.error("Cannot connect to server, using fallback playlist")
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
Logger.error("Server request timeout, using fallback playlist")
|
|
||||||
except Exception as e:
|
|
||||||
Logger.error(f"Failed to fetch playlist from server: {e}, using fallback playlist")
|
|
||||||
if not server_connection_successful:
|
|
||||||
self.status_label.config(text="Server unavailable\nLoading last playlist...")
|
|
||||||
self.root.update()
|
|
||||||
time.sleep(1)
|
|
||||||
self.status_label.place_forget()
|
|
||||||
self.load_fallback_playlist(fallback_playlist)
|
|
||||||
|
|
||||||
def load_fallback_playlist(self, fallback_playlist):
|
|
||||||
if fallback_playlist and len(fallback_playlist) > 0:
|
|
||||||
self.playlist = fallback_playlist
|
|
||||||
Logger.info(f"Loaded fallback playlist with {len(self.playlist)} items")
|
|
||||||
self.play_current_media()
|
|
||||||
else:
|
|
||||||
Logger.warning("No fallback playlist available, loading demo content")
|
|
||||||
self.load_demo_or_local_playlist()
|
|
||||||
|
|
||||||
def load_demo_or_local_playlist(self):
|
|
||||||
local_playlist_data = load_local_playlist()
|
|
||||||
self.playlist = local_playlist_data.get('playlist', [])
|
|
||||||
if self.playlist:
|
|
||||||
Logger.info(f"Loaded existing local playlist with {len(self.playlist)} items")
|
|
||||||
self.play_current_media()
|
|
||||||
return
|
|
||||||
Logger.info("No local playlist found, loading demo content")
|
|
||||||
self.create_demo_content()
|
|
||||||
if self.playlist:
|
|
||||||
self.play_current_media()
|
|
||||||
else:
|
|
||||||
self.show_no_content_message()
|
|
||||||
|
|
||||||
def create_demo_content(self):
|
|
||||||
demo_images = []
|
|
||||||
static_dir = os.path.join(os.path.dirname(__file__), 'static', 'resurse')
|
|
||||||
if os.path.exists(static_dir):
|
|
||||||
for file in os.listdir(static_dir):
|
|
||||||
if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')):
|
|
||||||
full_path = os.path.join(static_dir, file)
|
|
||||||
demo_images.append({
|
|
||||||
'file_name': file,
|
|
||||||
'url': full_path,
|
|
||||||
'duration': 5
|
|
||||||
})
|
|
||||||
if not demo_images:
|
|
||||||
demo_dir = './Resurse'
|
|
||||||
if os.path.exists(demo_dir):
|
|
||||||
for file in os.listdir(demo_dir):
|
|
||||||
if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')):
|
|
||||||
demo_images.append({
|
|
||||||
'file_name': file,
|
|
||||||
'url': os.path.join(demo_dir, file),
|
|
||||||
'duration': 5
|
|
||||||
})
|
|
||||||
if demo_images:
|
|
||||||
self.playlist = demo_images
|
|
||||||
Logger.info(f"Created demo playlist with {len(demo_images)} images")
|
|
||||||
else:
|
|
||||||
self.playlist = [{
|
|
||||||
'file_name': 'Demo Text',
|
|
||||||
'url': 'text://Welcome to Tkinter Media Player!\n\nPlease configure server settings',
|
|
||||||
'duration': 5
|
|
||||||
}]
|
|
||||||
|
|
||||||
def show_no_content_message(self):
|
|
||||||
self.image_label.config(image='')
|
|
||||||
self.status_label.config(
|
|
||||||
text="No media content available.\nPress Settings to configure server connection."
|
|
||||||
)
|
|
||||||
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
|
||||||
|
|
||||||
def show_error_message(self, message):
|
|
||||||
self.image_label.config(image='')
|
|
||||||
self.status_label.config(text=f"Error: {message}")
|
|
||||||
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
|
||||||
|
|
||||||
def play_current_media(self):
|
def play_current_media(self):
|
||||||
if not self.playlist or self.current_index >= len(self.playlist):
|
self.playback.play_current_media()
|
||||||
self.show_no_content_message()
|
|
||||||
return
|
|
||||||
media = self.playlist[self.current_index]
|
|
||||||
file_path = media.get('url', '')
|
|
||||||
file_name = media.get('file_name', '')
|
|
||||||
duration = media.get('duration', 10)
|
|
||||||
if file_path.startswith('static/resurse/'):
|
|
||||||
absolute_path = os.path.join(os.path.dirname(__file__), file_path)
|
|
||||||
file_path = absolute_path
|
|
||||||
Logger.info(f"Playing media: {file_name} from {file_path}")
|
|
||||||
self.log_event(file_name, "STARTED")
|
|
||||||
self.cancel_timers()
|
|
||||||
if file_path.startswith('text://'):
|
|
||||||
self.show_text_content(file_path[7:], duration)
|
|
||||||
elif file_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')):
|
|
||||||
self.play_video(file_path)
|
|
||||||
elif os.path.exists(file_path) and file_path.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp')):
|
|
||||||
self.show_image(file_path, duration)
|
|
||||||
else:
|
|
||||||
Logger.error(f"Unsupported or missing media: {file_path}")
|
|
||||||
self.status_label.config(text=f"Missing or unsupported media:\n{file_name}")
|
|
||||||
self.auto_advance_timer = self.root.after(5000, self.next_media)
|
|
||||||
|
|
||||||
def play_video(self, file_path):
|
def play_video(self, file_path):
|
||||||
self.status_label.place_forget()
|
self.playback.play_video(file_path)
|
||||||
def run_vlc_subprocess():
|
|
||||||
try:
|
|
||||||
Logger.info(f"Starting system VLC subprocess for video: {file_path}")
|
|
||||||
vlc_cmd = [
|
|
||||||
'cvlc',
|
|
||||||
'--fullscreen',
|
|
||||||
'--no-osd',
|
|
||||||
'--no-video-title-show',
|
|
||||||
'--play-and-exit',
|
|
||||||
'--quiet',
|
|
||||||
file_path
|
|
||||||
]
|
|
||||||
proc = subprocess.Popen(vlc_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
||||||
proc.wait()
|
|
||||||
Logger.info(f"VLC subprocess finished: {file_path}")
|
|
||||||
except Exception as e:
|
|
||||||
Logger.error(f"VLC subprocess error: {e}")
|
|
||||||
finally:
|
|
||||||
self.root.after_idle(lambda: setattr(self, 'auto_advance_timer', self.root.after(1000, self.next_media)))
|
|
||||||
threading.Thread(target=run_vlc_subprocess, daemon=True).start()
|
|
||||||
|
|
||||||
def _update_video_frame(self, photo):
|
|
||||||
try:
|
|
||||||
self.image_label.config(image=photo)
|
|
||||||
self.image_label.image = photo
|
|
||||||
except Exception as e:
|
|
||||||
Logger.error(f"Error updating video frame: {e}")
|
|
||||||
|
|
||||||
def _show_video_error(self, error_msg):
|
|
||||||
try:
|
|
||||||
self.status_label.config(text=f"Video Error:\n{error_msg}")
|
|
||||||
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
|
||||||
self.auto_advance_timer = self.root.after(5000, self.next_media)
|
|
||||||
except Exception as e:
|
|
||||||
Logger.error(f"Error showing video error: {e}")
|
|
||||||
self.auto_advance_timer = self.root.after(5000, self.next_media)
|
|
||||||
|
|
||||||
def show_text_content(self, text, duration):
|
|
||||||
self.image_label.config(image='')
|
|
||||||
self.status_label.config(text=text)
|
|
||||||
self.auto_advance_timer = self.root.after(
|
|
||||||
int(duration * 1000),
|
|
||||||
self.next_media
|
|
||||||
)
|
|
||||||
|
|
||||||
def show_image(self, file_path, duration):
|
def show_image(self, file_path, duration):
|
||||||
try:
|
self.playback.show_image(file_path, duration)
|
||||||
self.status_label.place_forget()
|
|
||||||
self.status_label.config(text="")
|
def show_text_content(self, text, duration):
|
||||||
if PIL_AVAILABLE:
|
self.playback.show_text_content(text, duration)
|
||||||
img = Image.open(file_path)
|
|
||||||
original_size = img.size
|
|
||||||
screen_width = self.root.winfo_width()
|
|
||||||
screen_height = self.root.winfo_height()
|
|
||||||
if screen_width <= 1 or screen_height <= 1:
|
|
||||||
screen_width = 1920
|
|
||||||
screen_height = 1080
|
|
||||||
final_img, offset = self.scale_image_to_screen(img, screen_width, screen_height, self.scaling_mode)
|
|
||||||
photo = ImageTk.PhotoImage(final_img)
|
|
||||||
self.image_label.config(image=photo)
|
|
||||||
self.image_label.image = photo
|
|
||||||
Logger.info(f"Successfully displayed image: {os.path.basename(file_path)} "
|
|
||||||
f"(Original: {original_size}, Screen: {screen_width}x{screen_height}, "
|
|
||||||
f"Mode: {self.scaling_mode}, Offset: {offset})")
|
|
||||||
else:
|
|
||||||
self.image_label.config(image='')
|
|
||||||
self.status_label.config(text=f"IMAGE: {os.path.basename(file_path)}\n\n(Install PIL for image display)")
|
|
||||||
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
|
||||||
Logger.warning("PIL not available - showing text placeholder for image")
|
|
||||||
self.auto_advance_timer = self.root.after(
|
|
||||||
int(duration * 1000),
|
|
||||||
self.next_media
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
Logger.error(f"Failed to show image {file_path}: {e}")
|
|
||||||
self.image_label.config(image='')
|
|
||||||
self.status_label.config(text=f"Image Error:\n{os.path.basename(file_path)}\n{str(e)}")
|
|
||||||
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
|
||||||
self.auto_advance_timer = self.root.after(5000, self.next_media)
|
|
||||||
|
|
||||||
def next_media(self):
|
def next_media(self):
|
||||||
self.cancel_timers()
|
self.playback.next_media()
|
||||||
if not self.playlist:
|
|
||||||
return
|
|
||||||
self.current_index = (self.current_index + 1) % len(self.playlist)
|
|
||||||
if self.current_index == 0:
|
|
||||||
threading.Thread(target=self.check_playlist_updates, daemon=True).start()
|
|
||||||
self.play_current_media()
|
|
||||||
|
|
||||||
def previous_media(self):
|
def previous_media(self):
|
||||||
self.cancel_timers()
|
self.playback.previous_media()
|
||||||
if not self.playlist:
|
|
||||||
return
|
|
||||||
self.current_index = (self.current_index - 1) % len(self.playlist)
|
|
||||||
self.play_current_media()
|
|
||||||
|
|
||||||
def toggle_play_pause(self):
|
def toggle_play_pause(self):
|
||||||
self.is_paused = not self.is_paused
|
self.playback.toggle_play_pause()
|
||||||
if self.is_paused:
|
|
||||||
self.play_pause_btn.config(text="▶ Play")
|
|
||||||
self.cancel_timers()
|
|
||||||
else:
|
|
||||||
self.play_pause_btn.config(text="⏸ Pause")
|
|
||||||
self.play_current_media()
|
|
||||||
Logger.info(f"Media {'paused' if self.is_paused else 'resumed'}")
|
|
||||||
|
|
||||||
def cancel_timers(self):
|
def cancel_timers(self):
|
||||||
if self.auto_advance_timer:
|
self.playback.cancel_timers()
|
||||||
self.root.after_cancel(self.auto_advance_timer)
|
|
||||||
self.auto_advance_timer = None
|
|
||||||
|
|
||||||
def show_controls(self):
|
def show_controls(self):
|
||||||
if self.control_frame:
|
if self.ui.control_frame:
|
||||||
self.control_frame.place(relx=0.98, rely=0.98, anchor='se')
|
self.ui.control_frame.place(relx=0.98, rely=0.98, anchor='se')
|
||||||
|
|
||||||
def hide_controls(self):
|
def hide_controls(self):
|
||||||
if self.control_frame:
|
if self.ui.control_frame:
|
||||||
self.control_frame.place_forget()
|
self.ui.control_frame.place_forget()
|
||||||
|
|
||||||
def schedule_hide_controls(self):
|
def schedule_hide_controls(self):
|
||||||
if self.hide_controls_timer:
|
if self.hide_controls_timer:
|
||||||
@@ -709,6 +377,16 @@ class SimpleMediaPlayerApp:
|
|||||||
Logger.error(f"Error in periodic check: {e}")
|
Logger.error(f"Error in periodic check: {e}")
|
||||||
threading.Thread(target=check_loop, daemon=True).start()
|
threading.Thread(target=check_loop, daemon=True).start()
|
||||||
|
|
||||||
|
def next_media(self):
|
||||||
|
self.cancel_timers()
|
||||||
|
if not self.playlist:
|
||||||
|
return
|
||||||
|
self.current_index = (self.current_index + 1) % len(self.playlist)
|
||||||
|
# At end of playlist, check for updates
|
||||||
|
if self.current_index == 0:
|
||||||
|
self.check_playlist_updates()
|
||||||
|
self.play_current_media()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
Logger.info("Starting Simple Tkinter Media Player")
|
Logger.info("Starting Simple Tkinter Media Player")
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -0,0 +1,833 @@
|
|||||||
|
# player_app.py
|
||||||
|
# Main player application logic moved from tkinter_simple_player.py
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, messagebox, simpledialog
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import requests # Required for server communication
|
||||||
|
import queue
|
||||||
|
import vlc # For video playback with hardware acceleration
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PIL import Image, ImageTk
|
||||||
|
PIL_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
PIL_AVAILABLE = False
|
||||||
|
print("WARNING: PIL not available. Image display functionality will be limited.")
|
||||||
|
|
||||||
|
from python_functions import (
|
||||||
|
load_local_playlist, download_media_files, clean_unused_files,
|
||||||
|
save_local_playlist, update_config_playlist_version, fetch_server_playlist,
|
||||||
|
load_config
|
||||||
|
)
|
||||||
|
from logging_config import Logger
|
||||||
|
from virtual_keyboard import VirtualKeyboard, TouchOptimizedEntry, TouchOptimizedButton
|
||||||
|
from settings_screen import SettingsWindow
|
||||||
|
|
||||||
|
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'app_config.txt')
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleMediaPlayerApp:
|
||||||
|
def __init__(self, root):
|
||||||
|
self.root = root
|
||||||
|
self.running = True
|
||||||
|
self.is_paused = False
|
||||||
|
self.is_fullscreen = True
|
||||||
|
self.playlist = []
|
||||||
|
self.current_index = 0
|
||||||
|
self.scaling_mode = 'fit'
|
||||||
|
self.auto_advance_timer = None
|
||||||
|
self.hide_controls_timer = None
|
||||||
|
self.control_frame = None
|
||||||
|
self.settings_window = None
|
||||||
|
self.content_frame = None
|
||||||
|
self.image_label = None
|
||||||
|
self.status_label = None
|
||||||
|
self.play_pause_btn = None
|
||||||
|
self.prev_btn = None
|
||||||
|
self.next_btn = None
|
||||||
|
self.exit_btn = None
|
||||||
|
self.settings_btn = None
|
||||||
|
self.setup_window()
|
||||||
|
self.play_splash_then_start()
|
||||||
|
|
||||||
|
def play_splash_then_start(self):
|
||||||
|
# Start playlist/server thread
|
||||||
|
self._playlist_ready = threading.Event()
|
||||||
|
self._playlist_data = None
|
||||||
|
def fetch_playlist_logic():
|
||||||
|
fallback_playlist = None
|
||||||
|
try:
|
||||||
|
local_playlist_data = load_local_playlist()
|
||||||
|
fallback_playlist = local_playlist_data.get('playlist', [])
|
||||||
|
if fallback_playlist:
|
||||||
|
Logger.info(f"Found fallback playlist with {len(fallback_playlist)} items")
|
||||||
|
except Exception as e:
|
||||||
|
Logger.warning(f"No fallback playlist available: {e}")
|
||||||
|
config = load_config()
|
||||||
|
server = config.get("server_ip", "")
|
||||||
|
host = config.get("screen_name", "")
|
||||||
|
quick = config.get("quickconnect_key", "")
|
||||||
|
port = config.get("port", "")
|
||||||
|
Logger.info(f"Initializing with settings: server={server}, host={host}, port={port}")
|
||||||
|
playlist = None
|
||||||
|
# Try to fetch from server
|
||||||
|
if server and host and quick and port:
|
||||||
|
try:
|
||||||
|
Logger.info("Attempting to connect to server...")
|
||||||
|
server_playlist_data = fetch_server_playlist()
|
||||||
|
server_playlist = server_playlist_data.get('playlist', [])
|
||||||
|
server_version = server_playlist_data.get('version', 0)
|
||||||
|
if server_playlist:
|
||||||
|
Logger.info(f"Server playlist found with {len(server_playlist)} items, version {server_version}")
|
||||||
|
download_media_files(server_playlist, server_version)
|
||||||
|
update_config_playlist_version(server_version)
|
||||||
|
local_playlist_data = load_local_playlist()
|
||||||
|
playlist = local_playlist_data.get('playlist', [])
|
||||||
|
if playlist:
|
||||||
|
Logger.info(f"Successfully loaded {len(playlist)} items from server")
|
||||||
|
else:
|
||||||
|
Logger.warning("Server playlist was empty, falling back to local playlist")
|
||||||
|
else:
|
||||||
|
Logger.warning("Server returned empty playlist, falling back to local playlist")
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"Failed to fetch playlist from server: {e}, using fallback playlist")
|
||||||
|
if not playlist:
|
||||||
|
playlist = fallback_playlist
|
||||||
|
if not playlist:
|
||||||
|
playlist = None
|
||||||
|
self._playlist_data = playlist
|
||||||
|
self._playlist_ready.set()
|
||||||
|
threading.Thread(target=fetch_playlist_logic, daemon=True).start()
|
||||||
|
# Only play intro on first playlist loop
|
||||||
|
self._first_playlist_loop = True
|
||||||
|
self.setup_ui()
|
||||||
|
self._playlist_ready.wait()
|
||||||
|
playlist = self._playlist_data
|
||||||
|
if playlist and len(playlist) > 0:
|
||||||
|
self.playlist = playlist
|
||||||
|
Logger.info(f"Loaded playlist with {len(self.playlist)} items")
|
||||||
|
self.play_current_media()
|
||||||
|
else:
|
||||||
|
Logger.warning("No playlist available, loading demo content")
|
||||||
|
self.load_demo_or_local_playlist()
|
||||||
|
self.start_periodic_checks()
|
||||||
|
|
||||||
|
# _after_splash is now handled inline in play_splash_then_start
|
||||||
|
|
||||||
|
|
||||||
|
def setup_window(self):
|
||||||
|
self.root.title("Simple Signage Player")
|
||||||
|
self.root.configure(bg='black')
|
||||||
|
try:
|
||||||
|
config = load_config()
|
||||||
|
width = int(config.get('screen_w', 1920))
|
||||||
|
height = int(config.get('screen_h', 1080))
|
||||||
|
self.scaling_mode = config.get('scaling_mode', 'fit')
|
||||||
|
except:
|
||||||
|
width, height = 800, 600
|
||||||
|
self.scaling_mode = 'fit'
|
||||||
|
self.root.geometry(f"{width}x{height}")
|
||||||
|
self.root.attributes('-fullscreen', True)
|
||||||
|
self.root.bind('<Key>', self.on_key_press)
|
||||||
|
self.root.bind('<Button-1>', self.on_mouse_click)
|
||||||
|
self.root.bind('<Motion>', self.on_mouse_motion)
|
||||||
|
self.root.focus_set()
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
self.content_frame = tk.Frame(self.root, bg='black')
|
||||||
|
self.content_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
self.image_label = tk.Label(self.content_frame, bg='black')
|
||||||
|
self.image_label.pack(fill=tk.BOTH, expand=True)
|
||||||
|
self.status_label = tk.Label(
|
||||||
|
self.content_frame,
|
||||||
|
bg='black',
|
||||||
|
fg='white',
|
||||||
|
font=('Arial', 20),
|
||||||
|
text=""
|
||||||
|
)
|
||||||
|
self.create_control_panel()
|
||||||
|
self.show_controls()
|
||||||
|
self.schedule_hide_controls()
|
||||||
|
|
||||||
|
# --- FULL METHOD BODIES FROM tkinter_simple_player_old.py BELOW ---
|
||||||
|
def create_control_panel(self):
|
||||||
|
"""Create touch-optimized control panel with larger buttons"""
|
||||||
|
self.control_frame = tk.Frame(
|
||||||
|
self.root,
|
||||||
|
bg='#1a1a1a',
|
||||||
|
bd=2,
|
||||||
|
relief=tk.RAISED,
|
||||||
|
padx=15,
|
||||||
|
pady=15
|
||||||
|
)
|
||||||
|
self.control_frame.place(relx=0.98, rely=0.98, anchor='se')
|
||||||
|
button_config = {
|
||||||
|
'bg': '#333333',
|
||||||
|
'fg': 'white',
|
||||||
|
'activebackground': '#555555',
|
||||||
|
'activeforeground': 'white',
|
||||||
|
'relief': tk.FLAT,
|
||||||
|
'borderwidth': 0,
|
||||||
|
'width': 10,
|
||||||
|
'height': 3,
|
||||||
|
'font': ('Segoe UI', 10, 'bold'),
|
||||||
|
'cursor': 'hand2'
|
||||||
|
}
|
||||||
|
self.prev_btn = tk.Button(
|
||||||
|
self.control_frame,
|
||||||
|
text="⏮ Prev",
|
||||||
|
command=self.previous_media,
|
||||||
|
**button_config
|
||||||
|
)
|
||||||
|
self.prev_btn.grid(row=0, column=0, padx=5)
|
||||||
|
self.play_pause_btn = tk.Button(
|
||||||
|
self.control_frame,
|
||||||
|
text="⏸ Pause" if not self.is_paused else "▶ Play",
|
||||||
|
command=self.toggle_play_pause,
|
||||||
|
bg='#27ae60',
|
||||||
|
activebackground='#35d974',
|
||||||
|
**{k: v for k, v in button_config.items() if k not in ['bg', 'activebackground']}
|
||||||
|
)
|
||||||
|
self.play_pause_btn.grid(row=0, column=1, padx=5)
|
||||||
|
self.next_btn = tk.Button(
|
||||||
|
self.control_frame,
|
||||||
|
text="Next ⏭",
|
||||||
|
command=self.next_media,
|
||||||
|
**button_config
|
||||||
|
)
|
||||||
|
self.next_btn.grid(row=0, column=2, padx=5)
|
||||||
|
self.settings_btn = tk.Button(
|
||||||
|
self.control_frame,
|
||||||
|
text="⚙️ Settings",
|
||||||
|
command=self.open_settings,
|
||||||
|
bg='#9b59b6',
|
||||||
|
activebackground='#bb8fce',
|
||||||
|
**{k: v for k, v in button_config.items() if k not in ['bg', 'activebackground']}
|
||||||
|
)
|
||||||
|
self.settings_btn.grid(row=0, column=3, padx=5)
|
||||||
|
self.exit_btn = tk.Button(
|
||||||
|
self.control_frame,
|
||||||
|
text="❌ EXIT",
|
||||||
|
command=self.show_exit_dialog,
|
||||||
|
bg='#e74c3c',
|
||||||
|
fg='white',
|
||||||
|
activebackground='#ec7063',
|
||||||
|
activeforeground='white',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
borderwidth=0,
|
||||||
|
width=8,
|
||||||
|
height=3,
|
||||||
|
font=('Segoe UI', 10, 'bold'),
|
||||||
|
cursor='hand2'
|
||||||
|
)
|
||||||
|
self.exit_btn.grid(row=0, column=4, padx=5)
|
||||||
|
for button in [self.prev_btn, self.play_pause_btn, self.next_btn, self.settings_btn, self.exit_btn]:
|
||||||
|
self.add_touch_feedback_to_control_button(button)
|
||||||
|
|
||||||
|
def scale_image_to_screen(self, img, screen_width, screen_height, mode='fit'):
|
||||||
|
img_width, img_height = img.size
|
||||||
|
if mode == 'stretch':
|
||||||
|
return img.resize((screen_width, screen_height), Image.LANCZOS), (0, 0)
|
||||||
|
elif mode == 'fill':
|
||||||
|
screen_ratio = screen_width / screen_height
|
||||||
|
img_ratio = img_width / img_height
|
||||||
|
if img_ratio > screen_ratio:
|
||||||
|
new_height = screen_height
|
||||||
|
new_width = int(screen_height * img_ratio)
|
||||||
|
x_offset = (screen_width - new_width) // 2
|
||||||
|
y_offset = 0
|
||||||
|
else:
|
||||||
|
new_width = screen_width
|
||||||
|
new_height = int(screen_width / img_ratio)
|
||||||
|
x_offset = 0
|
||||||
|
y_offset = (screen_height - new_height) // 2
|
||||||
|
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
|
||||||
|
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
|
||||||
|
if new_width > screen_width:
|
||||||
|
crop_x = (new_width - screen_width) // 2
|
||||||
|
img_resized = img_resized.crop((crop_x, 0, crop_x + screen_width, new_height))
|
||||||
|
x_offset = 0
|
||||||
|
if new_height > screen_height:
|
||||||
|
crop_y = (new_height - screen_height) // 2
|
||||||
|
img_resized = img_resized.crop((0, crop_y, new_width, crop_y + screen_height))
|
||||||
|
y_offset = 0
|
||||||
|
final_img.paste(img_resized, (x_offset, y_offset))
|
||||||
|
return final_img, (x_offset, y_offset)
|
||||||
|
else:
|
||||||
|
screen_ratio = screen_width / screen_height
|
||||||
|
img_ratio = img_width / img_height
|
||||||
|
if img_ratio > screen_ratio:
|
||||||
|
new_width = screen_width
|
||||||
|
new_height = int(screen_width / img_ratio)
|
||||||
|
else:
|
||||||
|
new_height = screen_height
|
||||||
|
new_width = int(screen_height * img_ratio)
|
||||||
|
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
|
||||||
|
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
|
||||||
|
x_offset = (screen_width - new_width) // 2
|
||||||
|
y_offset = (screen_height - new_height) // 2
|
||||||
|
final_img.paste(img_resized, (x_offset, y_offset))
|
||||||
|
return final_img, (x_offset, y_offset)
|
||||||
|
|
||||||
|
def add_touch_feedback_to_control_button(self, button):
|
||||||
|
original_bg = button.cget('bg')
|
||||||
|
def on_press(e):
|
||||||
|
button.configure(relief=tk.SUNKEN)
|
||||||
|
def on_release(e):
|
||||||
|
button.configure(relief=tk.FLAT)
|
||||||
|
def on_enter(e):
|
||||||
|
button.configure(relief=tk.RAISED)
|
||||||
|
def on_leave(e):
|
||||||
|
button.configure(relief=tk.FLAT)
|
||||||
|
button.bind("<Button-1>", on_press)
|
||||||
|
button.bind("<ButtonRelease-1>", on_release)
|
||||||
|
button.bind("<Enter>", on_enter)
|
||||||
|
button.bind("<Leave>", on_leave)
|
||||||
|
|
||||||
|
def initialize_playlist_from_server(self):
|
||||||
|
# No-op: logic now handled in play_splash_then_start
|
||||||
|
pass
|
||||||
|
def _play_intro_splash(self, on_finish=None, duration=5):
|
||||||
|
"""Embed intro1.mp4 as splash video in splash_frame using python-vlc. If video ends early, pause last frame until 'duration' seconds."""
|
||||||
|
intro_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'intro1.mp4')
|
||||||
|
self._splash_ended = False
|
||||||
|
def finish():
|
||||||
|
self._stop_intro_splash()
|
||||||
|
if on_finish:
|
||||||
|
self.root.after(0, on_finish)
|
||||||
|
def on_end(event=None):
|
||||||
|
if not self._splash_ended:
|
||||||
|
self._splash_ended = True
|
||||||
|
# Pause on last frame
|
||||||
|
if hasattr(self, '_splash_player') and self._splash_player:
|
||||||
|
self._splash_player.set_pause(1)
|
||||||
|
if os.path.exists(intro_path) and hasattr(self, 'splash_frame'):
|
||||||
|
try:
|
||||||
|
self._splash_vlc_instance = vlc.Instance('--no-osd', '--no-video-title-show', '--quiet')
|
||||||
|
self._splash_media = self._splash_vlc_instance.media_new(intro_path)
|
||||||
|
self._splash_player = self._splash_vlc_instance.media_player_new()
|
||||||
|
self._splash_player.set_media(self._splash_media)
|
||||||
|
self.splash_frame.update_idletasks()
|
||||||
|
window_id = self.splash_frame.winfo_id()
|
||||||
|
self._splash_player.set_xwindow(window_id)
|
||||||
|
# Attach event for end of video
|
||||||
|
event_manager = self._splash_player.event_manager()
|
||||||
|
event_manager.event_attach(vlc.EventType.MediaPlayerEndReached, on_end)
|
||||||
|
self._splash_player.play()
|
||||||
|
self.root.after(int(duration * 1000), finish)
|
||||||
|
except Exception as e:
|
||||||
|
Logger.warning(f"Could not play splash video with python-vlc: {e}")
|
||||||
|
finish()
|
||||||
|
else:
|
||||||
|
Logger.warning(f"Splash video not found or splash_frame missing: {intro_path}")
|
||||||
|
finish()
|
||||||
|
config = load_config()
|
||||||
|
server = config.get("server_ip", "")
|
||||||
|
host = config.get("screen_name", "")
|
||||||
|
quick = config.get("quickconnect_key", "")
|
||||||
|
port = config.get("port", "")
|
||||||
|
Logger.info(f"Initializing with settings: server={server}, host={host}, port={port}")
|
||||||
|
if not server or not host or not quick or not port:
|
||||||
|
Logger.warning("Missing server configuration, using fallback playlist")
|
||||||
|
self._stop_intro_splash()
|
||||||
|
self.load_fallback_playlist(fallback_playlist)
|
||||||
|
return
|
||||||
|
server_connection_successful = False
|
||||||
|
try:
|
||||||
|
Logger.info("Attempting to connect to server...")
|
||||||
|
server_playlist_data = fetch_server_playlist()
|
||||||
|
server_playlist = server_playlist_data.get('playlist', [])
|
||||||
|
server_version = server_playlist_data.get('version', 0)
|
||||||
|
if server_playlist:
|
||||||
|
Logger.info(f"Server playlist found with {len(server_playlist)} items, version {server_version}")
|
||||||
|
server_connection_successful = True
|
||||||
|
download_media_files(server_playlist, server_version)
|
||||||
|
update_config_playlist_version(server_version)
|
||||||
|
local_playlist_data = load_local_playlist()
|
||||||
|
self.playlist = local_playlist_data.get('playlist', [])
|
||||||
|
if self.playlist:
|
||||||
|
Logger.info(f"Successfully loaded {len(self.playlist)} items from server")
|
||||||
|
self._stop_intro_splash()
|
||||||
|
self.play_current_media()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
Logger.warning("Server playlist was empty, falling back to local playlist")
|
||||||
|
else:
|
||||||
|
Logger.warning("Server returned empty playlist, falling back to local playlist")
|
||||||
|
except requests.exceptions.ConnectTimeout:
|
||||||
|
Logger.error("Server connection timeout, using fallback playlist")
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
Logger.error("Cannot connect to server, using fallback playlist")
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
Logger.error("Server request timeout, using fallback playlist")
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"Failed to fetch playlist from server: {e}, using fallback playlist")
|
||||||
|
if not server_connection_successful:
|
||||||
|
self._stop_intro_splash()
|
||||||
|
self.load_fallback_playlist(fallback_playlist)
|
||||||
|
def _stop_intro_splash(self):
|
||||||
|
"""Stop the intro splash video if running (python-vlc version)."""
|
||||||
|
if hasattr(self, '_splash_player') and self._splash_player:
|
||||||
|
try:
|
||||||
|
self._splash_player.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._splash_player = None
|
||||||
|
if hasattr(self, '_splash_vlc_instance'):
|
||||||
|
self._splash_vlc_instance = None
|
||||||
|
|
||||||
|
def load_fallback_playlist(self, fallback_playlist):
|
||||||
|
if fallback_playlist and len(fallback_playlist) > 0:
|
||||||
|
self.playlist = fallback_playlist
|
||||||
|
Logger.info(f"Loaded fallback playlist with {len(self.playlist)} items")
|
||||||
|
self.play_current_media()
|
||||||
|
else:
|
||||||
|
Logger.warning("No fallback playlist available, loading demo content")
|
||||||
|
self.load_demo_or_local_playlist()
|
||||||
|
|
||||||
|
def load_demo_or_local_playlist(self):
|
||||||
|
local_playlist_data = load_local_playlist()
|
||||||
|
self.playlist = local_playlist_data.get('playlist', [])
|
||||||
|
if self.playlist:
|
||||||
|
Logger.info(f"Loaded existing local playlist with {len(self.playlist)} items")
|
||||||
|
self.play_current_media()
|
||||||
|
return
|
||||||
|
Logger.info("No local playlist found, loading demo content")
|
||||||
|
self.create_demo_content()
|
||||||
|
if self.playlist:
|
||||||
|
self.play_current_media()
|
||||||
|
else:
|
||||||
|
self.show_no_content_message()
|
||||||
|
|
||||||
|
def create_demo_content(self):
|
||||||
|
demo_images = []
|
||||||
|
static_dir = os.path.join(os.path.dirname(__file__), 'static', 'resurse')
|
||||||
|
if os.path.exists(static_dir):
|
||||||
|
for file in os.listdir(static_dir):
|
||||||
|
if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')):
|
||||||
|
full_path = os.path.join(static_dir, file)
|
||||||
|
demo_images.append({
|
||||||
|
'file_name': file,
|
||||||
|
'url': full_path,
|
||||||
|
'duration': 5
|
||||||
|
})
|
||||||
|
if not demo_images:
|
||||||
|
demo_dir = './Resurse'
|
||||||
|
if os.path.exists(demo_dir):
|
||||||
|
for file in os.listdir(demo_dir):
|
||||||
|
if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')):
|
||||||
|
demo_images.append({
|
||||||
|
'file_name': file,
|
||||||
|
'url': os.path.join(demo_dir, file),
|
||||||
|
'duration': 5
|
||||||
|
})
|
||||||
|
if demo_images:
|
||||||
|
self.playlist = demo_images
|
||||||
|
Logger.info(f"Created demo playlist with {len(demo_images)} images")
|
||||||
|
else:
|
||||||
|
self.playlist = [{
|
||||||
|
'file_name': 'Demo Text',
|
||||||
|
'url': 'text://Welcome to Tkinter Media Player!\n\nPlease configure server settings',
|
||||||
|
'duration': 5
|
||||||
|
}]
|
||||||
|
|
||||||
|
def show_no_content_message(self):
|
||||||
|
self.image_label.config(image='')
|
||||||
|
self.status_label.config(
|
||||||
|
text="No media content available.\nPress Settings to configure server connection."
|
||||||
|
)
|
||||||
|
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
||||||
|
|
||||||
|
def show_error_message(self, message):
|
||||||
|
self.image_label.config(image='')
|
||||||
|
self.status_label.config(text=f"Error: {message}")
|
||||||
|
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
||||||
|
|
||||||
|
def play_current_media(self):
|
||||||
|
# On first playlist loop, play intro splash, then continue playlist
|
||||||
|
if getattr(self, '_first_playlist_loop', False):
|
||||||
|
self._first_playlist_loop = False
|
||||||
|
# Create splash_frame for intro video
|
||||||
|
self.splash_frame = tk.Frame(self.root, bg='black')
|
||||||
|
self.splash_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
self.root.update_idletasks()
|
||||||
|
def after_intro():
|
||||||
|
if hasattr(self, 'splash_frame') and self.splash_frame:
|
||||||
|
self.splash_frame.destroy()
|
||||||
|
self.splash_frame = None
|
||||||
|
self._play_current_media_after_intro()
|
||||||
|
self._play_intro_splash(on_finish=after_intro, duration=5)
|
||||||
|
return
|
||||||
|
self._play_current_media_after_intro()
|
||||||
|
|
||||||
|
def _play_current_media_after_intro(self):
|
||||||
|
if not self.playlist or self.current_index >= len(self.playlist):
|
||||||
|
self.show_no_content_message()
|
||||||
|
return
|
||||||
|
media = self.playlist[self.current_index]
|
||||||
|
file_path = media.get('url', '')
|
||||||
|
file_name = media.get('file_name', '')
|
||||||
|
duration = media.get('duration', 10)
|
||||||
|
if file_path.startswith('static/resurse/'):
|
||||||
|
absolute_path = os.path.join(os.path.dirname(__file__), file_path)
|
||||||
|
file_path = absolute_path
|
||||||
|
Logger.info(f"Playing media: {file_name} from {file_path}")
|
||||||
|
self.log_event(file_name, "STARTED")
|
||||||
|
self.cancel_timers()
|
||||||
|
if file_path.startswith('text://'):
|
||||||
|
self.show_text_content(file_path[7:], duration)
|
||||||
|
elif file_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')):
|
||||||
|
self.play_video(file_path)
|
||||||
|
elif os.path.exists(file_path) and file_path.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp')):
|
||||||
|
self.show_image(file_path, duration)
|
||||||
|
else:
|
||||||
|
Logger.error(f"Unsupported or missing media: {file_path}")
|
||||||
|
self.status_label.config(text=f"Missing or unsupported media:\n{file_name}")
|
||||||
|
self.auto_advance_timer = self.root.after(5000, self.next_media)
|
||||||
|
|
||||||
|
def play_video(self, file_path):
|
||||||
|
self.status_label.place_forget()
|
||||||
|
def run_vlc_subprocess():
|
||||||
|
try:
|
||||||
|
Logger.info(f"Starting system VLC subprocess for video: {file_path}")
|
||||||
|
vlc_cmd = [
|
||||||
|
'cvlc',
|
||||||
|
'--fullscreen',
|
||||||
|
'--no-osd',
|
||||||
|
'--no-video-title-show',
|
||||||
|
'--play-and-exit',
|
||||||
|
'--quiet',
|
||||||
|
file_path
|
||||||
|
]
|
||||||
|
proc = subprocess.Popen(vlc_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
proc.wait()
|
||||||
|
Logger.info(f"VLC subprocess finished: {file_path}")
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"VLC subprocess error: {e}")
|
||||||
|
finally:
|
||||||
|
self.root.after_idle(lambda: setattr(self, 'auto_advance_timer', self.root.after(1000, self.next_media)))
|
||||||
|
threading.Thread(target=run_vlc_subprocess, daemon=True).start()
|
||||||
|
|
||||||
|
def _update_video_frame(self, photo):
|
||||||
|
try:
|
||||||
|
self.image_label.config(image=photo)
|
||||||
|
self.image_label.image = photo
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"Error updating video frame: {e}")
|
||||||
|
|
||||||
|
def _show_video_error(self, error_msg):
|
||||||
|
try:
|
||||||
|
self.status_label.config(text=f"Video Error:\n{error_msg}")
|
||||||
|
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
||||||
|
self.auto_advance_timer = self.root.after(5000, self.next_media)
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"Error showing video error: {e}")
|
||||||
|
self.auto_advance_timer = self.root.after(5000, self.next_media)
|
||||||
|
|
||||||
|
def show_text_content(self, text, duration):
|
||||||
|
self.image_label.config(image='')
|
||||||
|
self.status_label.config(text=text)
|
||||||
|
self.auto_advance_timer = self.root.after(
|
||||||
|
int(duration * 1000),
|
||||||
|
self.next_media
|
||||||
|
)
|
||||||
|
|
||||||
|
def show_image(self, file_path, duration):
|
||||||
|
try:
|
||||||
|
self.status_label.place_forget()
|
||||||
|
self.status_label.config(text="")
|
||||||
|
if PIL_AVAILABLE:
|
||||||
|
img = Image.open(file_path)
|
||||||
|
original_size = img.size
|
||||||
|
screen_width = self.root.winfo_width()
|
||||||
|
screen_height = self.root.winfo_height()
|
||||||
|
if screen_width <= 1 or screen_height <= 1:
|
||||||
|
screen_width = 1920
|
||||||
|
screen_height = 1080
|
||||||
|
final_img, offset = self.scale_image_to_screen(img, screen_width, screen_height, self.scaling_mode)
|
||||||
|
photo = ImageTk.PhotoImage(final_img)
|
||||||
|
self.image_label.config(image=photo)
|
||||||
|
self.image_label.image = photo
|
||||||
|
Logger.info(f"Successfully displayed image: {os.path.basename(file_path)} "
|
||||||
|
f"(Original: {original_size}, Screen: {screen_width}x{screen_height}, "
|
||||||
|
f"Mode: {self.scaling_mode}, Offset: {offset})")
|
||||||
|
else:
|
||||||
|
self.image_label.config(image='')
|
||||||
|
self.status_label.config(text=f"IMAGE: {os.path.basename(file_path)}\n\n(Install PIL for image display)")
|
||||||
|
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
||||||
|
Logger.warning("PIL not available - showing text placeholder for image")
|
||||||
|
self.auto_advance_timer = self.root.after(
|
||||||
|
int(duration * 1000),
|
||||||
|
self.next_media
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"Failed to show image {file_path}: {e}")
|
||||||
|
self.image_label.config(image='')
|
||||||
|
self.status_label.config(text=f"Image Error:\n{os.path.basename(file_path)}\n{str(e)}")
|
||||||
|
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
||||||
|
self.auto_advance_timer = self.root.after(5000, self.next_media)
|
||||||
|
|
||||||
|
def next_media(self):
|
||||||
|
self.cancel_timers()
|
||||||
|
if not self.playlist:
|
||||||
|
return
|
||||||
|
self.current_index = (self.current_index + 1) % len(self.playlist)
|
||||||
|
if self.current_index == 0:
|
||||||
|
threading.Thread(target=self.check_playlist_updates, daemon=True).start()
|
||||||
|
self.play_current_media()
|
||||||
|
|
||||||
|
def previous_media(self):
|
||||||
|
self.cancel_timers()
|
||||||
|
if not self.playlist:
|
||||||
|
return
|
||||||
|
self.current_index = (self.current_index - 1) % len(self.playlist)
|
||||||
|
self.play_current_media()
|
||||||
|
|
||||||
|
def toggle_play_pause(self):
|
||||||
|
self.is_paused = not self.is_paused
|
||||||
|
if self.is_paused:
|
||||||
|
self.play_pause_btn.config(text="▶ Play")
|
||||||
|
self.cancel_timers()
|
||||||
|
else:
|
||||||
|
self.play_pause_btn.config(text="⏸ Pause")
|
||||||
|
self.play_current_media()
|
||||||
|
Logger.info(f"Media {'paused' if self.is_paused else 'resumed'}")
|
||||||
|
|
||||||
|
def cancel_timers(self):
|
||||||
|
if self.auto_advance_timer:
|
||||||
|
self.root.after_cancel(self.auto_advance_timer)
|
||||||
|
self.auto_advance_timer = None
|
||||||
|
|
||||||
|
def show_controls(self):
|
||||||
|
if self.control_frame:
|
||||||
|
self.control_frame.place(relx=0.98, rely=0.98, anchor='se')
|
||||||
|
|
||||||
|
def hide_controls(self):
|
||||||
|
if self.control_frame:
|
||||||
|
self.control_frame.place_forget()
|
||||||
|
|
||||||
|
def schedule_hide_controls(self):
|
||||||
|
if self.hide_controls_timer:
|
||||||
|
self.root.after_cancel(self.hide_controls_timer)
|
||||||
|
self.hide_controls_timer = self.root.after(10000, self.hide_controls)
|
||||||
|
|
||||||
|
def on_mouse_click(self, event):
|
||||||
|
self.show_controls()
|
||||||
|
self.schedule_hide_controls()
|
||||||
|
|
||||||
|
def on_mouse_motion(self, event):
|
||||||
|
self.show_controls()
|
||||||
|
self.schedule_hide_controls()
|
||||||
|
|
||||||
|
def on_key_press(self, event):
|
||||||
|
key = event.keysym.lower()
|
||||||
|
if key == 'f':
|
||||||
|
self.toggle_fullscreen()
|
||||||
|
elif key == 'space':
|
||||||
|
self.toggle_play_pause()
|
||||||
|
elif key == 'left':
|
||||||
|
self.previous_media()
|
||||||
|
elif key == 'right':
|
||||||
|
self.next_media()
|
||||||
|
elif key == 'escape':
|
||||||
|
self.show_exit_dialog()
|
||||||
|
elif key == '1':
|
||||||
|
self.set_scaling_mode('fit')
|
||||||
|
elif key == '2':
|
||||||
|
self.set_scaling_mode('fill')
|
||||||
|
elif key == '3':
|
||||||
|
self.set_scaling_mode('stretch')
|
||||||
|
elif event.state & 0x4:
|
||||||
|
if key == 's':
|
||||||
|
self.open_settings()
|
||||||
|
self.show_controls()
|
||||||
|
self.schedule_hide_controls()
|
||||||
|
|
||||||
|
def set_scaling_mode(self, mode):
|
||||||
|
old_mode = self.scaling_mode
|
||||||
|
self.scaling_mode = mode
|
||||||
|
Logger.info(f"Scaling mode changed from '{old_mode}' to '{mode}'")
|
||||||
|
self.status_label.config(text=f"Scaling Mode: {mode.title()}\n"
|
||||||
|
f"1=Fit 2=Fill 3=Stretch")
|
||||||
|
self.status_label.place(relx=0.5, rely=0.05, anchor='center')
|
||||||
|
self.root.after(2000, lambda: self.status_label.place_forget())
|
||||||
|
if self.playlist and 0 <= self.current_index < len(self.playlist):
|
||||||
|
self.cancel_timers()
|
||||||
|
self.play_current_media()
|
||||||
|
|
||||||
|
def toggle_fullscreen(self):
|
||||||
|
self.is_fullscreen = not self.is_fullscreen
|
||||||
|
self.root.attributes('-fullscreen', self.is_fullscreen)
|
||||||
|
|
||||||
|
def open_settings(self):
|
||||||
|
if hasattr(self, 'settings_window') and self.settings_window and self.settings_window.winfo_exists():
|
||||||
|
self.settings_window.lift()
|
||||||
|
return
|
||||||
|
if not self.is_paused:
|
||||||
|
self.toggle_play_pause()
|
||||||
|
self.settings_window = SettingsWindow(self.root, self)
|
||||||
|
def on_settings_close():
|
||||||
|
if self.is_paused:
|
||||||
|
self.toggle_play_pause()
|
||||||
|
self.settings_window.protocol("WM_DELETE_WINDOW", on_settings_close)
|
||||||
|
|
||||||
|
def show_exit_dialog(self):
|
||||||
|
try:
|
||||||
|
config = load_config()
|
||||||
|
quickconnect_key = config.get('quickconnect_key', '')
|
||||||
|
except:
|
||||||
|
quickconnect_key = ''
|
||||||
|
exit_dialog = tk.Toplevel(self.root)
|
||||||
|
exit_dialog.title("Exit Application")
|
||||||
|
exit_dialog.geometry("400x200")
|
||||||
|
exit_dialog.configure(bg='#2d2d2d')
|
||||||
|
exit_dialog.transient(self.root)
|
||||||
|
exit_dialog.grab_set()
|
||||||
|
exit_dialog.resizable(False, False)
|
||||||
|
self.center_dialog_on_screen(exit_dialog, 400, 200)
|
||||||
|
header_frame = tk.Frame(exit_dialog, bg='#cc0000', height=60)
|
||||||
|
header_frame.pack(fill=tk.X)
|
||||||
|
header_frame.pack_propagate(False)
|
||||||
|
icon_label = tk.Label(header_frame, text="⚠", font=('Arial', 20, 'bold'),
|
||||||
|
fg='white', bg='#cc0000')
|
||||||
|
icon_label.pack(side=tk.LEFT, padx=15, pady=15)
|
||||||
|
title_label = tk.Label(header_frame, text="Exit Application",
|
||||||
|
font=('Arial', 14, 'bold'), fg='white', bg='#cc0000')
|
||||||
|
title_label.pack(side=tk.LEFT, pady=15)
|
||||||
|
content_frame = tk.Frame(exit_dialog, bg='#2d2d2d', padx=20, pady=20)
|
||||||
|
content_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
prompt_label = tk.Label(content_frame, text="Enter password to exit:",
|
||||||
|
font=('Arial', 11), fg='white', bg='#2d2d2d')
|
||||||
|
prompt_label.pack(pady=(0, 10))
|
||||||
|
password_var = tk.StringVar()
|
||||||
|
password_entry = tk.Entry(content_frame, textvariable=password_var,
|
||||||
|
font=('Arial', 11), show='*', width=25,
|
||||||
|
bg='#404040', fg='white', insertbackground='white',
|
||||||
|
relief=tk.FLAT, bd=5)
|
||||||
|
password_entry.pack(pady=(0, 15))
|
||||||
|
password_entry.focus_set()
|
||||||
|
button_frame = tk.Frame(content_frame, bg='#2d2d2d')
|
||||||
|
button_frame.pack(fill=tk.X)
|
||||||
|
def check_password():
|
||||||
|
if password_var.get() == quickconnect_key:
|
||||||
|
exit_dialog.destroy()
|
||||||
|
self.exit_application()
|
||||||
|
elif password_var.get():
|
||||||
|
error_label.config(text="✗ Incorrect password", fg='#ff4444')
|
||||||
|
password_entry.delete(0, tk.END)
|
||||||
|
password_entry.focus_set()
|
||||||
|
def cancel_exit():
|
||||||
|
exit_dialog.destroy()
|
||||||
|
error_label = tk.Label(content_frame, text="", font=('Arial', 9),
|
||||||
|
bg='#2d2d2d')
|
||||||
|
error_label.pack()
|
||||||
|
cancel_btn = tk.Button(button_frame, text="Cancel", command=cancel_exit,
|
||||||
|
bg='#555555', fg='white', font=('Arial', 10, 'bold'),
|
||||||
|
relief=tk.FLAT, padx=20, pady=8, width=10)
|
||||||
|
cancel_btn.pack(side=tk.RIGHT, padx=(10, 0))
|
||||||
|
exit_btn = tk.Button(button_frame, text="Exit", command=check_password,
|
||||||
|
bg='#cc0000', fg='white', font=('Arial', 10, 'bold'),
|
||||||
|
relief=tk.FLAT, padx=20, pady=8, width=10)
|
||||||
|
exit_btn.pack(side=tk.RIGHT)
|
||||||
|
password_entry.bind('<Return>', lambda e: check_password())
|
||||||
|
exit_dialog.bind('<Escape>', lambda e: cancel_exit())
|
||||||
|
|
||||||
|
def exit_application(self):
|
||||||
|
Logger.info("Application exit requested")
|
||||||
|
self.running = False
|
||||||
|
self.root.quit()
|
||||||
|
self.root.destroy()
|
||||||
|
|
||||||
|
def center_dialog_on_screen(self, dialog, width, height):
|
||||||
|
dialog.update_idletasks()
|
||||||
|
screen_width = dialog.winfo_screenwidth()
|
||||||
|
screen_height = dialog.winfo_screenheight()
|
||||||
|
center_x = int((screen_width - width) / 2)
|
||||||
|
center_y = int((screen_height - height) / 2)
|
||||||
|
center_x = max(0, min(center_x, screen_width - width))
|
||||||
|
center_y = max(0, min(center_y, screen_height - height))
|
||||||
|
dialog.geometry(f"{width}x{height}+{center_x}+{center_y}")
|
||||||
|
dialog.lift()
|
||||||
|
dialog.focus_force()
|
||||||
|
return center_x, center_y
|
||||||
|
|
||||||
|
def check_playlist_updates(self):
|
||||||
|
try:
|
||||||
|
config = load_config()
|
||||||
|
local_version = config.get('playlist_version', 0)
|
||||||
|
server_playlist_data = fetch_server_playlist()
|
||||||
|
server_version = server_playlist_data.get('version', 0)
|
||||||
|
if server_version > local_version:
|
||||||
|
Logger.info(f"Updating playlist: {local_version} -> {server_version}")
|
||||||
|
local_playlist_data = load_local_playlist()
|
||||||
|
clean_unused_files(local_playlist_data.get('playlist', []))
|
||||||
|
download_media_files(
|
||||||
|
server_playlist_data.get('playlist', []),
|
||||||
|
server_version
|
||||||
|
)
|
||||||
|
local_playlist_data = load_local_playlist()
|
||||||
|
self.playlist = local_playlist_data.get('playlist', [])
|
||||||
|
self.current_index = 0
|
||||||
|
Logger.info("Playlist updated successfully")
|
||||||
|
self.play_current_media()
|
||||||
|
else:
|
||||||
|
Logger.info("No playlist updates available")
|
||||||
|
except requests.exceptions.ConnectTimeout:
|
||||||
|
Logger.warning("Server connection timeout during update check - continuing with current playlist")
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
Logger.warning("Cannot connect to server during update check - continuing with current playlist")
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
Logger.warning("Server request timeout during update check - continuing with current playlist")
|
||||||
|
except Exception as e:
|
||||||
|
Logger.warning(f"Failed to check playlist updates: {e} - continuing with current playlist")
|
||||||
|
|
||||||
|
def log_event(self, file_name, event):
|
||||||
|
try:
|
||||||
|
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
log_message = f"{timestamp} - {event}: {file_name}\n"
|
||||||
|
log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'log.txt')
|
||||||
|
with open(log_file, 'a') as f:
|
||||||
|
f.write(log_message)
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"Failed to log event: {e}")
|
||||||
|
|
||||||
|
def start_periodic_checks(self):
|
||||||
|
def check_loop():
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
time.sleep(300)
|
||||||
|
if self.running:
|
||||||
|
self.check_playlist_updates()
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"Error in periodic check: {e}")
|
||||||
|
threading.Thread(target=check_loop, daemon=True).start()
|
||||||
|
|
||||||
|
def next_media(self):
|
||||||
|
self.cancel_timers()
|
||||||
|
if not self.playlist:
|
||||||
|
return
|
||||||
|
self.current_index = (self.current_index + 1) % len(self.playlist)
|
||||||
|
# At end of playlist, check for updates
|
||||||
|
if self.current_index == 0:
|
||||||
|
self.check_playlist_updates()
|
||||||
|
self.play_current_media()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
Logger.info("Starting Simple Tkinter Media Player")
|
||||||
|
try:
|
||||||
|
self.root.mainloop()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self.exit_application()
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"Application error: {e}")
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import tkinter as tk
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
class PlayerUI:
|
||||||
|
def __init__(self, root, control_callbacks=None):
|
||||||
|
self.root = root
|
||||||
|
self.control_frame = None
|
||||||
|
self.content_frame = None
|
||||||
|
self.image_label = None
|
||||||
|
self.status_label = None
|
||||||
|
self.play_pause_btn = None
|
||||||
|
self.prev_btn = None
|
||||||
|
self.next_btn = None
|
||||||
|
self.exit_btn = None
|
||||||
|
self.settings_btn = None
|
||||||
|
self.hide_controls_timer = None
|
||||||
|
self.setup_ui(control_callbacks)
|
||||||
|
|
||||||
|
def setup_ui(self, control_callbacks=None):
|
||||||
|
self.content_frame = tk.Frame(self.root, bg='black')
|
||||||
|
self.content_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
self.image_label = tk.Label(self.content_frame, bg='black')
|
||||||
|
self.image_label.pack(fill=tk.BOTH, expand=True)
|
||||||
|
self.status_label = tk.Label(
|
||||||
|
self.content_frame,
|
||||||
|
bg='black',
|
||||||
|
fg='white',
|
||||||
|
font=('Arial', 20),
|
||||||
|
text=""
|
||||||
|
)
|
||||||
|
self.create_control_panel(control_callbacks)
|
||||||
|
self.show_controls()
|
||||||
|
self.schedule_hide_controls()
|
||||||
|
|
||||||
|
def create_control_panel(self, control_callbacks=None):
|
||||||
|
self.control_frame = tk.Frame(
|
||||||
|
self.root,
|
||||||
|
bg='#1a1a1a',
|
||||||
|
bd=2,
|
||||||
|
relief=tk.RAISED,
|
||||||
|
padx=15,
|
||||||
|
pady=15
|
||||||
|
)
|
||||||
|
self.control_frame.place(relx=0.98, rely=0.98, anchor='se')
|
||||||
|
button_config = {
|
||||||
|
'bg': '#333333',
|
||||||
|
'fg': 'white',
|
||||||
|
'activebackground': '#555555',
|
||||||
|
'activeforeground': 'white',
|
||||||
|
'relief': tk.FLAT,
|
||||||
|
'borderwidth': 0,
|
||||||
|
'width': 10,
|
||||||
|
'height': 3,
|
||||||
|
'font': ('Segoe UI', 10, 'bold'),
|
||||||
|
'cursor': 'hand2'
|
||||||
|
}
|
||||||
|
self.prev_btn = tk.Button(
|
||||||
|
self.control_frame,
|
||||||
|
text="⏮ Prev",
|
||||||
|
command=control_callbacks.get('prev') if control_callbacks else None,
|
||||||
|
**button_config
|
||||||
|
)
|
||||||
|
self.prev_btn.grid(row=0, column=0, padx=5)
|
||||||
|
self.play_pause_btn = tk.Button(
|
||||||
|
self.control_frame,
|
||||||
|
text="⏸ Pause",
|
||||||
|
command=control_callbacks.get('play_pause') if control_callbacks else None,
|
||||||
|
bg='#27ae60',
|
||||||
|
activebackground='#35d974',
|
||||||
|
**{k: v for k, v in button_config.items() if k not in ['bg', 'activebackground']}
|
||||||
|
)
|
||||||
|
self.play_pause_btn.grid(row=0, column=1, padx=5)
|
||||||
|
self.next_btn = tk.Button(
|
||||||
|
self.control_frame,
|
||||||
|
text="Next ⏭",
|
||||||
|
command=control_callbacks.get('next') if control_callbacks else None,
|
||||||
|
**button_config
|
||||||
|
)
|
||||||
|
self.next_btn.grid(row=0, column=2, padx=5)
|
||||||
|
self.settings_btn = tk.Button(
|
||||||
|
self.control_frame,
|
||||||
|
text="⚙️ Settings",
|
||||||
|
command=control_callbacks.get('settings') if control_callbacks else None,
|
||||||
|
bg='#9b59b6',
|
||||||
|
activebackground='#bb8fce',
|
||||||
|
**{k: v for k, v in button_config.items() if k not in ['bg', 'activebackground']}
|
||||||
|
)
|
||||||
|
self.settings_btn.grid(row=0, column=3, padx=5)
|
||||||
|
self.exit_btn = tk.Button(
|
||||||
|
self.control_frame,
|
||||||
|
text="❌ EXIT",
|
||||||
|
command=control_callbacks.get('exit') if control_callbacks else None,
|
||||||
|
bg='#e74c3c',
|
||||||
|
fg='white',
|
||||||
|
activebackground='#ec7063',
|
||||||
|
activeforeground='white',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
borderwidth=0,
|
||||||
|
width=8,
|
||||||
|
height=3,
|
||||||
|
font=('Segoe UI', 10, 'bold'),
|
||||||
|
cursor='hand2'
|
||||||
|
)
|
||||||
|
self.exit_btn.grid(row=0, column=4, padx=5)
|
||||||
|
|
||||||
|
def show_controls(self):
|
||||||
|
if self.control_frame:
|
||||||
|
self.control_frame.place(relx=0.98, rely=0.98, anchor='se')
|
||||||
|
|
||||||
|
def hide_controls(self):
|
||||||
|
if self.control_frame:
|
||||||
|
self.control_frame.place_forget()
|
||||||
|
|
||||||
|
def schedule_hide_controls(self):
|
||||||
|
if self.hide_controls_timer:
|
||||||
|
self.root.after_cancel(self.hide_controls_timer)
|
||||||
|
self.hide_controls_timer = self.root.after(10000, self.hide_controls)
|
||||||
|
|
||||||
|
def scale_image_to_screen(self, img, screen_width, screen_height, mode='fit'):
|
||||||
|
img_width, img_height = img.size
|
||||||
|
if mode == 'stretch':
|
||||||
|
return img.resize((screen_width, screen_height), Image.LANCZOS), (0, 0)
|
||||||
|
elif mode == 'fill':
|
||||||
|
screen_ratio = screen_width / screen_height
|
||||||
|
img_ratio = img_width / img_height
|
||||||
|
if img_ratio > screen_ratio:
|
||||||
|
new_height = screen_height
|
||||||
|
new_width = int(screen_height * img_ratio)
|
||||||
|
x_offset = (screen_width - new_width) // 2
|
||||||
|
y_offset = 0
|
||||||
|
else:
|
||||||
|
new_width = screen_width
|
||||||
|
new_height = int(screen_width / img_ratio)
|
||||||
|
x_offset = 0
|
||||||
|
y_offset = (screen_height - new_height) // 2
|
||||||
|
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
|
||||||
|
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
|
||||||
|
if new_width > screen_width:
|
||||||
|
crop_x = (new_width - screen_width) // 2
|
||||||
|
img_resized = img_resized.crop((crop_x, 0, crop_x + screen_width, new_height))
|
||||||
|
x_offset = 0
|
||||||
|
if new_height > screen_height:
|
||||||
|
crop_y = (new_height - screen_height) // 2
|
||||||
|
img_resized = img_resized.crop((0, crop_y, new_width, crop_y + screen_height))
|
||||||
|
y_offset = 0
|
||||||
|
final_img.paste(img_resized, (x_offset, y_offset))
|
||||||
|
return final_img, (x_offset, y_offset)
|
||||||
|
else:
|
||||||
|
screen_ratio = screen_width / screen_height
|
||||||
|
img_ratio = img_width / img_height
|
||||||
|
if img_ratio > screen_ratio:
|
||||||
|
new_width = screen_width
|
||||||
|
new_height = int(screen_width / img_ratio)
|
||||||
|
else:
|
||||||
|
new_height = screen_height
|
||||||
|
new_width = int(screen_height * img_ratio)
|
||||||
|
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
|
||||||
|
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
|
||||||
|
x_offset = (screen_width - new_width) // 2
|
||||||
|
y_offset = (screen_height - new_height) // 2
|
||||||
|
final_img.paste(img_resized, (x_offset, y_offset))
|
||||||
|
return final_img, (x_offset, y_offset)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import threading
|
||||||
|
from logging_config import Logger
|
||||||
|
from python_functions import (
|
||||||
|
load_local_playlist, download_media_files, update_config_playlist_version, fetch_server_playlist, load_config
|
||||||
|
)
|
||||||
|
|
||||||
|
class PlaylistManager:
|
||||||
|
def __init__(self):
|
||||||
|
self._playlist_ready = threading.Event()
|
||||||
|
self._playlist_data = None
|
||||||
|
|
||||||
|
def fetch_playlist(self):
|
||||||
|
def fetch_logic():
|
||||||
|
fallback_playlist = None
|
||||||
|
try:
|
||||||
|
local_playlist_data = load_local_playlist()
|
||||||
|
fallback_playlist = local_playlist_data.get('playlist', [])
|
||||||
|
if fallback_playlist:
|
||||||
|
Logger.info(f"Found fallback playlist with {len(fallback_playlist)} items")
|
||||||
|
except Exception as e:
|
||||||
|
Logger.warning(f"No fallback playlist available: {e}")
|
||||||
|
config = load_config()
|
||||||
|
server = config.get("server_ip", "")
|
||||||
|
host = config.get("screen_name", "")
|
||||||
|
quick = config.get("quickconnect_key", "")
|
||||||
|
port = config.get("port", "")
|
||||||
|
Logger.info(f"Initializing with settings: server={server}, host={host}, port={port}")
|
||||||
|
playlist = None
|
||||||
|
if server and host and quick and port:
|
||||||
|
try:
|
||||||
|
Logger.info("Attempting to connect to server...")
|
||||||
|
server_playlist_data = fetch_server_playlist()
|
||||||
|
server_playlist = server_playlist_data.get('playlist', [])
|
||||||
|
server_version = server_playlist_data.get('version', 0)
|
||||||
|
if server_playlist:
|
||||||
|
Logger.info(f"Server playlist found with {len(server_playlist)} items, version {server_version}")
|
||||||
|
download_media_files(server_playlist, server_version)
|
||||||
|
update_config_playlist_version(server_version)
|
||||||
|
local_playlist_data = load_local_playlist()
|
||||||
|
playlist = local_playlist_data.get('playlist', [])
|
||||||
|
if playlist:
|
||||||
|
Logger.info(f"Successfully loaded {len(playlist)} items from server")
|
||||||
|
else:
|
||||||
|
Logger.warning("Server playlist was empty, falling back to local playlist")
|
||||||
|
else:
|
||||||
|
Logger.warning("Server returned empty playlist, falling back to local playlist")
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"Failed to fetch playlist from server: {e}, using fallback playlist")
|
||||||
|
if not playlist:
|
||||||
|
playlist = fallback_playlist
|
||||||
|
if not playlist:
|
||||||
|
playlist = None
|
||||||
|
self._playlist_data = playlist
|
||||||
|
self._playlist_ready.set()
|
||||||
|
threading.Thread(target=fetch_logic, daemon=True).start()
|
||||||
|
|
||||||
|
def wait_for_playlist(self):
|
||||||
|
self._playlist_ready.wait()
|
||||||
|
return self._playlist_data
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import tkinter as tk
|
||||||
|
from logging_config import Logger
|
||||||
|
|
||||||
|
class SplashScreen:
|
||||||
|
def __init__(self, root, video_path, duration=5):
|
||||||
|
self.root = root
|
||||||
|
self.video_path = video_path
|
||||||
|
self.duration = duration
|
||||||
|
|
||||||
|
def show(self, on_finish=None):
|
||||||
|
Logger.info(f"[SplashScreen] Running splash as standalone VLC subprocess: {self.video_path}")
|
||||||
|
if os.path.exists(self.video_path):
|
||||||
|
try:
|
||||||
|
vlc_cmd = [
|
||||||
|
'cvlc',
|
||||||
|
'--fullscreen',
|
||||||
|
'--no-osd',
|
||||||
|
'--no-video-title-show',
|
||||||
|
'--play-and-exit',
|
||||||
|
'--quiet',
|
||||||
|
self.video_path
|
||||||
|
]
|
||||||
|
Logger.info(f"[SplashScreen] Launching: {' '.join(vlc_cmd)}")
|
||||||
|
proc = subprocess.Popen(vlc_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
proc.wait() # Remove timeout for immediate transition
|
||||||
|
Logger.info("[SplashScreen] VLC splash finished.")
|
||||||
|
except Exception as e:
|
||||||
|
Logger.warning(f"[SplashScreen] VLC splash failed: {e}")
|
||||||
|
else:
|
||||||
|
Logger.warning(f"[SplashScreen] Splash video not found: {self.video_path}")
|
||||||
|
if on_finish:
|
||||||
|
self.root.after(0, on_finish)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
root = tk.Tk()
|
||||||
|
# Use the same logic as main.py to resolve the video path
|
||||||
|
base_dir = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
video_path = os.path.join(base_dir, 'resources', 'intro1.mp4')
|
||||||
|
Logger.info(f"[SplashScreen] Standalone test: video_path={video_path}")
|
||||||
|
def on_finish():
|
||||||
|
Logger.info("[SplashScreen] Standalone test: Splash finished, exiting app.")
|
||||||
|
root.destroy()
|
||||||
|
splash = SplashScreen(root, video_path, duration=10)
|
||||||
|
splash.show(on_finish=on_finish)
|
||||||
|
root.mainloop()
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import vlc
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
from logging_config import Logger
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
video_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'intro1.mp4')
|
||||||
|
Logger.info(f"[VLC TEST] Attempting to play video: {video_path}")
|
||||||
|
if not os.path.exists(video_path):
|
||||||
|
Logger.error(f"[VLC TEST] File does not exist: {video_path}")
|
||||||
|
exit(1)
|
||||||
|
instance = vlc.Instance('--no-osd', '--no-video-title-show', '--quiet', '--fullscreen')
|
||||||
|
player = instance.media_player_new()
|
||||||
|
media = instance.media_new(video_path)
|
||||||
|
player.set_media(media)
|
||||||
|
player.play()
|
||||||
|
Logger.info("[VLC TEST] player.play() called. Waiting for video to finish...")
|
||||||
|
# Wait for video to finish (max 30 seconds)
|
||||||
|
start = time.time()
|
||||||
|
while True:
|
||||||
|
state = player.get_state()
|
||||||
|
if state in (vlc.State.Ended, vlc.State.Error):
|
||||||
|
Logger.info(f"[VLC TEST] Playback ended with state: {state}")
|
||||||
|
break
|
||||||
|
if time.time() - start > 30:
|
||||||
|
Logger.warning("[VLC TEST] Timeout waiting for video to finish.")
|
||||||
|
break
|
||||||
|
time.sleep(0.2)
|
||||||
|
player.stop()
|
||||||
|
Logger.info("[VLC TEST] Done.")
|
||||||
Reference in New Issue
Block a user