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.
This commit is contained in:
ske087
2026-09-13 21:57:49 +03:00
parent f437aba1fc
commit 3ac7f836c4
68 changed files with 5604 additions and 9326 deletions
+41 -37
View File
@@ -5,11 +5,10 @@ This document describes how the **Kiwy-Signage player**
playlist item type (display a live web page / URL instead of an uploaded media
file).
> **Status: implemented.** The player supports `weblink` items on both
> Raspberry Pi (`chromium` subprocess) and Windows (embedded **WebView2**, with
> the CEF and Chrome/Edge subprocess engines as fallbacks). Sections 14
> describe the original design plan; section 6 documents the shipped
> architecture and the interaction model.
> **Status: implemented.** The player supports `weblink` items on
> Raspberry Pi / Linux via a Chromium kiosk subprocess.
> Sections 14 describe the original design plan; section 6 documents the
> shipped architecture and the interaction model.
---
@@ -207,7 +206,8 @@ Recommended options, in order of robustness:
- On Wayland/X11 the player already sets `SDL_VIDEODRIVER`; verify Chromium
launches on the same display/session.
2. **Embedded web view widget** (`kivy_garden.webview`, WebKit/GTK, or WebView2).
2. **Embedded web view widget** (`kivy_garden.webview`, WebKit/GTK, or a
platform WebView) — renders inside the Kivy window.
Cleaner UX (stays inside the Kivy widget tree) but fragile and poorly
supported on Pi/Wayland — only pursue if option 1 is unacceptable.
@@ -258,55 +258,59 @@ platform:
|--------------------------|----------------|
| `WeblinkSession` | Owns one weblink item: validate → launch → verify → watch → teardown. Generation-tokened so stale callbacks are ignored, and `atexit`-safe. |
| `WeblinkAdapter` | The only platform-specific part: launch / wait for the window / is it alive / tear it down / pre-warm. `extra_launch_args()` lets a subclass add browser flags without copying `launch()`. |
| `ChromiumSubprocessAdapter` | Default engine (Raspberry Pi `chromium`; on Windows the Chrome/Edge fallback). |
| `ChromiumSubprocessAdapter` | Default engine (Raspberry Pi `chromium`). |
| `InteractionWatcher` | Decides when the item is finished (see the interaction model below). |
| `WebInputSources` | Reads `/dev/input/event*` (Linux) and does a pointer-position tap (Windows, needed for embedded engines). |
| `webview2_browser.py` | **Windows, preferred**: embeds WebView2 as a child HWND of the Kivy window. |
| `webview2_runtime.py` | **Windows**: detects the WebView2 Runtime and installs it silently when missing. |
| `WebInputSources` | Reads `/dev/input/event*` to detect viewer interaction. |
| `linux_browser.py` | **Raspberry Pi / Linux**: `LinuxChromiumAdapter` — Chromium kiosk on Wayland (labwc). |
Platform wrappers inject their engines through
Platform entry points inject their engine through
`SignagePlayer.weblink_adapter_factory`:
* **Raspberry Pi / Linux** — built-in Chromium subprocess adapter.
* **Windows** (`windows/run_win.py`) — engines are tried in this order:
* **Raspberry Pi / Linux** (`linux/run_linux.py`) — injects
`LinuxChromiumAdapter`, which adds the flags Trixie needs:
| Order | Engine | Renders | Notes |
|-------|--------|---------|-------|
| 1 | **WebView2** (`webview2_browser.py`) | child window **inside** Kivy | Preferred. No subprocess, so no background/z-order/hand-off/leak problems. |
| 2 | CEF (`cef_browser.py`) | child window inside Kivy | Dormant: `cefpython3` has no wheels past Python 3.9. |
| 3 | Chrome/Edge subprocess | separate window | Last resort only; retains the old drawbacks. |
| Flag | Why |
|------|-----|
| `--kiosk` | What actually makes labwc give the window exclusive fullscreen. Windows needed `--start-maximized` instead; that is not sufficient here. |
| `--user-data-dir=<.kiosk-profile>` | **Mandatory.** Without it Chromium hands the URL to an already-running instance, the process we launched exits in ~2 s and the item is skipped as a failed launch. Also guarantees we never touch the operator's own browser profile. |
| `--ozone-platform-hint=auto` | Lets Chromium pick Wayland when available and fall back to X11/XWayland. |
| `--autoplay-policy=no-user-gesture-required` | Signage pages play media without a click. |
Teardown kills the **whole process group** (`os.killpg`). `proc.terminate()`
only reaps the parent, leaving Chromium's GPU/zygote/renderer children behind;
across a 24/7 playlist those accumulate until the Pi runs out of memory.
`WeblinkAdapter.extra_launch_args()` is the hook subclasses use to add flags
without duplicating `launch()` — the Chrome adapter uses it for
without duplicating `launch()` — the Linux adapter uses it for
`--user-data-dir` + `--kiosk`.
> **Do not give the factory a class-level `None` default combined with an
> unconditional instance assignment.** `SignagePlayer.__init__` originally set
> `self.weblink_adapter_factory = None`, which shadowed the class attribute the
> Windows wrapper installs — so the platform adapters were silently ignored and
> every weblink fell back to the generic adapter and failed. It now only sets
> platform entry point installs — so the platform adapters were silently ignored
> and every weblink fell back to the generic adapter and failed. It now only sets
> the instance attribute when the class attribute is absent.
### 6.2 Windows: WebView2 Runtime
### 6.2 Linux: Chromium kiosk on Wayland
WebView2 is two separate things, and they ship differently:
Web links on Raspberry Pi OS Trixie use a single dedicated `chromium` process
launched in kiosk mode with its own profile directory:
* the **SDK** (`Microsoft.Web.WebView2.Core.dll`, `WebView2Loader.dll`) — the
API surface, bundled in the exe from `windows\webview2_sdk\` (~860 KB);
* the **Runtime** (`msedgewebview2.exe`) — the actual Chromium engine, shipped
by Microsoft and **verified/installed at start-up** by
`windows\webview2_runtime.py`.
* `--kiosk` gives an exclusive-fullscreen window under the **labwc** compositor;
* `--user-data-dir=<data>/.kiosk-profile` guarantees a *fresh, trackable* browser
instead of a hand-off to a running instance;
* `--ozone-platform-hint=auto` selects Wayland natively and falls back to X11.
If the Runtime is absent the player runs an installer silently
(`/silent /install`) and **unelevated**, which produces a *per-user* install and
therefore never raises a UAC prompt on the signage display. A ~1.7 MB online
bootstrapper is bundled by default; a ~203 MB offline standalone installer can
be bundled instead (see `windows\webview2_runtime\download_runtime_installers.ps1`)
for machines with no internet.
Because a separate process cannot be introspected portably, visibility is
verified by combining "the process survived the health grace period" with a
best-effort check that the PID actually owns a Wayland/X11 socket. A launch that
dies sooner than `min_healthy_alive` is treated as a **failed launch** (hand-off,
missing binary, instant crash) rather than a finished item, so the playlist
never skips a weblink silently.
Install success is decided by **re-reading the installed version**, not by the
installer exit code — Edge Update returns a non-zero HRESULT (e.g.
`-2147219416`) when the Runtime is already current, which is not a failure.
Stale `SingletonLock`/`SingletonSocket` files left by a crashed Chromium are
cleared before each launch: our profile is private to the player, so removing
the lock is always safe and prevents Chromium refusing to start.
### 6.1 Interaction model — web links are not passive media