From 39565c576138f68eb72797bb090845d882d4cfc0 Mon Sep 17 00:00:00 2001 From: SKE087 Date: Mon, 20 Jul 2026 16:27:22 +0300 Subject: [PATCH] Improve Raspberry Pi startup and Chromium transitions --- install.sh | 12 +++ scripts/pi_boot_config_patch_once.sh | 76 +++++++++++++++++ src/main.py | 121 +++++++++++++++++---------- 3 files changed, 163 insertions(+), 46 deletions(-) create mode 100755 scripts/pi_boot_config_patch_once.sh diff --git a/install.sh b/install.sh index eae1ccd..8367d01 100755 --- a/install.sh +++ b/install.sh @@ -42,6 +42,7 @@ REQUIREMENTS_FILE="$SCRIPT_DIR/requirements.txt" SUDO_USERNAME="" SUDO_PASSWORD="" OFFLINE_MODE=false +PI_BOOT_PATCH_SCRIPT="$SCRIPT_DIR/scripts/pi_boot_config_patch_once.sh" # ============================================================ # Helper: Run sudo with optional password @@ -624,6 +625,17 @@ echo "" echo "Logs: $SCRIPT_DIR/logs/" echo "" +if [ -f "$PI_BOOT_PATCH_SCRIPT" ]; then + echo "Applying one-time Raspberry Pi boot config patch..." + run_sudo chmod +x "$PI_BOOT_PATCH_SCRIPT" 2>/dev/null || true + if run_sudo bash "$PI_BOOT_PATCH_SCRIPT"; then + echo "One-time boot patch applied (or already present)" + else + echo "Boot patch skipped or failed; continuing installation" + fi + echo "" +fi + if [ -z "$SUDO_PASSWORD" ]; then echo "==========================================" echo "System Reboot Recommended" diff --git a/scripts/pi_boot_config_patch_once.sh b/scripts/pi_boot_config_patch_once.sh new file mode 100755 index 0000000..a914608 --- /dev/null +++ b/scripts/pi_boot_config_patch_once.sh @@ -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}" diff --git a/src/main.py b/src/main.py index 84d7580..0ecbbd5 100644 --- a/src/main.py +++ b/src/main.py @@ -1307,60 +1307,59 @@ class SignagePlayer(Widget): Logger.info(f"SignagePlayer: Playing item {self.current_index + 1}/{len(self.playlist)}: {file_name} ({duration}s)") - # ── Weblink → media transition (Wayland-safe) ────────────────── - # On Wayland (Labwc) Window.raise_window() is a no-op, so we must - # ensure Chromium's fullscreen window is fully gone BEFORE we try - # to render Kivy content. We: - # 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 + # ── Weblink → media transition (desktop-flash safe) ────────────── + # For web→media transitions, render the next Kivy widget first, + # then close Chromium on the next frame. This prevents a brief + # desktop exposure while the compositor removes Chromium. if not _after_weblink: proc = getattr(self, '_weblink_proc', None) if proc is not None: - self._stop_inactivity_watchdog() - self._kill_weblink_preload() - self._weblink_proc = None + next_is_weblink = media_item.get('type') == 'weblink' + if next_is_weblink: + # Weblink -> weblink: close current Chromium first. + self._stop_inactivity_watchdog() + self._kill_weblink_preload() + 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: - try: - proc.terminate() - except Exception as exc: - Logger.warning(f"SignagePlayer: weblink terminate: {exc}") - - def _resume(dt): - # Force-kill if still alive after the delay if proc.poll() is None: try: - proc.kill() + proc.terminate() + except Exception as exc: + Logger.warning(f"SignagePlayer: weblink terminate: {exc}") + + def _resume(dt): + if proc.poll() is None: + try: + proc.kill() + except Exception: + pass + try: + Window.show() + Window.raise_window() except Exception: pass - # Restore content area and show Kivy window - try: - self.ids.content_area.opacity = 1 - except Exception: - pass - try: - Window.show() - Window.raise_window() - except Exception: - pass - # Now render the actual media - self.play_current_media( - force_reload=force_reload, _after_weblink=True - ) + 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 + 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: + self.ids.content_area.opacity = 1 + except Exception: + pass + try: + Window.show() + Window.raise_window() + except Exception: + pass + Logger.debug("SignagePlayer: Deferred Chromium close after next frame render") # ──────────────────────────────────────────────────────────────── # Handle web links before any file/path handling (no local file exists) @@ -1440,6 +1439,11 @@ class SignagePlayer(Widget): # Reset error counter on successful playback self.consecutive_errors = 0 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: Logger.error(f"SignagePlayer: Error playing media: {e}") @@ -1602,9 +1606,12 @@ class SignagePlayer(Widget): self._skip_to_next_media() return False + target_width, target_height = self._get_browser_target_size() + try: 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: Chromium target launch size: {target_width}x{target_height}") # Kill the hidden pre-warm instance first; its work (binary in RAM, # page in disk cache) makes the kiosk relaunch below near-instant. self._kill_weblink_preload() @@ -1616,6 +1623,7 @@ class SignagePlayer(Widget): # --start-fullscreen is a normal maximised window that the # compositor can un-stack without issues. '--start-fullscreen', + '--start-maximized', '--app=' + url, '--noerrdialogs', '--disable-infobars', @@ -1631,6 +1639,8 @@ class SignagePlayer(Widget): '--disable-background-networking', '--no-default-browser-check', '--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 @@ -1653,6 +1663,23 @@ class SignagePlayer(Widget): self._skip_to_next_media() 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): """Start a background thread that monitors /dev/input/* for touch/key activity. The thread resets the idle counter on every event; when the @@ -1893,6 +1920,8 @@ class SignagePlayer(Widget): if not browser: return + target_width, target_height = self._get_browser_target_size() + # Kill any stale preload first. self._kill_weblink_preload() @@ -1903,7 +1932,7 @@ class SignagePlayer(Widget): '--app=' + url, # Place window completely outside the visible display area. '--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', '--disable-infobars', '--incognito',