updated view

This commit is contained in:
Kivy Signage Player
2025-10-27 16:46:08 +02:00
parent e3831d1594
commit a2fddfa312
6 changed files with 267 additions and 30 deletions

View File

@@ -4,5 +4,6 @@
"screen_name": "rpi-tvholba1",
"quickconnect_key": "8887779",
"orientation": "Landscape",
"touch": "True"
"touch": "True",
"max_resolution": "1920x1080"
}

View File

@@ -1,15 +0,0 @@
{
"playlist": [
{
"file_name": "call-of-duty-black-3840x2160-23674.jpg",
"url": "media/call-of-duty-black-3840x2160-23674.jpg",
"duration": 30
},
{
"file_name": "HARTING_Safety_day_informare_2_page_005.jpg",
"url": "media/HARTING_Safety_day_informare_2_page_005.jpg",
"duration": 20
}
],
"version": 5
}

View File

@@ -1,6 +1,6 @@
#!/bin/bash
# Start Kivy Signage Player
cd "$(dirname "$0")" && source .venv/bin/activate
cd "$(dirname "$0")/src"
python3 main.py

View File

@@ -5,12 +5,20 @@ Displays content from DigiServer playlists using Kivy framework
import os
import json
import platform
import threading
import time
# Set environment variables for better video performance
os.environ['KIVY_VIDEO'] = 'ffpyplayer' # Use ffpyplayer as video provider
os.environ['FFPYPLAYER_CODECS'] = 'h264,h265,vp9,vp8' # Support common codecs
os.environ['SDL_VIDEO_ALLOW_SCREENSAVER'] = '0' # Prevent screen saver
from kivy.app import App
from kivy.config import Config
from kivy.uix.widget import Widget
from kivy.uix.label import Label
from kivy.uix.image import AsyncImage
from kivy.uix.image import AsyncImage, Image
from kivy.uix.video import Video
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
@@ -22,6 +30,8 @@ from kivy.properties import BooleanProperty
from kivy.logger import Logger
from kivy.animation import Animation
from kivy.lang import Builder
from kivy.graphics import Rectangle
from kivy.graphics.texture import Texture
from get_playlists import (
update_playlist_if_needed,
send_playing_status_feedback,
@@ -32,10 +42,44 @@ from get_playlists import (
# Load the KV file
Builder.load_file('signage_player.kv')
# Removed VLCVideoWidget - using Kivy's built-in Video widget instead
class ExitPasswordPopup(Popup):
def __init__(self, player_instance, **kwargs):
def __init__(self, player_instance, was_paused=False, **kwargs):
super(ExitPasswordPopup, self).__init__(**kwargs)
self.player = player_instance
self.was_paused = was_paused
# Cancel all scheduled cursor/control hide events
try:
if self.player.controls_timer:
self.player.controls_timer.cancel()
self.player.controls_timer = None
Clock.unschedule(self.player.hide_controls)
Clock.unschedule(self.player.schedule_cursor_hide)
except Exception as e:
Logger.debug(f"ExitPasswordPopup: Error canceling timers: {e}")
# Show cursor when password popup opens
try:
Window.show_cursor = True
except:
pass
# Bind to dismiss event to manage cursor visibility and resume playback
self.bind(on_dismiss=self.on_popup_dismiss)
def on_popup_dismiss(self, *args):
"""Handle popup dismissal - resume playback and restart cursor hide timer"""
# Resume playback if it wasn't paused before
if not self.was_paused:
self.player.is_paused = False
# Resume video if it was playing
if self.player.current_widget and isinstance(self.player.current_widget, Video):
self.player.current_widget.state = 'play'
# Restart the control hide timer
self.player.schedule_hide_controls()
def check_password(self):
"""Check if entered password matches quickconnect key"""
@@ -54,9 +98,26 @@ class ExitPasswordPopup(Popup):
Clock.schedule_once(lambda dt: self.dismiss(), 1)
class SettingsPopup(Popup):
def __init__(self, player_instance, **kwargs):
def __init__(self, player_instance, was_paused=False, **kwargs):
super(SettingsPopup, self).__init__(**kwargs)
self.player = player_instance
self.was_paused = was_paused
# Cancel all scheduled cursor/control hide events
try:
if self.player.controls_timer:
self.player.controls_timer.cancel()
self.player.controls_timer = None
Clock.unschedule(self.player.hide_controls)
Clock.unschedule(self.player.schedule_cursor_hide)
except Exception as e:
Logger.debug(f"SettingsPopup: Error canceling timers: {e}")
# Show cursor when settings open
try:
Window.show_cursor = True
except:
pass
# Populate current values
self.ids.server_input.text = self.player.config.get('server_ip', 'localhost')
@@ -64,11 +125,27 @@ class SettingsPopup(Popup):
self.ids.quickconnect_input.text = self.player.config.get('quickconnect_key', '1234567')
self.ids.orientation_input.text = self.player.config.get('orientation', 'Landscape')
self.ids.touch_input.text = self.player.config.get('touch', 'True')
self.ids.resolution_input.text = self.player.config.get('max_resolution', 'auto')
# Update status info
self.ids.playlist_info.text = f'Playlist Version: {self.player.playlist_version}'
self.ids.media_count_info.text = f'Media Count: {len(self.player.playlist)}'
self.ids.status_info.text = f'Status: {"Playing" if self.player.is_playing else "Paused" if self.player.is_paused else "Idle"}'
# Bind to dismiss event to manage cursor visibility and resume playback
self.bind(on_dismiss=self.on_popup_dismiss)
def on_popup_dismiss(self, *args):
"""Handle popup dismissal - resume playback and restart cursor hide timer"""
# Resume playback if it wasn't paused before
if not self.was_paused:
self.player.is_paused = False
# Resume video if it was playing
if self.player.current_widget and isinstance(self.player.current_widget, Video):
self.player.current_widget.state = 'play'
# Restart the control hide timer
self.player.schedule_hide_controls()
def save_and_close(self):
"""Save configuration and close popup"""
@@ -78,10 +155,15 @@ class SettingsPopup(Popup):
self.player.config['quickconnect_key'] = self.ids.quickconnect_input.text
self.player.config['orientation'] = self.ids.orientation_input.text
self.player.config['touch'] = self.ids.touch_input.text
self.player.config['max_resolution'] = self.ids.resolution_input.text
# Save to file
self.player.save_config()
# Notify user that resolution change requires restart
if self.ids.resolution_input.text != self.player.config.get('max_resolution', 'auto'):
Logger.info("SettingsPopup: Resolution changed - restart required")
# Close popup
self.dismiss()
@@ -109,6 +191,8 @@ class SignagePlayer(Widget):
self.is_paused = False
self.config = {}
self.playlist_version = None
self.consecutive_errors = 0 # Track consecutive playback errors
self.max_consecutive_errors = 10 # Maximum errors before stopping
# Paths
self.base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
self.config_dir = os.path.join(self.base_dir, 'config')
@@ -165,7 +249,8 @@ class SignagePlayer(Widget):
"server_ip": "localhost",
"port": "5000",
"screen_name": "kivy-player",
"quickconnect_key": "1234567"
"quickconnect_key": "1234567",
"max_resolution": "auto"
}
self.save_config()
Logger.info("SignagePlayer: Created default configuration")
@@ -322,30 +407,67 @@ class SignagePlayer(Widget):
if self.config:
send_playing_status_feedback(self.config, self.playlist_version, file_name)
# Reset error counter on successful playback
self.consecutive_errors = 0
except Exception as e:
Logger.error(f"SignagePlayer: Error playing media: {e}")
self.consecutive_errors += 1
# Check if we've exceeded max errors
if self.consecutive_errors >= self.max_consecutive_errors:
error_msg = f"Too many consecutive errors ({self.consecutive_errors}), stopping playback"
Logger.error(f"SignagePlayer: {error_msg}")
self.show_error(error_msg)
self.is_playing = False
return
self.show_error(f"Error playing media: {e}")
self.next_media()
def play_video(self, video_path, duration):
"""Play a video file"""
"""Play a video file using Kivy's Video widget"""
try:
# Create Video widget with optimized settings for quality
self.current_widget = Video(
source=video_path,
state='play',
options={'allow_stretch': True},
options={
'eos': 'stop', # Stop at end of stream
'autoplay': True, # Auto-play when loaded
'allow_stretch': True, # Allow stretching to fit
'sync': 'audio', # Sync video to audio for smooth playback
},
allow_stretch=True,
keep_ratio=True, # Maintain aspect ratio
size_hint=(1, 1),
pos_hint={'x': 0, 'y': 0}
pos_hint={'center_x': 0.5, 'center_y': 0.5}
)
# Bind to loaded event to log video info
self.current_widget.bind(loaded=self._on_video_loaded)
# Add to content area
self.ids.content_area.add_widget(self.current_widget)
Logger.info(f"SignagePlayer: Playing {os.path.basename(video_path)} for {duration}s")
# Schedule next media after duration
Clock.schedule_once(self.next_media, duration)
except Exception as e:
Logger.error(f"SignagePlayer: Error playing video {video_path}: {e}")
self.next_media()
self.consecutive_errors += 1
if self.consecutive_errors < self.max_consecutive_errors:
self.next_media()
def _on_video_loaded(self, instance, value):
"""Callback when video is loaded - log video information"""
if value:
try:
Logger.info(f"Video loaded: {instance.texture.size if instance.texture else 'No texture'}")
Logger.info(f"Video duration: {instance.duration}s")
except Exception as e:
Logger.debug(f"Could not log video info: {e}")
def play_image(self, image_path, duration):
"""Play an image file"""
@@ -353,15 +475,18 @@ class SignagePlayer(Widget):
self.current_widget = AsyncImage(
source=image_path,
allow_stretch=True,
keep_ratio=False,
size_hint=(1, 1)
keep_ratio=True, # Maintain aspect ratio
size_hint=(1, 1),
pos_hint={'center_x': 0.5, 'center_y': 0.5}
)
self.ids.content_area.add_widget(self.current_widget)
# Schedule next media after duration
Clock.schedule_once(self.next_media, duration)
except Exception as e:
Logger.error(f"SignagePlayer: Error playing image {image_path}: {e}")
self.next_media()
self.consecutive_errors += 1
if self.consecutive_errors < self.max_consecutive_errors:
self.next_media()
def next_media(self, dt=None):
"""Move to next media item"""
@@ -462,14 +587,41 @@ class SignagePlayer(Widget):
except:
pass
def schedule_cursor_hide(self):
"""Schedule cursor to hide after 3 seconds"""
try:
Clock.schedule_once(lambda dt: Window.__setattr__('show_cursor', False), 3)
except:
pass
def show_settings(self, instance=None):
"""Show settings popup"""
popup = SettingsPopup(player_instance=self)
# Pause playback when settings opens
was_paused = self.is_paused
if not was_paused:
self.is_paused = True
Clock.unschedule(self.next_media)
# Stop video if playing
if self.current_widget and isinstance(self.current_widget, Video):
self.current_widget.state = 'pause'
popup = SettingsPopup(player_instance=self, was_paused=was_paused)
popup.open()
def show_exit_popup(self, instance=None):
"""Show exit password popup"""
popup = ExitPasswordPopup(player_instance=self)
# Pause playback when exit popup opens
was_paused = self.is_paused
if not was_paused:
self.is_paused = True
Clock.unschedule(self.next_media)
# Stop video if playing
if self.current_widget and isinstance(self.current_widget, Video):
self.current_widget.state = 'pause'
popup = ExitPasswordPopup(player_instance=self, was_paused=was_paused)
popup.open()
def exit_app(self, instance=None):
@@ -479,7 +631,68 @@ class SignagePlayer(Widget):
class SignagePlayerApp(App):
def build_config(self, config):
"""Configure Kivy settings for optimal video playback"""
# Set graphics settings for smooth 30fps video
config.setdefaults('graphics', {
'multisamples': '2', # Anti-aliasing
'maxfps': '30', # Limit to 30fps (optimized for RPi and video content)
'vsync': '1', # Enable vertical sync
'resizable': '0', # Disable window resizing
'borderless': '1', # Borderless window
'fullscreen': 'auto' # Auto fullscreen
})
# Disable unnecessary modules to save resources
config.setdefaults('modules', {
'inspector': '', # Disable inspector
'monitor': '', # Disable monitor
'keybinding': '', # Disable keybinding
'showborder': '' # Disable border highlighting
})
# Input settings
config.setdefaults('input', {
'mouse': 'mouse', # Enable mouse input
})
Logger.info("SignagePlayerApp: Kivy config optimized for 30fps video playback")
def build(self):
# Load config to get resolution setting
config_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'config', 'app_config.json')
max_resolution = 'auto'
try:
if os.path.exists(config_file):
with open(config_file, 'r') as f:
config = json.load(f)
max_resolution = config.get('max_resolution', 'auto')
except Exception as e:
Logger.warning(f"SignagePlayerApp: Could not load resolution setting: {e}")
# Apply resolution constraint
if max_resolution != 'auto' and 'x' in max_resolution:
try:
max_width, max_height = map(int, max_resolution.split('x'))
current_width, current_height = Window.size
# Check if current resolution exceeds maximum
if current_width > max_width or current_height > max_height:
# Calculate scaling to fit within max resolution
scale = min(max_width / current_width, max_height / current_height)
new_width = int(current_width * scale)
new_height = int(current_height * scale)
Window.size = (new_width, new_height)
Logger.info(f"SignagePlayerApp: Resolution constrained from {current_width}x{current_height} to {new_width}x{new_height}")
else:
Logger.info(f"SignagePlayerApp: Current resolution {current_width}x{current_height} within max {max_resolution}")
except Exception as e:
Logger.error(f"SignagePlayerApp: Error parsing resolution setting '{max_resolution}': {e}")
else:
Logger.info(f"SignagePlayerApp: Using auto resolution (no constraint)")
# Force fullscreen and borderless
Window.fullscreen = True
Window.borderless = True

View File

@@ -261,6 +261,27 @@
multiline: False
font_size: sp(14)
# Resolution
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: dp(40)
spacing: dp(10)
Label:
text: 'Max Resolution:'
size_hint_x: 0.3
text_size: self.size
halign: 'left'
valign: 'middle'
TextInput:
id: resolution_input
size_hint_x: 0.7
multiline: False
font_size: sp(14)
hint_text: '1920x1080 or auto'
Widget:
size_hint_y: 0.1

17
src/signageplayer.ini Normal file
View File

@@ -0,0 +1,17 @@
[graphics]
multisamples = 2
maxfps = 30
vsync = 1
resizable = 0
borderless = 1
fullscreen = auto
[modules]
inspector =
monitor =
keybinding =
showborder =
[input]
mouse = mouse