Perf: Optimize playback and simplify playlist management
- Performance improvements: * Throttle drawing updates to 60fps (16ms intervals) * Optimize file I/O: use single os.stat() instead of exists+getsize * Reduce logger overhead: convert hot-path info logs to debug * Preload next media asynchronously for smoother transitions * Smart cache invalidation for edited images - Simplify playlist management: * Remove versioning: single server_playlist.json file * Create nested directories for edited_media downloads * Recursively delete unused media and empty folders * Cleaner version tracking without file proliferation - UI improvements: * Smoother intro-to-playlist transition * Fix edited media directory creation for nested paths
This commit is contained in:
@@ -1 +0,0 @@
|
||||
User requested exit via password
|
||||
@@ -30,6 +30,8 @@ class DrawingLayer(Widget):
|
||||
self.current_width = 3 # Default thickness
|
||||
self.drawing_enabled = True # Drawing always enabled in edit mode
|
||||
self.reset_callback = reset_callback # Callback to reset countdown timer
|
||||
self._last_draw_time = 0 # For throttling touch updates
|
||||
self._draw_throttle_interval = 0.016 # ~60fps (16ms between updates)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if not self.drawing_enabled or not self.collide_point(*touch.pos):
|
||||
@@ -48,7 +50,11 @@ class DrawingLayer(Widget):
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if 'line' in touch.ud and self.drawing_enabled:
|
||||
touch.ud['line'].points += [touch.x, touch.y]
|
||||
# Throttle updates to ~60fps for better performance
|
||||
current_time = time.time()
|
||||
if current_time - self._last_draw_time >= self._draw_throttle_interval:
|
||||
touch.ud['line'].points += [touch.x, touch.y]
|
||||
self._last_draw_time = current_time
|
||||
return True
|
||||
|
||||
def undo(self):
|
||||
@@ -68,12 +74,10 @@ class DrawingLayer(Widget):
|
||||
def set_color(self, color_tuple):
|
||||
"""Set drawing color (RGBA)"""
|
||||
self.current_color = color_tuple
|
||||
Logger.debug(f"DrawingLayer: Color set to {color_tuple}")
|
||||
|
||||
def set_thickness(self, value):
|
||||
"""Set line thickness"""
|
||||
self.current_width = value
|
||||
Logger.debug(f"DrawingLayer: Thickness set to {value}")
|
||||
|
||||
|
||||
class EditPopup(Popup):
|
||||
|
||||
@@ -188,10 +188,9 @@ def fetch_server_playlist(config):
|
||||
return {'playlist': [], 'version': 0}
|
||||
|
||||
|
||||
def save_playlist_with_version(playlist_data, playlist_dir):
|
||||
"""Save playlist to file with version number."""
|
||||
version = playlist_data.get('version', 0)
|
||||
playlist_file = os.path.join(playlist_dir, f'server_playlist_v{version}.json')
|
||||
def save_playlist(playlist_data, playlist_dir):
|
||||
"""Save playlist to a single file (no versioning)."""
|
||||
playlist_file = os.path.join(playlist_dir, 'server_playlist.json')
|
||||
|
||||
# Ensure directory exists
|
||||
os.makedirs(playlist_dir, exist_ok=True)
|
||||
@@ -222,6 +221,9 @@ def download_media_files(playlist, media_dir):
|
||||
logger.info(f"✓ File {file_name} already exists. Skipping download.")
|
||||
else:
|
||||
try:
|
||||
# Create parent directories if they don't exist (for nested paths like edited_media/5/)
|
||||
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
||||
|
||||
response = requests.get(file_url, timeout=30)
|
||||
if response.status_code == 200:
|
||||
with open(local_path, 'wb') as file:
|
||||
@@ -245,82 +247,59 @@ def download_media_files(playlist, media_dir):
|
||||
return updated_playlist
|
||||
|
||||
|
||||
def delete_old_playlists_and_media(current_version, playlist_dir, media_dir, keep_versions=1):
|
||||
"""Delete old playlist files and media files not referenced by the latest playlist version."""
|
||||
def delete_unused_media(playlist_data, media_dir):
|
||||
"""Delete media files not referenced in the current playlist."""
|
||||
try:
|
||||
# Find all playlist files
|
||||
playlist_files = [f for f in os.listdir(playlist_dir)
|
||||
if f.startswith('server_playlist_v') and f.endswith('.json')]
|
||||
|
||||
# Extract versions and sort
|
||||
versions = []
|
||||
for f in playlist_files:
|
||||
try:
|
||||
version = int(f.replace('server_playlist_v', '').replace('.json', ''))
|
||||
versions.append((version, f))
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
versions.sort(reverse=True)
|
||||
|
||||
# Keep only the latest N versions
|
||||
files_to_delete = [f for v, f in versions[keep_versions:]]
|
||||
|
||||
for f in files_to_delete:
|
||||
filepath = os.path.join(playlist_dir, f)
|
||||
os.remove(filepath)
|
||||
logger.info(f"🗑️ Deleted old playlist: {f}")
|
||||
|
||||
# Clean up unused media files
|
||||
logger.info("🔍 Checking for unused media files...")
|
||||
|
||||
# Get list of media files referenced in current playlist
|
||||
current_playlist_file = os.path.join(playlist_dir, f'server_playlist_v{current_version}.json')
|
||||
referenced_files = set()
|
||||
for media in playlist_data.get('playlist', []):
|
||||
file_name = media.get('file_name', '')
|
||||
if file_name:
|
||||
referenced_files.add(file_name)
|
||||
|
||||
if os.path.exists(current_playlist_file):
|
||||
try:
|
||||
with open(current_playlist_file, 'r') as f:
|
||||
playlist_data = json.load(f)
|
||||
for item in playlist_data.get('playlist', []):
|
||||
file_name = item.get('file_name', '')
|
||||
if file_name:
|
||||
referenced_files.add(file_name)
|
||||
|
||||
logger.info(f"📋 Current playlist references {len(referenced_files)} media files")
|
||||
|
||||
# Get all files in media directory (excluding edited_media subfolder)
|
||||
if os.path.exists(media_dir):
|
||||
media_files = [f for f in os.listdir(media_dir)
|
||||
if os.path.isfile(os.path.join(media_dir, f))]
|
||||
|
||||
deleted_count = 0
|
||||
for media_file in media_files:
|
||||
# Skip if file is in current playlist
|
||||
if media_file in referenced_files:
|
||||
continue
|
||||
|
||||
# Delete unreferenced file
|
||||
media_path = os.path.join(media_dir, media_file)
|
||||
try:
|
||||
os.remove(media_path)
|
||||
logger.info(f"🗑️ Deleted unused media: {media_file}")
|
||||
deleted_count += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Could not delete {media_file}: {e}")
|
||||
|
||||
if deleted_count > 0:
|
||||
logger.info(f"✅ Deleted {deleted_count} unused media files")
|
||||
else:
|
||||
logger.info("✅ No unused media files to delete")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error reading playlist for media cleanup: {e}")
|
||||
logger.info(f"📋 Current playlist references {len(referenced_files)} files")
|
||||
|
||||
logger.info(f"✅ Cleanup complete (kept {keep_versions} latest playlist versions)")
|
||||
|
||||
if os.path.exists(media_dir):
|
||||
# Recursively get all media files
|
||||
deleted_count = 0
|
||||
for root, dirs, files in os.walk(media_dir):
|
||||
for media_file in files:
|
||||
# Get relative path from media_dir
|
||||
full_path = os.path.join(root, media_file)
|
||||
rel_path = os.path.relpath(full_path, media_dir)
|
||||
|
||||
# Skip if file is in current playlist
|
||||
if rel_path in referenced_files:
|
||||
continue
|
||||
|
||||
# Delete unreferenced file
|
||||
try:
|
||||
os.remove(full_path)
|
||||
logger.info(f"🗑️ Deleted unused media: {rel_path}")
|
||||
deleted_count += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Could not delete {rel_path}: {e}")
|
||||
|
||||
# Clean up empty directories
|
||||
for root, dirs, files in os.walk(media_dir, topdown=False):
|
||||
for dir_name in dirs:
|
||||
dir_path = os.path.join(root, dir_name)
|
||||
try:
|
||||
if not os.listdir(dir_path): # If directory is empty
|
||||
os.rmdir(dir_path)
|
||||
logger.debug(f"🗑️ Removed empty directory: {os.path.relpath(dir_path, media_dir)}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if deleted_count > 0:
|
||||
logger.info(f"✅ Deleted {deleted_count} unused media files")
|
||||
else:
|
||||
logger.info("✅ No unused media files to delete")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error during cleanup: {e}")
|
||||
logger.error(f"❌ Error during media cleanup: {e}")
|
||||
|
||||
|
||||
|
||||
|
||||
def update_playlist_if_needed(config, playlist_dir, media_dir):
|
||||
@@ -334,22 +313,17 @@ def update_playlist_if_needed(config, playlist_dir, media_dir):
|
||||
logger.warning("⚠️ No valid playlist received from server")
|
||||
return None
|
||||
|
||||
# Check local version
|
||||
# Check local version from single playlist file
|
||||
local_version = 0
|
||||
local_playlist_file = None
|
||||
playlist_file = os.path.join(playlist_dir, 'server_playlist.json')
|
||||
|
||||
if os.path.exists(playlist_dir):
|
||||
playlist_files = [f for f in os.listdir(playlist_dir)
|
||||
if f.startswith('server_playlist_v') and f.endswith('.json')]
|
||||
|
||||
for f in playlist_files:
|
||||
try:
|
||||
version = int(f.replace('server_playlist_v', '').replace('.json', ''))
|
||||
if version > local_version:
|
||||
local_version = version
|
||||
local_playlist_file = os.path.join(playlist_dir, f)
|
||||
except ValueError:
|
||||
continue
|
||||
if os.path.exists(playlist_file):
|
||||
try:
|
||||
with open(playlist_file, 'r') as f:
|
||||
local_data = json.load(f)
|
||||
local_version = local_data.get('version', 0)
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Could not read local playlist: {e}")
|
||||
|
||||
logger.info(f"📊 Playlist versions - Server: v{server_version}, Local: v{local_version}")
|
||||
|
||||
@@ -361,17 +335,17 @@ def update_playlist_if_needed(config, playlist_dir, media_dir):
|
||||
updated_playlist = download_media_files(server_data['playlist'], media_dir)
|
||||
server_data['playlist'] = updated_playlist
|
||||
|
||||
# Save new playlist
|
||||
playlist_file = save_playlist_with_version(server_data, playlist_dir)
|
||||
# Save new playlist (single file, no versioning)
|
||||
playlist_file = save_playlist(server_data, playlist_dir)
|
||||
|
||||
# Clean up old versions
|
||||
delete_old_playlists_and_media(server_version, playlist_dir, media_dir)
|
||||
# Delete unused media files
|
||||
delete_unused_media(server_data, media_dir)
|
||||
|
||||
logger.info(f"✅ Playlist updated successfully to v{server_version}")
|
||||
return playlist_file
|
||||
else:
|
||||
logger.info("✓ Playlist is up to date")
|
||||
return local_playlist_file
|
||||
return playlist_file
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error updating playlist: {e}")
|
||||
|
||||
149
src/main.py
149
src/main.py
@@ -32,6 +32,7 @@ from kivy.uix.popup import Popup
|
||||
from kivy.uix.textinput import TextInput
|
||||
from kivy.uix.vkeyboard import VKeyboard
|
||||
from kivy.clock import Clock
|
||||
from kivy.loader import Loader
|
||||
from kivy.core.window import Window
|
||||
from kivy.properties import BooleanProperty
|
||||
from kivy.logger import Logger
|
||||
@@ -768,27 +769,20 @@ class SettingsPopup(Popup):
|
||||
"""Reset playlist version to 0 and rename playlist file"""
|
||||
try:
|
||||
playlists_dir = os.path.join(self.player.base_dir, 'playlists')
|
||||
playlist_file = os.path.join(playlists_dir, 'server_playlist.json')
|
||||
|
||||
# Find current playlist file
|
||||
import glob
|
||||
playlist_files = glob.glob(os.path.join(playlists_dir, 'server_playlist_v*.json'))
|
||||
|
||||
if playlist_files:
|
||||
# Get the first (should be only one) playlist file
|
||||
current_playlist = playlist_files[0]
|
||||
new_playlist = os.path.join(playlists_dir, 'server_playlist_v0.json')
|
||||
if os.path.exists(playlist_file):
|
||||
# Delete the playlist file to force re-download
|
||||
os.remove(playlist_file)
|
||||
Logger.info(f"SettingsPopup: Deleted playlist file - will resync from server")
|
||||
|
||||
# Rename to v0
|
||||
os.rename(current_playlist, new_playlist)
|
||||
Logger.info(f"SettingsPopup: Renamed {os.path.basename(current_playlist)} to server_playlist_v0.json")
|
||||
|
||||
# Update player's playlist version
|
||||
# Reset player's playlist version
|
||||
self.player.playlist_version = 0
|
||||
|
||||
# Update display
|
||||
self.ids.playlist_info.text = f'Playlist: v{self.player.playlist_version}'
|
||||
|
||||
self._show_temp_message('✓ Playlist reset to v0 - will resync from server', (0, 1, 0, 1))
|
||||
self._show_temp_message('✓ Playlist reset - will resync from server', (0, 1, 0, 1))
|
||||
else:
|
||||
Logger.info("SettingsPopup: No playlist file found")
|
||||
self._show_temp_message('No playlist file found', (1, 0.7, 0, 1))
|
||||
@@ -1046,22 +1040,8 @@ class SignagePlayer(Widget):
|
||||
Logger.debug(f"SignagePlayer: Error sending async feedback: {e}")
|
||||
|
||||
def get_latest_playlist_file(self):
|
||||
"""Get the path to the latest playlist file"""
|
||||
try:
|
||||
if os.path.exists(self.playlists_dir):
|
||||
playlist_files = [f for f in os.listdir(self.playlists_dir)
|
||||
if f.startswith('server_playlist_v') and f.endswith('.json')]
|
||||
if playlist_files:
|
||||
# Sort by version number and get the latest
|
||||
versions = [(int(f.split('_v')[-1].split('.json')[0]), f) for f in playlist_files]
|
||||
versions.sort(reverse=True)
|
||||
latest_file = versions[0][1]
|
||||
return os.path.join(self.playlists_dir, latest_file)
|
||||
|
||||
return os.path.join(self.playlists_dir, 'server_playlist_v0.json')
|
||||
except Exception as e:
|
||||
Logger.error(f"SignagePlayer: Error getting latest playlist: {e}")
|
||||
return os.path.join(self.playlists_dir, 'server_playlist_v0.json')
|
||||
"""Get the path to the playlist file"""
|
||||
return os.path.join(self.playlists_dir, 'server_playlist.json')
|
||||
|
||||
def load_playlist(self, dt=None):
|
||||
"""Load playlist from file"""
|
||||
@@ -1121,14 +1101,15 @@ class SignagePlayer(Widget):
|
||||
def on_intro_end(instance, value):
|
||||
if value == 'stop':
|
||||
Logger.info("SignagePlayer: Intro video finished")
|
||||
|
||||
# Mark intro as played before removing video
|
||||
self.intro_played = True
|
||||
|
||||
# Remove intro video
|
||||
if intro_video in self.ids.content_area.children:
|
||||
self.ids.content_area.remove_widget(intro_video)
|
||||
|
||||
# Mark intro as played
|
||||
self.intro_played = True
|
||||
|
||||
# Start normal playlist
|
||||
# Start normal playlist immediately to reduce white screen
|
||||
self.check_playlist_and_play(None)
|
||||
|
||||
intro_video.bind(state=on_intro_end)
|
||||
@@ -1186,23 +1167,22 @@ class SignagePlayer(Widget):
|
||||
file_name = media_item.get('file_name', '')
|
||||
duration = media_item.get('duration', 10)
|
||||
|
||||
Logger.info(f"SignagePlayer: ===== Playing item {self.current_index + 1}/{len(self.playlist)} =====")
|
||||
Logger.info(f"SignagePlayer: File: {file_name}")
|
||||
Logger.info(f"SignagePlayer: Duration: {duration}s")
|
||||
Logger.info(f"SignagePlayer: Playing item {self.current_index + 1}/{len(self.playlist)}: {file_name} ({duration}s)")
|
||||
|
||||
# Construct full path to media file
|
||||
media_path = os.path.join(self.media_dir, file_name)
|
||||
Logger.info(f"SignagePlayer: Full path: {media_path}")
|
||||
|
||||
if not os.path.exists(media_path):
|
||||
# Use single os.stat() call to check existence and get size (more efficient)
|
||||
try:
|
||||
file_stat = os.stat(media_path)
|
||||
Logger.debug(f"SignagePlayer: File size: {file_stat.st_size:,} bytes")
|
||||
except FileNotFoundError:
|
||||
Logger.error(f"SignagePlayer: ❌ Media file not found: {media_path}")
|
||||
Logger.error(f"SignagePlayer: Skipping to next media...")
|
||||
self.consecutive_errors += 1
|
||||
self.next_media()
|
||||
return
|
||||
|
||||
Logger.info(f"SignagePlayer: ✓ File exists (size: {os.path.getsize(media_path):,} bytes)")
|
||||
|
||||
# Remove status label if showing
|
||||
self.ids.status_label.opacity = 0
|
||||
|
||||
@@ -1211,7 +1191,7 @@ class SignagePlayer(Widget):
|
||||
# Properly stop video if it's playing to prevent resource leaks
|
||||
if isinstance(self.current_widget, Video):
|
||||
try:
|
||||
Logger.info(f"SignagePlayer: Stopping previous video widget...")
|
||||
Logger.debug(f"SignagePlayer: Stopping previous video widget...")
|
||||
self.current_widget.state = 'stop'
|
||||
self.current_widget.unload()
|
||||
except Exception as e:
|
||||
@@ -1219,19 +1199,18 @@ class SignagePlayer(Widget):
|
||||
|
||||
self.ids.content_area.remove_widget(self.current_widget)
|
||||
self.current_widget = None
|
||||
Logger.info(f"SignagePlayer: Previous widget removed")
|
||||
Logger.debug(f"SignagePlayer: Previous widget removed")
|
||||
|
||||
# Determine media type and create appropriate widget
|
||||
file_extension = os.path.splitext(file_name)[1].lower()
|
||||
Logger.info(f"SignagePlayer: Extension: {file_extension}")
|
||||
|
||||
if file_extension in ['.mp4', '.avi', '.mkv', '.mov', '.webm']:
|
||||
# Video file
|
||||
Logger.info(f"SignagePlayer: Media type: VIDEO")
|
||||
Logger.debug(f"SignagePlayer: Media type: VIDEO")
|
||||
self.play_video(media_path, duration)
|
||||
elif file_extension in ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp']:
|
||||
# Image file
|
||||
Logger.info(f"SignagePlayer: Media type: IMAGE")
|
||||
Logger.debug(f"SignagePlayer: Media type: IMAGE")
|
||||
self.play_image(media_path, duration, force_reload=force_reload)
|
||||
else:
|
||||
Logger.warning(f"SignagePlayer: ❌ Unsupported media type: {file_extension}")
|
||||
@@ -1254,7 +1233,7 @@ class SignagePlayer(Widget):
|
||||
|
||||
# Reset error counter on successful playback
|
||||
self.consecutive_errors = 0
|
||||
Logger.info(f"SignagePlayer: ✓ Media started successfully (consecutive_errors reset to 0)")
|
||||
Logger.debug(f"SignagePlayer: Media started successfully")
|
||||
|
||||
except Exception as e:
|
||||
Logger.error(f"SignagePlayer: Error playing media: {e}")
|
||||
@@ -1281,11 +1260,9 @@ class SignagePlayer(Widget):
|
||||
self.next_media()
|
||||
return
|
||||
|
||||
Logger.info(f"SignagePlayer: Loading video {os.path.basename(video_path)} for {duration}s")
|
||||
Logger.info(f"SignagePlayer: Video provider: {os.environ.get('KIVY_VIDEO', 'default')}")
|
||||
Logger.debug(f"SignagePlayer: Loading video {os.path.basename(video_path)} for {duration}s")
|
||||
|
||||
# Create Video widget with optimized settings
|
||||
Logger.info(f"SignagePlayer: Creating Video widget...")
|
||||
self.current_widget = Video(
|
||||
source=video_path,
|
||||
state='play', # Start playing immediately
|
||||
@@ -1301,14 +1278,16 @@ class SignagePlayer(Widget):
|
||||
self.current_widget.bind(on_eos=self._on_video_eos)
|
||||
|
||||
# Add to content area
|
||||
Logger.info(f"SignagePlayer: Adding video widget to content area...")
|
||||
self.ids.content_area.add_widget(self.current_widget)
|
||||
|
||||
# Schedule next media after duration (unschedule first to prevent overlaps)
|
||||
Logger.info(f"SignagePlayer: Scheduled next media in {duration}s")
|
||||
Logger.debug(f"SignagePlayer: Scheduled next media in {duration}s")
|
||||
Clock.unschedule(self.next_media)
|
||||
Clock.schedule_once(self.next_media, duration)
|
||||
|
||||
# Preload next media asynchronously for smoother transitions
|
||||
self.preload_next_media()
|
||||
|
||||
except Exception as e:
|
||||
Logger.error(f"SignagePlayer: Error playing video {video_path}: {e}")
|
||||
self.consecutive_errors += 1
|
||||
@@ -1317,30 +1296,24 @@ class SignagePlayer(Widget):
|
||||
|
||||
def _on_video_eos(self, instance):
|
||||
"""Callback when video reaches end of stream"""
|
||||
Logger.info("SignagePlayer: Video finished playing (EOS)")
|
||||
Logger.debug("SignagePlayer: Video finished playing (EOS)")
|
||||
|
||||
def _on_video_loaded(self, instance, value):
|
||||
"""Callback when video is loaded - log video information"""
|
||||
if value:
|
||||
try:
|
||||
Logger.info(f"SignagePlayer: Video loaded successfully")
|
||||
Logger.info(f"SignagePlayer: Video texture: {instance.texture.size if instance.texture else 'No texture'}")
|
||||
Logger.info(f"SignagePlayer: Video duration: {instance.duration}s")
|
||||
Logger.info(f"SignagePlayer: Video state: {instance.state}")
|
||||
Logger.debug(f"SignagePlayer: Video loaded: {instance.texture.size if instance.texture else 'No texture'}, {instance.duration}s")
|
||||
except Exception as e:
|
||||
Logger.warning(f"SignagePlayer: Could not log video info: {e}")
|
||||
Logger.debug(f"SignagePlayer: Could not log video info: {e}")
|
||||
|
||||
def play_image(self, image_path, duration, force_reload=False):
|
||||
"""Play an image file"""
|
||||
try:
|
||||
# Log file info before loading
|
||||
file_size = os.path.getsize(image_path)
|
||||
file_mtime = os.path.getmtime(image_path)
|
||||
Logger.info(f"SignagePlayer: Loading image: {os.path.basename(image_path)}")
|
||||
Logger.info(f" - Size: {file_size:,} bytes, Modified: {file_mtime}")
|
||||
Logger.debug(f"SignagePlayer: Loading image: {os.path.basename(image_path)} ({os.path.getsize(image_path):,} bytes)")
|
||||
|
||||
if force_reload:
|
||||
Logger.info(f"SignagePlayer: Force reload - using Image widget (no async cache)")
|
||||
Logger.debug(f"SignagePlayer: Force reload - bypassing cache")
|
||||
# Use regular Image widget instead of AsyncImage to bypass all caching
|
||||
from kivy.uix.image import Image
|
||||
self.current_widget = Image(
|
||||
@@ -1353,7 +1326,6 @@ class SignagePlayer(Widget):
|
||||
# Force reload the texture
|
||||
self.current_widget.reload()
|
||||
else:
|
||||
Logger.info(f"SignagePlayer: Creating AsyncImage widget...")
|
||||
self.current_widget = AsyncImage(
|
||||
source=image_path,
|
||||
allow_stretch=True,
|
||||
@@ -1361,13 +1333,14 @@ class SignagePlayer(Widget):
|
||||
size_hint=(1, 1),
|
||||
pos_hint={'center_x': 0.5, 'center_y': 0.5}
|
||||
)
|
||||
Logger.info(f"SignagePlayer: Adding image widget to content area...")
|
||||
self.ids.content_area.add_widget(self.current_widget)
|
||||
# Schedule next media after duration (unschedule first to prevent overlaps)
|
||||
Logger.info(f"SignagePlayer: Scheduled next media in {duration}s")
|
||||
Logger.debug(f"SignagePlayer: Scheduled next media in {duration}s")
|
||||
Clock.unschedule(self.next_media)
|
||||
Clock.schedule_once(self.next_media, duration)
|
||||
Logger.info(f"SignagePlayer: ✓ Image displayed successfully")
|
||||
|
||||
# Preload next media asynchronously for smoother transitions
|
||||
self.preload_next_media()
|
||||
except Exception as e:
|
||||
Logger.error(f"SignagePlayer: Error playing image {image_path}: {e}")
|
||||
self.consecutive_errors += 1
|
||||
@@ -1396,6 +1369,48 @@ class SignagePlayer(Widget):
|
||||
Clock.unschedule(self.next_media)
|
||||
self.play_current_media()
|
||||
|
||||
def preload_next_media(self):
|
||||
"""Preload the next media item asynchronously to improve transition smoothness
|
||||
|
||||
Only preloads images as videos are loaded on-demand.
|
||||
Respects force_reload scenarios (e.g., after editing).
|
||||
"""
|
||||
if not self.playlist:
|
||||
return
|
||||
|
||||
# Calculate next index (with wraparound)
|
||||
next_index = (self.current_index + 1) % len(self.playlist)
|
||||
|
||||
try:
|
||||
next_media_item = self.playlist[next_index]
|
||||
file_name = next_media_item.get('file_name', '')
|
||||
media_path = os.path.join(self.media_dir, file_name)
|
||||
|
||||
# Check if file exists before attempting preload
|
||||
if not os.path.exists(media_path):
|
||||
Logger.debug(f"SignagePlayer: Preload skipped - file not found: {file_name}")
|
||||
return
|
||||
|
||||
# Only preload images (videos are handled differently)
|
||||
file_extension = os.path.splitext(file_name)[1].lower()
|
||||
if file_extension in ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp']:
|
||||
# Check if this is the same file as current (e.g., after editing)
|
||||
# If so, we need to invalidate cache
|
||||
current_media = self.playlist[self.current_index]
|
||||
current_file = current_media.get('file_name', '')
|
||||
|
||||
if file_name == current_file:
|
||||
# Same file - likely edited, don't preload as we'll force reload
|
||||
Logger.debug(f"SignagePlayer: Preload skipped - same file after edit: {file_name}")
|
||||
return
|
||||
|
||||
# Use Kivy's Loader to preload asynchronously
|
||||
Logger.debug(f"SignagePlayer: Preloading next image: {file_name}")
|
||||
Loader.image(media_path)
|
||||
|
||||
except Exception as e:
|
||||
Logger.debug(f"SignagePlayer: Error preloading next media: {e}")
|
||||
|
||||
def toggle_pause(self, instance=None):
|
||||
"""Toggle pause/play with auto-resume after 5 minutes"""
|
||||
self.is_paused = not self.is_paused
|
||||
|
||||
Reference in New Issue
Block a user