Compare commits

..

5 Commits

Author SHA1 Message Date
SKE087 f7b889be11 updated files 2026-07-21 12:23:59 +03:00
SKE087 39565c5761 Improve Raspberry Pi startup and Chromium transitions 2026-07-20 16:27:22 +03:00
SKE087 fb9f46a94f Fix ffpyplayer offline wheel and update install scripts
- Rebuilt evdev wheel for cp313 aarch64 (was missing from repo)
- Updated install.sh: add --system-site-packages to venv, verify imports
- Updated start.sh: activate .venv before running player
- Updated run_player.sh: activate .venv before running player
- Added pip fallback for kivy/ffpyplayer if apt/pip install misses them
2026-07-17 13:02:59 +03:00
SKE087 9d8eb0cf21 Fix Python 3.13 compatibility and installation issues
- Replaced all cp311 wheels with cp313 wheels for Python 3.13 compatibility
- Added new dependency wheels: aiohappyeyeballs, evdev, ffpyplayer
- Rebuilt install.sh with virtual environment + offline wheel installation
- Added --sudo-user/-U and --sudo-password/-W CLI arguments
- Added run_sudo() helper to avoid password prompts during installation
- Fixed 'Setting Up Player Directories' hang (cat->stdin) by using touch
- Updated start.sh to activate virtual environment before running player
- Updated run_player.sh to activate virtual environment
- Removed old cp311 incompatible wheels
- Added backup of previous install.sh as install.sh.bak
2026-07-17 10:21:32 +03:00
Kiwy Player 64e49886df Fix install.sh bugs and add auto-reboot
- Fix: Replace broad pkill with PID-specific kill -9 in start.sh
- Fix: Remove unreachable deactivate call in start.sh
- Fix: Add find_boot_config helper for RPi config file detection
- Fix: Update autostart status message (remove fake systemd service claim)
- Fix: Add ACTUAL_USER/ACTUAL_HOME to power management function
- Add: Auto-reboot at end of install.sh with 5s cancel window
- Add: Ctrl+C cleanup trap during installation
2026-07-15 21:17:14 +03:00
41 changed files with 1855 additions and 658 deletions
-1
View File
@@ -1 +0,0 @@
1782752485.750587
+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"
+2 -5
View File
@@ -1,6 +1,3 @@
#!/bin/bash #\!/bin/bash
# Wait for desktop environment to be ready
sleep 15 sleep 15
cd /home/pi/kiwy-signage && bash start.sh
# Start the player
cd "/home/pi/Desktop/Kiwy-Signage" && bash start.sh
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"server_ip": "192.168.0.159", "server_ip": "192.168.0.109",
"port": "8080", "port": "8080",
"screen_name": "rpi-Receptie", "screen_name": "Birou_IT",
"quickconnect_key": "8887779", "quickconnect_key": "8887779",
"orientation": "Landscape", "orientation": "Landscape",
"touch": "True", "touch": "True",
Regular → Executable
+591 -563
View File
File diff suppressed because it is too large Load Diff
Executable
+1056
View File
File diff suppressed because it is too large Load Diff
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

+22 -8
View File
@@ -1,23 +1,37 @@
{ {
"count": 2, "count": 4,
"player_id": 1, "player_id": 1,
"player_name": "Receptie TV", "player_name": "Test_player1",
"playlist": [ "playlist": [
{ {
"file_name": "sean-oulashin-KMn4VEeEPR8-unsplash.jpg", "file_name": "sean-oulashin-KMn4VEeEPR8-unsplash.jpg",
"type": "image", "type": "image",
"url": "media/sean-oulashin-KMn4VEeEPR8-unsplash.jpg", "url": "media/sean-oulashin-KMn4VEeEPR8-unsplash.jpg",
"duration": 25, "duration": 14,
"edit_on_player": true
},
{
"file_name": "weblink-1e4a4d6d885a",
"type": "weblink",
"url": "https://moto-adv.com/",
"duration": 30,
"edit_on_player": false "edit_on_player": false
}, },
{ {
"file_name": "robert-lukeman-zNN6ubHmruI-unsplash.jpg", "file_name": "sample-30s.mp4",
"type": "image", "type": "video",
"url": "media/robert-lukeman-zNN6ubHmruI-unsplash.jpg", "url": "media/sample-30s.mp4",
"duration": 20, "duration": 31,
"edit_on_player": false "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_id": 1,
"playlist_version": 15 "playlist_version": 14
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -4
View File
@@ -1,7 +1,6 @@
kivy>=2.3.0 kivy>=2.3.0
ffpyplayer ffpyplayer
requests==2.32.4 requests>=2.32.0,<3.0.0
bcrypt==4.2.1 bcrypt>=4.2.0,<5.0.0
aiohttp==3.9.1 aiohttp>=3.9.0,<4.0.0
asyncio==3.4.3
evdev>=1.6.0 evdev>=1.6.0
-1
View File
@@ -1,6 +1,5 @@
#!/bin/bash #!/bin/bash
# Start Kivy Signage Player # Start Kivy Signage Player
cd "$(dirname "$0")" && source .venv/bin/activate
cd "$(dirname "$0")/src" cd "$(dirname "$0")/src"
python3 main.py python3 main.py
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
set -euo pipefail
MARKER_FILE="/etc/kiwy-signage/pi_boot_config_patched.marker"
is_pi() {
if [[ -f /proc/device-tree/model ]] && grep -qi "raspberry pi" /proc/device-tree/model; then
return 0
fi
if grep -qi "raspberry" /proc/cpuinfo 2>/dev/null; then
return 0
fi
return 1
}
find_boot_config() {
if [[ -f /boot/firmware/config.txt ]]; then
echo "/boot/firmware/config.txt"
elif [[ -f /boot/config.txt ]]; then
echo "/boot/config.txt"
else
echo ""
fi
}
upsert_key() {
local key="$1"
local value="$2"
local file="$3"
if grep -Eq "^[#[:space:]]*${key}=" "$file"; then
sed -i -E "s|^[#[:space:]]*${key}=.*|${key}=${value}|" "$file"
else
echo "${key}=${value}" >> "$file"
fi
}
if [[ "${EUID}" -ne 0 ]]; then
echo "[pi-boot-patch] Please run as root."
exit 1
fi
if ! is_pi; then
echo "[pi-boot-patch] Not a Raspberry Pi. Skipping."
exit 0
fi
if [[ -f "$MARKER_FILE" ]]; then
echo "[pi-boot-patch] Patch already applied. Skipping."
exit 0
fi
BOOT_CONFIG="$(find_boot_config)"
if [[ -z "$BOOT_CONFIG" ]]; then
echo "[pi-boot-patch] Boot config not found. Skipping."
exit 0
fi
echo "[pi-boot-patch] Applying patch to ${BOOT_CONFIG}"
# Graphics and memory settings for smoother Chromium playback on Pi.
upsert_key "dtoverlay" "vc4-kms-v3d" "$BOOT_CONFIG"
upsert_key "gpu_mem" "256" "$BOOT_CONFIG"
upsert_key "disable_overscan" "1" "$BOOT_CONFIG"
# Keep HDMI and display active for kiosk use.
upsert_key "hdmi_blanking" "0" "$BOOT_CONFIG"
upsert_key "hdmi_ignore_cec_init" "1" "$BOOT_CONFIG"
upsert_key "hdmi_force_hotplug" "1" "$BOOT_CONFIG"
mkdir -p "$(dirname "$MARKER_FILE")"
date -u +"%Y-%m-%dT%H:%M:%SZ" > "$MARKER_FILE"
echo "[pi-boot-patch] Patch complete. Marker written to ${MARKER_FILE}"
+56 -27
View File
@@ -1307,29 +1307,20 @@ class SignagePlayer(Widget):
Logger.info(f"SignagePlayer: Playing item {self.current_index + 1}/{len(self.playlist)}: {file_name} ({duration}s)") Logger.info(f"SignagePlayer: Playing item {self.current_index + 1}/{len(self.playlist)}: {file_name} ({duration}s)")
# ── Weblink → media transition (Wayland-safe) ────────────────── # ── Weblink → media transition (desktop-flash safe) ──────────────
# On Wayland (Labwc) Window.raise_window() is a no-op, so we must # For web→media transitions, render the next Kivy widget first,
# ensure Chromium's fullscreen window is fully gone BEFORE we try # then close Chromium on the next frame. This prevents a brief
# to render Kivy content. We: # desktop exposure while the compositor removes Chromium.
# 1. Stop watchdog + preload immediately
# 2. Terminate Chromium (non-blocking)
# 3. Hide the Kivy content_area so nothing stale is visible
# 4. Wait 200 ms for the compositor to remove Chromium's window
# 5. Show content_area + re-call play_current_media to render
if not _after_weblink: if not _after_weblink:
proc = getattr(self, '_weblink_proc', None) proc = getattr(self, '_weblink_proc', None)
if proc is not None: if proc is not None:
next_is_weblink = media_item.get('type') == 'weblink'
if next_is_weblink:
# Weblink -> weblink: close current Chromium first.
self._stop_inactivity_watchdog() self._stop_inactivity_watchdog()
self._kill_weblink_preload() self._kill_weblink_preload()
self._weblink_proc = None self._weblink_proc = None
# Hide content while Chromium is still closing
try:
self.ids.content_area.opacity = 0
except Exception:
pass
# Terminate Chromium
if proc.poll() is None: if proc.poll() is None:
try: try:
proc.terminate() proc.terminate()
@@ -1337,13 +1328,28 @@ class SignagePlayer(Widget):
Logger.warning(f"SignagePlayer: weblink terminate: {exc}") Logger.warning(f"SignagePlayer: weblink terminate: {exc}")
def _resume(dt): def _resume(dt):
# Force-kill if still alive after the delay
if proc.poll() is None: if proc.poll() is None:
try: try:
proc.kill() proc.kill()
except Exception: except Exception:
pass pass
# Restore content area and show Kivy window try:
Window.show()
Window.raise_window()
except Exception:
pass
self.play_current_media(
force_reload=force_reload, _after_weblink=True
)
Clock.schedule_once(_resume, 0.2)
return
# Weblink -> non-weblink: keep Chromium visible while we
# prepare next Kivy frame, then close Chromium deferred.
self._stop_inactivity_watchdog()
self._kill_weblink_preload()
self._weblink_proc = proc
try: try:
self.ids.content_area.opacity = 1 self.ids.content_area.opacity = 1
except Exception: except Exception:
@@ -1353,14 +1359,7 @@ class SignagePlayer(Widget):
Window.raise_window() Window.raise_window()
except Exception: except Exception:
pass pass
# Now render the actual media Logger.debug("SignagePlayer: Deferred Chromium close after next frame render")
self.play_current_media(
force_reload=force_reload, _after_weblink=True
)
# 200 ms gives Labwc time to remove the fullscreen surface
Clock.schedule_once(_resume, 0.2)
return
# ──────────────────────────────────────────────────────────────── # ────────────────────────────────────────────────────────────────
# Handle web links before any file/path handling (no local file exists) # Handle web links before any file/path handling (no local file exists)
@@ -1441,6 +1440,11 @@ class SignagePlayer(Widget):
self.consecutive_errors = 0 self.consecutive_errors = 0
Logger.debug(f"SignagePlayer: Media started successfully") Logger.debug(f"SignagePlayer: Media started successfully")
# If we arrived here from a weblink item, close Chromium after the
# next Kivy frame so the new widget is already visible underneath.
if media_item.get('type') != 'weblink' and getattr(self, '_weblink_proc', None) is not None:
self._kill_weblink_after_frame()
except Exception as e: except Exception as e:
Logger.error(f"SignagePlayer: Error playing media: {e}") Logger.error(f"SignagePlayer: Error playing media: {e}")
self.consecutive_errors += 1 self.consecutive_errors += 1
@@ -1602,9 +1606,12 @@ class SignagePlayer(Widget):
self._skip_to_next_media() self._skip_to_next_media()
return False return False
target_width, target_height = self._get_browser_target_size()
try: try:
Logger.info(f"SignagePlayer: Opening weblink in kiosk browser: {url}") Logger.info(f"SignagePlayer: Opening weblink in kiosk browser: {url}")
Logger.info(f"SignagePlayer: Inactivity timeout set to {duration}s (touch resets the countdown)") Logger.info(f"SignagePlayer: Inactivity timeout set to {duration}s (touch resets the countdown)")
Logger.info(f"SignagePlayer: Chromium target launch size: {target_width}x{target_height}")
# Kill the hidden pre-warm instance first; its work (binary in RAM, # Kill the hidden pre-warm instance first; its work (binary in RAM,
# page in disk cache) makes the kiosk relaunch below near-instant. # page in disk cache) makes the kiosk relaunch below near-instant.
self._kill_weblink_preload() self._kill_weblink_preload()
@@ -1616,6 +1623,7 @@ class SignagePlayer(Widget):
# --start-fullscreen is a normal maximised window that the # --start-fullscreen is a normal maximised window that the
# compositor can un-stack without issues. # compositor can un-stack without issues.
'--start-fullscreen', '--start-fullscreen',
'--start-maximized',
'--app=' + url, '--app=' + url,
'--noerrdialogs', '--noerrdialogs',
'--disable-infobars', '--disable-infobars',
@@ -1631,6 +1639,8 @@ class SignagePlayer(Widget):
'--disable-background-networking', '--disable-background-networking',
'--no-default-browser-check', '--no-default-browser-check',
'--window-position=0,0', '--window-position=0,0',
f'--window-size={target_width},{target_height}',
'--force-device-scale-factor=1',
]) ])
# Unschedule any previous fixed timer — the watchdog thread takes # Unschedule any previous fixed timer — the watchdog thread takes
@@ -1653,6 +1663,23 @@ class SignagePlayer(Widget):
self._skip_to_next_media() self._skip_to_next_media()
return False return False
def _get_browser_target_size(self):
"""Return a stable browser launch size to avoid 800x600 first-paint flash."""
default_width, default_height = 1920, 1080
try:
width, height = Window.size
width = int(width)
height = int(height)
except Exception:
return default_width, default_height
# Chromium sometimes first-paints at 800x600 before fullscreen;
# force at least Full HD when reported dimensions are too small.
if width < 1280 or height < 720:
return default_width, default_height
return width, height
def _start_inactivity_watchdog(self, duration): def _start_inactivity_watchdog(self, duration):
"""Start a background thread that monitors /dev/input/* for touch/key """Start a background thread that monitors /dev/input/* for touch/key
activity. The thread resets the idle counter on every event; when the activity. The thread resets the idle counter on every event; when the
@@ -1893,6 +1920,8 @@ class SignagePlayer(Widget):
if not browser: if not browser:
return return
target_width, target_height = self._get_browser_target_size()
# Kill any stale preload first. # Kill any stale preload first.
self._kill_weblink_preload() self._kill_weblink_preload()
@@ -1903,7 +1932,7 @@ class SignagePlayer(Widget):
'--app=' + url, '--app=' + url,
# Place window completely outside the visible display area. # Place window completely outside the visible display area.
'--window-position=-9999,-9999', '--window-position=-9999,-9999',
'--window-size=1920,1080', # pre-render at full resolution f'--window-size={target_width},{target_height}', # pre-render at target resolution
'--noerrdialogs', '--noerrdialogs',
'--disable-infobars', '--disable-infobars',
'--incognito', '--incognito',
+4 -4
View File
@@ -1,10 +1,10 @@
{ {
"hostname": "rpi-Receptie", "hostname": "Birou_IT",
"auth_code": "BrJrGzX_IT9oP_Lfke8Qgo4sDjMc49T1EhSnWmKKpDM", "auth_code": "CZncd_2dlTZGieBEdqAbUTjf3qNyEUPDXr8jLVx7NLs",
"player_id": 1, "player_id": 1,
"player_name": "Receptie TV", "player_name": "Test_player1",
"playlist_id": 1, "playlist_id": 1,
"orientation": "Landscape", "orientation": "Landscape",
"authenticated": true, "authenticated": true,
"server_url": "http://192.168.0.159:8080" "server_url": "http://192.168.0.109:8080"
} }
+7 -18
View File
@@ -218,19 +218,6 @@ rm -f "$STOP_FLAG_FILE"
# Change to the project directory # Change to the project directory
cd "$SCRIPT_DIR" cd "$SCRIPT_DIR"
# Check if virtual environment exists
if [ -d ".venv" ]; then
log_message "✓ Virtual environment found"
source .venv/bin/activate
else
log_message "⚠️ Creating virtual environment..."
python3 -m venv .venv
source .venv/bin/activate
log_message "📦 Installing dependencies..."
pip3 install -r requirements.txt
log_message "✓ Virtual environment ready"
fi
# Check if configuration exists # Check if configuration exists
if [ ! -f "config/app_config.json" ]; then if [ ! -f "config/app_config.json" ]; then
log_message "⚠️ WARNING: Configuration file not found!" log_message "⚠️ WARNING: Configuration file not found!"
@@ -250,6 +237,11 @@ while true; do
# Clean old heartbeat # Clean old heartbeat
rm -f "$HEARTBEAT_FILE" rm -f "$HEARTBEAT_FILE"
# Activate virtual environment if it exists
if [ -f "$SCRIPT_DIR/.venv/bin/activate" ]; then
source "$SCRIPT_DIR/.venv/bin/activate"
fi
# Start the player # Start the player
cd "$SCRIPT_DIR/src" cd "$SCRIPT_DIR/src"
python3 main.py & python3 main.py &
@@ -292,8 +284,8 @@ while true; do
log_message "⏳ Waiting ${RESTART_DELAY}s before restart..." log_message "⏳ Waiting ${RESTART_DELAY}s before restart..."
sleep $RESTART_DELAY sleep $RESTART_DELAY
# Cleanup any zombie processes # Ensure old player process is gone before restarting
pkill -9 -f "python3 main.py" 2>/dev/null kill -9 $PLAYER_PID 2>/dev/null
done done
@@ -301,6 +293,3 @@ log_message ""
log_message "==========================================" log_message "=========================================="
log_message "Watchdog stopped" log_message "Watchdog stopped"
log_message "==========================================" log_message "=========================================="
# Deactivate virtual environment (this line is never reached in watchdog mode)
deactivate