11 Commits

Author SHA1 Message Date
ske087 d0ea94447a Exclude cefpython3 other-Python-version .pyd and GStreamer from build
- Added kivy.lib.gstplayer to excluded_imports (we use ffpyplayer)
- Added all cefpython3_py{27,34-311}.pyd to excluded_imports
- Removes ~300 lines of library-not-found warnings from build output
- Reduces .exe size from 143 MB to 96 MB
2026-07-24 16:45:31 +03:00
ske087 12f2880201 Rewrite CEF browser as child of Kivy window (v2)
v1 created a separate Win32 window — same problem as external Chrome.
v2 creates CEF as a CHILD WINDOW of Kivy's SDL_app window:
  - No separate taskbar entry
  - No z-order fighting (CEF is INSIDE Kivy)
  - No desktop flash
  - CEF message loop pumped via Kivy Clock (main thread)
  - Resize handler attached so CEF follows Kivy window changes
  - build.spec includes cef_browser in hidden imports
2026-07-24 16:22:33 +03:00
ske087 5a030671a2 Fix 3 bugs: delete_unused_media path mismatch, media not downloading on same-version, video EOS not advancing
- delete_unused_media: normalize Windows backslashes to forward slashes
  when comparing with playlist file_name (fixes file deleted after download)
- update_playlist_if_needed: download media even when playlist version matches
- _on_video_eos: now schedules next_media() — was an empty stub
2026-07-24 16:10:23 +03:00
ske087 a2add88f04 Fix video not advancing + media not downloading on same-version startup
BUG-009: _on_video_eos was empty (stub). Added Clock.schedule_once
for next_media() when video reaches end of stream.

BUG-008: download_media_files only ran when server_version >
local_version. Added download check in up-to-date branch so media
files are synced even when playlist version hasn't changed.
2026-07-24 15:43:56 +03:00
ske087 c4e8381898 Remove venv_build from git tracking (should not commit venv) 2026-07-24 15:10:20 +03:00
ske087 ced6e10919 Rebuild .exe with CEF + win32gui fixes
- Updated build.spec with win32gui/win32con hidden imports
- Updated requirements_win.txt with cefpython3 docs
- Rebuilt .exe at 2026-07-24 14:41 (143 MB with CEF)
- All 12 resources bundled (icons, intro1.mp4)
- development-track.md updated with BUG-007, BUG-008
2026-07-24 15:09:48 +03:00
ske087 844e5eeebb Add CEF embedded browser + win32gui for weblink handling
Windows-specific fixes:
- New cef_browser.py: embedded Chromium via cefpython3, no subprocess
- _bring_kivy_to_front(): uses win32gui.SetForegroundWindow (reliable)
- _windows_kill_weblink_after_frame: kills Chrome IMMEDIATELY
- prewarm_weblink disabled on Windows (desktop launch is fast)
- _windows_play_weblink tries CEF first, falls back to subprocess
- Updated requirements_win.txt (cefpython3, pywin32 confirmed)
- Added development-track.md for change tracking
2026-07-24 14:39:12 +03:00
ske087 7efc023327 Add development-track.md — session log, bug tracker, rejected solutions, build info 2026-07-24 13:52:28 +03:00
ske087 6abde5a767 Fix Windows weblink handling: fullscreen Chrome, black overlay masking, proper process tree kill
Windows-specific fixes:
- _windows_play_weblink: uses --start-maximized + --app=URL for true fullscreen
- Shows black Win32 overlay before opening/closing Chrome to mask desktop
- _windows_kill_process_tree: uses taskkill /F /T to kill all Chrome child processes
- _Win32Overlay class: fullscreen borderless always-on-top black window
- Updated README to note Python 3.12 requirement and local data dir behavior
2026-07-24 13:48:49 +03:00
ske087 362f5096a0 Fix infinite recursion when restarting player with empty playlist
- Added empty playlist guard in play_current_media() to return
  early instead of calling restart_playlist()
- Added empty playlist guard in restart_playlist() to return
  early instead of calling play_current_media()
- Wrapped SettingsPopup content in ScrollView so fields
  are not cut off on smaller screens
2026-07-24 09:53:29 +03:00
ske087 3845830a86 Add Windows Player support and related files
- New windows/ directory with build scripts, specs, and configuration
- Windows-specific requirements (requirements_win.txt)
- Launch and runtime scripts for Windows (run_win.py, launch_player.bat)
- PyInstaller build configuration (build.spec)
- Updated .gitignore to exclude windows/venv312/
- Updated config and source files for Windows compatibility
- Moved working_files to proper directory
2026-07-24 08:34:00 +03:00
77 changed files with 3870 additions and 968 deletions
+79
View File
@@ -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
View File
@@ -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/
+54
View File
@@ -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
+11
View File
@@ -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"
+3
View File
@@ -0,0 +1,3 @@
#\!/bin/bash
sleep 15
cd /home/pi/kiwy-signage && bash start.sh
+54
View File
@@ -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
+12
View File
@@ -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
}
+53 -175
View File
@@ -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 \
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
run_sudo apt-get install -f -y || true
cd "$SCRIPT_DIR"
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 \
curl wget"
gstreamer1.0-plugins-base gstreamer1.0-plugins-good
# 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..."
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
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
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
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)..."
echo ""
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
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

+37
View File
@@ -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
}
+1
View File
@@ -0,0 +1 @@
Python 3.12.9
+6 -10
View File
@@ -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
View File
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-347
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+10
View File
@@ -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"
}
-4
View File
@@ -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,
+62 -49
View File
@@ -352,20 +352,27 @@
# 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)
ScrollView:
BoxLayout:
orientation: 'vertical'
spacing: dp(8)
size_hint_y: None
height: self.minimum_height
# Server configuration
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: dp(40)
spacing: dp(10)
height: dp(36)
spacing: dp(8)
Label:
text: 'Server IP:'
@@ -378,7 +385,7 @@
id: server_input
size_hint_x: 0.7
multiline: False
font_size: sp(14)
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
@@ -386,8 +393,8 @@
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: dp(40)
spacing: dp(10)
height: dp(36)
spacing: dp(8)
Label:
text: 'Port:'
@@ -400,7 +407,7 @@
id: port_input
size_hint_x: 0.7
multiline: False
font_size: sp(14)
font_size: sp(13)
hint_text: '80 or 8080 (leave empty for default)'
input_filter: 'int'
write_tab: False
@@ -410,8 +417,8 @@
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: dp(40)
spacing: dp(10)
height: dp(36)
spacing: dp(8)
Label:
text: 'Screen Name:'
@@ -424,7 +431,7 @@
id: screen_input
size_hint_x: 0.7
multiline: False
font_size: sp(14)
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
@@ -432,8 +439,8 @@
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: dp(40)
spacing: dp(10)
height: dp(36)
spacing: dp(8)
Label:
text: 'Quickconnect:'
@@ -446,7 +453,7 @@
id: quickconnect_input
size_hint_x: 0.7
multiline: False
font_size: sp(14)
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
@@ -454,8 +461,8 @@
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: dp(40)
spacing: dp(10)
height: dp(36)
spacing: dp(8)
Label:
text: 'Orientation:'
@@ -468,7 +475,7 @@
id: orientation_input
size_hint_x: 0.7
multiline: False
font_size: sp(14)
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
@@ -476,8 +483,8 @@
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: dp(40)
spacing: dp(10)
height: dp(36)
spacing: dp(8)
Label:
text: 'Touch:'
@@ -490,7 +497,7 @@
id: touch_input
size_hint_x: 0.7
multiline: False
font_size: sp(14)
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
@@ -498,8 +505,8 @@
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: dp(40)
spacing: dp(10)
height: dp(36)
spacing: dp(8)
Label:
text: 'Max Resolution:'
@@ -512,7 +519,7 @@
id: resolution_input
size_hint_x: 0.7
multiline: False
font_size: sp(14)
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
@@ -521,11 +528,11 @@
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: dp(40)
spacing: dp(10)
height: dp(36)
spacing: dp(8)
Label:
text: 'Enable Edit Feature:'
text: 'Enable Edit:'
size_hint_x: 0.3
text_size: self.size
halign: 'left'
@@ -534,56 +541,61 @@
CheckBox:
id: edit_enabled_checkbox
size_hint_x: None
width: dp(40)
width: dp(36)
active: True
on_active: root.on_edit_feature_toggle(self.active)
Label:
text: '(Allow editing images on this player)'
text: '(Allow editing images)'
size_hint_x: 0.4
font_size: sp(12)
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: 0.05
size_hint_y: None
height: dp(5)
# Reset Buttons Section
Label:
text: 'Reset Options:'
size_hint_y: None
height: dp(30)
height: dp(26)
text_size: self.size
halign: 'left'
valign: 'middle'
bold: True
font_size: sp(16)
font_size: sp(14)
# Reset Buttons Row
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: dp(50)
spacing: dp(10)
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
@@ -591,8 +603,9 @@
id: test_connection_btn
text: 'Test Server Connection'
size_hint_y: None
height: dp(50)
height: dp(44)
background_color: 0.2, 0.4, 0.8, 1
font_size: sp(13)
on_press: root.test_connection()
# Connection Status Label
@@ -600,21 +613,24 @@
id: connection_status
text: 'Click button to test connection'
size_hint_y: None
height: dp(40)
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: 0.05
size_hint_y: None
height: dp(5)
# Status information row
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: dp(30)
spacing: dp(10)
height: dp(26)
spacing: dp(8)
Label:
id: playlist_info
@@ -622,7 +638,7 @@
text_size: self.size
halign: 'center'
valign: 'middle'
font_size: sp(12)
font_size: sp(11)
Label:
id: media_count_info
@@ -630,7 +646,7 @@
text_size: self.size
halign: 'center'
valign: 'middle'
font_size: sp(12)
font_size: sp(11)
Label:
id: status_info
@@ -638,17 +654,14 @@
text_size: self.size
halign: 'center'
valign: 'middle'
font_size: sp(12)
font_size: sp(11)
Widget:
size_hint_y: 0.05
# Action buttons
# Action buttons (always visible, outside scroll)
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: dp(50)
spacing: dp(20)
height: dp(44)
spacing: dp(15)
Button:
text: 'Save & Close'
+2 -28
View File
@@ -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() {
-1
View File
@@ -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"
+34
View File
@@ -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)
+160
View File
@@ -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)
```
+18
View File
@@ -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")
+256
View File
@@ -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',
)
+126
View File
@@ -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
+192
View File
@@ -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)
+186
View File
@@ -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`
+27
View File
@@ -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
+131
View File
@@ -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
+39
View File
@@ -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
+843
View File
@@ -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
+39
View File
@@ -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)
@@ -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
+43
View File
@@ -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 $
@@ -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
+10
View File
@@ -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"
}