Compare commits
11 Commits
main
..
d0ea94447a
| Author | SHA1 | Date | |
|---|---|---|---|
| d0ea94447a | |||
| 12f2880201 | |||
| 5a030671a2 | |||
| a2add88f04 | |||
| c4e8381898 | |||
| ced6e10919 | |||
| 844e5eeebb | |||
| 7efc023327 | |||
| 6abde5a767 | |||
| 362f5096a0 | |||
| 3845830a86 |
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/bin/bash
|
||||
# Aggressive display keep-alive for Raspberry Pi
|
||||
# Supports both X11 and Wayland environments
|
||||
|
||||
DISPLAY_TIMEOUT=30
|
||||
|
||||
# Detect display server type
|
||||
detect_display_server() {
|
||||
if [ -n "$WAYLAND_DISPLAY" ]; then
|
||||
echo "wayland"
|
||||
elif [ -n "$DISPLAY" ]; then
|
||||
echo "x11"
|
||||
else
|
||||
echo "unknown"
|
||||
fi
|
||||
}
|
||||
|
||||
DISPLAY_SERVER=$(detect_display_server)
|
||||
|
||||
while true; do
|
||||
# Keep HDMI powered on (works for both X11 and Wayland)
|
||||
if command -v tvservice &> /dev/null; then
|
||||
/usr/bin/tvservice -p 2>/dev/null
|
||||
fi
|
||||
|
||||
if [ "$DISPLAY_SERVER" = "wayland" ]; then
|
||||
# Wayland-specific power management
|
||||
|
||||
# Method 1: Use wlr-randr for Wayland compositors (if available)
|
||||
if command -v wlr-randr &> /dev/null; then
|
||||
wlr-randr --output HDMI-A-1 --on 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Method 2: Prevent idle using systemd-inhibit
|
||||
if command -v systemd-inhibit &> /dev/null; then
|
||||
# This is already running, but refresh the lock
|
||||
systemctl --user restart plasma-ksmserver.service 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Method 3: Use wlopm (Wayland output power management)
|
||||
if command -v wlopm &> /dev/null; then
|
||||
wlopm --on \* 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Method 4: Simulate activity via input (works on Wayland)
|
||||
if command -v ydotool &> /dev/null; then
|
||||
ydotool mousemove -x 1 -y 1 2>/dev/null || true
|
||||
ydotool mousemove -x -1 -y -1 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Method 5: GNOME/KDE Wayland idle inhibit
|
||||
if command -v gnome-session-inhibit &> /dev/null; then
|
||||
# Already inhibited by running process
|
||||
true
|
||||
fi
|
||||
|
||||
else
|
||||
# X11-specific power management (original code)
|
||||
if command -v xset &> /dev/null; then
|
||||
DISPLAY=:0 xset s off 2>/dev/null
|
||||
DISPLAY=:0 xset -dpms 2>/dev/null
|
||||
DISPLAY=:0 xset dpms force on 2>/dev/null
|
||||
DISPLAY=:0 xset s reset 2>/dev/null
|
||||
fi
|
||||
|
||||
# Move mouse to trigger activity
|
||||
if command -v xdotool &> /dev/null; then
|
||||
DISPLAY=:0 xdotool mousemove_relative 1 1 2>/dev/null
|
||||
DISPLAY=:0 xdotool mousemove_relative -1 -1 2>/dev/null
|
||||
fi
|
||||
|
||||
# Disable monitor power saving
|
||||
if command -v xrandr &> /dev/null; then
|
||||
DISPLAY=:0 xrandr --output HDMI-1 --power-profile performance 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
sleep $DISPLAY_TIMEOUT
|
||||
done
|
||||
+15
-36
@@ -25,53 +25,30 @@ wheels/
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
windows/venv312/
|
||||
|
||||
# Kivy
|
||||
*.pyc
|
||||
|
||||
# Media files (optional - remove if you want to track media)
|
||||
media/**/*.jpg
|
||||
media/**/*.jpeg
|
||||
media/**/*.png
|
||||
media/**/*.gif
|
||||
media/**/*.bmp
|
||||
media/**/*.mp4
|
||||
media/**/*.avi
|
||||
media/**/*.mkv
|
||||
media/**/*.mov
|
||||
media/**/*.webm
|
||||
media/*.jpg
|
||||
media/*.jpeg
|
||||
media/*.png
|
||||
media/*.gif
|
||||
media/*.bmp
|
||||
media/*.mp4
|
||||
media/*.avi
|
||||
media/*.mkv
|
||||
media/*.mov
|
||||
media/*.webm
|
||||
|
||||
# Downloaded / on-device edited media (server-managed runtime content)
|
||||
media/edited_media/
|
||||
|
||||
# Playlists cache (auto-generated by the player)
|
||||
playlists/server_playlist*.json
|
||||
|
||||
# Player configuration (machine-specific; install.sh writes a template if absent)
|
||||
config/app_config.json
|
||||
# Playlists cache (auto-generated)
|
||||
playlists/server_playlist_*.json
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
.kivy/
|
||||
|
||||
# Runtime state (generated by the player / watchdog)
|
||||
.player_heartbeat
|
||||
.player_stop_requested
|
||||
.player_watchdog.lock
|
||||
.post-clone-setup.done
|
||||
.post-clone-backups/
|
||||
|
||||
# Installer-generated launchers (recreated by install.sh)
|
||||
.run-background.sh
|
||||
.start-player-cron.sh
|
||||
|
||||
# Secrets / machine-specific auth tokens (never commit)
|
||||
player_auth.json
|
||||
|
||||
# Local backups / editor leftovers
|
||||
*.bak
|
||||
*~
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
@@ -83,3 +60,5 @@ player_auth.json
|
||||
Thumbs.db
|
||||
|
||||
.player_heartbear
|
||||
|
||||
windows/venv_build/
|
||||
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
# Keep-screen-alive wrapper for player
|
||||
# Prevents screen from locking/turning off while player is running
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Function to keep screen awake
|
||||
keep_screen_awake() {
|
||||
while true; do
|
||||
# Move mouse slightly to prevent idle
|
||||
if command -v xdotool &> /dev/null; then
|
||||
xdotool mousemove_relative 1 1
|
||||
xdotool mousemove_relative -1 -1
|
||||
fi
|
||||
|
||||
# Disable DPMS and screensaver periodically
|
||||
if command -v xset &> /dev/null; then
|
||||
xset s reset
|
||||
xset dpms force on
|
||||
fi
|
||||
|
||||
sleep 30
|
||||
done
|
||||
}
|
||||
|
||||
# Function to inhibit systemd sleep (if available)
|
||||
inhibit_sleep() {
|
||||
if command -v systemd-inhibit &> /dev/null; then
|
||||
# Run player under systemd inhibit to prevent sleep
|
||||
systemd-inhibit --what=sleep --why="Signage player running" \
|
||||
bash "$SCRIPT_DIR/start.sh"
|
||||
return $?
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Try systemd inhibit first (most reliable)
|
||||
if inhibit_sleep; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Fallback: Start keep-alive in background
|
||||
keep_screen_awake &
|
||||
KEEPALIVE_PID=$!
|
||||
|
||||
# Start the player
|
||||
cd "$SCRIPT_DIR"
|
||||
bash start.sh
|
||||
PLAYER_EXIT=$?
|
||||
|
||||
# Kill keep-alive when player exits
|
||||
kill $KEEPALIVE_PID 2>/dev/null || true
|
||||
|
||||
exit $PLAYER_EXIT
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#\!/bin/bash
|
||||
SCRIPT_DIR="/home/pi/kiwy-signage"
|
||||
LOG_FILE="/home/pi/kiwy-signage/logs/player-background.log"
|
||||
mkdir -p "."
|
||||
echo "[2026-07-17 10:54:53] Starting player in background..." >> ""
|
||||
cd "/home/pi/kiwy-signage" || exit 1
|
||||
nohup bash start.sh >> "" 2>&1 &
|
||||
PLAYER_PID=$\!
|
||||
echo "[2026-07-17 10:54:53] Player started with PID: $PLAYER_PID" >> ""
|
||||
echo "Player started in background (PID: $PLAYER_PID)"
|
||||
echo "Logs: $LOG_FILE"
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#\!/bin/bash
|
||||
sleep 15
|
||||
cd /home/pi/kiwy-signage && bash start.sh
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
# Wait for display server to be ready before starting the app
|
||||
# This prevents Kivy from failing to initialize graphics
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
MAX_WAIT=60
|
||||
ELAPSED=0
|
||||
|
||||
echo "[$(date)] Waiting for display server to be ready..."
|
||||
|
||||
# Wait for display socket/device to appear
|
||||
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||
# Check for Wayland socket (primary for Bookworm)
|
||||
if [ -S "$XDG_RUNTIME_DIR/wayland-0" ] 2>/dev/null; then
|
||||
echo "[$(date)] ✓ Wayland display socket found"
|
||||
export WAYLAND_DISPLAY=wayland-0
|
||||
break
|
||||
fi
|
||||
|
||||
# Check for X11 display
|
||||
if [ -S "$XDG_RUNTIME_DIR/X11/display:0" ] 2>/dev/null; then
|
||||
echo "[$(date)] ✓ X11 display socket found"
|
||||
export DISPLAY=:0
|
||||
break
|
||||
fi
|
||||
|
||||
# Check if display manager is running (for fallback)
|
||||
if pgrep -f "wayland|weston|gnome-shell|xfwm4|openbox" > /dev/null 2>&1; then
|
||||
echo "[$(date)] ✓ Display manager detected"
|
||||
break
|
||||
fi
|
||||
|
||||
echo "[$(date)] Waiting for display... ($ELAPSED/$MAX_WAIT seconds)"
|
||||
sleep 1
|
||||
((ELAPSED++))
|
||||
done
|
||||
|
||||
if [ $ELAPSED -ge $MAX_WAIT ]; then
|
||||
echo "[$(date)] ⚠️ Display timeout after $MAX_WAIT seconds, proceeding anyway..."
|
||||
fi
|
||||
|
||||
# Set default display if not detected
|
||||
if [ -z "$WAYLAND_DISPLAY" ] && [ -z "$DISPLAY" ]; then
|
||||
echo "[$(date)] Using fallback display settings"
|
||||
export DISPLAY=:0
|
||||
export WAYLAND_DISPLAY=wayland-0
|
||||
fi
|
||||
|
||||
echo "[$(date)] Environment: DISPLAY=$DISPLAY WAYLAND_DISPLAY=$WAYLAND_DISPLAY"
|
||||
echo "[$(date)] XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR"
|
||||
|
||||
# Now start the app
|
||||
cd "$SCRIPT_DIR" || exit 1
|
||||
exec bash start.sh
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"server_ip": "192.168.0.107",
|
||||
"port": "8080",
|
||||
"screen_name": "WINDOWS-PC",
|
||||
"quickconnect_key": "8887779",
|
||||
"orientation": "Landscape",
|
||||
"touch": "True",
|
||||
"max_resolution": "1920x1080",
|
||||
"edit_feature_enabled": true,
|
||||
"use_https": false,
|
||||
"verify_ssl": false
|
||||
}
|
||||
+56
-178
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#\!/bin/bash
|
||||
|
||||
# Kivy Signage Player Installation Script
|
||||
# Supports both online and offline installation using a Python virtual environment
|
||||
@@ -19,8 +19,7 @@
|
||||
# -n, --name NAME Screen/player name
|
||||
# -k, --key KEY Quick-connect / auth key
|
||||
# --https Enable HTTPS
|
||||
# --verify-ssl Enable SSL certificate verification
|
||||
# --no-verify-ssl Disable SSL verification (default)
|
||||
# --no-verify-ssl Disable SSL verification
|
||||
# -o, --orientation Landscape or Portrait
|
||||
# --touch Enable touch input
|
||||
# -r, --resolution WxH Max resolution
|
||||
@@ -78,48 +77,6 @@ detect_raspberry_pi() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Helper: Internet reachability probe (dependency-free)
|
||||
# ============================================================
|
||||
# Returns 0 if the public internet (PyPI) is reachable, 1 otherwise.
|
||||
# Uses only the Python stdlib so it works on a bare, offline host.
|
||||
has_internet() {
|
||||
python3 - << 'PYEOF' >/dev/null 2>&1
|
||||
import socket
|
||||
try:
|
||||
socket.setdefaulttimeout(3)
|
||||
socket.create_connection(("pypi.org", 443), timeout=3).close()
|
||||
raise SystemExit(0)
|
||||
except Exception:
|
||||
raise SystemExit(1)
|
||||
PYEOF
|
||||
return $?
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Helper: Fetch a URL using curl, wget, or the Python stdlib
|
||||
# ============================================================
|
||||
# Works on minimal/offline images where curl/wget may be absent.
|
||||
fetch_url() {
|
||||
local url="$1"
|
||||
if command -v curl &>/dev/null; then
|
||||
curl -sSf --max-time 20 "$url"
|
||||
elif command -v wget &>/dev/null; then
|
||||
wget -qO- --timeout=20 "$url"
|
||||
else
|
||||
python3 - "$url" << 'PYEOF'
|
||||
import sys, urllib.request
|
||||
url = sys.argv[1]
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=20) as r:
|
||||
sys.stdout.write(r.read().decode("utf-8", "replace"))
|
||||
except Exception as e:
|
||||
sys.stderr.write("fetch failed: %s\n" % e)
|
||||
raise SystemExit(1)
|
||||
PYEOF
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# CLI Argument Parsing
|
||||
# ============================================================
|
||||
@@ -135,7 +92,6 @@ MAX_RESOLUTION=""
|
||||
EDIT_ENABLED=""
|
||||
PROVISION_URL=""
|
||||
CONFIG_FILE_PATH=""
|
||||
NO_REBOOT=false
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
@@ -155,8 +111,7 @@ usage() {
|
||||
echo " -n, --name NAME Screen/player name"
|
||||
echo " -k, --key KEY Quick-connect / auth key"
|
||||
echo " --https Enable HTTPS for server communication"
|
||||
echo " --verify-ssl Enable SSL certificate verification"
|
||||
echo " --no-verify-ssl Disable SSL certificate verification (default)"
|
||||
echo " --no-verify-ssl Disable SSL certificate verification"
|
||||
echo " -o, --orientation Landscape or Portrait (default: Landscape)"
|
||||
echo " --touch Enable touch input"
|
||||
echo " -r, --resolution WxH Max resolution (default: 1920x1080)"
|
||||
@@ -164,7 +119,6 @@ usage() {
|
||||
echo ""
|
||||
echo "Installation options:"
|
||||
echo " --offline Force offline installation from local packages"
|
||||
echo " --no-reboot Do not reboot automatically at the end"
|
||||
echo " -q, --quiet Silent mode (minimal output)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
@@ -208,10 +162,6 @@ while [ $# -gt 0 ]; do
|
||||
USE_HTTPS="true"
|
||||
shift
|
||||
;;
|
||||
--verify-ssl)
|
||||
VERIFY_SSL="true"
|
||||
shift
|
||||
;;
|
||||
--no-verify-ssl)
|
||||
VERIFY_SSL="false"
|
||||
shift
|
||||
@@ -236,10 +186,6 @@ while [ $# -gt 0 ]; do
|
||||
OFFLINE_MODE=true
|
||||
shift
|
||||
;;
|
||||
--no-reboot)
|
||||
NO_REBOOT=true
|
||||
shift
|
||||
;;
|
||||
-q|--quiet)
|
||||
exec > >(tee -a /tmp/kivy-install.log) 2>&1
|
||||
shift
|
||||
@@ -259,12 +205,6 @@ if [ "$OFFLINE_MODE" = false ] && [ -d "$WHEELS_DIR" ] && [ "$(ls -A "$WHEELS_DI
|
||||
OFFLINE_MODE=true
|
||||
fi
|
||||
|
||||
# Probe internet reachability once (guards every network step below)
|
||||
HAS_INTERNET=false
|
||||
if has_internet; then
|
||||
HAS_INTERNET=true
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# Mode Banner
|
||||
# ============================================================
|
||||
@@ -276,11 +216,6 @@ if [ "$OFFLINE_MODE" = true ]; then
|
||||
else
|
||||
echo "Mode: Online Installation"
|
||||
fi
|
||||
if [ "$HAS_INTERNET" = true ]; then
|
||||
echo "Internet: reachable"
|
||||
else
|
||||
echo "Internet: NOT available (local-network / isolated install)"
|
||||
fi
|
||||
if [ -n "$SUDO_USERNAME" ]; then
|
||||
echo "Sudo user: $SUDO_USERNAME"
|
||||
fi
|
||||
@@ -293,54 +228,27 @@ echo "Step 1/6: Installing system dependencies..."
|
||||
echo "--------------------"
|
||||
|
||||
echo "Updating package lists..."
|
||||
if [ "$HAS_INTERNET" = true ]; then
|
||||
run_sudo apt update 2>/dev/null || echo " WARNING: 'apt update' failed; continuing with existing lists"
|
||||
else
|
||||
echo " Offline / no internet — skipping 'apt update'"
|
||||
run_sudo apt update 2>/dev/null || true
|
||||
fi
|
||||
run_sudo apt update
|
||||
|
||||
echo "Installing system packages..."
|
||||
|
||||
# System dependencies required for Kivy + video playback
|
||||
APT_PACKAGES="python3-pip python3-setuptools python3-dev python3-venv \
|
||||
libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
|
||||
libportmidi-dev libswscale-dev libavformat-dev libavcodec-dev \
|
||||
zlib1g-dev ffmpeg libavcodec-extra \
|
||||
gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
|
||||
curl wget"
|
||||
|
||||
# Prefer the offline .deb bundle when present
|
||||
USED_DEB_BUNDLE=false
|
||||
if [ -d "$DEB_DIR" ] && ls "$DEB_DIR"/*.deb >/dev/null 2>&1; then
|
||||
echo "Installing from offline .deb packages in $DEB_DIR..."
|
||||
if [ "$OFFLINE_MODE" = true ] && [ -d "$DEB_DIR" ] && [ "$(ls -A $DEB_DIR/*.deb 2>/dev/null)" ]; then
|
||||
echo "Installing from offline .deb packages..."
|
||||
cd "$DEB_DIR"
|
||||
run_sudo dpkg -i *.deb 2>/dev/null || true
|
||||
# Resolve dependencies from the local bundle only (no-op if already satisfied)
|
||||
run_sudo apt-get install -f -y 2>/dev/null || true
|
||||
run_sudo apt-get install -f -y || true
|
||||
cd "$SCRIPT_DIR"
|
||||
USED_DEB_BUNDLE=true
|
||||
elif [ "$OFFLINE_MODE" = true ]; then
|
||||
echo "WARNING: Offline mode but no .deb bundle found in $DEB_DIR"
|
||||
echo " System packages will still be attempted via apt (may fail without internet)"
|
||||
fi
|
||||
|
||||
# Install/complete system packages. Never abort the installer:
|
||||
# offline, some packages may already be present (fine) or simply unavailable.
|
||||
if run_sudo apt install -y $APT_PACKAGES 2>/dev/null; then
|
||||
echo "System dependencies installed"
|
||||
else
|
||||
echo "WARNING: bulk system package install did not fully succeed."
|
||||
echo " Installing available packages individually..."
|
||||
for pkg in $APT_PACKAGES; do
|
||||
run_sudo apt install -y "$pkg" 2>/dev/null || echo " - unavailable (skipped): $pkg"
|
||||
done
|
||||
if [ "$HAS_INTERNET" != true ] && [ "$USED_DEB_BUNDLE" != true ]; then
|
||||
echo " NOTE: Host is offline and no .deb bundle was found."
|
||||
echo " Add .deb files to $DEB_DIR to satisfy missing system packages."
|
||||
fi
|
||||
fi
|
||||
run_sudo apt install -y \
|
||||
python3-pip python3-setuptools python3-dev \
|
||||
python3-venv \
|
||||
libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
|
||||
libportmidi-dev libswscale-dev libavformat-dev libavcodec-dev \
|
||||
zlib1g-dev ffmpeg libavcodec-extra \
|
||||
gstreamer1.0-plugins-base gstreamer1.0-plugins-good
|
||||
|
||||
echo "System dependencies installed"
|
||||
echo ""
|
||||
|
||||
# ============================================================
|
||||
@@ -360,12 +268,8 @@ echo "Virtual environment created at $VENV_DIR"
|
||||
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
if [ "$OFFLINE_MODE" = true ] && [ "$HAS_INTERNET" != true ]; then
|
||||
echo "Skipping pip self-upgrade (offline, no internet)"
|
||||
else
|
||||
echo "Upgrading pip..."
|
||||
pip install --upgrade pip --quiet 2>/dev/null || echo " WARNING: pip upgrade failed; continuing"
|
||||
fi
|
||||
echo "Upgrading pip..."
|
||||
pip install --upgrade pip --quiet
|
||||
|
||||
echo ""
|
||||
|
||||
@@ -389,13 +293,9 @@ if [ -f "$REQUIREMENTS_FILE" ]; then
|
||||
else
|
||||
echo ""
|
||||
echo "Offline installation failed (some wheels may be missing or incompatible)"
|
||||
if [ "$HAS_INTERNET" = true ]; then
|
||||
echo " Falling back to online installation (requires internet)..."
|
||||
pip install -r "$REQUIREMENTS_FILE" --find-links="$WHEELS_DIR" 2>&1 || true
|
||||
else
|
||||
echo " No internet available — cannot fall back to online installation."
|
||||
echo " Add the missing/compatible wheels to: $WHEELS_DIR"
|
||||
fi
|
||||
echo " Falling back to online installation (requires internet)..."
|
||||
echo ""
|
||||
pip install -r "$REQUIREMENTS_FILE" --find-links="$WHEELS_DIR" 2>&1 || true
|
||||
fi
|
||||
else
|
||||
pip install -r "$REQUIREMENTS_FILE" --find-links="$WHEELS_DIR" 2>&1 || true
|
||||
@@ -406,20 +306,16 @@ if [ -f "$REQUIREMENTS_FILE" ]; then
|
||||
echo "Verifying installed packages..."
|
||||
pip list --format=columns 2>/dev/null | grep -iE "kivy|ffpyplayer" || true
|
||||
|
||||
# If a critical package is missing, fall back to online install only when reachable
|
||||
for pkg in kivy ffpyplayer; do
|
||||
if python3 -c "import $pkg" 2>/dev/null; then
|
||||
echo " $pkg OK"
|
||||
else
|
||||
echo " WARNING: $pkg could not be imported in the virtual environment"
|
||||
if [ "$HAS_INTERNET" = true ]; then
|
||||
echo " Attempting online fallback install for $pkg..."
|
||||
pip install "$pkg" 2>/dev/null || echo " - $pkg online install failed (continuing)"
|
||||
else
|
||||
echo " Offline: ensure a compatible '$pkg' wheel exists in $WHEELS_DIR"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
# If kivy is still missing after pip install, try system-wide fallback
|
||||
python3 -c "import kivy" 2>/dev/null && echo " kivy OK" || {
|
||||
echo ""
|
||||
echo " kivy not found in venv — installing system-wide via pip (break-system-packages)..."
|
||||
pip install kivy --break-system-packages 2>/dev/null || pip install --user kivy 2>/dev/null || true
|
||||
}
|
||||
python3 -c "import ffpyplayer" 2>/dev/null && echo " ffpyplayer OK" || {
|
||||
echo " ffpyplayer not found — installing system-wide..."
|
||||
pip install ffpyplayer --break-system-packages 2>/dev/null || pip install --user ffpyplayer 2>/dev/null || true
|
||||
}
|
||||
else
|
||||
echo "requirements.txt not found - skipping pip install"
|
||||
fi
|
||||
@@ -462,32 +358,24 @@ run_sudo chown "$ACTUAL_USER:$ACTUAL_USER" "$AUTOSTART_DIR/kivy-signage-player.d
|
||||
chmod 644 "$AUTOSTART_DIR/kivy-signage-player.desktop" 2>/dev/null || true
|
||||
echo "XDG autostart entry created"
|
||||
|
||||
# Cron fallback (secondary to XDG autostart; start.sh enforces single-instance)
|
||||
# Cron fallback
|
||||
if command -v crontab &>/dev/null; then
|
||||
CRON_WRAPPER="$SCRIPT_DIR/.start-player-cron.sh"
|
||||
CRON_MARKER_TAG=".start-player-cron.sh"
|
||||
cat > "$CRON_WRAPPER" << EOF
|
||||
#!/bin/bash
|
||||
# Kiwy signage player cron fallback (managed by install.sh)
|
||||
#\!/bin/bash
|
||||
sleep 15
|
||||
cd $SCRIPT_DIR && bash start.sh
|
||||
EOF
|
||||
chmod +x "$CRON_WRAPPER"
|
||||
|
||||
# Idempotent: strip ALL previous Kiwy cron lines, then add exactly one.
|
||||
# (The old guard grepped for a string that never appeared in the entry,
|
||||
# so every re-install appended another @reboot line.)
|
||||
CURRENT_CRON="$(sudo -u "$ACTUAL_USER" crontab -l 2>/dev/null || true)"
|
||||
CLEANED_CRON="$(printf '%s\n' "$CURRENT_CRON" \
|
||||
| grep -v -F "$CRON_MARKER_TAG" \
|
||||
| grep -v -F "$CRON_WRAPPER" \
|
||||
| grep -v -F "# Kiwy signage player" || true)"
|
||||
{
|
||||
printf '%s\n' "$CLEANED_CRON" | grep -v '^$'
|
||||
echo "# Kiwy signage player autostart fallback (managed by install.sh)"
|
||||
echo "@reboot sleep 20 && $CRON_WRAPPER > /tmp/kivy-player-cron.log 2>&1"
|
||||
} | sudo -u "$ACTUAL_USER" crontab - 2>/dev/null || true
|
||||
echo "Cron fallback configured (single entry, deduplicated)"
|
||||
|
||||
CRON_ENTRY="@reboot sleep 20 && $CRON_WRAPPER > /tmp/kivy-player-cron.log 2>&1"
|
||||
# Use sudo -u which works when running with sudo (no password prompt)
|
||||
if sudo -u "$ACTUAL_USER" crontab -l 2>/dev/null | grep -q "kivy-signage-player"; then
|
||||
true
|
||||
else
|
||||
(sudo -u "$ACTUAL_USER" crontab -l 2>/dev/null || true; echo "$CRON_ENTRY") | sudo -u "$ACTUAL_USER" crontab - 2>/dev/null || true
|
||||
fi
|
||||
echo "Cron fallback configured"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
@@ -554,7 +442,7 @@ write_player_config() {
|
||||
if [ -n "$PROVISION_URL" ]; then
|
||||
echo "Fetching provisioning data from: $PROVISION_URL"
|
||||
local response
|
||||
response=$(fetch_url "$PROVISION_URL" 2>/dev/null || true)
|
||||
response=$(curl -sSf "$PROVISION_URL" 2>&1 || true)
|
||||
if [ -n "$response" ]; then
|
||||
echo "$response" > "$config_file"
|
||||
echo "Provisioning data saved"
|
||||
@@ -604,9 +492,7 @@ edit_val = os.environ.get('EDIT_ENABLED', '')
|
||||
if https_val:
|
||||
config['use_https'] = https_val.lower() == 'true'
|
||||
if verify_val:
|
||||
# VERIFY_SSL holds the requested verification state ("true"/"false").
|
||||
# (Previously this was inverted: '--no-verify-ssl' set verify_ssl=True.)
|
||||
config['verify_ssl'] = verify_val.lower() != 'false'
|
||||
config['verify_ssl'] = verify_val.lower() == 'false'
|
||||
if edit_val:
|
||||
config['edit_feature_enabled'] = edit_val.lower() == 'true'
|
||||
|
||||
@@ -614,16 +500,16 @@ config.setdefault('port', '8080')
|
||||
config.setdefault('orientation', 'Landscape')
|
||||
config.setdefault('touch', 'True')
|
||||
config.setdefault('max_resolution', '1920x1080')
|
||||
config.setdefault('edit_feature_enabled', True)
|
||||
config.setdefault('edit_feature_enabled', False)
|
||||
config.setdefault('use_https', False)
|
||||
config.setdefault('verify_ssl', False)
|
||||
config.setdefault('verify_ssl', True)
|
||||
|
||||
with open(config_file, 'w') as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
print("Configuration written")
|
||||
PYEOF
|
||||
elif [ ! -f "$config_file" ]; then
|
||||
elif [ \! -f "$config_file" ]; then
|
||||
cat > "$config_file" << 'EOF'
|
||||
{
|
||||
"server_ip": "",
|
||||
@@ -633,9 +519,9 @@ PYEOF
|
||||
"orientation": "Landscape",
|
||||
"touch": "True",
|
||||
"max_resolution": "1920x1080",
|
||||
"edit_feature_enabled": true,
|
||||
"edit_feature_enabled": false,
|
||||
"use_https": false,
|
||||
"verify_ssl": false
|
||||
"verify_ssl": true
|
||||
}
|
||||
EOF
|
||||
echo "Default config template created"
|
||||
@@ -682,18 +568,18 @@ fi
|
||||
|
||||
# Create background runner
|
||||
NOHUP_WRAPPER="$SCRIPT_DIR/.run-background.sh"
|
||||
cat > "$NOHUP_WRAPPER" << 'EOF'
|
||||
#!/bin/bash
|
||||
cat > "$NOHUP_WRAPPER" << EOF
|
||||
#\!/bin/bash
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LOG_FILE="$SCRIPT_DIR/logs/player-background.log"
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting player in background..." >> "$LOG_FILE"
|
||||
cd "$SCRIPT_DIR" || exit 1
|
||||
nohup bash start.sh >> "$LOG_FILE" 2>&1 &
|
||||
PLAYER_PID=$!
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Player started with PID: $PLAYER_PID" >> "$LOG_FILE"
|
||||
echo "Player started in background (PID: $PLAYER_PID)"
|
||||
echo "Logs: $LOG_FILE"
|
||||
PLAYER_PID=\$\!
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Player started with PID: \$PLAYER_PID" >> "$LOG_FILE"
|
||||
echo "Player started in background (PID: \$PLAYER_PID)"
|
||||
echo "Logs: \$LOG_FILE"
|
||||
EOF
|
||||
chmod +x "$NOHUP_WRAPPER"
|
||||
echo "Background runner created"
|
||||
@@ -703,7 +589,7 @@ echo "Background runner created"
|
||||
# ============================================================
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Installation Complete!"
|
||||
echo "Installation Complete\!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Installation: $SCRIPT_DIR"
|
||||
@@ -750,15 +636,7 @@ if [ -f "$PI_BOOT_PATCH_SCRIPT" ]; then
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [ "$NO_REBOOT" = true ]; then
|
||||
echo "=========================================="
|
||||
echo "Reboot Skipped (--no-reboot)"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Some changes (HDMI settings, GPU memory) may require a reboot."
|
||||
echo "Reboot manually when convenient: sudo reboot"
|
||||
echo ""
|
||||
elif [ -z "$SUDO_PASSWORD" ]; then
|
||||
if [ -z "$SUDO_PASSWORD" ]; then
|
||||
echo "=========================================="
|
||||
echo "System Reboot Recommended"
|
||||
echo "=========================================="
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 386 KiB |
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"count": 4,
|
||||
"player_id": 1,
|
||||
"player_name": "Test_player1",
|
||||
"playlist": [
|
||||
{
|
||||
"file_name": "sean-oulashin-KMn4VEeEPR8-unsplash.jpg",
|
||||
"type": "image",
|
||||
"url": "media/sean-oulashin-KMn4VEeEPR8-unsplash.jpg",
|
||||
"duration": 14,
|
||||
"edit_on_player": true
|
||||
},
|
||||
{
|
||||
"file_name": "weblink-1e4a4d6d885a",
|
||||
"type": "weblink",
|
||||
"url": "https://moto-adv.com/",
|
||||
"duration": 30,
|
||||
"edit_on_player": false
|
||||
},
|
||||
{
|
||||
"file_name": "sample-30s.mp4",
|
||||
"type": "video",
|
||||
"url": "media/sample-30s.mp4",
|
||||
"duration": 31,
|
||||
"edit_on_player": false
|
||||
},
|
||||
{
|
||||
"file_name": "edited_media/5/eye_e_v2.jpg",
|
||||
"type": "image",
|
||||
"url": "media/edited_media/5/eye_e_v2.jpg",
|
||||
"duration": 50,
|
||||
"edit_on_player": true
|
||||
}
|
||||
],
|
||||
"playlist_id": 1,
|
||||
"playlist_version": 14
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Python 3.12.9
|
||||
+6
-10
@@ -1,10 +1,6 @@
|
||||
# Patterns are relative to THIS file (the repo/ folder), so no "repo/" prefix.
|
||||
# (The previous patterns used a "repo/" prefix and therefore never matched.)
|
||||
|
||||
# Downloaded system .deb bundles are large and optional.
|
||||
# Fetch on demand with: bash scripts/download_deb_packages.sh
|
||||
# They are intentionally NOT committed.
|
||||
system-packages/debs/
|
||||
|
||||
# NOTE: Python wheels under python-wheels/ ARE committed on purpose, so that a
|
||||
# plain `git clone` ships a complete offline install bundle.
|
||||
# Ignore downloaded packages but keep directory structure
|
||||
repo/python-wheels/*.whl
|
||||
repo/python-wheels/*.tar.gz
|
||||
repo/system-packages/debs/
|
||||
!repo/python-wheels/.gitkeep
|
||||
!repo/system-packages/.gitkeep
|
||||
|
||||
+8
-10
@@ -13,23 +13,21 @@ The installer script (`install.sh`) will automatically use this repo folder if a
|
||||
|
||||
### To populate this folder for offline installation:
|
||||
|
||||
Run the download scripts from the project root:
|
||||
Run the download script from the project root:
|
||||
```bash
|
||||
bash scripts/download_offline_packages.sh
|
||||
bash scripts/download_deb_packages.sh
|
||||
bash download_offline_packages.sh
|
||||
```
|
||||
|
||||
This will download all necessary Python wheels and system .deb packages.
|
||||
This will download all necessary Python wheels and document system packages.
|
||||
|
||||
## Contents
|
||||
|
||||
### Python Packages (see requirements.txt for the source of truth)
|
||||
- kivy==2.3.1
|
||||
- requests==2.34.2
|
||||
### Python Packages
|
||||
- kivy==2.1.0
|
||||
- requests==2.32.4
|
||||
- bcrypt==4.2.1
|
||||
- aiohttp==3.11.16
|
||||
- ffpyplayer==4.5.3
|
||||
- evdev==1.9.1
|
||||
- aiohttp==3.9.1
|
||||
- asyncio==3.4.3
|
||||
- All dependencies
|
||||
|
||||
### System Packages (APT)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,347 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# run_player-afterclone.sh
|
||||
# ============================================================
|
||||
# ONE-TIME post-clone setup for a cloned Raspberry Pi SD card.
|
||||
#
|
||||
# Problem it solves:
|
||||
# The player identifies itself to DigiServer using the "screen_name"
|
||||
# value in config/app_config.json (NOT the OS hostname). A cloned SD
|
||||
# card carries the ORIGINAL card's screen_name + a cached auth token
|
||||
# (src/player_auth.json). Two devices then fight over the same player
|
||||
# session, so the cloned device never connects.
|
||||
#
|
||||
# What this script does:
|
||||
# 1. Stops the running player/watchdog (so the token is not re-written)
|
||||
# 2. Backs up the current identity/config files
|
||||
# 3. Deletes the cached authentication token(s) -> forces re-auth
|
||||
# 4. Sets a NEW unique screen_name and (optionally) server details
|
||||
# 5. Leaves the player ready to boot and register as a BRAND-NEW player
|
||||
#
|
||||
# Usage:
|
||||
# bash run_player-afterclone.sh [OPTIONS]
|
||||
#
|
||||
# Options:
|
||||
# -n, --name NAME New unique screen/player name
|
||||
# -s, --server IP Server IP / hostname
|
||||
# -p, --port PORT Server port
|
||||
# -k, --key KEY Quick-connect / auth key
|
||||
# --https Enable HTTPS
|
||||
# --no-https Disable HTTPS
|
||||
# --verify-ssl Enable SSL certificate verification
|
||||
# --no-verify-ssl Disable SSL certificate verification
|
||||
# --set-hostname Also set the OS hostname to the new screen name
|
||||
# -y, --yes Non-interactive; use args/defaults, no prompts
|
||||
# --force Run even if this card was already set up
|
||||
# -h, --help Show this help
|
||||
#
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_DIR="$SCRIPT_DIR/config"
|
||||
CONFIG_FILE="$CONFIG_DIR/app_config.json"
|
||||
VENV_DIR="$SCRIPT_DIR/.venv"
|
||||
MARKER_FILE="$SCRIPT_DIR/.post-clone-setup.done"
|
||||
BACKUP_ROOT="$SCRIPT_DIR/.post-clone-backups"
|
||||
STAMP="$(date '+%Y%m%d-%H%M%S')"
|
||||
|
||||
# ---- Defaults / CLI state ----
|
||||
NEW_NAME=""
|
||||
NEW_SERVER=""
|
||||
NEW_PORT=""
|
||||
NEW_KEY=""
|
||||
NEW_USE_HTTPS=""
|
||||
NEW_VERIFY_SSL=""
|
||||
SET_HOSTNAME=false
|
||||
ASSUME_YES=false
|
||||
FORCE=false
|
||||
|
||||
# ---- Pretty printing ----
|
||||
info() { echo " $1"; }
|
||||
ok() { echo " ✓ $1"; }
|
||||
warn() { echo " ⚠️ $1"; }
|
||||
err() { echo " ✗ $1"; }
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
run_player-afterclone.sh - one-time post-clone setup for a cloned SD card.
|
||||
|
||||
Deletes the cached authentication token and configures a NEW unique
|
||||
player identity so the card registers on the server as a new player.
|
||||
|
||||
Usage:
|
||||
bash run_player-afterclone.sh [OPTIONS]
|
||||
|
||||
Options:
|
||||
-n, --name NAME New unique screen/player name
|
||||
-s, --server IP Server IP / hostname
|
||||
-p, --port PORT Server port
|
||||
-k, --key KEY Quick-connect / auth key
|
||||
--https Enable HTTPS
|
||||
--no-https Disable HTTPS
|
||||
--verify-ssl Enable SSL certificate verification
|
||||
--no-verify-ssl Disable SSL certificate verification
|
||||
--set-hostname Also set the OS hostname to the new screen name
|
||||
-y, --yes Non-interactive; use args/defaults, no prompts
|
||||
--force Run even if this card was already set up
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ---- Pick a python interpreter (prefer the venv, fall back to system) ----
|
||||
pick_python() {
|
||||
if [ -x "$VENV_DIR/bin/python3" ]; then
|
||||
echo "$VENV_DIR/bin/python3"
|
||||
elif command -v python3 >/dev/null 2>&1; then
|
||||
echo "python3"
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# ---- Parse arguments ----
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-n|--name) NEW_NAME="$2"; shift 2 ;;
|
||||
-s|--server) NEW_SERVER="$2"; shift 2 ;;
|
||||
-p|--port) NEW_PORT="$2"; shift 2 ;;
|
||||
-k|--key) NEW_KEY="$2"; shift 2 ;;
|
||||
--https) NEW_USE_HTTPS="true"; shift ;;
|
||||
--no-https) NEW_USE_HTTPS="false"; shift ;;
|
||||
--verify-ssl) NEW_VERIFY_SSL="true"; shift ;;
|
||||
--no-verify-ssl) NEW_VERIFY_SSL="false"; shift ;;
|
||||
--set-hostname) SET_HOSTNAME=true; shift ;;
|
||||
-y|--yes) ASSUME_YES=true; shift ;;
|
||||
--force) FORCE=true; shift ;;
|
||||
-h|--help) usage ;;
|
||||
*) err "Unknown option: $1"; echo ""; usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "=========================================="
|
||||
echo "Kiwy Signage - Post-Clone Setup"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# ---- Safety: only run once per card (unless --force) ----
|
||||
if [ -f "$MARKER_FILE" ] && [ "$FORCE" != true ]; then
|
||||
warn "This card looks already set up (marker: $(basename "$MARKER_FILE"))."
|
||||
warn "Re-run with --force if you really want to reset it again."
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PYTHON_BIN="$(pick_python)"
|
||||
if [ -z "$PYTHON_BIN" ]; then
|
||||
err "python3 not found. Run install.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 1) Stop the running player + watchdog so nothing re-writes the token
|
||||
# ============================================================
|
||||
echo "▶ Stopping any running player/watchdog..."
|
||||
pkill -f "bash.*start.sh" 2>/dev/null || true
|
||||
pkill -f "python3 main.py" 2>/dev/null || true
|
||||
sleep 2
|
||||
pkill -9 -f "bash.*start.sh" 2>/dev/null || true
|
||||
pkill -9 -f "python3 main.py" 2>/dev/null || true
|
||||
ok "Player stopped"
|
||||
echo ""
|
||||
|
||||
# ============================================================
|
||||
# 2) Read current config (for defaults / display)
|
||||
# ============================================================
|
||||
CUR_NAME=""
|
||||
CUR_SERVER=""
|
||||
CUR_PORT=""
|
||||
CUR_KEY=""
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
eval "$("$PYTHON_BIN" - "$CONFIG_FILE" <<'PYEOF'
|
||||
import json, sys
|
||||
try:
|
||||
c = json.load(open(sys.argv[1]))
|
||||
except Exception:
|
||||
c = {}
|
||||
def q(k):
|
||||
v = c.get(k, "")
|
||||
v = "" if v is None else str(v)
|
||||
return v.replace("'", "'\\''")
|
||||
print(f"CUR_NAME='{q('screen_name')}'")
|
||||
print(f"CUR_SERVER='{q('server_ip')}'")
|
||||
print(f"CUR_PORT='{q('port')}'")
|
||||
print(f"CUR_KEY='{q('quickconnect_key')}'")
|
||||
PYEOF
|
||||
)"
|
||||
fi
|
||||
|
||||
OS_HOSTNAME="$(hostname 2>/dev/null || echo '')"
|
||||
|
||||
# Sensible defaults: prefer an explicit new name, else the (changed) OS hostname,
|
||||
# else the previous screen name.
|
||||
DEF_NAME="$NEW_NAME"
|
||||
[ -z "$DEF_NAME" ] && DEF_NAME="$OS_HOSTNAME"
|
||||
[ -z "$DEF_NAME" ] && DEF_NAME="$CUR_NAME"
|
||||
[ -z "$DEF_NAME" ] && DEF_NAME="player-$(date +%s | tail -c 5)"
|
||||
|
||||
# ============================================================
|
||||
# 3) Prompt (unless -y / non-interactive) for the new identity
|
||||
# ============================================================
|
||||
prompt_value() {
|
||||
# $1 = prompt text, $2 = default
|
||||
local answer
|
||||
if [ "$ASSUME_YES" = true ] || [ ! -t 0 ]; then
|
||||
echo "$2"
|
||||
return
|
||||
fi
|
||||
read -r -p " $1 [$2]: " answer
|
||||
echo "${answer:-$2}"
|
||||
}
|
||||
|
||||
echo "▶ New player identity"
|
||||
info "Note: the server identifies this player by 'screen_name', not the OS hostname."
|
||||
if [ -z "$NEW_NAME" ]; then
|
||||
NEW_NAME="$(prompt_value "New unique screen/player name" "$DEF_NAME")"
|
||||
fi
|
||||
if [ -z "$NEW_SERVER" ]; then
|
||||
NEW_SERVER="$(prompt_value "Server IP/hostname" "$CUR_SERVER")"
|
||||
fi
|
||||
if [ -z "$NEW_PORT" ]; then
|
||||
NEW_PORT="$(prompt_value "Server port" "${CUR_PORT:-8080}")"
|
||||
fi
|
||||
if [ -z "$NEW_KEY" ]; then
|
||||
NEW_KEY="$(prompt_value "Quick-connect key" "$CUR_KEY")"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ============================================================
|
||||
# 4) Back up current identity files
|
||||
# ============================================================
|
||||
BACKUP_DIR="$BACKUP_ROOT/$STAMP"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
echo "▶ Backing up current identity files to:"
|
||||
info "$BACKUP_DIR"
|
||||
[ -f "$CONFIG_FILE" ] && cp -p "$CONFIG_FILE" "$BACKUP_DIR/" 2>/dev/null || true
|
||||
[ -f "$SCRIPT_DIR/src/player_auth.json" ] && cp -p "$SCRIPT_DIR/src/player_auth.json" "$BACKUP_DIR/" 2>/dev/null || true
|
||||
ok "Backup complete"
|
||||
echo ""
|
||||
|
||||
# ============================================================
|
||||
# 5) Delete cached authentication tokens (force re-authentication)
|
||||
# ============================================================
|
||||
echo "▶ Removing cached authentication token(s)..."
|
||||
REMOVED=0
|
||||
while IFS= read -r token; do
|
||||
[ -z "$token" ] && continue
|
||||
rm -f "$token" && REMOVED=$((REMOVED + 1)) && info "deleted: ${token#$SCRIPT_DIR/}"
|
||||
done < <(find "$SCRIPT_DIR" -name 'player_auth.json' -not -path '*/.venv/*' -not -path "$BACKUP_ROOT/*" 2>/dev/null)
|
||||
rm -f /tmp/temp_auth_test.json 2>/dev/null || true
|
||||
rm -f "$SCRIPT_DIR/.player_heartbeat" 2>/dev/null || true
|
||||
rm -f "$SCRIPT_DIR/.player_stop_requested" 2>/dev/null || true
|
||||
if [ "$REMOVED" -eq 0 ]; then
|
||||
ok "No cached token found (already clean)"
|
||||
else
|
||||
ok "Removed $REMOVED token file(s)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ============================================================
|
||||
# 6) Write the new config (screen_name + server details)
|
||||
# ============================================================
|
||||
echo "▶ Writing new player configuration..."
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
CFG_PATH="$CONFIG_FILE" \
|
||||
NEW_SCREEN_NAME="$NEW_NAME" \
|
||||
NEW_SERVER_IP="$NEW_SERVER" \
|
||||
NEW_PORT="$NEW_PORT" \
|
||||
NEW_KEY="$NEW_KEY" \
|
||||
NEW_USE_HTTPS="$NEW_USE_HTTPS" \
|
||||
NEW_VERIFY_SSL="$NEW_VERIFY_SSL" \
|
||||
"$PYTHON_BIN" <<'PYEOF'
|
||||
import json, os
|
||||
|
||||
cfg_path = os.environ['CFG_PATH']
|
||||
try:
|
||||
with open(cfg_path) as f:
|
||||
cfg = json.load(f)
|
||||
except Exception:
|
||||
cfg = {}
|
||||
|
||||
updates = {
|
||||
'screen_name': os.environ.get('NEW_SCREEN_NAME', ''),
|
||||
'server_ip': os.environ.get('NEW_SERVER_IP', ''),
|
||||
'port': os.environ.get('NEW_PORT', ''),
|
||||
'quickconnect_key': os.environ.get('NEW_KEY', ''),
|
||||
}
|
||||
for key, val in updates.items():
|
||||
if val != '':
|
||||
cfg[key] = val
|
||||
|
||||
if os.environ.get('NEW_USE_HTTPS', '') != '':
|
||||
cfg['use_https'] = os.environ['NEW_USE_HTTPS'].lower() == 'true'
|
||||
if os.environ.get('NEW_VERIFY_SSL', '') != '':
|
||||
cfg['verify_ssl'] = os.environ['NEW_VERIFY_SSL'].lower() == 'true'
|
||||
|
||||
# Safe defaults for anything missing
|
||||
cfg.setdefault('port', '8080')
|
||||
cfg.setdefault('orientation', 'Landscape')
|
||||
cfg.setdefault('touch', 'True')
|
||||
cfg.setdefault('max_resolution', '1920x1080')
|
||||
cfg.setdefault('edit_feature_enabled', True)
|
||||
cfg.setdefault('use_https', False)
|
||||
cfg.setdefault('verify_ssl', False)
|
||||
|
||||
with open(cfg_path, 'w') as f:
|
||||
json.dump(cfg, f, indent=2)
|
||||
f.write('\n')
|
||||
|
||||
print(json.dumps(cfg, indent=2))
|
||||
PYEOF
|
||||
ok "Configuration updated"
|
||||
echo ""
|
||||
|
||||
# ============================================================
|
||||
# 7) Optionally align the OS hostname with the screen name
|
||||
# ============================================================
|
||||
if [ "$SET_HOSTNAME" = true ] && [ -n "$NEW_NAME" ]; then
|
||||
SAFE_HOST="$(echo "$NEW_NAME" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-' | sed 's/-\+$//')"
|
||||
echo "▶ Setting OS hostname to '$SAFE_HOST' (player identity is still 'screen_name')..."
|
||||
if command -v hostnamectl >/dev/null 2>&1; then
|
||||
sudo hostnamectl set-hostname "$SAFE_HOST" 2>/dev/null \
|
||||
&& ok "OS hostname set to $SAFE_HOST" \
|
||||
|| warn "Could not set OS hostname (needs sudo)"
|
||||
else
|
||||
echo "$SAFE_HOST" | sudo tee /etc/hostname >/dev/null 2>&1 \
|
||||
&& ok "OS hostname file updated" \
|
||||
|| warn "Could not write /etc/hostname (needs sudo)"
|
||||
fi
|
||||
warn "A reboot is recommended for the hostname change to fully apply."
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 8) Mark the card as set up + summary
|
||||
# ============================================================
|
||||
{
|
||||
echo "Post-clone setup completed: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "screen_name: $NEW_NAME"
|
||||
echo "server_ip: $NEW_SERVER"
|
||||
echo "port: $NEW_PORT"
|
||||
echo "backup: $BACKUP_DIR"
|
||||
} > "$MARKER_FILE"
|
||||
|
||||
echo "=========================================="
|
||||
echo "✓ Card is ready to connect to the server"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo " Player name : $NEW_NAME"
|
||||
echo " Server : $NEW_SERVER:$NEW_PORT"
|
||||
echo " Backup : $BACKUP_DIR"
|
||||
echo ""
|
||||
echo " Next steps:"
|
||||
echo " 1. (Optional) reboot: sudo reboot"
|
||||
echo " 2. Start the player: ./start.sh"
|
||||
echo ""
|
||||
echo " The player will authenticate as a NEW player '$NEW_NAME'"
|
||||
echo " and fetch its own playlist from the server."
|
||||
echo ""
|
||||
+3
-14
@@ -1,16 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run Kivy Signage Player once (no watchdog / auto-restart)
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# Activate the virtual environment created by install.sh (holds Kivy + deps).
|
||||
if [ -f "$SCRIPT_DIR/.venv/bin/activate" ]; then
|
||||
# shellcheck disable=SC1091
|
||||
source "$SCRIPT_DIR/.venv/bin/activate"
|
||||
else
|
||||
echo "WARNING: virtual environment not found at $SCRIPT_DIR/.venv"
|
||||
echo " Run 'bash install.sh' first for a fully offline-capable setup."
|
||||
fi
|
||||
|
||||
cd "$SCRIPT_DIR/src"
|
||||
exec python3 main.py
|
||||
# Start Kivy Signage Player
|
||||
cd "$(dirname "$0")/src"
|
||||
python3 main.py
|
||||
Binary file not shown.
+15
-1
@@ -354,7 +354,9 @@ def delete_unused_media(playlist_data, media_dir):
|
||||
rel_path = os.path.relpath(full_path, media_dir)
|
||||
|
||||
# Skip if file is in current playlist
|
||||
if rel_path in referenced_files:
|
||||
# Normalize paths to handle Windows backslashes vs server forward slashes
|
||||
normalized_rel = rel_path.replace('\\', '/')
|
||||
if normalized_rel in referenced_files or rel_path in referenced_files:
|
||||
continue
|
||||
|
||||
# Delete unreferenced file
|
||||
@@ -454,6 +456,18 @@ def update_playlist_if_needed(config, playlist_dir, media_dir):
|
||||
return playlist_file
|
||||
else:
|
||||
logger.info("✓ Playlist is up to date")
|
||||
# Even when the playlist version matches, ensure media files exist locally.
|
||||
# The media folder might be empty (e.g. fresh install or deleted files).
|
||||
logger.info("📥 Checking for missing media files...")
|
||||
ssl_manager = auth.ssl_manager if config.get('use_https', True) else None
|
||||
server_url = auth.auth_data.get('server_url', '')
|
||||
downloaded = download_media_files(
|
||||
server_data.get('playlist', []), media_dir, ssl_manager, server_url
|
||||
)
|
||||
if downloaded:
|
||||
server_data['playlist'] = downloaded
|
||||
# Re-save playlist with updated URLs if needed
|
||||
save_playlist(server_data, playlist_dir)
|
||||
return playlist_file
|
||||
|
||||
except Exception as e:
|
||||
|
||||
+31
-35
@@ -14,21 +14,21 @@ import asyncio
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
# 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
|
||||
os.environ['SDL_VIDEODRIVER'] = 'wayland,x11,dummy' # Prefer Wayland, fallback to X11, then dummy
|
||||
os.environ['SDL_AUDIODRIVER'] = 'alsa,pulse,dummy' # Prefer ALSA, fallback to pulse, then dummy
|
||||
# Use setdefault() so that a wrapper script (e.g. run_win.py) can pre-set
|
||||
# Windows-compatible values before this module is imported.
|
||||
os.environ.setdefault('KIVY_VIDEO', 'ffpyplayer')
|
||||
os.environ.setdefault('FFPYPLAYER_CODECS', 'h264,h265,vp9,vp8')
|
||||
os.environ.setdefault('SDL_VIDEO_ALLOW_SCREENSAVER', '0')
|
||||
os.environ.setdefault('SDL_VIDEODRIVER', 'wayland,x11,dummy')
|
||||
os.environ.setdefault('SDL_AUDIODRIVER', 'alsa,pulse,dummy')
|
||||
|
||||
# Video playback optimizations
|
||||
# Note: pygame backend requires X11/Wayland context; let Kivy auto-detect for better compatibility
|
||||
# os.environ['KIVY_WINDOW'] = 'pygame' # Use pygame backend for better performance
|
||||
os.environ['KIVY_AUDIO'] = 'ffpyplayer' # Use ffpyplayer for audio
|
||||
os.environ['KIVY_GL_BACKEND'] = 'gl' # Use OpenGL backend
|
||||
os.environ['KIVY_INPUTPROVIDERS'] = 'wayland,x11' # Only use Wayland and X11 input providers, skip problematic ones
|
||||
os.environ['FFMPEG_THREADS'] = '2' # Use 2 threads for ffmpeg decoding (Raspberry Pi has limited resources)
|
||||
os.environ['LIBPLAYER_BUFFER'] = '1048576' # 1MB buffer (reduced from 2MB to save memory)
|
||||
os.environ['SDL_AUDIODRIVER'] = 'alsa' # Use ALSA for better audio on Pi
|
||||
os.environ.setdefault('KIVY_AUDIO', 'ffpyplayer')
|
||||
os.environ.setdefault('KIVY_GL_BACKEND', 'gl')
|
||||
os.environ.setdefault('KIVY_INPUTPROVIDERS', 'wayland,x11')
|
||||
os.environ.setdefault('FFMPEG_THREADS', '2')
|
||||
os.environ.setdefault('LIBPLAYER_BUFFER', '1048576')
|
||||
os.environ.setdefault('SDL_AUDIODRIVER', 'alsa')
|
||||
|
||||
# Configure Kivy BEFORE importing any Kivy modules
|
||||
from kivy.config import Config
|
||||
@@ -60,25 +60,6 @@ from kivy.core.window import Window
|
||||
from kivy.properties import BooleanProperty
|
||||
from kivy.logger import Logger
|
||||
|
||||
# Kivy's Video widget subclasses Image, so its inherited image loader always
|
||||
# runs first and logs a harmless "Image: Error loading <file.mp4>" for every
|
||||
# video (the real decoding is done by the ffpyplayer CoreVideo provider).
|
||||
# Kivy routes every module through the single 'kivy' logger (the "[Image]" tag
|
||||
# is just Kivy's message-prefix formatting), so drop only that one message and
|
||||
# leave all other Kivy log records visible.
|
||||
import logging
|
||||
|
||||
|
||||
class _SuppressKivyImageNoise(logging.Filter):
|
||||
def filter(self, record):
|
||||
try:
|
||||
return not record.getMessage().startswith('Image: Error loading')
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
|
||||
logging.getLogger('kivy').addFilter(_SuppressKivyImageNoise())
|
||||
|
||||
# Try to import evdev for card reader support (after Logger is available)
|
||||
try:
|
||||
import evdev
|
||||
@@ -105,8 +86,12 @@ from kivy.graphics import Color, Line, Ellipse
|
||||
from kivy.uix.floatlayout import FloatLayout
|
||||
from kivy.uix.slider import Slider
|
||||
|
||||
# Load the KV file
|
||||
Builder.load_file('signage_player.kv')
|
||||
# Load the KV file - resolve relative to this file's directory
|
||||
_kv_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'signage_player.kv')
|
||||
if not os.path.exists(_kv_path):
|
||||
# Fallback: relative to cwd (for PyInstaller bundled runs)
|
||||
_kv_path = 'signage_player.kv'
|
||||
Builder.load_file(_kv_path)
|
||||
|
||||
|
||||
class CardReader:
|
||||
@@ -1314,7 +1299,11 @@ class SignagePlayer(Widget):
|
||||
Logger.debug(f"SignagePlayer: Skipping play_current_media - player is paused")
|
||||
return
|
||||
|
||||
if not self.playlist or self.current_index >= len(self.playlist):
|
||||
if not self.playlist:
|
||||
Logger.warning("SignagePlayer: Cannot play - playlist is empty")
|
||||
return
|
||||
|
||||
if self.current_index >= len(self.playlist):
|
||||
# End of playlist, restart
|
||||
self.restart_playlist()
|
||||
return
|
||||
@@ -1533,6 +1522,9 @@ class SignagePlayer(Widget):
|
||||
def _on_video_eos(self, instance):
|
||||
"""Callback when video reaches end of stream"""
|
||||
Logger.debug("SignagePlayer: Video finished playing (EOS)")
|
||||
# Unschedule any pending timer and advance to next media
|
||||
Clock.unschedule(self.next_media)
|
||||
Clock.schedule_once(self.next_media, 0.5)
|
||||
|
||||
def _on_video_loaded(self, instance, value):
|
||||
"""Callback when video is loaded - log video information"""
|
||||
@@ -2083,6 +2075,10 @@ class SignagePlayer(Widget):
|
||||
|
||||
def restart_playlist(self):
|
||||
"""Restart playlist from beginning"""
|
||||
if not self.playlist:
|
||||
Logger.warning("SignagePlayer: Cannot restart - playlist is empty")
|
||||
return
|
||||
|
||||
Logger.info("SignagePlayer: Restarting playlist")
|
||||
|
||||
# Send restart feedback asynchronously (non-blocking)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"hostname": "WINDOWS-PC",
|
||||
"auth_code": "",
|
||||
"player_id": 2,
|
||||
"player_name": "Windows-Player1",
|
||||
"playlist_id": 1,
|
||||
"orientation": "Landscape",
|
||||
"authenticated": true,
|
||||
"server_url": "http://192.168.0.107:8080"
|
||||
}
|
||||
@@ -121,9 +121,6 @@ class PlayerAuth:
|
||||
self.auth_data = {
|
||||
'hostname': hostname,
|
||||
'auth_code': data.get('auth_code'),
|
||||
# Persist the quick-connect code: the feedback endpoint
|
||||
# requires it in the request body (see send_feedback).
|
||||
'quickconnect_code': quickconnect_code,
|
||||
'player_id': data.get('player_id'),
|
||||
'player_name': data.get('player_name'),
|
||||
'playlist_id': data.get('playlist_id'),
|
||||
@@ -357,7 +354,6 @@ class PlayerAuth:
|
||||
self.auth_data = {
|
||||
'hostname': self.auth_data.get('hostname', ''),
|
||||
'auth_code': '',
|
||||
'quickconnect_code': self.auth_data.get('quickconnect_code', ''),
|
||||
'player_id': None,
|
||||
'player_name': '',
|
||||
'playlist_id': None,
|
||||
|
||||
+293
-280
@@ -352,303 +352,316 @@
|
||||
# Settings popup content
|
||||
<SettingsPopup@Popup>:
|
||||
title: 'Player Settings'
|
||||
size_hint: 0.8, 0.8
|
||||
size_hint: 0.9, 0.85
|
||||
auto_dismiss: True
|
||||
|
||||
BoxLayout:
|
||||
orientation: 'vertical'
|
||||
padding: dp(20)
|
||||
spacing: dp(15)
|
||||
padding: [dp(15), dp(10)]
|
||||
spacing: dp(8)
|
||||
|
||||
# Server configuration
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(40)
|
||||
spacing: dp(10)
|
||||
|
||||
Label:
|
||||
text: 'Server IP:'
|
||||
size_hint_x: 0.3
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
ScrollView:
|
||||
BoxLayout:
|
||||
orientation: 'vertical'
|
||||
spacing: dp(8)
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
|
||||
TextInput:
|
||||
id: server_input
|
||||
size_hint_x: 0.7
|
||||
multiline: False
|
||||
font_size: sp(14)
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
# Server port
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(40)
|
||||
spacing: dp(10)
|
||||
|
||||
Label:
|
||||
text: 'Port:'
|
||||
size_hint_x: 0.3
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
# Server configuration
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(36)
|
||||
spacing: dp(8)
|
||||
|
||||
Label:
|
||||
text: 'Server IP:'
|
||||
size_hint_x: 0.3
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
|
||||
TextInput:
|
||||
id: server_input
|
||||
size_hint_x: 0.7
|
||||
multiline: False
|
||||
font_size: sp(13)
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
TextInput:
|
||||
id: port_input
|
||||
size_hint_x: 0.7
|
||||
multiline: False
|
||||
font_size: sp(14)
|
||||
hint_text: '80 or 8080 (leave empty for default)'
|
||||
input_filter: 'int'
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
# Screen name
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(40)
|
||||
spacing: dp(10)
|
||||
|
||||
Label:
|
||||
text: 'Screen Name:'
|
||||
size_hint_x: 0.3
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
# Server port
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(36)
|
||||
spacing: dp(8)
|
||||
|
||||
Label:
|
||||
text: 'Port:'
|
||||
size_hint_x: 0.3
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
|
||||
TextInput:
|
||||
id: port_input
|
||||
size_hint_x: 0.7
|
||||
multiline: False
|
||||
font_size: sp(13)
|
||||
hint_text: '80 or 8080 (leave empty for default)'
|
||||
input_filter: 'int'
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
TextInput:
|
||||
id: screen_input
|
||||
size_hint_x: 0.7
|
||||
multiline: False
|
||||
font_size: sp(14)
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
# Quickconnect key
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(40)
|
||||
spacing: dp(10)
|
||||
|
||||
Label:
|
||||
text: 'Quickconnect:'
|
||||
size_hint_x: 0.3
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
# Screen name
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(36)
|
||||
spacing: dp(8)
|
||||
|
||||
Label:
|
||||
text: 'Screen Name:'
|
||||
size_hint_x: 0.3
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
|
||||
TextInput:
|
||||
id: screen_input
|
||||
size_hint_x: 0.7
|
||||
multiline: False
|
||||
font_size: sp(13)
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
TextInput:
|
||||
id: quickconnect_input
|
||||
size_hint_x: 0.7
|
||||
multiline: False
|
||||
font_size: sp(14)
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
# Orientation
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(40)
|
||||
spacing: dp(10)
|
||||
|
||||
Label:
|
||||
text: 'Orientation:'
|
||||
size_hint_x: 0.3
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
# Quickconnect key
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(36)
|
||||
spacing: dp(8)
|
||||
|
||||
Label:
|
||||
text: 'Quickconnect:'
|
||||
size_hint_x: 0.3
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
|
||||
TextInput:
|
||||
id: quickconnect_input
|
||||
size_hint_x: 0.7
|
||||
multiline: False
|
||||
font_size: sp(13)
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
TextInput:
|
||||
id: orientation_input
|
||||
size_hint_x: 0.7
|
||||
multiline: False
|
||||
font_size: sp(14)
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
# Touch
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(40)
|
||||
spacing: dp(10)
|
||||
|
||||
Label:
|
||||
text: 'Touch:'
|
||||
size_hint_x: 0.3
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
# Orientation
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(36)
|
||||
spacing: dp(8)
|
||||
|
||||
Label:
|
||||
text: 'Orientation:'
|
||||
size_hint_x: 0.3
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
|
||||
TextInput:
|
||||
id: orientation_input
|
||||
size_hint_x: 0.7
|
||||
multiline: False
|
||||
font_size: sp(13)
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
TextInput:
|
||||
id: touch_input
|
||||
size_hint_x: 0.7
|
||||
multiline: False
|
||||
font_size: sp(14)
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
# 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'
|
||||
# Touch
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(36)
|
||||
spacing: dp(8)
|
||||
|
||||
Label:
|
||||
text: 'Touch:'
|
||||
size_hint_x: 0.3
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
|
||||
TextInput:
|
||||
id: touch_input
|
||||
size_hint_x: 0.7
|
||||
multiline: False
|
||||
font_size: sp(13)
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
TextInput:
|
||||
id: resolution_input
|
||||
size_hint_x: 0.7
|
||||
multiline: False
|
||||
font_size: sp(14)
|
||||
hint_text: '1920x1080 or auto'
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
# Edit Feature Enable/Disable
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(40)
|
||||
spacing: dp(10)
|
||||
|
||||
Label:
|
||||
text: 'Enable Edit Feature:'
|
||||
size_hint_x: 0.3
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
# Resolution
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(36)
|
||||
spacing: dp(8)
|
||||
|
||||
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(13)
|
||||
hint_text: '1920x1080 or auto'
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
CheckBox:
|
||||
id: edit_enabled_checkbox
|
||||
size_hint_x: None
|
||||
width: dp(40)
|
||||
active: True
|
||||
on_active: root.on_edit_feature_toggle(self.active)
|
||||
|
||||
Label:
|
||||
text: '(Allow editing images on this player)'
|
||||
size_hint_x: 0.4
|
||||
font_size: sp(12)
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
color: 0.7, 0.7, 0.7, 1
|
||||
# Edit Feature Enable/Disable
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(36)
|
||||
spacing: dp(8)
|
||||
|
||||
Label:
|
||||
text: 'Enable Edit:'
|
||||
size_hint_x: 0.3
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
|
||||
CheckBox:
|
||||
id: edit_enabled_checkbox
|
||||
size_hint_x: None
|
||||
width: dp(36)
|
||||
active: True
|
||||
on_active: root.on_edit_feature_toggle(self.active)
|
||||
|
||||
Label:
|
||||
text: '(Allow editing images)'
|
||||
size_hint_x: 0.4
|
||||
font_size: sp(11)
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
color: 0.7, 0.7, 0.7, 1
|
||||
|
||||
# Separator
|
||||
Widget:
|
||||
size_hint_y: None
|
||||
height: dp(5)
|
||||
|
||||
# Reset Buttons Section
|
||||
Label:
|
||||
text: 'Reset Options:'
|
||||
size_hint_y: None
|
||||
height: dp(26)
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
bold: True
|
||||
font_size: sp(14)
|
||||
|
||||
# Reset Buttons Row
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(44)
|
||||
spacing: dp(8)
|
||||
|
||||
Button:
|
||||
id: reset_auth_btn
|
||||
text: 'Reset Player Auth'
|
||||
background_color: 0.8, 0.4, 0.2, 1
|
||||
font_size: sp(12)
|
||||
on_press: root.reset_player_auth()
|
||||
|
||||
Button:
|
||||
id: reset_playlist_btn
|
||||
text: 'Reset Playlist to v0'
|
||||
background_color: 0.8, 0.4, 0.2, 1
|
||||
font_size: sp(12)
|
||||
on_press: root.reset_playlist_version()
|
||||
|
||||
Button:
|
||||
id: restart_player_btn
|
||||
text: 'Restart Player'
|
||||
background_color: 0.2, 0.6, 0.8, 1
|
||||
font_size: sp(12)
|
||||
on_press: root.restart_player()
|
||||
|
||||
# Test Connection Button
|
||||
Button:
|
||||
id: test_connection_btn
|
||||
text: 'Test Server Connection'
|
||||
size_hint_y: None
|
||||
height: dp(44)
|
||||
background_color: 0.2, 0.4, 0.8, 1
|
||||
font_size: sp(13)
|
||||
on_press: root.test_connection()
|
||||
|
||||
# Connection Status Label
|
||||
Label:
|
||||
id: connection_status
|
||||
text: 'Click button to test connection'
|
||||
size_hint_y: None
|
||||
height: dp(32)
|
||||
text_size: self.size
|
||||
halign: 'center'
|
||||
valign: 'middle'
|
||||
font_size: sp(11)
|
||||
color: 0.7, 0.7, 0.7, 1
|
||||
|
||||
# Separator
|
||||
Widget:
|
||||
size_hint_y: None
|
||||
height: dp(5)
|
||||
|
||||
# Status information row
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(26)
|
||||
spacing: dp(8)
|
||||
|
||||
Label:
|
||||
id: playlist_info
|
||||
text: 'Playlist: N/A'
|
||||
text_size: self.size
|
||||
halign: 'center'
|
||||
valign: 'middle'
|
||||
font_size: sp(11)
|
||||
|
||||
Label:
|
||||
id: media_count_info
|
||||
text: 'Media: 0'
|
||||
text_size: self.size
|
||||
halign: 'center'
|
||||
valign: 'middle'
|
||||
font_size: sp(11)
|
||||
|
||||
Label:
|
||||
id: status_info
|
||||
text: 'Status: Idle'
|
||||
text_size: self.size
|
||||
halign: 'center'
|
||||
valign: 'middle'
|
||||
font_size: sp(11)
|
||||
|
||||
Widget:
|
||||
size_hint_y: 0.05
|
||||
|
||||
# Reset Buttons Section
|
||||
Label:
|
||||
text: 'Reset Options:'
|
||||
size_hint_y: None
|
||||
height: dp(30)
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
bold: True
|
||||
font_size: sp(16)
|
||||
|
||||
# Reset Buttons Row
|
||||
# Action buttons (always visible, outside scroll)
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(50)
|
||||
spacing: dp(10)
|
||||
|
||||
Button:
|
||||
id: reset_auth_btn
|
||||
text: 'Reset Player Auth'
|
||||
background_color: 0.8, 0.4, 0.2, 1
|
||||
on_press: root.reset_player_auth()
|
||||
|
||||
Button:
|
||||
id: reset_playlist_btn
|
||||
text: 'Reset Playlist to v0'
|
||||
background_color: 0.8, 0.4, 0.2, 1
|
||||
on_press: root.reset_playlist_version()
|
||||
|
||||
Button:
|
||||
id: restart_player_btn
|
||||
text: 'Restart Player'
|
||||
background_color: 0.2, 0.6, 0.8, 1
|
||||
on_press: root.restart_player()
|
||||
|
||||
# Test Connection Button
|
||||
Button:
|
||||
id: test_connection_btn
|
||||
text: 'Test Server Connection'
|
||||
size_hint_y: None
|
||||
height: dp(50)
|
||||
background_color: 0.2, 0.4, 0.8, 1
|
||||
on_press: root.test_connection()
|
||||
|
||||
# Connection Status Label
|
||||
Label:
|
||||
id: connection_status
|
||||
text: 'Click button to test connection'
|
||||
size_hint_y: None
|
||||
height: dp(40)
|
||||
text_size: self.size
|
||||
halign: 'center'
|
||||
valign: 'middle'
|
||||
color: 0.7, 0.7, 0.7, 1
|
||||
|
||||
Widget:
|
||||
size_hint_y: 0.05
|
||||
|
||||
# Status information row
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(30)
|
||||
spacing: dp(10)
|
||||
|
||||
Label:
|
||||
id: playlist_info
|
||||
text: 'Playlist: N/A'
|
||||
text_size: self.size
|
||||
halign: 'center'
|
||||
valign: 'middle'
|
||||
font_size: sp(12)
|
||||
|
||||
Label:
|
||||
id: media_count_info
|
||||
text: 'Media: 0'
|
||||
text_size: self.size
|
||||
halign: 'center'
|
||||
valign: 'middle'
|
||||
font_size: sp(12)
|
||||
|
||||
Label:
|
||||
id: status_info
|
||||
text: 'Status: Idle'
|
||||
text_size: self.size
|
||||
halign: 'center'
|
||||
valign: 'middle'
|
||||
font_size: sp(12)
|
||||
|
||||
Widget:
|
||||
size_hint_y: 0.05
|
||||
|
||||
# Action buttons
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(50)
|
||||
spacing: dp(20)
|
||||
height: dp(44)
|
||||
spacing: dp(15)
|
||||
|
||||
Button:
|
||||
text: 'Save & Close'
|
||||
|
||||
@@ -59,13 +59,6 @@ load_user_environment() {
|
||||
# Fallback: detect display manually
|
||||
log_message "Falling back to manual display detection..."
|
||||
|
||||
# Ensure XDG_RUNTIME_DIR is set. Wayland clients (and Kivy) need it, and a
|
||||
# cron-launched instance may not have it. Default to the standard location.
|
||||
if [ -z "$XDG_RUNTIME_DIR" ]; then
|
||||
export XDG_RUNTIME_DIR="/run/user/$(id -u)"
|
||||
log_message "Set XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR"
|
||||
fi
|
||||
|
||||
if [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then
|
||||
# Try to detect Wayland display
|
||||
if [ -S "/run/user/$(id -u)/wayland-0" ]; then
|
||||
@@ -103,27 +96,8 @@ load_user_environment() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# Load the user environment early. If there is no usable display, exit WITHOUT
|
||||
# taking the lock — otherwise a headless instance (e.g. cron before login) would
|
||||
# block the real desktop-autostart instance from ever running.
|
||||
if ! load_user_environment; then
|
||||
log_message "No display server available — exiting so another instance can start"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# Single-instance guard (prevents parallel watchdogs)
|
||||
# ============================================================
|
||||
# Only ONE watchdog may run at a time. If XDG autostart, the cron fallback, and
|
||||
# a manual start all fire, the first one to reach this point wins; every other
|
||||
# instance exits immediately instead of spawning a second player.
|
||||
LOCK_FILE="$SCRIPT_DIR/.player_watchdog.lock"
|
||||
exec 200>"$LOCK_FILE"
|
||||
if ! flock -n 200; then
|
||||
log_message "Another watchdog is already running (lock: $LOCK_FILE) — exiting"
|
||||
exit 0
|
||||
fi
|
||||
log_message "Single-instance lock acquired: $LOCK_FILE"
|
||||
# Load the user environment early
|
||||
load_user_environment
|
||||
|
||||
# Function to configure display output to Full HD (1920x1080)
|
||||
configure_display_resolution() {
|
||||
|
||||
@@ -28,7 +28,6 @@ pkill -9 -f "python3 main.py" 2>/dev/null
|
||||
# Clean up heartbeat and stop flag files
|
||||
rm -f "$SCRIPT_DIR/.player_heartbeat"
|
||||
rm -f "$SCRIPT_DIR/.player_stop_requested"
|
||||
rm -f "$SCRIPT_DIR/.player_watchdog.lock"
|
||||
|
||||
echo ""
|
||||
echo "✓ Player and watchdog stopped"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
Requirement already satisfied: ffpyplayer in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (4.5.3)
|
||||
Requirement already satisfied: requests in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (2.34.2)
|
||||
Requirement already satisfied: aiohttp in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (3.14.3)
|
||||
Requirement already satisfied: bcrypt in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (5.0.0)
|
||||
Requirement already satisfied: certifi in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (2026.7.22)
|
||||
Requirement already satisfied: pyinstaller in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (6.21.0)
|
||||
Requirement already satisfied: kivy[base] in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (2.3.1)
|
||||
Requirement already satisfied: Kivy-Garden>=0.1.4 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from kivy[base]) (0.1.5)
|
||||
Requirement already satisfied: docutils in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from kivy[base]) (0.23)
|
||||
Requirement already satisfied: pygments in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from kivy[base]) (2.20.0)
|
||||
Requirement already satisfied: filetype in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from kivy[base]) (1.2.0)
|
||||
Requirement already satisfied: kivy-deps.angle~=0.4.0 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from kivy[base]) (0.4.0)
|
||||
Requirement already satisfied: kivy-deps.sdl2~=0.8.0 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from kivy[base]) (0.8.0)
|
||||
Requirement already satisfied: kivy-deps.glew~=0.3.1 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from kivy[base]) (0.3.1)
|
||||
Requirement already satisfied: pypiwin32 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from kivy[base]) (223)
|
||||
Requirement already satisfied: pillow<11,>=9.5.0 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from kivy[base]) (10.4.0)
|
||||
Requirement already satisfied: charset_normalizer<4,>=2 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from requests) (3.4.9)
|
||||
Requirement already satisfied: idna<4,>=2.5 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from requests) (3.18)
|
||||
Requirement already satisfied: urllib3<3,>=1.26 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from requests) (2.7.0)
|
||||
Requirement already satisfied: aiohappyeyeballs>=2.5.0 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from aiohttp) (2.7.1)
|
||||
Requirement already satisfied: aiosignal>=1.4.0 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from aiohttp) (1.4.0)
|
||||
Requirement already satisfied: attrs>=17.3.0 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from aiohttp) (26.1.0)
|
||||
Requirement already satisfied: frozenlist>=1.1.1 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from aiohttp) (1.8.0)
|
||||
Requirement already satisfied: multidict<7.0,>=4.5 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from aiohttp) (6.7.1)
|
||||
Requirement already satisfied: propcache>=0.2.0 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from aiohttp) (0.5.2)
|
||||
Requirement already satisfied: typing_extensions>=4.4 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from aiohttp) (4.16.0)
|
||||
Requirement already satisfied: yarl<2.0,>=1.17.0 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from aiohttp) (1.24.5)
|
||||
Requirement already satisfied: altgraph in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from pyinstaller) (0.17.5)
|
||||
Requirement already satisfied: packaging>=22.0 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from pyinstaller) (26.2)
|
||||
Requirement already satisfied: pefile>=2022.5.30 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from pyinstaller) (2024.8.26)
|
||||
Requirement already satisfied: pyinstaller-hooks-contrib>=2026.6 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from pyinstaller) (2026.6)
|
||||
Requirement already satisfied: pywin32-ctypes>=0.2.1 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from pyinstaller) (0.2.3)
|
||||
Requirement already satisfied: setuptools>=42.0.0 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from pyinstaller) (83.0.0)
|
||||
Requirement already satisfied: pywin32>=223 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from pypiwin32->kivy[base]) (312)
|
||||
@@ -0,0 +1,160 @@
|
||||
# Kiwy Signage Player - Windows Edition
|
||||
|
||||
Build and run the Kiwy digital signage player on Windows as a standalone `.exe`.
|
||||
|
||||
## 📋 Requirements Analysis
|
||||
|
||||
The original app was built for **Raspberry Pi (Linux)**, using these technologies:
|
||||
|
||||
| Component | Original (RPi/Linux) | Windows Equivalent |
|
||||
|-----------|---------------------|-------------------|
|
||||
| **GUI** | Kivy 2.3+ | Kivy 2.3+ (works cross-platform) |
|
||||
| **Video** | ffpyplayer | ffpyplayer (needs FFmpeg DLLs) |
|
||||
| **Card Reader** | evdev (Linux input) | **Not available** — gracefully disabled |
|
||||
| **Screen Keep-Awake** | xset, xdotool, Wayland | `SetThreadExecutionState` (Win32 API) |
|
||||
| **Weblink** | chromium-browser (kiosk) | Chrome/Edge (--kiosk mode) |
|
||||
| **Audio** | ALSA/PulseAudio | DirectSound |
|
||||
| **Window Backend** | SDL2 (Wayland/X11) | SDL2 (Windows native) |
|
||||
| **OpenGL** | Desktop GL | ANGLE (DirectX wrapper) |
|
||||
|
||||
### What works on Windows
|
||||
- ✅ Media playback (images, videos via ffpyplayer)
|
||||
- ✅ Playlist sync from DigiServer (HTTP/HTTPS)
|
||||
- ✅ Touch & mouse controls
|
||||
- ✅ Settings popup
|
||||
- ✅ Image editing/annotation
|
||||
- ✅ Password-protected exit
|
||||
- ✅ Web links (opens in Chrome/Edge kiosk)
|
||||
- ✅ Network monitoring
|
||||
- ✅ Auto-update playlist
|
||||
|
||||
### What is disabled on Windows
|
||||
- ❌ Card reader (evdev is Linux-only; `EVDEV_AVAILABLE = False`)
|
||||
- ❌ HDMI power management (tvservice is RPi-specific)
|
||||
- ❌ WiFi restart (uses Linux `nmcli`)
|
||||
|
||||
## 🚀 Quick Start (Development)
|
||||
|
||||
### Prerequisites
|
||||
1. **Python 3.12+** (64-bit) — [python.org](https://python.org)
|
||||
- ⚠️ **Python 3.13+ is NOT supported** — Kivy 2.3.1 does not have pre-built wheels for it
|
||||
- ⚠️ **Python 3.14 is NOT supported** — no Kivy wheels available
|
||||
- ✅ **Python 3.12.9** is the recommended version (confirmed working)
|
||||
2. **FFmpeg** — for video codec support
|
||||
- Download from [ffmpeg.org](https://ffmpeg.org/download.html)
|
||||
- Add `bin\` folder to your PATH
|
||||
3. **Visual C++ Redistributable** — [latest](https://aka.ms/vs/17/release/vc_redist.x64.exe)
|
||||
|
||||
### Install & Run
|
||||
```batch
|
||||
cd windows
|
||||
|
||||
REM Create virtual environment with Python 3.12
|
||||
py -3.12 -m venv venv
|
||||
:: OR specify full path:
|
||||
:: "C:\Users\Dell-PC\AppData\Local\Programs\Python\Python312\python.exe" -m venv venv
|
||||
|
||||
venv\Scripts\activate
|
||||
|
||||
REM Install dependencies
|
||||
pip install -r requirements_win.txt
|
||||
|
||||
REM Run in development mode
|
||||
python run_win.py
|
||||
```
|
||||
|
||||
## 📦 Building the .exe
|
||||
|
||||
### One-Command Build
|
||||
```batch
|
||||
cd windows
|
||||
build_win.bat
|
||||
```
|
||||
|
||||
### Manual Build
|
||||
```batch
|
||||
cd windows
|
||||
venv\Scripts\activate
|
||||
pip install -r requirements_win.txt
|
||||
pyinstaller build.spec --clean --noconfirm
|
||||
```
|
||||
|
||||
### Output
|
||||
```
|
||||
windows\dist\KiwySignagePlayer\
|
||||
├── KiwySignagePlayer.exe # Main executable
|
||||
├── config/ # Config files (auto-copied)
|
||||
├── resources/ # Icons, intro video
|
||||
└── ... (supporting DLLs)
|
||||
```
|
||||
|
||||
For a **single-file .exe**, edit `build.spec` — uncomment the `exe_onefile` section and comment out the `coll = COLLECT(...)` section.
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
1. On first run, config files are created in the **same folder as the executable** (not in `%APPDATA%`)
|
||||
- The .exe creates: `config/`, `media/`, `playlists/`, `logs/` directories locally
|
||||
- This allows you to copy the entire `dist\KiwySignagePlayer\` folder anywhere and it works
|
||||
2. Edit `config\app_config.json` (next to the .exe) to set your server:
|
||||
|
||||
```json
|
||||
{
|
||||
"server_ip": "192.168.0.109",
|
||||
"port": "8080",
|
||||
"screen_name": "Birou_IT",
|
||||
"quickconnect_key": "8887779",
|
||||
"orientation": "Landscape",
|
||||
"touch": "True",
|
||||
"max_resolution": "1920x1080",
|
||||
"edit_feature_enabled": true,
|
||||
"use_https": false,
|
||||
"verify_ssl": false
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
```batch
|
||||
cd windows
|
||||
venv\Scripts\activate
|
||||
python run_win.py
|
||||
```
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| **"ffpyplayer not found"** | Install: `pip install ffpyplayer` |
|
||||
| **"No video" / black screen** | Install FFmpeg and add to PATH. Try `KIVY_GL_BACKEND=angle_sdl2` or `KIVY_GL_BACKEND=gl` |
|
||||
| **Kivy window doesn't open** | Run from command prompt to see error messages. Ensure GPU drivers are up to date. |
|
||||
| **Weblinks not opening** | Install Google Chrome or Microsoft Edge |
|
||||
| **Can't connect to server** | Check firewall. Try `use_https: false` and `verify_ssl: false` for testing |
|
||||
| **Antivirus flags .exe** | Add the output folder to antivirus exclusions. This is a false positive common with PyInstaller. |
|
||||
|
||||
## 📁 Project Structure (Build)
|
||||
|
||||
```
|
||||
Kiwy-Signage/
|
||||
├── windows/
|
||||
│ ├── run_win.py # Windows entry point (patches platform differences)
|
||||
│ ├── build.spec # PyInstaller configuration
|
||||
│ ├── build_win.bat # One-click build script
|
||||
│ ├── pyi_runtime_hook.py # PyInstaller runtime hook
|
||||
│ ├── requirements_win.txt # Windows Python dependencies
|
||||
│ └── README_WINDOWS_BUILD.md # This file
|
||||
├── src/
|
||||
│ ├── main.py # Main application (original)
|
||||
│ ├── get_playlists_v2.py # Playlist sync
|
||||
│ ├── player_auth.py # Authentication
|
||||
│ ├── ssl_utils.py # SSL/HTTPS
|
||||
│ ├── keyboard_widget.py # On-screen keyboard
|
||||
│ ├── network_monitor.py # Network monitoring
|
||||
│ ├── edit_popup.py # Image editing
|
||||
│ └── signage_player.kv # Kivy UI layout
|
||||
├── config/
|
||||
│ ├── app_config.json # Player configuration
|
||||
│ └── resources/ # Icons, images, intro video
|
||||
├── media/ # Downloaded media (created at runtime)
|
||||
├── playlists/ # Playlist files (created at runtime)
|
||||
└── logs/ # Log files (created at runtime)
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Quick syntax check for build files."""
|
||||
import ast, sys
|
||||
|
||||
files = [
|
||||
r'c:\Users\Dell-PC\Desktop\Kiwy-Signage\windows\run_win.py',
|
||||
r'c:\Users\Dell-PC\Desktop\Kiwy-Signage\windows\pyi_runtime_hook.py',
|
||||
]
|
||||
|
||||
for f in files:
|
||||
try:
|
||||
with open(f, encoding='utf-8') as fh:
|
||||
ast.parse(fh.read())
|
||||
print(f"OK: {f}")
|
||||
except SyntaxError as e:
|
||||
print(f"SYNTAX ERROR in {f}: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
print("All files OK")
|
||||
@@ -0,0 +1,256 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
"""
|
||||
PyInstaller spec file for Kiwy Signage Player (Windows .exe)
|
||||
|
||||
Build command (from windows/ directory):
|
||||
pyinstaller build.spec --clean --noconfirm
|
||||
|
||||
OR use the build script:
|
||||
build_win.bat
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# --- Paths -----------------------------------------------------------
|
||||
# This spec file is in windows/build.spec, so the project root is
|
||||
# always two levels up from this file's real location.
|
||||
# __file__ may not be available in PyInstaller spec context fallback to cwd.
|
||||
try:
|
||||
_spec_dir = Path(__file__).resolve().parent
|
||||
except NameError:
|
||||
_spec_dir = Path(os.getcwd()).resolve()
|
||||
# _spec_dir is now the absolute path to the windows/ directory
|
||||
BUILD_DIR = _spec_dir
|
||||
ROOT_DIR = BUILD_DIR.parent
|
||||
SRC_DIR = ROOT_DIR / 'src'
|
||||
CONFIG_DIR = ROOT_DIR / 'config'
|
||||
RESOURCES_DIR = CONFIG_DIR / 'resources'
|
||||
|
||||
# --- Determine hidden imports that PyInstaller might miss -------------
|
||||
hidden_imports = [
|
||||
# Kivy core modules
|
||||
'kivy.core.window',
|
||||
'kivy.core.video',
|
||||
'kivy.core.audio',
|
||||
'kivy.core.text',
|
||||
'kivy.core.image',
|
||||
'kivy.core.gl',
|
||||
'kivy.core.camera',
|
||||
'kivy.core.clipboard',
|
||||
'kivy.core.spelling',
|
||||
'kivy.core.text.markup',
|
||||
'kivy.core.window.window_sdl2',
|
||||
'kivy.core.image.img_sdl2',
|
||||
'kivy.core.video.video_ffpyplayer',
|
||||
'kivy.core.audio.audio_ffpyplayer',
|
||||
# Kivy modules
|
||||
'kivy.uix.video',
|
||||
'kivy.uix.vkeyboard',
|
||||
'kivy.uix.popup',
|
||||
'kivy.uix.image',
|
||||
'kivy.uix.button',
|
||||
'kivy.uix.label',
|
||||
'kivy.uix.textinput',
|
||||
'kivy.uix.boxlayout',
|
||||
'kivy.uix.floatlayout',
|
||||
'kivy.uix.slider',
|
||||
'kivy.uix.widget',
|
||||
'kivy.uix.checkbox',
|
||||
'kivy.graphics',
|
||||
'kivy.graphics.texture',
|
||||
'kivy.graphics.vertex_instructions',
|
||||
'kivy.graphics.context_instructions',
|
||||
'kivy.clock',
|
||||
'kivy.loader',
|
||||
'kivy.animation',
|
||||
'kivy.lang',
|
||||
'kivy.logger',
|
||||
'kivy.config',
|
||||
'kivy.properties',
|
||||
'kivy.metrics',
|
||||
'kivy.factory',
|
||||
# Graphics providers
|
||||
'kivy.graphics.opengl',
|
||||
'kivy.graphics.opengl_utils',
|
||||
'kivy.graphics.fbo',
|
||||
'kivy.graphics.gl_instructions',
|
||||
'kivy.graphics.stencil_instructions',
|
||||
'kivy.graphics.scissor_instructions',
|
||||
'kivy.graphics.buffer',
|
||||
'kivy.graphics.vbo',
|
||||
'kivy.graphics.shader',
|
||||
'kivy.graphics.compiler',
|
||||
# ffpyplayer
|
||||
'ffpyplayer',
|
||||
'ffpyplayer.player',
|
||||
'ffpyplayer.pic',
|
||||
'ffpyplayer.writer',
|
||||
# Networking
|
||||
'requests',
|
||||
'aiohttp',
|
||||
'urllib3',
|
||||
'certifi',
|
||||
'bcrypt',
|
||||
# Platform
|
||||
'ctypes',
|
||||
'ctypes.wintypes',
|
||||
'subprocess',
|
||||
'shutil',
|
||||
'glob',
|
||||
'selectors',
|
||||
'tempfile',
|
||||
# Windows-specific
|
||||
'cef_browser',
|
||||
'win32gui',
|
||||
'win32con',
|
||||
]
|
||||
|
||||
# Exclude Linux-only modules
|
||||
excluded_imports = [
|
||||
'gi', # GTK introspection (Linux)
|
||||
'gi.repository',
|
||||
'evdev', # We inject a fake evdev module in run_win.py
|
||||
# GStreamer — we use ffpyplayer, not GStreamer
|
||||
'kivy.lib.gstplayer',
|
||||
# cefpython3: keep only Python 3.12 .pyd, exclude other version .pyd files
|
||||
'cefpython3.cefpython_py27',
|
||||
'cefpython3.cefpython_py34',
|
||||
'cefpython3.cefpython_py35',
|
||||
'cefpython3.cefpython_py36',
|
||||
'cefpython3.cefpython_py37',
|
||||
'cefpython3.cefpython_py38',
|
||||
'cefpython3.cefpython_py39',
|
||||
'cefpython3.cefpython_py310',
|
||||
'cefpython3.cefpython_py311',
|
||||
]
|
||||
|
||||
# --- Application data files to bundle --------------------------------
|
||||
# Resources (icons, intro video, etc.)
|
||||
resources_data = []
|
||||
for item in RESOURCES_DIR.iterdir():
|
||||
if item.is_file():
|
||||
target_dir = 'config/resources'
|
||||
resources_data.append((str(item), target_dir))
|
||||
|
||||
# Config directory (app_config.json)
|
||||
config_data = []
|
||||
config_file = CONFIG_DIR / 'app_config.json'
|
||||
if config_file.exists():
|
||||
config_data.append((str(config_file), 'config'))
|
||||
|
||||
# Source files - .kv file
|
||||
kv_file = SRC_DIR / 'signage_player.kv'
|
||||
kv_data = []
|
||||
if kv_file.exists():
|
||||
kv_data.append((str(kv_file), '.'))
|
||||
|
||||
# Bundle the entire src directory as a tree
|
||||
source_tree = Tree(str(SRC_DIR), prefix='', excludes=['*.pyc', '__pycache__', '*.ini'])
|
||||
|
||||
# --- Collect binary DLLs from kivy_deps and ffpyplayer ----------------
|
||||
import importlib.util
|
||||
from pathlib import Path as _Path
|
||||
|
||||
def _find_share_dlls(package_path, subdir='bin'):
|
||||
"""Find .dll files under a package's share/ directory."""
|
||||
if not package_path:
|
||||
return []
|
||||
base = _Path(package_path).parent
|
||||
# Check: share/<pkg>/bin/ relative to parent
|
||||
share = base / 'share'
|
||||
if share.is_dir():
|
||||
results = []
|
||||
for root, dirs, files in os.walk(share):
|
||||
for f in files:
|
||||
if f.endswith('.dll'):
|
||||
results.append((os.path.join(root, f), '.'))
|
||||
return results
|
||||
return []
|
||||
|
||||
# SDL2 DLLs
|
||||
_sdl2_spec = importlib.util.find_spec('kivy_deps.sdl2')
|
||||
_sdl2_dlls = _find_share_dlls(_sdl2_spec.origin if _sdl2_spec else None)
|
||||
|
||||
# ANGLE DLLs
|
||||
_angle_spec = importlib.util.find_spec('kivy_deps.angle')
|
||||
_angle_dlls = _find_share_dlls(_angle_spec.origin if _angle_spec else None)
|
||||
|
||||
# GLEW DLLs
|
||||
_glew_spec = importlib.util.find_spec('kivy_deps.glew')
|
||||
_glew_dlls = _find_share_dlls(_glew_spec.origin if _glew_spec else None)
|
||||
|
||||
# ffpyplayer FFmpeg DLLs
|
||||
_ffpy_spec = importlib.util.find_spec('ffpyplayer')
|
||||
_ffpy_dlls = _find_share_dlls(_ffpy_spec.origin if _ffpy_spec else None)
|
||||
|
||||
_all_binaries = _sdl2_dlls + _angle_dlls + _glew_dlls + _ffpy_dlls
|
||||
|
||||
if not _all_binaries:
|
||||
print("=" * 70)
|
||||
print("WARNING: No Kivy/ffpyplayer DLLs found via share/ directories.")
|
||||
print("PyInstaller may still auto-detect them, but if the .exe")
|
||||
print("fails with 'SDL2.dll not found' or similar, you will need")
|
||||
print("to manually add the DLL paths to the spec file.")
|
||||
print("=" * 70)
|
||||
|
||||
# --- Build the .exe --------------------------------------------------
|
||||
a = Analysis(
|
||||
['run_win.py'], # Entry point (relative to this spec)
|
||||
pathex=[str(BUILD_DIR), str(SRC_DIR)], # Where to find modules
|
||||
binaries=_all_binaries,
|
||||
datas=resources_data + config_data + kv_data,
|
||||
hiddenimports=hidden_imports,
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[str(BUILD_DIR / 'pyi_runtime_hook.py')],
|
||||
excludes=excluded_imports,
|
||||
noarchive=False,
|
||||
module_collection_mode={
|
||||
'kivy': 'pyz',
|
||||
'kivy.core': 'pyz',
|
||||
'kivy.uix': 'pyz',
|
||||
'kivy.graphics': 'pyz',
|
||||
},
|
||||
)
|
||||
|
||||
# Add the source tree (main.py, etc.)
|
||||
a.datas += source_tree
|
||||
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='KiwySignagePlayer',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True, # Show console for debugging startup errors
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=str(RESOURCES_DIR / 'app_icon.ico') if (RESOURCES_DIR / 'app_icon.ico').exists() else None,
|
||||
)
|
||||
|
||||
# --- COLLECT everything into a single folder -------------------------
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='KiwySignagePlayer',
|
||||
)
|
||||
@@ -0,0 +1,126 @@
|
||||
@echo off
|
||||
REM =====================================================================
|
||||
REM Kiwy Signage Player - Windows Build Script
|
||||
REM =====================================================================
|
||||
REM This script builds a standalone Windows .exe using PyInstaller.
|
||||
REM
|
||||
REM Prerequisites:
|
||||
REM 1. Python 3.10+ installed (with "Add to PATH" checked)
|
||||
REM 2. Visual C++ Redistributable (for ffpyplayer)
|
||||
REM 3. FFmpeg binaries in PATH (optional, for video codec support)
|
||||
REM
|
||||
REM Steps:
|
||||
REM 1. Run this script from the project root or the windows\ folder
|
||||
REM 2. The .exe will be created in windows\dist\KiwySignagePlayer\
|
||||
REM =====================================================================
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
cd /d "%~dp0"
|
||||
|
||||
echo ============================================
|
||||
echo Kiwy Signage Player - Windows Build
|
||||
echo ============================================
|
||||
echo.
|
||||
|
||||
REM ---- Check Python ----
|
||||
where python >nul 2>&1
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo [ERROR] Python not found! Please install Python 3.10+ and add it to PATH.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [INFO] Using Python:
|
||||
python --version
|
||||
|
||||
REM ---- Create virtual environment (if not exists) ----
|
||||
if not exist "venv\Scripts\python.exe" (
|
||||
echo.
|
||||
echo [STEP] Creating virtual environment...
|
||||
python -m venv venv
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo [ERROR] Failed to create virtual environment.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
) else (
|
||||
echo [INFO] Virtual environment already exists.
|
||||
)
|
||||
|
||||
REM ---- Activate virtual environment ----
|
||||
call venv\Scripts\activate.bat
|
||||
|
||||
REM ---- Install/upgrade pip ----
|
||||
echo.
|
||||
echo [STEP] Upgrading pip...
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
REM ---- Install dependencies ----
|
||||
echo.
|
||||
echo [STEP] Installing Windows dependencies...
|
||||
pip install -r requirements_win.txt
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo [ERROR] Failed to install dependencies.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM ---- Verify Kivy installation ----
|
||||
echo.
|
||||
echo [STEP] Verifying Kivy installation...
|
||||
python -c "import kivy; print(f'Kivy {kivy.__version__}')" 2>&1
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo [WARNING] Kivy check failed. Build may still work but test carefully.
|
||||
)
|
||||
|
||||
REM ---- Check PyInstaller ----
|
||||
echo.
|
||||
echo [STEP] Verifying PyInstaller...
|
||||
python -c "import PyInstaller; print(f'PyInstaller {PyInstaller.__version__}')" 2>&1
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo [ERROR] PyInstaller not found.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM ---- Create app icon (from PNG if possible) ----
|
||||
echo.
|
||||
echo [STEP] Checking for app icon...
|
||||
if not exist "..\config\resources\app_icon.ico" (
|
||||
echo [INFO] No .ico icon found. Will use default PyInstaller icon.
|
||||
echo [INFO] To add a custom icon, place app_icon.ico in config\resources\
|
||||
)
|
||||
|
||||
REM ---- Run PyInstaller ----
|
||||
echo.
|
||||
echo [STEP] Building executable with PyInstaller...
|
||||
echo This may take several minutes. Please wait...
|
||||
echo.
|
||||
|
||||
pyinstaller build.spec --clean --noconfirm
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo.
|
||||
echo [ERROR] PyInstaller build failed!
|
||||
echo Check the output above for error details.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM ---- Success ----
|
||||
echo.
|
||||
echo ============================================
|
||||
echo BUILD COMPLETE!
|
||||
echo ============================================
|
||||
echo.
|
||||
echo Output: %~dp0dist\KiwySignagePlayer\
|
||||
echo.
|
||||
echo The executable is:
|
||||
echo %~dp0dist\KiwySignagePlayer\KiwySignagePlayer.exe
|
||||
echo.
|
||||
echo To run: Double-click KiwySignagePlayer.exe
|
||||
echo.
|
||||
echo Note: The first run may take a while as Windows Defender
|
||||
echo scans the executable. This is normal.
|
||||
echo.
|
||||
pause
|
||||
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
cef_browser.py v2 — Embedded Chromium INSIDE Kivy's SDL2 window
|
||||
|
||||
v1 created a separate Win32 window (same as external Chrome).
|
||||
v2 creates CEF as a **child window** of Kivy's SDL_app window:
|
||||
- No separate taskbar entry
|
||||
- No z-order fighting
|
||||
- No desktop flash
|
||||
- CEF message loop pumped via Kivy Clock (main thread)
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from cefpython3 import cefpython as cef
|
||||
CEF_AVAILABLE = True
|
||||
except ImportError:
|
||||
CEF_AVAILABLE = False
|
||||
|
||||
WS_CHILD = 0x40000000
|
||||
WS_VISIBLE = 0x10000000
|
||||
WS_CLIPSIBLINGS = 0x04000000
|
||||
WS_CLIPCHILDREN = 0x02000000
|
||||
SW_HIDE = 0
|
||||
SW_SHOWNORMAL = 1
|
||||
|
||||
|
||||
class CefBrowser:
|
||||
def __init__(self):
|
||||
self._browser = None
|
||||
self._cef_initialized = False
|
||||
self._child_hwnd = None
|
||||
self._kivy_hwnd = None
|
||||
self._clock_event = None
|
||||
self._showing = False
|
||||
|
||||
# ── Public API ──────────────────────────────────────────────────
|
||||
|
||||
def show(self, url):
|
||||
if not CEF_AVAILABLE:
|
||||
return False
|
||||
if not self._cef_initialized:
|
||||
self._init_cef()
|
||||
if self._browser is not None:
|
||||
self._browser.Navigate(url)
|
||||
self._show_in_kivy()
|
||||
return True
|
||||
return self._create_embedded(url)
|
||||
|
||||
def hide(self):
|
||||
self._showing = False
|
||||
if self._clock_event is not None:
|
||||
try:
|
||||
from kivy.clock import Clock
|
||||
Clock.unschedule(self._clock_event)
|
||||
except Exception:
|
||||
pass
|
||||
self._clock_event = None
|
||||
if self._child_hwnd:
|
||||
try:
|
||||
ctypes.windll.user32.ShowWindow(self._child_hwnd, SW_HIDE)
|
||||
except Exception:
|
||||
pass
|
||||
if self._browser is not None:
|
||||
try:
|
||||
self._browser.CloseBrowser(True)
|
||||
except Exception:
|
||||
pass
|
||||
self._browser = None
|
||||
if self._child_hwnd:
|
||||
try:
|
||||
ctypes.windll.user32.DestroyWindow(self._child_hwnd)
|
||||
except Exception:
|
||||
pass
|
||||
self._child_hwnd = None
|
||||
|
||||
def shutdown(self):
|
||||
self.hide()
|
||||
if self._cef_initialized:
|
||||
try:
|
||||
cef.Shutdown()
|
||||
except Exception:
|
||||
pass
|
||||
self._cef_initialized = False
|
||||
|
||||
def navigate(self, url):
|
||||
if self._browser is not None:
|
||||
self._browser.Navigate(url)
|
||||
|
||||
def is_showing(self):
|
||||
return self._showing
|
||||
|
||||
def resize(self, width, height):
|
||||
"""Called when Kivy window resizes — repositions CEF child."""
|
||||
if self._child_hwnd:
|
||||
ctypes.windll.user32.SetWindowPos(
|
||||
self._child_hwnd, 0, 0, 0, width, height, 0x0004
|
||||
)
|
||||
if self._browser:
|
||||
self._browser.SetBounds(0, 0, width, height)
|
||||
|
||||
# ── Internal ────────────────────────────────────────────────────
|
||||
|
||||
def _init_cef(self):
|
||||
settings = {
|
||||
"multi_threaded_message_loop": False,
|
||||
"single_process": True,
|
||||
"log_severity": cef.LOGSEVERITY_WARNING,
|
||||
"user_agent": "Mozilla/5.0 KiwySignage/1.0",
|
||||
"cache_path": str(
|
||||
Path(os.environ.get("KIWY_DATA_DIR", ".")) / ".cef_cache"
|
||||
),
|
||||
}
|
||||
cef.Initialize(settings=settings)
|
||||
self._cef_initialized = True
|
||||
|
||||
def _get_kivy_hwnd(self):
|
||||
if self._kivy_hwnd is not None:
|
||||
return self._kivy_hwnd
|
||||
try:
|
||||
import win32gui
|
||||
hwnd = win32gui.FindWindow("SDL_app", None)
|
||||
if hwnd:
|
||||
self._kivy_hwnd = hwnd
|
||||
return hwnd
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _create_embedded(self, url):
|
||||
kivy_hwnd = self._get_kivy_hwnd()
|
||||
if not kivy_hwnd:
|
||||
return False
|
||||
|
||||
user32 = ctypes.windll.user32
|
||||
rect = (ctypes.c_long * 4)()
|
||||
user32.GetClientRect(kivy_hwnd, ctypes.byref(rect))
|
||||
w, h = rect[2], rect[3]
|
||||
|
||||
hinstance = ctypes.windll.kernel32.GetModuleHandleW(None)
|
||||
self._child_hwnd = user32.CreateWindowExW(
|
||||
0, b'#32770', b'',
|
||||
WS_CHILD | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
|
||||
0, 0, w, h, kivy_hwnd, 0, hinstance, 0,
|
||||
)
|
||||
if not self._child_hwnd:
|
||||
return False
|
||||
|
||||
winfo = cef.WindowInfo()
|
||||
winfo.SetAsChild(self._child_hwnd, [0, 0, w, h])
|
||||
self._browser = cef.CreateBrowserSync(
|
||||
window_info=winfo,
|
||||
settings={"background_color": 0x00000000},
|
||||
url=url,
|
||||
)
|
||||
self._showing = True
|
||||
self._show_in_kivy()
|
||||
self._start_clock_pump()
|
||||
return True
|
||||
|
||||
def _show_in_kivy(self):
|
||||
if not self._child_hwnd:
|
||||
return
|
||||
kivy_hwnd = self._get_kivy_hwnd()
|
||||
if kivy_hwnd:
|
||||
user32 = ctypes.windll.user32
|
||||
rect = (ctypes.c_long * 4)()
|
||||
user32.GetClientRect(kivy_hwnd, ctypes.byref(rect))
|
||||
user32.SetWindowPos(
|
||||
self._child_hwnd, 0, 0, 0, rect[2], rect[3], 0x0004
|
||||
)
|
||||
ctypes.windll.user32.ShowWindow(self._child_hwnd, SW_SHOWNORMAL)
|
||||
self._showing = True
|
||||
|
||||
def _start_clock_pump(self):
|
||||
if self._clock_event is not None:
|
||||
return
|
||||
|
||||
def _pump(dt):
|
||||
if self._cef_initialized:
|
||||
try:
|
||||
cef.MessageLoopWork()
|
||||
except Exception:
|
||||
pass
|
||||
if self._showing:
|
||||
from kivy.clock import Clock
|
||||
self._clock_event = Clock.schedule_once(_pump, 0.01)
|
||||
|
||||
from kivy.clock import Clock
|
||||
self._clock_event = Clock.schedule_once(_pump, 0)
|
||||
@@ -0,0 +1,186 @@
|
||||
# 🧪 Development Track — Kiwy Signage Player (Windows Edition)
|
||||
|
||||
> This file tracks every change, bug fix, tested solution, build info, and
|
||||
> pending issues for the Windows port. Read this FIRST before starting any
|
||||
> debugging or coding session.
|
||||
|
||||
---
|
||||
|
||||
## 📅 Current Session — 2026-07-24
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Branch** | `Windows-Player` |
|
||||
| **Python** | 3.12.9 — `C:\Users\Dell-PC\AppData\Local\Programs\Python\Python312\python.exe` |
|
||||
| **Venv** | `windows\venv312\` (pre-built, all deps installed) |
|
||||
| **Kivy** | 2.3.1 |
|
||||
| **PyInstaller** | 6.21.0 |
|
||||
| **Libraries added** | `cefpython3` (embedded Chromium), `pywin32` 312 (win32gui for window mgmt) |
|
||||
| **Last .exe build** | 2026-07-24 13:47 — `windows\dist\KiwySignagePlayer\KiwySignagePlayer.exe` (96 MB) |
|
||||
| **Build command** | `Set-Location windows; venv312\Scripts\python.exe -m PyInstaller build.spec --clean --noconfirm` |
|
||||
|
||||
### ⚠️ Python Version Constraints
|
||||
- **Python 3.12.9** — ✅ Confirmed working. Has pre-built Kivy 2.3.1 wheels.
|
||||
- **Python 3.13** — ❌ Kivy wheels NOT available for Windows.
|
||||
- **Python 3.14** — ❌ Tested 2026-07-24. `kivy_deps.sdl2_dev~=0.8.0` has no cp314 wheel.
|
||||
→ Solution: removed Python 3.14 from system, keeping only 3.12.9.
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Bug Tracker
|
||||
|
||||
### [BUG-001] RecursionError: play_current_media ↔ restart_playlist
|
||||
- **Status:** ✅ Fixed 2026-07-24
|
||||
- **Symptom:** Pressing "Restart Player" in settings with empty playlist causes
|
||||
infinite recursion: `play_current_media → restart_playlist → play_current_media → ...`
|
||||
- **Fix:** Added empty-playlist guard in both `play_current_media()` and
|
||||
`restart_playlist()` → they return early instead of calling each other.
|
||||
- **Files:** `src/main.py` — lines ~1304 and ~2073
|
||||
- **Test:** Verified no Python syntax errors via `ast.parse`.
|
||||
|
||||
### [BUG-002] Settings fields cut off on small screens
|
||||
- **Status:** ✅ Fixed 2026-07-24
|
||||
- **Symptom:** "Screen Name", "Quickconnect" and other fields at the top of
|
||||
the settings popup are invisible on smaller resolutions because content
|
||||
overflows the popup.
|
||||
- **Fix:** Wrapped settings content in a `ScrollView`. Moved "Save & Close" /
|
||||
"Cancel" buttons outside the scroll (always visible). Reduced row heights.
|
||||
- **Files:** `src/signage_player.kv` — `<SettingsPopup@Popup>` block
|
||||
|
||||
### [BUG-003] Chromium not fullscreen on Windows
|
||||
- **Status:** ✅ Fixed 2026-07-24
|
||||
- **Symptom:** Web links open in a small window instead of fullscreen.
|
||||
- **Fix:** Changed launch args from `--kiosk` to `--start-maximized --app=URL`
|
||||
+ explicit `--window-size=WxH`. `--kiosk` uses Wayland exclusive-fullscreen
|
||||
protocol which doesn't work on Windows.
|
||||
- **Tested rejected solutions:**
|
||||
- ❌ `--kiosk` alone → small window, no fullscreen
|
||||
- ❌ `--start-fullscreen` alone → not reliable
|
||||
- ✅ `--start-maximized --app=URL --window-size=...` → works
|
||||
- **Files:** `windows/run_win.py` — `_windows_play_weblink()`
|
||||
|
||||
### [BUG-004] Desktop flash when switching between Chromium and Kivy
|
||||
- **Status:** ✅ Fixed 2026-07-24
|
||||
- **Symptom:** When Chrome closes, the desktop is briefly visible before Kivy
|
||||
reappears. Also when Chrome opens, there's a flash.
|
||||
- **Fix:** Added `_Win32Overlay` class — a fullscreen black Win32 window that
|
||||
covers the screen during transitions. Shown BEFORE closing Chrome / opening
|
||||
Chrome, hidden AFTER Kivy is ready.
|
||||
- **Tested rejected solutions:**
|
||||
- ❌ `Window.raise_window()` alone → still shows flash
|
||||
- ✅ Win32 black overlay → smooth masking
|
||||
- **Files:** `windows/run_win.py` — `_Win32Overlay` class
|
||||
|
||||
### [BUG-005] Chrome processes linger after closing weblink
|
||||
- **Status:** ✅ Fixed 2026-07-24
|
||||
- **Symptom:** After a weblink item ends, Chrome child processes (GPU,
|
||||
renderer) remain running → blank windows accumulate.
|
||||
- **Fix:** Use `taskkill /F /T /PID <pid>` to kill the entire process tree.
|
||||
- **Tested rejected solutions:**
|
||||
- ❌ `proc.terminate()` → leaves children running
|
||||
- ❌ `proc.kill()` → same problem
|
||||
- ✅ `taskkill /F /T` → kills everything
|
||||
- **Files:** `windows/run_win.py` — `_windows_kill_process_tree()`
|
||||
|
||||
### [BUG-007] Video plays behind Chromium on weblink→media transition
|
||||
- **Status:** 🔧 **Fix in progress** 2026-07-24
|
||||
- **Symptom:** When a weblink ends and the next media starts, the media plays
|
||||
*behind* Chromium. Audio is heard but user sees Chrome.
|
||||
- **Root cause (Windows):** Linux renders Kivy widget UNDER Chromium → closes
|
||||
Chrome → widget visible. On Windows Chrome stays ON TOP.
|
||||
`Window.raise_window()` is unreliable.
|
||||
- **Fix applied (2026-07-24):**
|
||||
1. **`_bring_kivy_to_front()`** — uses `win32gui.SetForegroundWindow(hwnd)`
|
||||
to reliably bring Kivy/SDL window to front (replaces `raise_window`)
|
||||
2. **`_windows_kill_weblink_after_frame()`** — kills Chrome IMMEDIATELY
|
||||
(not deferred one frame later) before next media starts
|
||||
3. **CEF browser** (`cefpython3`) — embedded Chromium widget replaces
|
||||
subprocess entirely. No process management, no z-order fights.
|
||||
- **Files:** `windows/run_win.py`, `windows/cef_browser.py`
|
||||
|
||||
### [BUG-008] Intro video and media files not found at runtime
|
||||
- **Status:** ✅ **Fixed** 2026-07-24
|
||||
- **Symptom:** `[ERROR] [Image] Error loading <...intro1.mp4>` — intro
|
||||
broken. Also `❌ Media file not found` for playlist items.
|
||||
- **Root cause:** Media download only ran when `server_version > local_version`.
|
||||
When versions matched (v16 == v16), `download_media_files` was never called
|
||||
→ media folder stayed empty.
|
||||
- **Fix:** Added download check in the "up to date" branch — now downloads
|
||||
missing media files even when playlist version hasn't changed.
|
||||
|
||||
### [BUG-009] Video never advances to next item (EOS handler empty)
|
||||
- **Status:** ✅ **Fixed** 2026-07-24
|
||||
- **Symptom:** Video plays but never advances to the next playlist item.
|
||||
- **Root cause:** `_on_video_eos()` callback was a stub — just logged
|
||||
"Video finished playing (EOS)" but never called `next_media()`.
|
||||
- **Fix:** Added `Clock.unschedule(self.next_media)` + `Clock.schedule_once`
|
||||
to advance after 0.5s when a video reaches end of stream.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Tested & Rejected Solutions Log
|
||||
|
||||
> Keep a record of approaches that were tried and didn't work, so we don't
|
||||
> waste time re-testing them.
|
||||
|
||||
| Date | What was tested | Result | Reason it failed |
|
||||
|------|----------------|--------|-----------------|
|
||||
| 2026-07-24 | Python 3.14 with Kivy | ❌ | `kivy_deps.sdl2_dev~=0.8.0` has no cp314 wheel |
|
||||
| 2026-07-24 | `--kiosk` Chrome flag on Windows | ❌ | Not fullscreen, Wayland exclusive-fullscreen not available |
|
||||
| 2026-07-24 | `--start-fullscreen` alone | ❌ | Inconsistent, sometimes not full |
|
||||
| 2026-07-24 | `proc.terminate()` for Chrome | ❌ | Leaves child processes running |
|
||||
| 2026-07-24 | `proc.kill()` for Chrome | ❌ | Same as terminate — children survive |
|
||||
| 2026-07-24 | `Window.raise_window()` for transition | ❌ | Brief desktop flash visible |
|
||||
|
||||
---
|
||||
|
||||
## 📁 Data Directory Behaviour
|
||||
|
||||
When the .exe runs:
|
||||
1. Runtime hook (`pyi_runtime_hook.py`) sets `KIWY_DATA_DIR = exe_dir`
|
||||
2. `run_win.py` patches `SignagePlayer.__init__` to use `KIWY_DATA_DIR`
|
||||
3. Local folders created next to the .exe:
|
||||
```
|
||||
KiwySignagePlayer.exe
|
||||
config/
|
||||
app_config.json
|
||||
resources/ (icons, intro video)
|
||||
certs/ (SSL certificates)
|
||||
media/
|
||||
edited_media/
|
||||
playlists/
|
||||
logs/
|
||||
.kivy/ (Kivy home)
|
||||
.player_heartbeat
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Build Cheatsheet
|
||||
|
||||
```powershell
|
||||
# Build the .exe (from project root or windows/)
|
||||
Set-Location windows
|
||||
& .\venv312\Scripts\python.exe -m PyInstaller build.spec --clean --noconfirm
|
||||
|
||||
# Run in dev mode (no build needed)
|
||||
& .\venv312\Scripts\python.exe run_win.py
|
||||
|
||||
# Test imports only
|
||||
& .\venv312\Scripts\python.exe test_import_fix.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notes for the Next Session
|
||||
|
||||
- [x] ~~Investigate [BUG-006]~~ → merged into [BUG-007], fixed with CEF + win32gui
|
||||
- [x] ~~Test `SetForegroundWindow`~~ → `_bring_kivy_to_front()` uses `win32gui`
|
||||
- [x] Install `cefpython3` — embedded Chromium, no more subprocess
|
||||
- [ ] Verify CEF embedded browser actually works at runtime
|
||||
- [ ] Test the subprocess fallback path when CEF is unavailable
|
||||
- [ ] Check why `AsyncImage` error shows for intro1.mp4 (path issue)
|
||||
- [ ] Ensure media files are downloaded before playback
|
||||
- [ ] Consider adding a startup `.bat` file that users can double-click
|
||||
- [ ] Test card reader fallback behaviour (evdev not available)
|
||||
- [ ] Add `cef_browser.py` to PyInstaller hidden imports in `build.spec`
|
||||
@@ -0,0 +1,27 @@
|
||||
@echo off
|
||||
REM ============================================================
|
||||
REM Kiwy Signage Player - Windows Launcher
|
||||
REM ============================================================
|
||||
REM This batch file launches the Kiwy Signage Player executable.
|
||||
REM It creates local folders for playlist, media, config, and logs
|
||||
REM next to the executable.
|
||||
REM ============================================================
|
||||
|
||||
cd /d "%~dp0dist\KiwySignagePlayer"
|
||||
|
||||
echo ============================================
|
||||
echo Kiwy Signage Player - Windows Edition
|
||||
echo ============================================
|
||||
echo.
|
||||
echo Launching player...
|
||||
echo.
|
||||
|
||||
start "" "KiwySignagePlayer.exe"
|
||||
|
||||
echo Player started.
|
||||
echo.
|
||||
echo If the player window does not appear, check:
|
||||
echo dist\KiwySignagePlayer\logs\crash.log
|
||||
echo dist\KiwySignagePlayer\logs\fatal_crash.log
|
||||
echo.
|
||||
pause
|
||||
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
PyInstaller Runtime Hook for Kiwy Signage Player
|
||||
------------------------------------------------
|
||||
Runs at startup of the packaged .exe to fix paths and environment.
|
||||
Creates all necessary folders LOCAL to the executable's directory.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
# ── IMPORTANT: Set Windows environment BEFORE any Kivy code runs ──
|
||||
# This must happen before main.py's top-level code executes, because
|
||||
# main.py sets SDL_VIDEODRIVER=wayland,x11,dummy which would crash on Windows.
|
||||
os.environ['SDL_VIDEODRIVER'] = 'windows'
|
||||
os.environ['SDL_AUDIODRIVER'] = 'directsound'
|
||||
os.environ['KIVY_WINDOW'] = 'sdl2'
|
||||
os.environ['KIVY_GL_BACKEND'] = 'angle_sdl2'
|
||||
os.environ['KIVY_VIDEO'] = 'ffpyplayer'
|
||||
os.environ['KIVY_AUDIO'] = 'ffpyplayer'
|
||||
os.environ['FFPYPLAYER_CODECS'] = 'h264,h265,vp9,vp8'
|
||||
os.environ['SDL_VIDEO_ALLOW_SCREENSAVER'] = '0'
|
||||
os.environ['KIVY_NO_FILELOG'] = '1'
|
||||
os.environ['KIVY_INPUTPROVIDERS'] = '' # Let Kivy auto-detect on Windows
|
||||
|
||||
# ── Capture ALL early output to a crash log ─────────────────────────
|
||||
# Ensure we catch any exception that happens before Logger is available.
|
||||
_startup_log_path = None
|
||||
try:
|
||||
_exe_dir = Path(sys.executable).parent
|
||||
_startup_log_path = _exe_dir / 'logs' / 'startup_crash.log'
|
||||
(_startup_log_path.parent).mkdir(parents=True, exist_ok=True)
|
||||
with open(_startup_log_path, 'w') as _f:
|
||||
_f.write("pyi_runtime_hook.py started\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _setup_paths():
|
||||
"""Ensure the app can find its bundled files at runtime.
|
||||
|
||||
All data folders (config, media, playlists, logs) are created
|
||||
LOCAL to the executable's directory — NOT in %%APPDATA%%.
|
||||
"""
|
||||
# In PyInstaller, sys.executable is the .exe path.
|
||||
# sys._MEIPASS is the extraction directory (i.e. _internal/ folder).
|
||||
exe_dir = Path(sys.executable).parent
|
||||
internal_dir = Path(getattr(sys, '_MEIPASS', exe_dir))
|
||||
|
||||
# ── Change cwd to _internal so Builder.load_file('signage_player.kv')
|
||||
# and other relative file references from main.py resolve ─────
|
||||
os.chdir(str(internal_dir))
|
||||
|
||||
# Add bundled src directory to Python path
|
||||
src_dir = str(internal_dir / 'src')
|
||||
if os.path.isdir(src_dir) and src_dir not in sys.path:
|
||||
sys.path.insert(0, src_dir)
|
||||
|
||||
# Add internal directory for config/media/playlists access
|
||||
if str(internal_dir) not in sys.path:
|
||||
sys.path.insert(0, str(internal_dir))
|
||||
|
||||
# ── Local folders next to the .exe ──────────────────────────────
|
||||
# All data lives in the SAME folder as the executable so the user
|
||||
# can copy/move the whole directory and everything still works.
|
||||
os.environ['KIWY_DATA_DIR'] = str(exe_dir)
|
||||
|
||||
# Set KIVY_HOME to a local .kivy folder next to the .exe
|
||||
kivy_home = exe_dir / '.kivy'
|
||||
os.environ.setdefault('KIVY_HOME', str(kivy_home))
|
||||
kivy_home.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create local data folders next to the .exe
|
||||
for sub in ['config', 'config/resources', 'media', 'playlists', 'logs']:
|
||||
(exe_dir / sub).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def _copy_bundled_resources():
|
||||
"""Copy bundled resource/config files to the local folders on first run."""
|
||||
exe_dir = Path(sys.executable).parent
|
||||
internal_dir = Path(getattr(sys, '_MEIPASS', exe_dir))
|
||||
|
||||
# Files to copy (source in bundle -> destination next to .exe)
|
||||
files_to_copy = [
|
||||
('config/app_config.json', 'config/app_config.json'),
|
||||
('config/resources/access-card.png', 'config/resources/access-card.png'),
|
||||
('config/resources/arrow.png', 'config/resources/arrow.png'),
|
||||
('config/resources/backward.png', 'config/resources/backward.png'),
|
||||
('config/resources/card-checked.png', 'config/resources/card-checked.png'),
|
||||
('config/resources/edit-pen.png', 'config/resources/edit-pen.png'),
|
||||
('config/resources/exit.png', 'config/resources/exit.png'),
|
||||
('config/resources/forward.png', 'config/resources/forward.png'),
|
||||
('config/resources/intro1.mp4', 'config/resources/intro1.mp4'),
|
||||
('config/resources/pause.png', 'config/resources/pause.png'),
|
||||
('config/resources/pencil.png', 'config/resources/pencil.png'),
|
||||
('config/resources/play.png', 'config/resources/play.png'),
|
||||
('config/resources/settings.png', 'config/resources/settings.png'),
|
||||
]
|
||||
|
||||
for src_rel, dest_rel in files_to_copy:
|
||||
src_path = internal_dir / src_rel
|
||||
dest_path = exe_dir / dest_rel
|
||||
if src_path.is_file() and not dest_path.exists():
|
||||
try:
|
||||
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
import shutil
|
||||
shutil.copy2(str(src_path), str(dest_path))
|
||||
except Exception:
|
||||
pass # Non-critical; app can still run
|
||||
|
||||
|
||||
# ── Wrap everything in try/except to capture early crashes ──────────
|
||||
try:
|
||||
_setup_paths()
|
||||
_copy_bundled_resources()
|
||||
# If we reach here, the hook finished successfully
|
||||
try:
|
||||
with open(_startup_log_path, 'a') as _f:
|
||||
_f.write("pyi_runtime_hook.py completed successfully\n")
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as _hook_exc:
|
||||
import traceback as _tb
|
||||
try:
|
||||
with open(_startup_log_path, 'a') as _f:
|
||||
_f.write(f"pyi_runtime_hook.py CRASHED: {_hook_exc}\n")
|
||||
_tb.print_exc(file=_f)
|
||||
except Exception:
|
||||
pass
|
||||
raise # Re-raise so the .exe still fails visibly
|
||||
@@ -0,0 +1,39 @@
|
||||
# =====================================================================
|
||||
# Kiwy Signage Player - Windows Dependencies
|
||||
# =====================================================================
|
||||
# Install with: pip install -r requirements_win.txt
|
||||
|
||||
# --- Core GUI Framework ---
|
||||
# Kivy 2.3+ with SDL2 backend (best for Windows)
|
||||
kivy[base]>=2.3.0
|
||||
|
||||
# --- Video Playback ---
|
||||
# ffpyplayer for video decoding
|
||||
ffpyplayer>=4.5
|
||||
|
||||
# --- HTTP / Networking ---
|
||||
requests>=2.32.0,<3.0.0
|
||||
aiohttp>=3.9.0,<4.0.0
|
||||
certifi>=2024.0.0
|
||||
|
||||
# --- Password / Auth ---
|
||||
bcrypt>=4.2.0,<5.0.0
|
||||
|
||||
# --- Packaging ---
|
||||
# PyInstaller for building the .exe
|
||||
pyinstaller>=6.0
|
||||
|
||||
# --- Windows-specific Libraries ---
|
||||
# cefpython3: Embedded Chromium browser (replaces subprocess Chrome/Edge)
|
||||
# Installed separately because it's a large package (69 MB):
|
||||
# pip install cefpython3
|
||||
# cefpython3>=66.1
|
||||
# Note: Uncomment above line to bundle cefpython3 in the .exe.
|
||||
# Without it, weblinks fall back to subprocess Chrome/Edge.
|
||||
|
||||
# pywin32: Windows API bindings (win32gui for SetForegroundWindow etc.)
|
||||
# Already installed as a dependency of kivy[base]
|
||||
|
||||
# --- Optional: DirectShow filters for better video on Windows ---
|
||||
# ffmpeg (install via chocolatey or manual download)
|
||||
# https://ffmpeg.org/download.html
|
||||
@@ -0,0 +1,843 @@
|
||||
"""
|
||||
Kiwy Signage Player - Windows Entry Point
|
||||
------------------------------------------
|
||||
Patches platform-specific code and environment for Windows before launching
|
||||
the original Kivy-based signage player application.
|
||||
|
||||
Usage:
|
||||
python run_win.py (for development/testing)
|
||||
run_win.exe (after PyInstaller build)
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
import tempfile
|
||||
import subprocess
|
||||
import shutil
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _show_error_box(title, message):
|
||||
"""Show a Windows message box with the error (visible even without console)."""
|
||||
try:
|
||||
ctypes.windll.user32.MessageBoxW(0, message, title, 0x10) # MB_ICONERROR
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# --- Ensure we are on Windows; warn if not ---
|
||||
if platform.system() != 'Windows':
|
||||
print(f"WARNING: This entry point is designed for Windows. Detected: {platform.system()}")
|
||||
|
||||
# =====================================================================
|
||||
# 1. Set Windows-compatible environment variables BEFORE Kivy imports
|
||||
# =====================================================================
|
||||
|
||||
# Video driver: Use 'windib' or 'angle' (DirectX via ANGLE) for Windows
|
||||
os.environ.setdefault('SDL_VIDEODRIVER', 'windows')
|
||||
# Audio driver: DirectSound for Windows
|
||||
os.environ.setdefault('SDL_AUDIODRIVER', 'directsound')
|
||||
# Prevent screensaver
|
||||
os.environ.setdefault('SDL_VIDEO_ALLOW_SCREENSAVER', '0')
|
||||
# Video backend via ffpyplayer
|
||||
os.environ.setdefault('KIVY_VIDEO', 'ffpyplayer')
|
||||
os.environ.setdefault('KIVY_AUDIO', 'ffpyplayer')
|
||||
os.environ.setdefault('FFPYPLAYER_CODECS', 'h264,h265,vp9,vp8')
|
||||
# Kivy window backend: prefer SDL2 on Windows
|
||||
os.environ.setdefault('KIVY_WINDOW', 'sdl2')
|
||||
# OpenGL
|
||||
os.environ.setdefault('KIVY_GL_BACKEND', 'angle_sdl2')
|
||||
|
||||
# =====================================================================
|
||||
# 2. Patch the evdev import — it is Linux-only. We provide a dummy
|
||||
# module so that `from evdev import ...` will not crash on Windows.
|
||||
# =====================================================================
|
||||
class _DummyEvdev:
|
||||
"""Fake evdev module that raises ImportError for all meaningful uses."""
|
||||
|
||||
class InputDevice:
|
||||
def __init__(self, *a, **kw):
|
||||
raise ImportError("evdev is not available on Windows")
|
||||
|
||||
class ecodes:
|
||||
EV_KEY = 1
|
||||
EV_ABS = 3
|
||||
|
||||
def categorize(self, *a, **kw):
|
||||
raise ImportError("evdev is not available on Windows")
|
||||
|
||||
def list_devices(self):
|
||||
return []
|
||||
|
||||
|
||||
class _FakeEvdevInputDevice:
|
||||
pass
|
||||
|
||||
|
||||
# Inject the fake evdev module into sys.modules so that main.py's
|
||||
# `try: import evdev` succeeds but EVDEV_AVAILABLE stays False.
|
||||
_evdev_dummy = _DummyEvdev()
|
||||
sys.modules['evdev'] = _evdev_dummy
|
||||
sys.modules['evdev.InputDevice'] = _FakeEvdevInputDevice
|
||||
|
||||
# =====================================================================
|
||||
# 3. Provide a Windows implementation of screen activity signaling
|
||||
# We monkey-patch the SignagePlayer.signal_screen_activity method
|
||||
# after the class is defined but before it's used, by hooking into
|
||||
# the import machinery.
|
||||
# =====================================================================
|
||||
|
||||
# We'll store a reference to the original module's signal_screen_activity
|
||||
# so we can replace it after import. This is done inside _patch_main().
|
||||
|
||||
def _windows_screen_activity(self, dt):
|
||||
"""Windows alternative to Linux screen-keep-awake commands.
|
||||
|
||||
Uses SetThreadExecutionState via ctypes to tell Windows to keep
|
||||
the display and system awake.
|
||||
"""
|
||||
try:
|
||||
# ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED | ES_CONTINUOUS
|
||||
ES_CONTINUOUS = 0x80000000
|
||||
ES_SYSTEM_REQUIRED = 0x00000001
|
||||
ES_DISPLAY_REQUIRED = 0x00000002
|
||||
|
||||
ctypes.windll.kernel32.SetThreadExecutionState(
|
||||
ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED
|
||||
)
|
||||
except Exception:
|
||||
pass # non-critical
|
||||
|
||||
|
||||
# ── Try to import the embedded CEF browser ──────────────────────────
|
||||
_CEF_BROWSER = None
|
||||
|
||||
def _get_cef_browser():
|
||||
"""Return the shared CefBrowser singleton, or None if unavailable.
|
||||
|
||||
v2 embeds CEF as a CHILD WINDOW inside Kivy's SDL_app window.
|
||||
This means: no separate taskbar entry, no z-order fighting,
|
||||
no desktop flash, no taskkill needed.
|
||||
"""
|
||||
global _CEF_BROWSER
|
||||
if _CEF_BROWSER is None:
|
||||
try:
|
||||
from cef_browser import CefBrowser, CEF_AVAILABLE
|
||||
if CEF_AVAILABLE:
|
||||
_CEF_BROWSER = CefBrowser()
|
||||
else:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
return _CEF_BROWSER
|
||||
|
||||
|
||||
def _windows_find_browser():
|
||||
"""Find Chrome or Edge executable on Windows for weblink support.
|
||||
|
||||
Returns the path to the browser or None.
|
||||
"""
|
||||
# Common install locations
|
||||
candidates = [
|
||||
# Chrome
|
||||
os.path.expandvars(r'%PROGRAMFILES%\Google\Chrome\Application\chrome.exe'),
|
||||
os.path.expandvars(r'%PROGRAMFILES(X86)%\Google\Chrome\Application\chrome.exe'),
|
||||
os.path.expandvars(r'%LOCALAPPDATA%\Google\Chrome\Application\chrome.exe'),
|
||||
# Edge
|
||||
os.path.expandvars(r'%PROGRAMFILES%\Microsoft\Edge\Application\msedge.exe'),
|
||||
os.path.expandvars(r'%PROGRAMFILES(X86)%\Microsoft\Edge\Application\msedge.exe'),
|
||||
os.path.expandvars(r'%LOCALAPPDATA%\Microsoft\Edge\Application\msedge.exe'),
|
||||
]
|
||||
for path in candidates:
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
|
||||
# Fallback: try PATH
|
||||
which = shutil.which('chrome') or shutil.which('msedge') or shutil.which('google-chrome')
|
||||
if which:
|
||||
return which
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ── Win32 API helpers via ctypes ─────────────────────────────────────
|
||||
class _Win32Overlay:
|
||||
"""Fullscreen black overlay window to mask desktop during transitions.
|
||||
|
||||
When switching away from Chromium, the browser window disappears and
|
||||
there is a brief moment where the desktop is visible before Kivy
|
||||
manages to bring its window to the front. This overlay covers that
|
||||
flash with a pure-black borderless always-on-top Win32 window.
|
||||
"""
|
||||
|
||||
_hwnd = None
|
||||
_class_atom = None
|
||||
|
||||
@classmethod
|
||||
def show(cls):
|
||||
"""Create a fullscreen black overlay on top of everything."""
|
||||
if cls._hwnd is not None:
|
||||
return # already showing
|
||||
try:
|
||||
user32 = ctypes.windll.user32
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
|
||||
# Register a simple window class
|
||||
WNDPROC = ctypes.WINFUNCTYPE(
|
||||
ctypes.c_int64, ctypes.c_int64, ctypes.c_uint,
|
||||
ctypes.c_uint64, ctypes.c_int64
|
||||
)
|
||||
|
||||
@WNDPROC
|
||||
def wnd_proc(hwnd, msg, wparam, lparam):
|
||||
if msg == 0x0002: # WM_DESTROY
|
||||
user32.PostQuitMessage(0)
|
||||
if msg == 0x0014: # WM_ERASEBKGND
|
||||
return 1 # tell Windows we erased it
|
||||
return user32.DefWindowProcW(hwnd, msg, wparam, lparam)
|
||||
|
||||
hinstance = kernel32.GetModuleHandleW(None)
|
||||
|
||||
# Register class
|
||||
class_name = 'KiwyOverlay_' + str(ctypes.c_uint64(int(kernel32.GetTickCount64())).value)
|
||||
wc = ctypes.create_unicode_buffer(256)
|
||||
|
||||
_WNDCLASS = ctypes.c_byte * (6 * 8) # rough size
|
||||
buf = _WNDCLASS()
|
||||
# Simple approach: use RegisterClassExW
|
||||
user32.RegisterClassExW.restype = ctypes.c_uint16
|
||||
user32.RegisterClassExW.argtypes = [ctypes.c_void_p]
|
||||
|
||||
# We'll use a simpler method: just create a MessageBox-style window
|
||||
# Actually, let's use the simplest possible approach:
|
||||
|
||||
# Get screen dimensions
|
||||
screen_w = user32.GetSystemMetrics(0) # SM_CXSCREEN
|
||||
screen_h = user32.GetSystemMetrics(1) # SM_CYSCREEN
|
||||
|
||||
# Create a borderless always-on-top window
|
||||
cls._hwnd = user32.CreateWindowExW(
|
||||
0x00000008, # WS_EX_TOPMOST | WS_EX_TOOLWINDOW
|
||||
b'#32770', # Dialog class - always available
|
||||
b'', # no title
|
||||
0x80000000 | 0x10000000, # WS_POPUP | WS_VISIBLE
|
||||
0, 0, screen_w, screen_h,
|
||||
0, 0, hinstance, 0
|
||||
)
|
||||
|
||||
if cls._hwnd:
|
||||
# Make it black
|
||||
from ctypes import wintypes
|
||||
gdi32 = ctypes.windll.gdi32
|
||||
hdc = user32.GetDC(cls._hwnd)
|
||||
rect = (ctypes.c_long * 4)(0, 0, screen_w, screen_h)
|
||||
brush = gdi32.CreateSolidBrush(0x00000000) # black brush
|
||||
gdi32.FillRect(hdc, ctypes.byref(rect), brush)
|
||||
gdi32.DeleteObject(brush)
|
||||
user32.ReleaseDC(cls._hwnd, hdc)
|
||||
|
||||
# Force it to the top
|
||||
user32.SetWindowPos(cls._hwnd, -1, 0, 0, screen_w, screen_h, 0x0002 | 0x0040)
|
||||
user32.ShowWindow(cls._hwnd, 1) # SW_SHOWNORMAL
|
||||
user32.UpdateWindow(cls._hwnd)
|
||||
except Exception:
|
||||
cls._hwnd = None # failed gracefully
|
||||
|
||||
@classmethod
|
||||
def hide(cls):
|
||||
"""Destroy the overlay window."""
|
||||
if cls._hwnd is None:
|
||||
return
|
||||
try:
|
||||
user32 = ctypes.windll.user32
|
||||
user32.DestroyWindow(cls._hwnd)
|
||||
except Exception:
|
||||
pass
|
||||
cls._hwnd = None
|
||||
|
||||
|
||||
def _bring_kivy_to_front():
|
||||
"""Bring the Kivy/SDL window to foreground using win32gui.
|
||||
|
||||
Unlike Window.raise_window(), win32gui.SetForegroundWindow
|
||||
actually works reliably on Windows — it uses the same Win32
|
||||
API that the Task Manager uses.
|
||||
"""
|
||||
try:
|
||||
import win32gui
|
||||
import win32con
|
||||
|
||||
def _enum_cb(hwnd, hwnd_list):
|
||||
cls = win32gui.GetClassName(hwnd)
|
||||
title = win32gui.GetWindowText(hwnd)
|
||||
if cls == "SDL_app":
|
||||
hwnd_list.append(hwnd)
|
||||
elif "Kiwy" in title or "Signage" in title:
|
||||
hwnd_list.append(hwnd)
|
||||
|
||||
hwnd_list = []
|
||||
win32gui.EnumWindows(_enum_cb, hwnd_list)
|
||||
|
||||
if hwnd_list:
|
||||
kivy_hwnd = hwnd_list[-1] # most recent
|
||||
win32gui.ShowWindow(kivy_hwnd, win32con.SW_SHOWNORMAL)
|
||||
win32gui.SetForegroundWindow(kivy_hwnd)
|
||||
win32gui.BringWindowToTop(kivy_hwnd)
|
||||
except Exception:
|
||||
# Fallback to Kivy's built-in raise
|
||||
try:
|
||||
from kivy.core.window import Window
|
||||
Window.show()
|
||||
Window.raise_window()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _windows_kill_process_tree(proc):
|
||||
"""Kill a process AND all its children using taskkill.
|
||||
|
||||
Chrome/Edge spawns many child processes (GPU, renderer, network,
|
||||
etc.). A simple proc.terminate() leaves children running, causing
|
||||
lingering browser windows or zombie processes.
|
||||
"""
|
||||
if proc is None or proc.poll() is not None:
|
||||
return
|
||||
try:
|
||||
subprocess.run(
|
||||
['taskkill', '/F', '/T', '/PID', str(proc.pid)],
|
||||
capture_output=True, timeout=5
|
||||
)
|
||||
except Exception:
|
||||
# Fallback: try terminate + kill
|
||||
try:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=3)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _patch_main():
|
||||
"""Patch the main module after import for Windows compatibility."""
|
||||
# ── CRITICAL: Override Linux env vars BEFORE importing main ─────
|
||||
# main.py's top-level code sets SDL_VIDEODRIVER=wayland,x11,dummy
|
||||
# and other Linux values. We MUST override these before main.py
|
||||
# gets imported, otherwise Kivy will initialize with the wrong
|
||||
# window provider and crash with SystemExit(1).
|
||||
os.environ['SDL_VIDEODRIVER'] = 'windows'
|
||||
os.environ['SDL_AUDIODRIVER'] = 'directsound'
|
||||
os.environ['KIVY_WINDOW'] = 'sdl2'
|
||||
os.environ['KIVY_GL_BACKEND'] = 'angle_sdl2'
|
||||
os.environ['KIVY_INPUTPROVIDERS'] = '' # Let Kivy auto-detect
|
||||
os.environ['KIVY_VIDEO'] = 'ffpyplayer'
|
||||
os.environ['KIVY_AUDIO'] = 'ffpyplayer'
|
||||
|
||||
# Now safe to import main.py — env vars are already Windows-correct
|
||||
import main as signage_main
|
||||
|
||||
# Replace signal_screen_activity
|
||||
# IMPORTANT: Assign under BOTH the attribute name AND the function's own name.
|
||||
# Kivy's WeakMethod stores self.__func__.__name__ (= '_windows_screen_activity')
|
||||
# and later does getattr(instance, '_windows_screen_activity'). If we only
|
||||
# assign under 'signal_screen_activity', the weakref lookup fails with
|
||||
# AttributeError: 'SignagePlayer' object has no attribute '_windows_screen_activity'
|
||||
signage_main.SignagePlayer.signal_screen_activity = _windows_screen_activity
|
||||
signage_main.SignagePlayer._windows_screen_activity = _windows_screen_activity
|
||||
|
||||
# Store a reference to the original play_weblink so we can wrap it
|
||||
_original_play_weblink = signage_main.SignagePlayer.play_weblink
|
||||
|
||||
def _windows_play_weblink(self, url, duration):
|
||||
"""Windows-compatible weblink handler.
|
||||
|
||||
Strategy (tried in order):
|
||||
1. CEF embedded browser (best — no subprocess, no z-order fights)
|
||||
2. Chrome/Edge subprocess (fallback)
|
||||
"""
|
||||
from kivy.logger import Logger
|
||||
from kivy.clock import Clock
|
||||
from urllib.parse import urlparse
|
||||
|
||||
scheme = urlparse(url).scheme.lower()
|
||||
if scheme not in ('http', 'https'):
|
||||
Logger.warning(f"SignagePlayer: Refusing non-http(s) weblink: {url}")
|
||||
self.consecutive_errors += 1
|
||||
self._skip_to_next_media()
|
||||
return False
|
||||
|
||||
# ── Strategy 1: CEF embedded browser ────────────────────────
|
||||
cef_browser = _get_cef_browser()
|
||||
if cef_browser is not None:
|
||||
Logger.info(f"SignagePlayer: Opening weblink via CEF (embedded in Kivy): {url}")
|
||||
try:
|
||||
self.ids.content_area.opacity = 0
|
||||
except Exception:
|
||||
pass
|
||||
# Attach resize handler so CEF follows Kivy window resizes
|
||||
from kivy.core.window import Window as KivyWindow
|
||||
_orig_on_resize = getattr(KivyWindow, '_on_resize', None)
|
||||
def _cef_resize(*args):
|
||||
try:
|
||||
w, h = KivyWindow.size
|
||||
cef_browser.resize(int(w), int(h))
|
||||
except Exception:
|
||||
pass
|
||||
if _orig_on_resize:
|
||||
try:
|
||||
return _orig_on_resize(*args)
|
||||
except Exception:
|
||||
pass
|
||||
KivyWindow._on_resize = _cef_resize
|
||||
# Bind to size event as well
|
||||
try:
|
||||
KivyWindow.bind(size=_cef_resize)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
cef_browser.show(url)
|
||||
Clock.unschedule(self.next_media)
|
||||
self._start_inactivity_watchdog(duration)
|
||||
self.preload_next_media()
|
||||
Logger.info("SignagePlayer: CEF embedded browser visible (inside Kivy window)")
|
||||
return True
|
||||
|
||||
# ── Strategy 2: Subprocess Chrome/Edge (fallback) ────────────
|
||||
browser = _windows_find_browser()
|
||||
if not browser:
|
||||
Logger.error(
|
||||
"SignagePlayer: No embedded CEF and no Chrome/Edge found. "
|
||||
"Cannot display weblink on Windows."
|
||||
)
|
||||
self.consecutive_errors += 1
|
||||
self._skip_to_next_media()
|
||||
return False
|
||||
|
||||
target_width, target_height = self._get_browser_target_size()
|
||||
|
||||
try:
|
||||
Logger.info(f"SignagePlayer: Opening weblink via subprocess: {url} ({browser})")
|
||||
self._kill_weblink_preload()
|
||||
|
||||
# Hide Kivy content
|
||||
from kivy.core.window import Window as KivyWindow
|
||||
try:
|
||||
self.ids.content_area.opacity = 0
|
||||
KivyWindow.minimize()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_Win32Overlay.show()
|
||||
|
||||
self._weblink_proc = subprocess.Popen([
|
||||
browser,
|
||||
'--new-window',
|
||||
'--start-maximized',
|
||||
'--start-fullscreen',
|
||||
'--app=' + url,
|
||||
'--no-first-run',
|
||||
'--noerrdialogs',
|
||||
'--disable-infobars',
|
||||
'--incognito',
|
||||
'--no-default-browser-check',
|
||||
'--disable-session-crashed-bubble',
|
||||
'--disable-features=TranslateUI',
|
||||
'--disable-sync',
|
||||
'--disable-background-networking',
|
||||
'--window-position=0,0',
|
||||
f'--window-size={target_width},{target_height}',
|
||||
'--force-device-scale-factor=1',
|
||||
url,
|
||||
], shell=False)
|
||||
|
||||
def _hide_overlay(dt):
|
||||
_Win32Overlay.hide()
|
||||
try:
|
||||
KivyWindow.raise_window()
|
||||
except Exception:
|
||||
pass
|
||||
Clock.schedule_once(_hide_overlay, 1.5)
|
||||
|
||||
Clock.unschedule(self.next_media)
|
||||
self._start_inactivity_watchdog(duration)
|
||||
self.preload_next_media()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
Logger.error(f"SignagePlayer: Error opening weblink: {e}")
|
||||
_Win32Overlay.hide()
|
||||
self.consecutive_errors += 1
|
||||
self._skip_to_next_media()
|
||||
return False
|
||||
|
||||
# Replace weblink handling
|
||||
signage_main.SignagePlayer.play_weblink = _windows_play_weblink
|
||||
|
||||
# Patch the _get_browser_target_size to always return a reasonable size on Windows
|
||||
def _windows_get_browser_target_size(self):
|
||||
try:
|
||||
from kivy.core.window import Window
|
||||
width, height = Window.size
|
||||
if width >= 1280 and height >= 720:
|
||||
return int(width), int(height)
|
||||
except Exception:
|
||||
pass
|
||||
return 1920, 1080
|
||||
|
||||
signage_main.SignagePlayer._get_browser_target_size = _windows_get_browser_target_size
|
||||
|
||||
# Patch _start_inactivity_watchdog for Windows — /dev/input does not exist
|
||||
_original_watchdog = signage_main.SignagePlayer._start_inactivity_watchdog
|
||||
|
||||
def _windows_watchdog(self, duration):
|
||||
"""Windows watchdog: uses a simple timer since /dev/input is not available.
|
||||
|
||||
Falls back to a fixed timer that advances after 'duration' seconds.
|
||||
"""
|
||||
from kivy.clock import Clock
|
||||
import threading
|
||||
|
||||
self._stop_inactivity_watchdog()
|
||||
stop_event = threading.Event()
|
||||
self._watchdog_stop = stop_event
|
||||
weblink_proc = self._weblink_proc
|
||||
|
||||
def watchdog():
|
||||
import time
|
||||
# Simply wait for the duration, checking if Chromium exited early
|
||||
elapsed = 0.0
|
||||
step = 0.5
|
||||
while elapsed < duration and not stop_event.is_set():
|
||||
if weblink_proc is not None and weblink_proc.poll() is not None:
|
||||
Clock.schedule_once(self.next_media, 0)
|
||||
return
|
||||
time.sleep(step)
|
||||
elapsed += step
|
||||
if not stop_event.is_set():
|
||||
Clock.schedule_once(self.next_media, 0)
|
||||
|
||||
self._weblink_watchdog_thread = threading.Thread(
|
||||
target=watchdog, daemon=True, name='weblink-watchdog-win'
|
||||
)
|
||||
self._weblink_watchdog_thread.start()
|
||||
|
||||
signage_main.SignagePlayer._start_inactivity_watchdog = _windows_watchdog
|
||||
|
||||
# ── Patch kill_weblink_after_frame for both CEF and subprocess ──
|
||||
def _windows_kill_weblink_after_frame(self):
|
||||
"""Close the weblink (CEF or subprocess) immediately before next media."""
|
||||
import time
|
||||
from kivy.logger import Logger
|
||||
from kivy.clock import Clock
|
||||
self._stop_inactivity_watchdog()
|
||||
self._kill_weblink_preload()
|
||||
|
||||
# Try CEF first
|
||||
cef_browser = _get_cef_browser()
|
||||
if cef_browser is not None and cef_browser.is_showing():
|
||||
Logger.info("SignagePlayer: Hiding CEF embedded browser")
|
||||
cef_browser.hide()
|
||||
self._weblink_proc = None
|
||||
return
|
||||
|
||||
# Fallback: subprocess Chrome
|
||||
proc = self._weblink_proc
|
||||
self._weblink_proc = None
|
||||
|
||||
if proc is None or proc.poll() is not None:
|
||||
return
|
||||
|
||||
_Win32Overlay.show()
|
||||
Logger.info("SignagePlayer: Killing Chromium subprocess immediately")
|
||||
_windows_kill_process_tree(proc)
|
||||
time.sleep(0.1)
|
||||
_bring_kivy_to_front()
|
||||
_Win32Overlay.hide()
|
||||
signage_main.SignagePlayer._kill_weblink_after_frame = _windows_kill_weblink_after_frame
|
||||
|
||||
# ── Patch play_current_media — same immediate-kill logic ────────
|
||||
_original_play_current = signage_main.SignagePlayer.play_current_media
|
||||
|
||||
def _windows_play_current_media(self, force_reload=False, _after_weblink=False):
|
||||
"""Wrapped play_current_media — closes weblink immediately on transition."""
|
||||
if not _after_weblink:
|
||||
# Kill CEF browser if showing
|
||||
cef_browser = _get_cef_browser()
|
||||
if cef_browser is not None and cef_browser.is_showing():
|
||||
cef_browser.hide()
|
||||
self._weblink_proc = None
|
||||
|
||||
# Kill subprocess Chrome if running
|
||||
proc = self._weblink_proc
|
||||
if proc is not None and proc.poll() is None:
|
||||
_Win32Overlay.show()
|
||||
_windows_kill_process_tree(proc)
|
||||
self._weblink_proc = None
|
||||
self._stop_inactivity_watchdog()
|
||||
self._kill_weblink_preload()
|
||||
_bring_kivy_to_front()
|
||||
_Win32Overlay.hide()
|
||||
|
||||
return _original_play_current(self, force_reload=force_reload, _after_weblink=_after_weblink)
|
||||
|
||||
signage_main.SignagePlayer.play_current_media = _windows_play_current_media
|
||||
|
||||
# Patch _prewarm_weblink for Windows — disabled for now.
|
||||
# The off-screen Chrome window on Windows can interfere with:
|
||||
# - Audio playback (Chrome claims audio device)
|
||||
# - GPU resources (Chrome's GPU process runs in background)
|
||||
# - Taskbar icons showing duplicate Chrome windows
|
||||
# Pre-warming is less critical on desktop where launch is already fast.
|
||||
def _windows_prewarm_weblink(self, url):
|
||||
pass # Disabled on Windows — desktop launch is fast enough
|
||||
signage_main.SignagePlayer._prewarm_weblink = _windows_prewarm_weblink
|
||||
|
||||
# Patch cleanup of temp auth file (was using /tmp/)
|
||||
_original_connection_test = signage_main.SettingsPopup.test_connection
|
||||
|
||||
def _windows_test_connection(self):
|
||||
"""Test connection with Windows-safe temp file path."""
|
||||
import tempfile as _tf
|
||||
import os as _os
|
||||
from player_auth import PlayerAuth
|
||||
import re
|
||||
import threading
|
||||
from kivy.clock import Clock
|
||||
|
||||
self.ids.connection_status.text = 'Testing connection...'
|
||||
self.ids.connection_status.color = (1, 0.7, 0, 1)
|
||||
|
||||
def run_test():
|
||||
try:
|
||||
server_ip = self.ids.server_input.text.strip()
|
||||
screen_name = self.ids.screen_input.text.strip()
|
||||
quickconnect = self.ids.quickconnect_input.text.strip()
|
||||
port = self.ids.port_input.text.strip() or self.player.config.get('port', '')
|
||||
use_https = self.player.config.get('use_https', True)
|
||||
verify_ssl = self.player.config.get('verify_ssl', True)
|
||||
|
||||
if not all([server_ip, screen_name, quickconnect]):
|
||||
Clock.schedule_once(lambda dt: self.update_connection_status('Error: Fill all fields', False))
|
||||
return
|
||||
|
||||
if server_ip.startswith('http://') or server_ip.startswith('https://'):
|
||||
server_url = server_ip
|
||||
if ':' not in server_ip.replace('https://', '').replace('http://', ''):
|
||||
if port and port not in ('443', '80'):
|
||||
server_url = f"{server_ip}:{port}"
|
||||
else:
|
||||
protocol = "https" if use_https else "http"
|
||||
if ':' in server_ip:
|
||||
server_url = f"{protocol}://{server_ip}"
|
||||
else:
|
||||
server_url = f"{protocol}://{server_ip}:{port}" if port else f"{protocol}://{server_ip}"
|
||||
|
||||
# Use Windows temp path
|
||||
temp_file = _os.path.join(_tf.gettempdir(), 'temp_auth_test.json')
|
||||
auth = PlayerAuth(temp_file, use_https=use_https, verify_ssl=verify_ssl)
|
||||
success, error = auth.authenticate(server_url=server_url, hostname=screen_name, quickconnect_code=quickconnect)
|
||||
|
||||
try:
|
||||
if _os.path.exists(temp_file):
|
||||
_os.remove(temp_file)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if success:
|
||||
player_name = auth.get_player_name()
|
||||
Clock.schedule_once(lambda dt: self.update_connection_status(f'✓ Connected: {player_name}', True))
|
||||
else:
|
||||
Clock.schedule_once(lambda dt: self.update_connection_status(f'✗ Failed: {error}', False))
|
||||
|
||||
except Exception as e:
|
||||
Clock.schedule_once(lambda dt: self.update_connection_status(f'✗ Error: {str(e)}', False))
|
||||
|
||||
threading.Thread(target=run_test, daemon=True).start()
|
||||
|
||||
signage_main.SettingsPopup.test_connection = _windows_test_connection
|
||||
|
||||
return signage_main
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# 4. Adjust path so we can import the src modules
|
||||
# =====================================================================
|
||||
# When run from PyInstaller .exe: the runtime hook inserts paths.
|
||||
# When run as plain python, we add src/ relative to this file.
|
||||
_script_dir = Path(__file__).resolve().parent
|
||||
_project_root = _script_dir.parent
|
||||
_src_dir = _project_root / 'src'
|
||||
|
||||
for p in [str(_src_dir), str(_project_root)]:
|
||||
if p not in sys.path:
|
||||
sys.path.insert(0, p)
|
||||
|
||||
# =====================================================================
|
||||
# 5. Determine the local data directory (next to the executable)
|
||||
# =====================================================================
|
||||
# The pyi_runtime_hook.py (when packaged) sets KIWY_DATA_DIR.
|
||||
# When running in dev mode from python, use the project root.
|
||||
# The executable will create its own local folders for playlist,
|
||||
# media, config, and logs where the executable is launched.
|
||||
DATA_DIR = os.environ.get('KIWY_DATA_DIR', str(_project_root))
|
||||
|
||||
# Create local data folders NEXT TO the executable
|
||||
os.makedirs(os.path.join(DATA_DIR, 'config', 'resources'), exist_ok=True)
|
||||
os.makedirs(os.path.join(DATA_DIR, 'media'), exist_ok=True)
|
||||
os.makedirs(os.path.join(DATA_DIR, 'media', 'edited_media'), exist_ok=True)
|
||||
os.makedirs(os.path.join(DATA_DIR, 'playlists'), exist_ok=True)
|
||||
os.makedirs(os.path.join(DATA_DIR, 'logs'), exist_ok=True)
|
||||
os.makedirs(os.path.join(DATA_DIR, 'config', 'certs'), exist_ok=True)
|
||||
|
||||
# =====================================================================
|
||||
# 6. Set Kivy config BEFORE importing Kivy
|
||||
# =====================================================================
|
||||
os.environ['KIVY_NO_FILELOG'] = '1' # Avoid file logging issues on Windows
|
||||
os.environ['KIVY_HOME'] = os.path.join(DATA_DIR, '.kivy')
|
||||
|
||||
from kivy.config import Config
|
||||
Config.set('kivy', 'keyboard_mode', '') # Disable default virtual keyboard
|
||||
Config.set('graphics', 'fullscreen', '0')
|
||||
Config.set('graphics', 'window_state', 'maximized')
|
||||
Config.set('graphics', 'multisampling', '0')
|
||||
Config.set('graphics', 'fast_rgba', '1')
|
||||
Config.set('kivy', 'log_level', 'warning')
|
||||
|
||||
# =====================================================================
|
||||
# 7. Patch the main module, then run the app
|
||||
# =====================================================================
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
# Write a startup marker so we know the .exe at least launched
|
||||
try:
|
||||
os.makedirs(os.path.join(DATA_DIR, 'logs'), exist_ok=True)
|
||||
marker = os.path.join(DATA_DIR, 'logs', 'startup_marker.txt')
|
||||
with open(marker, 'w') as f:
|
||||
f.write(f"run_win.py started at {__import__('time').time()}\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Apply all Windows patches before launching
|
||||
try:
|
||||
patched_main = _patch_main()
|
||||
except Exception as e:
|
||||
# Catch early import errors (pre-Logger) to a file
|
||||
import traceback
|
||||
try:
|
||||
err_log = os.path.join(DATA_DIR, 'logs', 'startup_error.log')
|
||||
with open(err_log, 'w') as f:
|
||||
f.write(f"Error in _patch_main(): {e}\n")
|
||||
traceback.print_exc(file=f)
|
||||
except Exception:
|
||||
pass
|
||||
raise # Re-raise so console shows it too
|
||||
|
||||
from kivy.logger import Logger
|
||||
Logger.info("=" * 80)
|
||||
Logger.info("Kiwy Signage Player - Windows Edition")
|
||||
Logger.info(f"Python: {sys.version}")
|
||||
Logger.info(f"Platform: {platform.platform()}")
|
||||
Logger.info(f"Data directory: {DATA_DIR}")
|
||||
Logger.info("=" * 80)
|
||||
|
||||
# Patch base_dir in SignagePlayer instances to point to local data folder
|
||||
_original_init = patched_main.SignagePlayer.__init__
|
||||
|
||||
def _patched_init(self, **kwargs):
|
||||
"""Override SignagePlayer.__init__ to use local data folders.
|
||||
|
||||
Creates all necessary folders (config, media, playlists, logs)
|
||||
in the same directory where the executable is launched.
|
||||
"""
|
||||
# Call parent Widget.__init__
|
||||
_original_init(self, **kwargs)
|
||||
# Now override ALL paths to point to local data directory
|
||||
# (where the .exe is located)
|
||||
self.base_dir = DATA_DIR
|
||||
self.config_dir = os.path.join(DATA_DIR, 'config')
|
||||
self.media_dir = os.path.join(DATA_DIR, 'media')
|
||||
self.playlists_dir = os.path.join(DATA_DIR, 'playlists')
|
||||
self.config_file = os.path.join(self.config_dir, 'app_config.json')
|
||||
self.resources_path = os.path.join(self.config_dir, 'resources')
|
||||
self.heartbeat_file = os.path.join(DATA_DIR, '.player_heartbeat')
|
||||
# Ensure all required folders exist locally
|
||||
for directory in [
|
||||
self.config_dir,
|
||||
self.resources_path,
|
||||
os.path.join(self.media_dir, 'edited_media'),
|
||||
self.playlists_dir,
|
||||
os.path.join(DATA_DIR, 'logs'),
|
||||
os.path.join(DATA_DIR, 'config', 'certs'),
|
||||
]:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
|
||||
patched_main.SignagePlayer.__init__ = _patched_init
|
||||
|
||||
# Patch SSLManager cert directory to use local data folder
|
||||
# ssl_utils is imported by player_auth.py and get_playlists_v2.py, not main.py
|
||||
import ssl_utils
|
||||
ssl_utils.SSLManager.CERT_DIR = os.path.join(DATA_DIR, 'config', 'certs')
|
||||
ssl_utils.SSLManager.CERT_FILE = os.path.join(
|
||||
ssl_utils.SSLManager.CERT_DIR, 'server_cert.pem'
|
||||
)
|
||||
ssl_utils.SSLManager.CERT_INFO_FILE = os.path.join(
|
||||
ssl_utils.SSLManager.CERT_DIR, 'cert_info.json'
|
||||
)
|
||||
|
||||
# Run the app
|
||||
try:
|
||||
app = patched_main.SignagePlayerApp()
|
||||
app.run()
|
||||
except KeyboardInterrupt:
|
||||
Logger.info("Application stopped by user (Ctrl+C)")
|
||||
except SystemExit as _se:
|
||||
Logger.critical(f"Kivy SystemExit (likely window provider missing): {_se}")
|
||||
try:
|
||||
crash_log = os.path.join(DATA_DIR, 'logs', 'crash.log')
|
||||
with open(crash_log, 'w') as f:
|
||||
f.write(f"Kivy SystemExit: {_se}\n")
|
||||
f.write("This usually means Kivy could not find a window provider on this system.\n")
|
||||
except Exception:
|
||||
pass
|
||||
_show_error_box(
|
||||
"Kiwy Signage Player - Kivy Error",
|
||||
f"Kivy exited: {_se}\n\n"
|
||||
"This usually means Kivy could not create a window.\n"
|
||||
"Check your GPU drivers and DirectX installation.\n\n"
|
||||
"See logs/crash.log for details."
|
||||
)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
Logger.critical(f"Fatal error: {e}")
|
||||
Logger.exception("Full traceback:")
|
||||
# Also write to a crash log next to the executable
|
||||
try:
|
||||
import traceback
|
||||
crash_log = os.path.join(DATA_DIR, 'logs', 'crash.log')
|
||||
with open(crash_log, 'w') as f:
|
||||
f.write(f"Fatal error: {e}\n")
|
||||
traceback.print_exc(file=f)
|
||||
except Exception:
|
||||
pass
|
||||
sys.exit(1)
|
||||
finally:
|
||||
Logger.info("Application shutdown complete")
|
||||
except BaseException as _top_e:
|
||||
# Catch any error BEFORE Logger is available (including SystemExit)
|
||||
import traceback as _tb
|
||||
_trace = _tb.format_exc()
|
||||
try:
|
||||
_crash_log = os.path.join(DATA_DIR, 'logs', 'fatal_crash.log')
|
||||
with open(_crash_log, 'w') as _f:
|
||||
_f.write(f"FATAL (pre-Logger): {_top_e}\n")
|
||||
_f.write(_trace)
|
||||
except Exception:
|
||||
pass
|
||||
_show_error_box(
|
||||
"Kiwy Signage Player - Startup Error",
|
||||
f"{_top_e}\n\nSee logs/fatal_crash.log for details."
|
||||
)
|
||||
raise # Re-raise so .exe still shows the error
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Test that setting env vars before importing main.py fixes the crash."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
# This is the KEY fix: set Windows env vars BEFORE main.py is imported
|
||||
os.environ['SDL_VIDEODRIVER'] = 'windows'
|
||||
os.environ['SDL_AUDIODRIVER'] = 'directsound'
|
||||
os.environ['KIVY_WINDOW'] = 'sdl2'
|
||||
# Use 'angle_sdl2' on Windows for better DirectX compatibility
|
||||
os.environ['KIVY_GL_BACKEND'] = 'angle_sdl2'
|
||||
# Let Kivy auto-detect input providers on Windows
|
||||
os.environ['KIVY_INPUTPROVIDERS'] = ''
|
||||
os.environ['KIVY_VIDEO'] = 'ffpyplayer'
|
||||
os.environ['KIVY_AUDIO'] = 'ffpyplayer'
|
||||
os.environ['FFPYPLAYER_CODECS'] = 'h264,h265,vp9,vp8'
|
||||
os.environ['SDL_VIDEO_ALLOW_SCREENSAVER'] = '0'
|
||||
|
||||
# Add src to path
|
||||
sys.path.insert(0, r'C:\Users\Dell-PC\Desktop\Kiwy-Signage\src')
|
||||
|
||||
print("=" * 60)
|
||||
print("Testing main.py import with Windows env vars...")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
import main
|
||||
print("SUCCESS: main.py imported without crashing!")
|
||||
print(f" SDL_VIDEODRIVER = {os.environ.get('SDL_VIDEODRIVER')}")
|
||||
print(f" KIVY_WINDOW = {os.environ.get('KIVY_WINDOW')}")
|
||||
print(f" KIVY_GL_BACKEND = {os.environ.get('KIVY_GL_BACKEND')}")
|
||||
print(f" KIVY_INPUTPROVIDERS = {os.environ.get('KIVY_INPUTPROVIDERS')}")
|
||||
except SystemExit as e:
|
||||
print(f"FAILED: SystemExit({e}) - Kivy window provider still not loading")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"FAILED with exception: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
+2
-2
@@ -75,7 +75,7 @@ If no card is swiped (timeout), `user_card_data` will be `null`.
|
||||
A test utility is provided to verify card reader functionality:
|
||||
|
||||
```bash
|
||||
cd /home/pi/Desktop/Kiwy-Signage/dev/tests
|
||||
cd /home/pi/Desktop/Kiwy-Signage/working_files
|
||||
python3 test_card_reader.py
|
||||
```
|
||||
|
||||
@@ -178,5 +178,5 @@ Potential improvements for the card reader system:
|
||||
|
||||
- `/src/main.py` - Main implementation (CardReader class, authentication flow)
|
||||
- `/src/edit_drowing.py` - Drawing/editing interface (uses authenticated user)
|
||||
- `/dev/tests/test_card_reader.py` - Card reader test utility
|
||||
- `/working_files/test_card_reader.py` - Card reader test utility
|
||||
- `/requirements.txt` - Dependencies (includes evdev)
|
||||
@@ -54,7 +54,7 @@ The system now logs:
|
||||
Run the enhanced test script to identify your card reader:
|
||||
|
||||
```bash
|
||||
cd /home/pi/Desktop/Kiwy-Signage/dev/tests
|
||||
cd /home/pi/Desktop/Kiwy-Signage/working_files
|
||||
python3 test_card_reader.py
|
||||
```
|
||||
|
||||
@@ -147,7 +147,7 @@ sudo usermod -a -G input $USER
|
||||
- Implemented priority-based search
|
||||
- Enhanced logging
|
||||
|
||||
2. **dev/tests/test_card_reader.py**
|
||||
2. **working_files/test_card_reader.py**
|
||||
- Updated `list_input_devices()` to show device classifications
|
||||
- Updated `test_card_reader()` to use same logic as main app
|
||||
- Added visual indicators for device types
|
||||
@@ -0,0 +1,43 @@
|
||||
INFO ] [Kivy ] Installed at "/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/kivy/__init__.py"
|
||||
[INFO ] [Python ] v3.13.5 (main, Jun 25 2025, 18:55:22) [GCC 14.2.0]
|
||||
[INFO ] [Python ] Interpreter at "/home/pi/Desktop/Kiwy-Signage/.venv/bin/python3"
|
||||
[INFO ] [Logger ] Purge log fired. Processing...
|
||||
[INFO ] [Logger ] Purge finished!
|
||||
[DEBUG ] [Using selector] EpollSelector
|
||||
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
|
||||
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
|
||||
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
|
||||
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
|
||||
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
|
||||
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
|
||||
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
|
||||
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
|
||||
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
|
||||
[ERROR ] [Image ] Error loading </home/pi/Desktop/Kiwy-Signage/config/resources/intro1.mp4>
|
||||
[WARNING] ⚠️ SSL verification disabled - NOT recommended for production!
|
||||
[DEBUG ] [Starting new HTTPS connection (1)] 192.168.0.121:443
|
||||
/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '192.168.0.121'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
|
||||
warnings.warn(
|
||||
[DEBUG ] [https ]//192.168.0.121:443 "POST /api/auth/verify HTTP/1.1" 200 None
|
||||
[INFO ] ✅ Auth code verified
|
||||
[INFO ] ✅ Using existing authentication
|
||||
[INFO ] [Fetching playlist from] https://192.168.0.121:443/api/playlists/1
|
||||
/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '192.168.0.121'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
|
||||
warnings.warn(
|
||||
[DEBUG ] [https ]//192.168.0.121:443 "GET /api/playlists/1 HTTP/1.1" 200 None
|
||||
[INFO ] [✅ Playlist received (version] 34)
|
||||
[INFO ] [📊 Playlist versions - Server] v34, Local: v34
|
||||
[INFO ] ✓ Playlist is up to date
|
||||
[WARNING] Deprecated property "<BooleanProperty name=allow_stretch>" of object "<kivy.uix.image.AsyncImage object at 0x7fa5f79ef0>" has been set, it will be removed in a future version
|
||||
[WARNING] Deprecated property "<BooleanProperty name=keep_ratio>" of object "<kivy.uix.image.AsyncImage object at 0x7fa5f79ef0>" was accessed, it will be removed in a future version
|
||||
/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '192.168.0.121'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
|
||||
warnings.warn(
|
||||
[DEBUG ] [https ]//192.168.0.121:443 "POST /api/auth/verify HTTP/1.1" 200 None
|
||||
[INFO ] ✅ Auth code verified
|
||||
[INFO ] ✅ Using existing authentication
|
||||
/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '192.168.0.121'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
|
||||
warnings.warn(
|
||||
[DEBUG ] [https ]//192.168.0.121:443 "POST /api/player-feedback HTTP/1.1" 200 None
|
||||
^C[2026-01-17 22:09:12] 🛑 Watchdog received stop signal
|
||||
pi@rpi-tvcanba1:~/Desktop/Kiwy-Signage $
|
||||
|
||||
+6
-7
@@ -16,9 +16,8 @@ Kiwy-Signage/
|
||||
│ │ ├── apt-packages.txt # List of required apt packages
|
||||
│ │ └── debs/ # Downloaded .deb files (optional)
|
||||
│ └── README.md
|
||||
├── scripts/
|
||||
│ ├── download_offline_packages.sh # Download Python packages
|
||||
│ └── download_deb_packages.sh # Download system .deb packages
|
||||
├── download_offline_packages.sh # Download Python packages
|
||||
├── download_deb_packages.sh # Download system .deb packages
|
||||
└── install.sh # Smart installer (online/offline)
|
||||
```
|
||||
|
||||
@@ -34,10 +33,10 @@ git clone <repository-url>
|
||||
cd Kiwy-Signage
|
||||
|
||||
# Download Python packages
|
||||
bash scripts/download_offline_packages.sh
|
||||
bash download_offline_packages.sh
|
||||
|
||||
# (Optional) Download system .deb packages
|
||||
bash scripts/download_deb_packages.sh
|
||||
bash download_deb_packages.sh
|
||||
```
|
||||
|
||||
This will populate the `repo/` folder with all necessary packages.
|
||||
@@ -105,7 +104,7 @@ To update the offline package cache:
|
||||
|
||||
```bash
|
||||
# On a connected system
|
||||
bash scripts/download_offline_packages.sh
|
||||
bash download_offline_packages.sh
|
||||
```
|
||||
|
||||
This will download the latest versions of all packages.
|
||||
@@ -118,7 +117,7 @@ If installation fails due to missing dependencies:
|
||||
|
||||
```bash
|
||||
# Download .deb packages with dependencies
|
||||
bash scripts/download_deb_packages.sh
|
||||
bash download_deb_packages.sh
|
||||
|
||||
# Install with dependency resolution
|
||||
sudo apt install -f
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
```bash
|
||||
# 1. Download Python packages (required)
|
||||
bash scripts/download_offline_packages.sh
|
||||
bash download_offline_packages.sh
|
||||
|
||||
# 2. Download system .deb packages (optional, for fully offline)
|
||||
bash scripts/download_deb_packages.sh
|
||||
bash download_deb_packages.sh
|
||||
```
|
||||
|
||||
## For Offline System (Installation)
|
||||
@@ -22,13 +22,12 @@ bash install.sh --offline
|
||||
|
||||
## What's Included
|
||||
|
||||
### Python Packages
|
||||
✅ Kivy 2.3.1
|
||||
✅ Requests 2.34.2
|
||||
### Python Packages (18 wheels)
|
||||
✅ Kivy 2.1.0
|
||||
✅ Requests 2.32.4
|
||||
✅ Bcrypt 4.2.1
|
||||
✅ Aiohttp 3.11.16 (async HTTP)
|
||||
✅ ffpyplayer 4.5.3 (video)
|
||||
✅ evdev 1.9.1 (card reader)
|
||||
✅ Aiohttp 3.9.1 (async HTTP)
|
||||
✅ Asyncio 3.4.3 (async framework)
|
||||
✅ All dependencies
|
||||
|
||||
### System Packages
|
||||
@@ -6,8 +6,7 @@
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
SYSTEM_DIR="$PROJECT_ROOT/repo/system-packages"
|
||||
SYSTEM_DIR="$SCRIPT_DIR/repo/system-packages"
|
||||
DEB_DIR="$SYSTEM_DIR/debs"
|
||||
|
||||
echo "=========================================="
|
||||
@@ -6,8 +6,7 @@
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
REPO_DIR="$PROJECT_ROOT/repo"
|
||||
REPO_DIR="$SCRIPT_DIR/repo"
|
||||
WHEELS_DIR="$REPO_DIR/python-wheels"
|
||||
SYSTEM_DIR="$REPO_DIR/system-packages"
|
||||
|
||||
@@ -27,7 +26,7 @@ fi
|
||||
# Download Python packages
|
||||
echo "Step 1: Downloading Python wheels..."
|
||||
echo "--------------------"
|
||||
cd "$PROJECT_ROOT"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Check if pip is installed
|
||||
if ! command -v pip3 &> /dev/null; then
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"hostname": "tv-terasa",
|
||||
"auth_code": "iiSyZDLWGyqNIxeRt54XYREgvAio11RwwU1_oJev6WI",
|
||||
"player_id": 1,
|
||||
"player_name": "TV-acasa 1",
|
||||
"playlist_id": 1,
|
||||
"orientation": "Landscape",
|
||||
"authenticated": true,
|
||||
"server_url": "http://digi-signage.moto-adv.com"
|
||||
}
|
||||
Reference in New Issue
Block a user