Files
Kiwy-Signage/.github/instructions/kiwy-linux-rpi.instructions.md
T
ske087 3ac7f836c4 Port the player to Raspberry Pi OS Trixie 64-bit (Linux-only branch)
Replaces the Windows port with a Raspberry Pi / Linux implementation on
Raspberry Pi OS "Trixie" (Debian 13, aarch64, Wayland/labwc). The Windows
code is removed here but preserved on the Windows-Player branch.

Entry point
-----------
linux/run_linux.py replaces windows/run_win.py. src/main.py stays
platform-neutral; all Pi-specific behaviour is injected from linux/.

Five bugs that prevented the port (all measured on real hardware)
----------------------------------------------------------------
1. Kivy's PyPI wheel bundles an SDL2 built WITHOUT the wayland driver, so
   no window could be created (Trixie has no X server). linux/fix_kivy_sdl2.sh
   symlinks the system SDL2 over the bundled filename.
2. SDL2 requires WAYLAND_DISPLAY to be *set* - the socket alone is not enough,
   unlike wlopm. This broke every systemd/cron/autostart launch.
   linux_display.ensure_session_environment() detects and exports it.
3. Kivy's Clock resolves callbacks via func.__name__; a patch assigned under a
   different name crashed the player ~20s after a successful start.
4. The inherited signal_screen_activity() shelled out to tvservice, xdotool and
   ydotool - none exist on Trixie - and mis-escaped 'wlopm --on \*', so the
   display blanked after 10 minutes.
5. The launchers ran src/main.py directly, bypassing every platform patch and
   resolving the data directory one level too high.

Web links
---------
- --ozone-platform-hint=auto does NOT fall back to Wayland on Chromium 152; it
  aborts. The platform is now chosen explicitly.
- The keyring password prompt is suppressed via the ENVIRONMENT, not the flags:
  launch_env() strips DBUS_SESSION_BUS_ADDRESS for the child so Chromium cannot
  reach gnome-keyring-daemon.
- Teardown kills the whole process group (needs start_new_session=True);
  previously it silently fell back to terminate() and orphaned children.

Video normalisation
-------------------
A 4K video cannot play on a Pi 4: ffpyplayer decodes in software, measured at
0.90x realtime (1080p is 3.03x). Oversized media is downscaled to 1920x1080 at
sync time using the hardware h264_v4l2m2m encoder (~31s for an 18s clip),
triggered by resolution only so already-playable files are untouched.

src/media_state.py owns the shared on-disk contract: a .kiwy-converting marker
makes the player skip the item while it is being rebuilt, then the converted
file is played instead. If nothing is playable at all (a single-item playlist
whose only video is converting), the player loops the intro video rather than
leaving a blank screen.

Also fixed
----------
- network_monitor: replaced netsh/ifconfig/dhclient with nmcli (Trixie uses
  NetworkManager; ifconfig and dhclient are not even installed).
- Removed the Windows-only focus keeper/guardian from main.py.
- main.py: duplicate SDL_AUDIODRIVER setdefault (a silent no-op); Settings
  "Test connection" now uses tempfile.gettempdir().
- config/app_config.json: credentials blanked so a fresh clone runs the
  first-run setup flow.

Verification
------------
linux/test_media_state.py 18/18, test_linux_patches.py 21/21,
test_linux_browser_flags.py 27/27. Verified live against a real DigiServer:
image -> weblink -> image -> video with correct durations, zero leaked Chromium
processes, and no throttling over a 10 minute monitored run.
2026-09-13 21:57:49 +03:00

11 KiB
Raw Blame History

description, name
description name
Use when installing, deploying, running or debugging the Kiwy Signage Player on Raspberry Pi OS Trixie 64-bit: venv setup, Kivy/SDL2 Wayland requirements, the linux/ entry point, autostart, systemd, watchdog supervision, or adding new modules under src/. Covers the exact commands, environment constraints and verification steps. Kiwy Linux / Raspberry Pi Build & Development

Kiwy Signage Player — Linux / Raspberry Pi Build & Development

Kivy digital signage player targeting Raspberry Pi OS "Trixie" (Debian 13, aarch64, Wayland/labwc).

  • Entry pointlinux/run_linux.py. Not src/main.py.
  • Target hardware — Raspberry Pi 4 / 5. Verified on a Pi 4 Model B Rev 1.4, kernel 6.18.39+rpt-rpi-v8.

Ground Rules

  • Never launch src/main.py directly. It is a shared module, not the Pi entry point. Running it skips every platform patch (session environment, display keep-awake, Chromium adapter) and resolves the data directory one level too high. Always go through linux/run_linux.py (bash run_player.sh / bash start.sh).
  • Keep src/main.py platform-neutral. Pi-specific behaviour belongs in linux/. src/main.py may only contain guarded capability checks, never a platform-specific import.
  • A patched method must be resolvable by its own __name__. Kivy's Clock wraps callbacks in a WeakMethod keyed on func.__name__ and re-resolves it with getattr(instance, name). Assigning a replacement under a different name than it was defined with raises AttributeError the first time the Clock fires — minutes after a successful start, with a traceback that points nowhere near the patch. Use _bind_name(), and cover new patches in linux/test_linux_patches.py. This has already caused two outages.
  • No sudo at runtime. The player runs as an unprivileged user. Anything needing root must be set up at install time (sudoers entry, udev rule, systemd unit).

Environment

Item Value
OS Raspberry Pi OS Trixie / Debian 13, aarch64
Session Wayland via rpd-labwc (labwc). No X server.
Python 3.13.5 — project venv at .venv (created --system-site-packages)
Kivy 2.3.1 (PyPI wheel)
ffpyplayer 4.5.3 (video/audio backend)
evdev 2.0.0 — sdist only, built locally (needs python3-dev, build-essential) or apt python3-dev
Browser /usr/bin/chromium (there is no chromium-browser on Trixie)

The SDL2 trap (the #1 way to break the install)

Kivy's PyPI wheel bundles a private SDL2 without the Wayland driver:

Kivy.libs/libSDL2-2-*.so     ->  x11, KMSDRM, offscreen, dummy, evdev     # no wayland!
/usr/lib/.../libSDL2-2.0.so.0 ->  x11, wayland, KMSDRM, offscreen, dummy, evdev

On Trixie there is no X server, so the bundled build cannot create a window:

[CRITICAL] Unable to find any valuable Window provider.
sdl2 - RuntimeError: b'wayland,x11,dummy not available'

linux/fix_kivy_sdl2.sh symlinks the system SDL2 over the bundled filename. Re-run it after every pip install --upgrade kivy. linux/test_linux_patches.py asserts the driver is present, so a regression fails the test suite rather than the player.

SDL2 requires WAYLAND_DISPLAY to be set

The socket existing is not enough — unlike wlopm, SDL2 does not scan XDG_RUNTIME_DIR. A launch from systemd, cron, an autostart entry or SSH has the variable empty, and Kivy then fails with wayland not available. linux_display.ensure_session_environment() detects the socket and exports the name; run_linux.py calls it before importing Kivy.

Install

Required system packages:

sudo apt install -y \
    python3-venv python3-dev build-essential \
    libsdl2-2.0-0 libsdl2-image-2.0-0 libsdl2-mixer-2.0-0 libsdl2-ttf-2.0-0 \
    libgl1-mesa-dri libgles2 \
    chromium wlopm wlr-randr ffmpeg

Python dependencies (all have cp313 aarch64 wheels except evdev):

python3 -m venv --system-site-packages .venv
.venv/bin/pip install kivy ffpyplayer requests bcrypt aiohttp
.venv/bin/pip install evdev          # builds from source
bash linux/fix_kivy_sdl2.sh          # REQUIRED: system SDL2 with Wayland

Run

bash run_player.sh      # single run, no supervision
bash start.sh           # watchdog: auto-restart on crash/hang (24/7)

start.sh supervises through a heartbeat file (.player_heartbeat, rewritten every 10 s; stale after 60 s ⇒ hung ⇒ restart) and honours .player_stop_requested so a password exit is not resurrected.

Verify Before Committing

.venv/bin/python -m py_compile src/*.py linux/*.py   # syntax, seconds
bash -n start.sh run_player.sh stop_player.sh check_player_status.sh

.venv/bin/python linux/test_linux_patches.py        # expect: 21/21 passed
.venv/bin/python linux/test_linux_browser_flags.py  # expect: 27/27 passed
.venv/bin/python linux/test_media_state.py          # expect: 18/18 passed
bash linux/fix_kivy_sdl2.sh --check                 # expect: STATUS: fixed
.venv/bin/python linux/_probe_video.py              # video decodes + advances
.venv/bin/python linux/_probe_chromium_footprint.py https://example.com/  # PSS per profile

Close the running player before editing src/ if you intend to test immediately — the watchdog will restart it and you will test stale code.

24/7 Supervision

start.sh restarts the player when it:

  • crashed — the process disappeared; or
  • hung — the process is alive but the heartbeat is stale (>60 s).

Two failure modes must not be confused:

Situation Correct reaction
Was healthy, then heartbeat went stale Restart promptly (a hang)
Running but never became healthy (slow start, the first-run setup screen) Be patient — do not kill a player an operator is configuring

Diagnostics for playback transitions live in logs/playback_trace.log, written by src/playback_trace.py independently of Kivy's log level.

Runtime pitfalls

  • ~/dev/... cwd matters. The player's data directory is the repo root. Launch from there (all provided scripts do).
  • The desktop blanker fights the player. ~/.config/labwc/autostart ships swayidle -w timeout 600 'wlopm --off *', which powers the panel off after 10 minutes. linux_display.neutralise_idle_blanker() stops it at start-up. Do not remove that call.
  • tvservice, xdotool, ydotool do not exist on Trixie. Any code using them silently does nothing.
  • Web links need a dedicated --user-data-dir. Without it Chromium hands the URL to a running instance, our process exits in ~2 s and the item is skipped as a failed launch.
  • The keyring prompt is suppressed via the ENVIRONMENT, not the flags. --password-store=basic alone does not stop Chromium asking for the login keyring password: it inherits DBUS_SESSION_BUS_ADDRESS, reaches the running gnome-keyring-daemon and prompts. LinuxChromiumAdapter.launch_env() strips the bus for the child process, which is what actually fixes it. Do not remove launch_env(), and do not add a flag list that nothing references — test_linux_browser_flags.py fails if a list becomes dead code, because that is exactly how this bug hid before.
  • --ozone-platform-hint=auto does not work on Chromium 152. It aborts with "Missing X server" instead of falling back to Wayland; --ozone-platform=wayland must be stated explicitly.
  • Kill the whole process group on teardown. proc.terminate() leaves Chromium's GPU/zygote/renderer children behind; they accumulate until the Pi runs out of memory. This needs start_new_session=True at launch.
  • Measure memory with PSS, never RSS. Chromium shares pages across its processes, so summing RSS roughly doubles the figure and even ranks smaller configurations as larger. linux/_probe_chromium_footprint.py does it correctly.
  • --disable-gpu increases memory for a real page (1038 MB vs 513 MB). It is intentionally not used anywhere.

Video: 4K cannot play, and is converted automatically

A 4K video will not play on a Pi 4. ffpyplayer decodes in software and there is no hardware H.264 decode in its pipeline. Measured: 1080p decodes at 3.03× realtime, 3840×2160 at 0.90× — below 1× the picture simply stops.

The player therefore normalises oversized media to at most 1920×1080:

  • src/media_state.py — the shared on-disk contract (markers, path resolution, and a dependency-free MP4 header parser so the playback path spawns nothing).
  • linux/video_normalizer.py — the converter (hardware h264_v4l2m2m, ~31 s for an 18 s 4K clip; libx264 fallback).
  • Triggered automatically from get_playlists_v2.normalize_oversized_media() after every sync, in both the "updated" and "up-to-date" branches.
Marker next to the media Meaning
<media>.kiwy-converting in flight → the player skips the item
<media>_kiwy1080p.mp4 + .kiwy-normalized.json done → the player plays this file

Do not remove these markers, and do not let delete_unused_media() prune them — the converted output is deliberately absent from the playlist and would otherwise be deleted while the player is using it.

The conversion is only triggered by resolution. A file within 1920×1080 is left byte-identical. When nothing is playable yet (a single-item playlist whose only video is still converting), the player loops config/resources/intro1.mp4 rather than showing a blank screen.

Run it manually:

.venv/bin/python linux/video_normalizer.py --dry-run media/   # report only
.venv/bin/python linux/video_normalizer.py media/             # convert
.venv/bin/python linux/video_normalizer.py --max-height 720 media/

Prefer normalising on the server before upload: it avoids both the 4K download and the 31 s conversion. The player-side path is a safety net, not the preferred workflow.

Diagnostics

Command Purpose
.venv/bin/python linux/linux_display.py Wayland/X11 status, outputs, available tools
bash linux/fix_kivy_sdl2.sh --check Which SDL2 Kivy loads and its drivers
bash check_player_status.sh Is the player running
bash stop_player.sh Stop player + watchdog

Escape hatches:

Variable Effect
KIWY_DISPLAY_TOOLS_DISABLED=1 Disable wlopm/vcgencmd/swayidle work
KIWY_CHROMIUM_MODE=light Default footprint profile (safe reductions)
KIWY_CHROMIUM_MODE=minimal Adds --single-process; ~15% less memory, less stable
KIWY_CHROMIUM_MODE=safe No footprint flags at all (when debugging)
KIWY_VENV=/path Point the SDL2 fix script at another virtualenv

Commit Hygiene

  • Never commit .venv/, .kivy/, logs/, .kiosk-profile/ (git-ignored).
  • Do not commit player_auth.json — it holds live credentials (auth_code, player_id, server_url).
  • Do not commit config/app_config.json values that are host-specific (server_ip, screen_name, quickconnect_key). The repo default is blank on purpose so a fresh install runs the first-run setup flow.

Release Checklist

  1. Bump PLAYER_VERSION in src/main.py.
  2. Run the verification suite above on the target Pi.
  3. Confirm bash linux/fix_kivy_sdl2.sh --check reports fixed.
  4. Smoke-test a mixed playlist: image → video → weblink → image, verifying durations, audio (audio: off / muted) and a clean exit/restart.
  5. 24/7 soak (≥12 h): check the heartbeat, pgrep -c chromium for leaks and memory growth.