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:
@@ -1,273 +0,0 @@
|
||||
---
|
||||
description: "Use when building, compiling, packaging or releasing the Kiwy Signage Player: PyInstaller build, .exe generation, build.spec, hiddenimports, code signing, Smart App Control, Windows development, Raspberry Pi deployment, or adding new modules under src/. Covers the exact build commands, environment constraints, bundling rules and verification steps."
|
||||
name: "Kiwy Build & Development"
|
||||
---
|
||||
|
||||
# Kiwy Signage Player — Build & Development
|
||||
|
||||
Cross-platform Kivy digital signage player.
|
||||
|
||||
- **Raspberry Pi / Linux** — `src/main.py` is the entry point (root `install.sh`, `start.sh`).
|
||||
- **Windows** — `windows/run_win.py` is the entry point; it sets Windows env vars, imports
|
||||
`main.py`, then monkey-patches platform differences. Packaged to `.exe` with PyInstaller.
|
||||
|
||||
## Ground Rules
|
||||
|
||||
- **Keep `src/main.py` cross-platform.** Windows-specific behaviour belongs in
|
||||
`windows/run_win.py` (see `_patch_main()`), Pi-specific behaviour in `main.py` guarded
|
||||
by capability checks. Do not add Windows-only imports to `main.py`.
|
||||
- **Rebuild is mandatory.** The `.exe` bundles `src/`, so Python edits are invisible until
|
||||
you rebuild. There is no hot reload in the packaged app.
|
||||
- **`dist/` is a runtime folder, not just build output.** It holds `config/`, `media/`,
|
||||
`playlists/`, `logs/`, `.kiosk-profile/` and `.player_heartbeat`. Never wipe `dist/`
|
||||
blindly; a `--clean` rebuild preserves it, but a manual delete destroys player state.
|
||||
|
||||
## Environment
|
||||
|
||||
| Item | Value |
|
||||
|------|-------|
|
||||
| Interpreter | **Python 3.12.9** (64-bit) — project venv at `windows\venv` |
|
||||
| Kivy | 2.3.1 |
|
||||
| PyInstaller | 6.21.0 |
|
||||
| Linux target | Python 3.13 (`repo/python-wheels/`, `repo/system-packages/`) |
|
||||
|
||||
- **Python 3.13+/3.14 is NOT supported for the Windows build** — Kivy 2.3.1 has no wheels
|
||||
for them.
|
||||
- `cefpython3` is **not installed** in the project venv (its wheels stop at Python 3.9).
|
||||
The embedded-CEF engine in `windows/cef_browser.py` is therefore dormant on this
|
||||
build — do not rely on it.
|
||||
- **Web links use embedded WebView2** (`windows/webview2_browser.py`), driven through
|
||||
`pythonnet` (installed). Two extra things ship with the exe:
|
||||
- `windows/webview2_sdk/` — ~860 KB of SDK DLLs (`Microsoft.Web.WebView2.Core.dll`,
|
||||
`WebView2Loader.dll`). **Tracked in git on purpose**: without them WebView2
|
||||
silently downgrades to the Chrome/Edge subprocess engine.
|
||||
- `windows/webview2_runtime/MicrosoftEdgeWebview2Setup.exe` — ~1.7 MB bootstrapper,
|
||||
used to install the Runtime on a machine that lacks it. The ~203 MB offline
|
||||
standalone installer is **git-ignored**; fetch it with
|
||||
`windows/webview2_runtime/download_runtime_installers.ps1 -Offline` when you need
|
||||
to deploy to machines with no internet (it makes the exe ~305 MB).
|
||||
|
||||
## Build
|
||||
|
||||
From `windows\`:
|
||||
|
||||
```
|
||||
venv\Scripts\python.exe -m PyInstaller build.spec --clean --noconfirm
|
||||
```
|
||||
|
||||
Or the full one-click path (creates venv, installs deps, builds, then signs):
|
||||
|
||||
```
|
||||
build_win.bat
|
||||
```
|
||||
|
||||
Output (folder mode, via `COLLECT`):
|
||||
|
||||
```
|
||||
windows\dist\KiwySignagePlayer\KiwySignagePlayer.exe <- the real build output
|
||||
```
|
||||
|
||||
**Deployment hazard:** `windows\dist\KiwySignagePlayer.exe` (one level up) is a stale
|
||||
leftover from an older single-file build of the same name. It is not refreshed by the
|
||||
current spec, so deploying it ships old code. Always take the executable from the
|
||||
`KiwySignagePlayer\` subfolder, and delete the stray copy if it reappears.
|
||||
|
||||
### `build.spec` facts
|
||||
|
||||
- Entry point `run_win.py`; `pathex=[BUILD_DIR, SRC_DIR]` so `src/` modules resolve.
|
||||
- `runtime_hooks=[pyi_runtime_hook.py]` — sets per-monitor DPI awareness **before** SDL/Kivy
|
||||
initialise. Keep DPI work here; it must run before any window is created.
|
||||
- `datas += Tree(src)` bundles all of `src/`. `console=True` so startup errors and the Kivy
|
||||
log remain visible — do not flip this to `False` without a reason.
|
||||
- `hiddenimports` must list modules that PyInstaller's static analysis **cannot** see:
|
||||
lazily imported ones (`cef_browser` is imported inside a function) or dynamic imports
|
||||
(`getattr`, `importlib`). Top-level imports such as `weblink_session` are found
|
||||
automatically via `pathex`, but listing them is harmless insurance.
|
||||
**When you add a module under `src/` that is imported lazily or by string, add it to
|
||||
`hiddenimports` or the packaged exe will fail at runtime with `ModuleNotFoundError`.**
|
||||
- `excluded_imports` drops Linux-only packages (`evdev`, `gi`, GStreamer) and the
|
||||
non-matching `cefpython3` `.pyd` variants.
|
||||
|
||||
## First-run setup (no bundled credentials)
|
||||
|
||||
`config/app_config.json` is **deliberately NOT bundled** into the exe, and
|
||||
`player_auth.json` is excluded from `Tree(src)`. Shipping either one planted the
|
||||
build machine's `server_ip` / `screen_name` / `auth_code` into every install, so
|
||||
a fresh machine silently connected to the wrong server (or played a stale
|
||||
playlist) instead of asking.
|
||||
|
||||
What happens on a machine with no usable config:
|
||||
|
||||
1. The splash video (`config/resources/intro1.mp4`) plays.
|
||||
2. A notice appears: *"Player is not configured"*.
|
||||
3. After `SETUP_NOTICE_SECONDS` (**5 s**, `src/main.py`) the Settings screen
|
||||
opens automatically.
|
||||
4. Saving valid values writes `config/app_config.json` next to the **.exe** and
|
||||
playback starts immediately — no restart needed.
|
||||
|
||||
On a machine that **is** configured, the notice and Settings are skipped and the
|
||||
cached playlist plays straight away.
|
||||
|
||||
Key functions in `src/main.py`:
|
||||
|
||||
| Name | Role |
|
||||
|------|------|
|
||||
| `config_is_configured(config)` | Single source of truth. Missing file, empty/unparseable JSON, missing keys and leftover placeholders (`localhost`, `kivy-player`, `1234567`, `127.0.0.1`) all count as **unconfigured**. |
|
||||
| `DEFAULT_CONFIG` | Blank-credential starting point, so a fresh install can never look configured. |
|
||||
| `SignagePlayer.on_intro_finished()` | The one decision point after the splash: setup vs. playback. Both intro paths (video end and "no intro file") go through it. |
|
||||
| `show_setup_required_notice()` | Shows the notice, then opens Settings after 5 s. |
|
||||
| `on_first_run_config_saved()` | Re-syncs and starts playback. |
|
||||
|
||||
`config_is_configured()` is covered by `windows/test_first_run_setup.py` — run it
|
||||
after touching that logic.
|
||||
|
||||
> When adding a new **required** config key, add it to `CONFIG_REQUIRED_KEYS`
|
||||
> (and to `CONFIG_PLACEHOLDER_VALUES` if it has a placeholder default), or the
|
||||
> first-run flow will not ask for it.
|
||||
|
||||
## 24/7 Supervision (Windows)
|
||||
|
||||
A signage player must survive crashes and hangs unattended. On **Linux/Pi** this is
|
||||
`start.sh` (systemd or `./start.sh`). On **Windows** it is `windows/watchdog.ps1`,
|
||||
launched by `windows/start_player_watchdog.bat`.
|
||||
|
||||
It restarts the player when it:
|
||||
|
||||
- **crashed** — the process disappeared; or
|
||||
- **hung** — the process is alive but `.player_heartbeat` has gone stale
|
||||
(the player rewrites it every 10 s; the watchdog treats >60 s as hung). A frozen
|
||||
player is otherwise indistinguishable from a working one.
|
||||
|
||||
It also backs off if the player cannot stay up (crash-loop breaker:
|
||||
`CrashLoopMaxFailures` failures in `CrashLoopWindowMin` minutes without ever
|
||||
becoming healthy → `CrashLoopBackoffMin` minutes of quiet).
|
||||
|
||||
### The stop flag is SESSION-SCOPED
|
||||
|
||||
The only supported way to stop the player is the **exit-screen password**. On
|
||||
success the player writes `.player_stop_requested` next to its `.exe`, and the
|
||||
watchdog then **stands down instead of restarting**.
|
||||
|
||||
The flag is cleared **every time the watchdog starts**, which is what makes it
|
||||
session-scoped: the password exit ends the *current* session, and the next launch
|
||||
begins a new one. There is no file to delete by hand.
|
||||
|
||||
> **Ordering matters:** the flag is the operationally-correct choice over a
|
||||
> "remember the last exit" setting, because a reboot must come back up playing.
|
||||
> Clearing on start also means a power cut cannot leave the player permanently off.
|
||||
|
||||
### Two failure modes that must not be confused
|
||||
|
||||
| Situation | Correct reaction |
|
||||
|---|---|
|
||||
| Was healthy, then heartbeat went stale | **Restart promptly** (this is a hang) |
|
||||
| Running but never became healthy (slow start, bad install, the **first-run setup screen**) | **Be patient**, then restart after `2 × StartupGraceSec` |
|
||||
|
||||
Never kill a player that has not yet been healthy — it may be an operator entering
|
||||
server settings on the first-run setup screen.
|
||||
|
||||
### Important Windows-specific facts
|
||||
|
||||
- The packaged player is **TWO processes** (PyInstaller bootloader parent + the
|
||||
child that owns the SDL window). Any kill must use `taskkill /T` or the visible
|
||||
window survives and the next launch collides with it.
|
||||
- The watchdog can only *restart* an install that already has a working config.
|
||||
It cannot help before the player has first run.
|
||||
- Hiding the console window (`console=False`) is **not** done: the console is the
|
||||
only way to see a start-up failure, and the build instructions warn against
|
||||
flipping it without reason.
|
||||
- **No Windows service.** A service runs in session 0 with no desktop, so the
|
||||
player could not render to the screen. A login-triggered task is the correct
|
||||
Windows counterpart to the Pi's systemd unit — that is what
|
||||
`start_player_watchdog.bat` is for (add it to the Startup folder for autostart).
|
||||
|
||||
### Verify the watchdog
|
||||
|
||||
```
|
||||
cd windows
|
||||
venv\Scripts\python.exe test_watchdog.py # crash + hang + stop-flag + new-session
|
||||
```
|
||||
|
||||
Check state at any time (read-only):
|
||||
|
||||
```
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File windows\watchdog.ps1 -Status
|
||||
```
|
||||
|
||||
> When testing a hang, do **not** just backdate the heartbeat file's mtime — the
|
||||
> healthy player rewrites it immediately, so nothing is actually stale and the
|
||||
> test passes for the wrong reason. Hold the file open with an exclusive Windows
|
||||
> lock (share mode 0) so the player's own write fails; that reproduces a genuine
|
||||
> stale heartbeat while the process stays alive.
|
||||
|
||||
## Code Signing (production constraint)
|
||||
|
||||
Production PCs run with **Smart App Control enforced** (`VerifiedAndReputablePolicyState=1`,
|
||||
UMCI enforced) with **no "Run anyway" bypass** — an unsigned exe is blocked at kernel level.
|
||||
|
||||
- A **self-signed certificate does NOT satisfy Smart App Control.** A cert from a reputable
|
||||
public CA is required. `create_self_signed_cert.ps1` is dev-only.
|
||||
- Provide a cert as `KIWY_SIGN_PFX` (+ optional `KIWY_SIGN_PFX_PASSWORD`) or drop
|
||||
`kiwy_signing.pfx` in `windows\`; `build_win.bat` then signs via `sign_exe.ps1`
|
||||
(signtool with RFC3161 timestamp when available).
|
||||
- See `documentation/CODE_SIGNING_SMART_APP_CONTROL.md`.
|
||||
|
||||
## Verify Before Committing
|
||||
|
||||
Fast syntax gate (no build, seconds):
|
||||
|
||||
```
|
||||
python -m py_compile src/main.py src/weblink_session.py windows/run_win.py windows/webview2_browser.py windows/webview2_runtime.py
|
||||
```
|
||||
|
||||
Web-link engine checks (no build, no player):
|
||||
|
||||
```
|
||||
cd windows
|
||||
venv\Scripts\python.exe test_webview2_embed.py # embeds WebView2 in a bare Win32 window
|
||||
venv\Scripts\python.exe test_webview2_runtime.py # Runtime detection + installer discovery
|
||||
```
|
||||
|
||||
Rebuild warning: the bundled offline installer makes the exe ~305 MB. If you do not
|
||||
need offline machines, delete
|
||||
`windows/webview2_runtime/MicrosoftEdgeWebView2RuntimeInstallerX64.exe` before building.
|
||||
|
||||
**Close the running player before rebuilding.** A running
|
||||
`dist\KiwySignagePlayer\KiwySignagePlayer.exe` locks the output and the build fails with
|
||||
"Access is denied". Chrome/Edge kiosk processes left over from web-link playback can also
|
||||
hold locks; if the build fails, check for stray `msedge.exe` / `chrome.exe` before retrying.
|
||||
|
||||
Because `.exe` is a **reserved Windows device name** as well as a real file, use
|
||||
`Test-Path -LiteralPath` when probing for the executable, otherwise the path silently
|
||||
resolves to the console device.
|
||||
|
||||
Development run — **use this on a SAC-locked host**, since the unsigned exe cannot launch:
|
||||
|
||||
```
|
||||
cd windows
|
||||
venv\Scripts\activate
|
||||
python run_win.py
|
||||
```
|
||||
|
||||
Diagnose playback transitions via the trace log (`logs\playback_trace.log`), written by
|
||||
`src/playback_trace.py`.
|
||||
|
||||
## Release Checklist
|
||||
|
||||
1. Bump `PLAYER_VERSION` in `src/main.py` **and** `filevers`/`prodvers`/`FileVersion`/
|
||||
`ProductVersion` in `windows\version_info.txt`; keep them in sync.
|
||||
2. Rebuild (`build_win.bat`).
|
||||
3. Confirm the exe is signed (`Get-AuthenticodeSignature`); unsigned builds are blocked on
|
||||
production hosts.
|
||||
4. Smoke-test a mixed playlist: image → video → weblink → image, verifying durations, audio
|
||||
(`audio: off` / `muted`), and a clean exit/restart.
|
||||
|
||||
## Commit Hygiene
|
||||
|
||||
- Never commit `windows\build\` or `windows\dist\` (git-ignored). Tracked build output such
|
||||
as `windows\archive_list.txt` and `windows\build_last.txt` is the exception, not the rule.
|
||||
- **Do not commit `player_auth.json` or `src/player_auth.json`** — they contain live
|
||||
credentials (`auth_code`, `player_id`, `server_url`). These are currently tracked; treat any
|
||||
future change to them as a deliberate, reviewed decision.
|
||||
- Do not commit `config\app_config.json` values that are host-specific without checking
|
||||
whether they belong in the repo default.
|
||||
@@ -0,0 +1,253 @@
|
||||
---
|
||||
description: "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."
|
||||
name: "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 point** — `linux/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:
|
||||
|
||||
```bash
|
||||
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):
|
||||
|
||||
```bash
|
||||
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
|
||||
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
|
||||
|
||||
```bash
|
||||
.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:
|
||||
|
||||
```bash
|
||||
.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.
|
||||
Reference in New Issue
Block a user