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.
The exe shipped config/app_config.json AND src/player_auth.json inside the
bundle, and because a frozen app runs with cwd = _internal/, the player loaded
that snapshot as its live auth state. A stale snapshot therefore made a fresh
build boot "already authenticated" against an old server and play an outdated
playlist. It also meant every install inherited the build machine's server_ip,
screen_name and auth_code.
Both files are now excluded from the bundle (app_config.json is no longer added
to datas, player_auth.json is excluded from Tree(src)), and the runtime hook no
longer copies a config into place on first run.
New behaviour, all in src/main.py so it applies to the Pi build too:
- The player starts with blank credentials. A missing file, an empty or
unparseable file, missing keys, and leftover placeholder values
(localhost, 127.0.0.1, kivy-player, 1234567) all count as UNCONFIGURED.
config_is_configured() is the single source of truth for that decision.
- After the splash video a notice appears ("Player is not configured"), and
after 5 seconds the Settings screen opens automatically so the operator can
enter the server details.
- Saving valid values writes config/app_config.json next to the .exe and
starts playback immediately - no restart needed.
- On a machine that IS configured, the notice and Settings are skipped and the
cached playlist plays straight away.
- Settings refuses to close while the three required fields are blank, so it
cannot be dismissed into a permanently blank screen with no way back.
- The 30s playlist timer does not fight the setup flow while unconfigured.
on_intro_finished() is the single decision point after the splash; both intro
paths (video end and "no intro file") go through it so they cannot drift apart.
Also loads config over DEFAULT_CONFIG rather than replacing it, so a partial or
older config file keeps working defaults instead of losing keys.
Note for future changes: when adding a new REQUIRED config key, add it to
CONFIG_REQUIRED_KEYS or the first-run flow will not ask for it.
Verified on the packaged exe by removing the config to simulate a fresh install:
setup_required_shown -> setup_opening_settings exactly 5s later ->
setup_completed, with the config written next to the exe and playback resuming.
Restarting with that config produced no setup events at all.
Covered by windows/test_first_run_setup.py.
Web links were implemented three times (main.py subprocess, run_win.py
subprocess + Win32 overlay, cef_browser.py embedded CEF), each owning its own
process handle, watchdog and teardown. That ambiguity caused leaked browsers,
skipped items, lost foreground and blank screens when a page failed to load.
Replace all three with a single owner in src/weblink_session.py:
- WeblinkSession: validate -> launch -> verify -> watch -> teardown.
Generation-tokened so stale callbacks are ignored, idempotent close(),
atexit-safe, never more than one browser alive.
- WeblinkAdapter: the only platform-specific part (launch / wait_visible /
is_alive / teardown / prewarm). Platform layers inject engines through
SignagePlayer.weblink_adapter_factory.
- ChromiumSubprocessAdapter: default engine (Pi chromium, Windows chrome/msedge).
- InteractionWatcher: decides when an item is finished.
- WebInputSources: /dev/input/event* (Linux) plus a GetCursorPos pointer tap
(needed on Windows for embedded CEF, which has no child process).
Interaction model: web links are an interactive surface, not timed media.
The player advances only when the configured duration has elapsed AND the
viewer has not interacted for 10s, measured from the most recent interaction.
A touch in the final seconds of a slot therefore pushes the advance 10s past
that touch, and each further touch pushes it again, so a page is never pulled
out from under someone using it. An untouched page still advances on schedule.
A drag burst counts as one interaction but the countdown tracks its last event,
so an item cannot be cut off mid-gesture. max_dwell (duration x factor, floored
by min_max_dwell) is an absolute backstop against a wedged browser or a jammed
touchscreen.
Verified start-up: the visibility wait runs on the watcher thread, never on
Kivy's main thread. If the browser window never appears the item is reported
failed and skipped, instead of resetting the error counter and leaving a black
screen up for the whole duration.
Config: new "weblink" block in config/app_config.json (engine,
interaction_postpone, interaction_debounce, interaction_grace,
max_dwell_factor, min_max_dwell, launch_timeout, prewarm) with safe defaults,
so an absent block still works.
Also add weblink_session to the PyInstaller hiddenimports so the frozen exe
bundles the new module.
- New windows/ directory with build scripts, specs, and configuration
- Windows-specific requirements (requirements_win.txt)
- Launch and runtime scripts for Windows (run_win.py, launch_player.bat)
- PyInstaller build configuration (build.spec)
- Updated .gitignore to exclude windows/venv312/
- Updated config and source files for Windows compatibility
- Moved working_files to proper directory
- 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
- 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
- Modified ssl_utils.py to treat 404 errors as expected when server doesn't have /api/certificate endpoint
- Changed verify_ssl setting to false in app_config.json to allow HTTPS connections without certificate verification
- This allows the player to connect to servers that don't implement the certificate endpoint
- Added checkbox in Settings screen to enable/disable edit feature
- Setting stored in app_config.json as 'edit_feature_enabled'
- Edit workflow now validates: player setting, media type, server permission, card auth
- Shows appropriate error message when edit is blocked at any validation step
- Defaults to enabled (true) if not set
- All conditions must be met for edit interface to open
- Implemented CardReader class to read data from USB card readers
- Added CardSwipePopup with 5-second timeout and visual feedback
- Card data is captured and included in edit metadata
- Card data sent to server when edited images are uploaded
- Added evdev dependency for USB input device handling
- Fallback mode when evdev not available (for development)
- Created test utility (test_card_reader.py) for card reader testing
- Added comprehensive documentation (CARD_READER_AUTHENTICATION.md)
- Added access-card.png icon for authentication popup
- Edit interface requires card swipe or times out after 5 seconds
- Complete Kivy-based signage player application
- Fetches playlists from DigiServer via REST API
- Supports images (JPG, PNG, GIF, BMP) and videos (MP4, AVI, MKV, MOV, WEBM)
- Modern UI with touch controls and settings popup
- Separated UI (KV file) from business logic (Python)
- Fullscreen media display with proper scaling
- Server feedback system for player status reporting
- Auto-hide controls with mouse/touch activation
- Virtual environment setup with requirements.txt
- Installation and run scripts included
Features:
✅ Cross-platform Kivy framework
✅ Server integration with DigiServer
✅ Media playback with duration control
✅ Player feedback and status reporting
✅ Settings management with persistent config
✅ Error handling and recovery
✅ Clean architecture with KV file separation