3ac7f836c4
Replaces the Windows port with a Raspberry Pi / Linux implementation on Raspberry Pi OS "Trixie" (Debian 13, aarch64, Wayland/labwc). The Windows code is removed here but preserved on the Windows-Player branch. Entry point ----------- linux/run_linux.py replaces windows/run_win.py. src/main.py stays platform-neutral; all Pi-specific behaviour is injected from linux/. Five bugs that prevented the port (all measured on real hardware) ---------------------------------------------------------------- 1. Kivy's PyPI wheel bundles an SDL2 built WITHOUT the wayland driver, so no window could be created (Trixie has no X server). linux/fix_kivy_sdl2.sh symlinks the system SDL2 over the bundled filename. 2. SDL2 requires WAYLAND_DISPLAY to be *set* - the socket alone is not enough, unlike wlopm. This broke every systemd/cron/autostart launch. linux_display.ensure_session_environment() detects and exports it. 3. Kivy's Clock resolves callbacks via func.__name__; a patch assigned under a different name crashed the player ~20s after a successful start. 4. The inherited signal_screen_activity() shelled out to tvservice, xdotool and ydotool - none exist on Trixie - and mis-escaped 'wlopm --on \*', so the display blanked after 10 minutes. 5. The launchers ran src/main.py directly, bypassing every platform patch and resolving the data directory one level too high. Web links --------- - --ozone-platform-hint=auto does NOT fall back to Wayland on Chromium 152; it aborts. The platform is now chosen explicitly. - The keyring password prompt is suppressed via the ENVIRONMENT, not the flags: launch_env() strips DBUS_SESSION_BUS_ADDRESS for the child so Chromium cannot reach gnome-keyring-daemon. - Teardown kills the whole process group (needs start_new_session=True); previously it silently fell back to terminate() and orphaned children. Video normalisation ------------------- A 4K video cannot play on a Pi 4: ffpyplayer decodes in software, measured at 0.90x realtime (1080p is 3.03x). Oversized media is downscaled to 1920x1080 at sync time using the hardware h264_v4l2m2m encoder (~31s for an 18s clip), triggered by resolution only so already-playable files are untouched. src/media_state.py owns the shared on-disk contract: a .kiwy-converting marker makes the player skip the item while it is being rebuilt, then the converted file is played instead. If nothing is playable at all (a single-item playlist whose only video is converting), the player loops the intro video rather than leaving a blank screen. Also fixed ---------- - network_monitor: replaced netsh/ifconfig/dhclient with nmcli (Trixie uses NetworkManager; ifconfig and dhclient are not even installed). - Removed the Windows-only focus keeper/guardian from main.py. - main.py: duplicate SDL_AUDIODRIVER setdefault (a silent no-op); Settings "Test connection" now uses tempfile.gettempdir(). - config/app_config.json: credentials blanked so a fresh clone runs the first-run setup flow. Verification ------------ linux/test_media_state.py 18/18, test_linux_patches.py 21/21, test_linux_browser_flags.py 27/27. Verified live against a real DigiServer: image -> weblink -> image -> video with correct durations, zero leaked Chromium processes, and no throttling over a 10 minute monitored run.
303 lines
11 KiB
Bash
Executable File
303 lines
11 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Kivy Signage Player Startup Script with Watchdog
|
|
# This script monitors and auto-restarts the player if it crashes
|
|
|
|
# Get the directory where this script is located
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
# Configuration
|
|
MAX_RETRIES=999999 # Effectively unlimited retries
|
|
RESTART_DELAY=5 # Seconds to wait before restart
|
|
HEALTH_CHECK_INTERVAL=30 # Seconds between health checks
|
|
HEARTBEAT_FILE="$SCRIPT_DIR/.player_heartbeat"
|
|
STOP_FLAG_FILE="$SCRIPT_DIR/.player_stop_requested"
|
|
LOG_FILE="$SCRIPT_DIR/player_watchdog.log"
|
|
|
|
# Ensure log file is writable
|
|
if [ ! -w "$(dirname "$LOG_FILE")" ]; then
|
|
LOG_FILE="/tmp/kivy-player-watchdog.log"
|
|
fi
|
|
|
|
# Function to log messages (MUST be defined before use)
|
|
log_message() {
|
|
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
|
echo "$msg"
|
|
# Try to write to log file, ignore errors if permission denied
|
|
echo "$msg" >> "$LOG_FILE" 2>/dev/null || true
|
|
}
|
|
|
|
# Load user's environment from systemd user session (most reliable method)
|
|
# This ensures we get the proper DISPLAY/WAYLAND_DISPLAY and session variables
|
|
load_user_environment() {
|
|
# Try to get environment from active user session via systemctl
|
|
local user_env
|
|
if command -v systemctl &>/dev/null; then
|
|
user_env=$(systemctl --user show-environment 2>/dev/null)
|
|
if [ -n "$user_env" ]; then
|
|
# Extract display-related variables without subshell to preserve exports
|
|
while IFS='=' read -r key value; do
|
|
case "$key" in
|
|
DISPLAY|WAYLAND_DISPLAY|XDG_RUNTIME_DIR|DBUS_SESSION_BUS_ADDRESS)
|
|
export "$key=$value"
|
|
;;
|
|
esac
|
|
done <<< "$user_env"
|
|
|
|
log_message "Loaded user environment from systemctl --user"
|
|
|
|
# Verify we got valid display
|
|
if [ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ]; then
|
|
log_message "Display environment ready: DISPLAY='$DISPLAY' WAYLAND_DISPLAY='$WAYLAND_DISPLAY'"
|
|
return 0
|
|
else
|
|
log_message "Systemctl didn't provide display variables, trying fallback..."
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Fallback: detect display manually
|
|
log_message "Falling back to manual display detection..."
|
|
|
|
if [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then
|
|
# Try to detect Wayland display
|
|
if [ -S "/run/user/$(id -u)/wayland-0" ]; then
|
|
export WAYLAND_DISPLAY=wayland-0
|
|
log_message "Detected Wayland display: $WAYLAND_DISPLAY"
|
|
# Try to detect X11 display
|
|
elif [ -S "/tmp/.X11-unix/X0" ]; then
|
|
export DISPLAY=:0
|
|
log_message "Detected X11 display: $DISPLAY"
|
|
else
|
|
# Wait for display to come up (useful for systemd or delayed starts)
|
|
log_message "Waiting for display server to be ready (up to 30 seconds)..."
|
|
for i in {1..30}; do
|
|
sleep 1
|
|
if [ -S "/run/user/$(id -u)/wayland-0" ] 2>/dev/null; then
|
|
export WAYLAND_DISPLAY=wayland-0
|
|
log_message "Display server detected on attempt $i: Wayland"
|
|
return 0
|
|
elif [ -S "/tmp/.X11-unix/X0" ] 2>/dev/null; then
|
|
export DISPLAY=:0
|
|
log_message "Display server detected on attempt $i: X11"
|
|
return 0
|
|
fi
|
|
done
|
|
fi
|
|
fi
|
|
|
|
# Verify we have a display now
|
|
if [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then
|
|
log_message "WARNING: No display server detected. This may cause graphics issues."
|
|
return 1
|
|
fi
|
|
|
|
log_message "Display environment ready: DISPLAY=$DISPLAY WAYLAND_DISPLAY=$WAYLAND_DISPLAY"
|
|
return 0
|
|
}
|
|
|
|
# Load the user environment early
|
|
load_user_environment
|
|
|
|
# Function to configure display output to Full HD (1920x1080)
|
|
configure_display_resolution() {
|
|
log_message "🖥️ Configuring display output to Full HD (1920x1080)..."
|
|
|
|
# Method 1: Try xrandr (works on X11 systems)
|
|
if command -v xrandr &>/dev/null && [ -n "$DISPLAY" ]; then
|
|
log_message "Attempting to set resolution via xrandr..."
|
|
|
|
# Get the primary display
|
|
primary_display=$(xrandr --query 2>/dev/null | grep " connected" | grep "primary\|preferred" | head -n 1 | awk '{print $1}')
|
|
|
|
if [ -z "$primary_display" ]; then
|
|
primary_display=$(xrandr --query 2>/dev/null | grep " connected" | head -n 1 | awk '{print $1}')
|
|
fi
|
|
|
|
if [ -n "$primary_display" ]; then
|
|
# Set output to 1920x1080 @ 60Hz
|
|
xrandr --output "$primary_display" --mode 1920x1080 --rate 60 2>/dev/null
|
|
if [ $? -eq 0 ]; then
|
|
log_message "✓ Display resolution set to 1920x1080 via xrandr"
|
|
return 0
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Method 2: Try tvservice (native Raspberry Pi tool)
|
|
if command -v tvservice &>/dev/null; then
|
|
log_message "Attempting to set resolution via tvservice (RPi native)..."
|
|
|
|
# Get current HDMI status
|
|
hdmi_mode=$(tvservice -s 2>/dev/null | grep -oP 'DMT mode \K\d+')
|
|
|
|
# Set to HDMI mode 16 (1920x1080 @ 60Hz) - standard Full HD
|
|
tvservice -e "DMT 16" 2>/dev/null
|
|
if [ $? -eq 0 ]; then
|
|
log_message "✓ Display resolution set to 1920x1080 via tvservice (DMT mode 16)"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Method 3: Configure via /boot/config.txt (persistent RPi config)
|
|
if [ -f "/boot/config.txt" ] && command -v sudo &>/dev/null; then
|
|
log_message "Checking /boot/config.txt for display settings..."
|
|
|
|
# These settings ensure Full HD output on large displays
|
|
if ! grep -q "hdmi_group=2" /boot/config.txt; then
|
|
log_message "Adding HDMI display configuration to /boot/config.txt..."
|
|
# Backup config first
|
|
sudo cp /boot/config.txt /boot/config.txt.backup.$(date +%s)
|
|
|
|
# Add Full HD configuration
|
|
echo "" | sudo tee -a /boot/config.txt > /dev/null
|
|
echo "# Kiwy Signage Player - Full HD Display Configuration" | sudo tee -a /boot/config.txt > /dev/null
|
|
echo "hdmi_group=2" | sudo tee -a /boot/config.txt > /dev/null # DMT (monitor timings)
|
|
echo "hdmi_mode=16" | sudo tee -a /boot/config.txt > /dev/null # 1920x1080 @ 60Hz
|
|
echo "hdmi_drive=2" | sudo tee -a /boot/config.txt > /dev/null # Normal HDMI mode
|
|
echo "disable_overscan=1" | sudo tee -a /boot/config.txt > /dev/null # Disable overscan
|
|
echo "framebuffer_width=1920" | sudo tee -a /boot/config.txt > /dev/null
|
|
echo "framebuffer_height=1080" | sudo tee -a /boot/config.txt > /dev/null
|
|
|
|
log_message "✓ HDMI configuration added to /boot/config.txt"
|
|
log_message "⚠️ Display settings require reboot to take effect"
|
|
return 0
|
|
else
|
|
log_message "✓ /boot/config.txt already configured for Full HD"
|
|
fi
|
|
fi
|
|
|
|
log_message "Display configuration completed (1920x1080 Full HD target)"
|
|
}
|
|
|
|
# Configure display before starting player
|
|
configure_display_resolution
|
|
|
|
# Function to check if player is healthy
|
|
check_health() {
|
|
# Check if heartbeat file exists and is recent (within last 60 seconds)
|
|
if [ -f "$HEARTBEAT_FILE" ]; then
|
|
local last_update=$(stat -c %Y "$HEARTBEAT_FILE" 2>/dev/null || echo 0)
|
|
local current_time=$(date +%s)
|
|
local diff=$((current_time - last_update))
|
|
|
|
if [ $diff -lt 60 ]; then
|
|
return 0 # Healthy
|
|
else
|
|
log_message "⚠️ Player heartbeat stale (${diff}s old)"
|
|
return 1 # Unhealthy
|
|
fi
|
|
else
|
|
# If heartbeat file doesn't exist yet, assume player is starting
|
|
return 0
|
|
fi
|
|
}
|
|
|
|
# Cleanup function
|
|
cleanup() {
|
|
log_message "🛑 Watchdog received stop signal"
|
|
rm -f "$HEARTBEAT_FILE"
|
|
rm -f "$STOP_FLAG_FILE"
|
|
exit 0
|
|
}
|
|
|
|
# Trap signals for graceful shutdown
|
|
trap cleanup SIGINT SIGTERM
|
|
|
|
log_message "=========================================="
|
|
log_message "🚀 Kivy Signage Player Watchdog Started"
|
|
log_message "=========================================="
|
|
log_message "Project directory: $SCRIPT_DIR"
|
|
log_message "Max retries: Unlimited"
|
|
log_message "Restart delay: ${RESTART_DELAY}s"
|
|
log_message ""
|
|
|
|
# Remove old stop flag if exists (fresh start)
|
|
rm -f "$STOP_FLAG_FILE"
|
|
|
|
# Change to the project directory
|
|
cd "$SCRIPT_DIR"
|
|
|
|
# Check if configuration exists
|
|
if [ ! -f "config/app_config.json" ]; then
|
|
log_message "⚠️ WARNING: Configuration file not found!"
|
|
log_message "Player may not function correctly without configuration"
|
|
fi
|
|
|
|
# Main watchdog loop
|
|
retry_count=0
|
|
while true; do
|
|
retry_count=$((retry_count + 1))
|
|
|
|
log_message ""
|
|
log_message "=========================================="
|
|
log_message "▶️ Starting player (attempt #${retry_count})"
|
|
log_message "=========================================="
|
|
|
|
# Clean old heartbeat
|
|
rm -f "$HEARTBEAT_FILE"
|
|
|
|
# Activate virtual environment if it exists
|
|
if [ -f "$SCRIPT_DIR/.venv/bin/activate" ]; then
|
|
source "$SCRIPT_DIR/.venv/bin/activate"
|
|
fi
|
|
|
|
# Start the player.
|
|
#
|
|
# NOTE: the entry point is linux/run_linux.py, NOT src/main.py. Running
|
|
# main.py directly skips the Raspberry Pi platform patches (Wayland session
|
|
# environment, display keep-awake, Chromium kiosk adapter), which is why
|
|
# the player used to blank after 10 minutes and fail to show web links.
|
|
# Run from the project root so the data directory is the repo root and the
|
|
# relative config/media/playlists paths resolve as the app expects.
|
|
cd "$SCRIPT_DIR"
|
|
python3 linux/run_linux.py &
|
|
PLAYER_PID=$!
|
|
|
|
log_message "Player PID: $PLAYER_PID"
|
|
|
|
# Monitor the player
|
|
while true; do
|
|
sleep $HEALTH_CHECK_INTERVAL
|
|
|
|
# Check if process is still running
|
|
if ! kill -0 $PLAYER_PID 2>/dev/null; then
|
|
log_message "❌ Player process crashed or stopped (PID: $PLAYER_PID)"
|
|
break
|
|
fi
|
|
|
|
# Check health via heartbeat
|
|
if ! check_health; then
|
|
log_message "❌ Player health check failed - may be frozen"
|
|
kill $PLAYER_PID 2>/dev/null
|
|
sleep 2
|
|
kill -9 $PLAYER_PID 2>/dev/null
|
|
break
|
|
fi
|
|
|
|
# Player is healthy, continue monitoring
|
|
done
|
|
|
|
# Player stopped or crashed
|
|
# Check if user requested intentional exit
|
|
if [ -f "$STOP_FLAG_FILE" ]; then
|
|
log_message "✋ Stop flag detected - user requested exit via password"
|
|
log_message "Watchdog will NOT restart the player"
|
|
log_message "To restart, run ./start.sh again"
|
|
rm -f "$HEARTBEAT_FILE"
|
|
break
|
|
fi
|
|
|
|
log_message "⏳ Waiting ${RESTART_DELAY}s before restart..."
|
|
sleep $RESTART_DELAY
|
|
|
|
# Ensure old player process is gone before restarting
|
|
kill -9 $PLAYER_PID 2>/dev/null
|
|
|
|
done
|
|
|
|
log_message ""
|
|
log_message "=========================================="
|
|
log_message "Watchdog stopped"
|
|
log_message "=========================================="
|