d8c6ab0bc5
Two independent failures were killing long unattended runs.
1. HANG at the end of a video (Windows AppHangB1)
The player froze after ~30-45 minutes of looping, always at a video item. The
playback trace stopped dead right after "video_loaded" with no "video_eos" and
no "advance_after_video_eos", and Windows logged AppHangB1 rather than a crash.
Cause, all inside Kivy and verified against the installed source:
1. ffpyplayer fires on_eos.
2. Kivy's Video widget binds its OWN handler first (kivy/uix/video.py
_do_video_load), and that handler sets state = 'stop' DURING the event
dispatch.
3. state = 'stop' -> VideoFFPy.stop() -> unload(), which calls
self._thread.join() with no timeout (the source even carries the comment
"TODO: use callback, don't block here").
4. When that decode thread is slow to exit, the Kivy/SDL main thread never
returns, so the window stops pumping messages.
It is a race, which is why it looked random and only appeared after many videos.
src/video_safety.py bounds that join. ffpyplayer has already been told to quit
and its thread woken before the join, so limiting the wait does not leak work;
it only stops an unresponsive thread from taking the whole player down. The
guard is installed before the Video widget is constructed, because the decode
thread is created during play().
The intro video had the same hazard on the main thread (state='stop' followed by
unload() inside the state callback) and is now torn down on a worker thread like
playlist videos.
2. CRASH in SDL2_mixer.dll (0xc0000005) on a video with no audio stream
Triggered when a silent 4K clip entered the playlist while the item was marked
audio: on. ffpyplayer initialises SDL2_mixer from the FIRST audio file it opens
and reuses those parameters, so a file with no audio stream (rate/channels 0)
makes SDL2_mixer dereference garbage. Muting via volume=0.0 does NOT avoid it -
the audio stream itself must be disabled.
play_video now probes the file with ffprobe and forces mute when it has no audio
track, so such a file can never reach ffpyplayer with sound enabled. The probe
fails safe (assumes audio present) if ffprobe is unavailable.
3. 24/7 supervision (solution A + C)
windows/watchdog.ps1 + start_player_watchdog.bat restart the player when it
crashes (process gone) or hangs (process alive but .player_heartbeat stale),
with a crash-loop breaker that backs off when it cannot stay up. This is the
Windows counterpart of the proven Linux start.sh watchdog.
The exit-screen password remains the only supported way to stop the player. On
success it writes .player_stop_requested next to the .exe and the watchdog
stands down instead of restarting. The flag is SESSION SCOPED: the watchdog
clears it on every start, so launching again begins a new session and there is
no file to delete by hand. Clearing on start also means a power cut cannot leave
the player permanently off.
Deliberately NOT done: a Windows service. A service runs in session 0 with no
desktop, so the player could not render to the screen at all. A login-triggered
startup entry is the correct Windows analogue of the Pi's systemd unit.
Important detail: the packaged player is TWO processes (PyInstaller bootloader
parent plus the child that owns the SDL window), so any kill uses taskkill /T or
the visible window survives and the next launch collides with it.
Verified in the packaged exe over a 7-hour run: 170 playlist restarts, 1365
items, 171 web links launched/visible/ended with zero failures, and no crashes,
no hangs and no leaked browser processes.
Tests: windows/test_video_hang.py and windows/test_watchdog.py. The hang test
deliberately holds the heartbeat open with an exclusive Windows lock (share mode
0) so the player's own write fails - backdating the file's mtime does NOT
simulate a hang, because the healthy player rewrites it immediately and the test
would then pass for the wrong reason.
287 lines
11 KiB
Markdown
287 lines
11 KiB
Markdown
# Kiwy Signage Player - Windows Edition
|
|
|
|
Build and run the Kiwy digital signage player on Windows as a standalone `.exe`.
|
|
|
|
## 📋 Requirements Analysis
|
|
|
|
The original app was built for **Raspberry Pi (Linux)**, using these technologies:
|
|
|
|
| Component | Original (RPi/Linux) | Windows Equivalent |
|
|
|-----------|---------------------|-------------------|
|
|
| **GUI** | Kivy 2.3+ | Kivy 2.3+ (works cross-platform) |
|
|
| **Video** | ffpyplayer | ffpyplayer (needs FFmpeg DLLs) |
|
|
| **Card Reader** | evdev (Linux input) | ✅ Raw Input API + LL-hook fallback |
|
|
| **Screen Keep-Awake** | xset, xdotool, Wayland | `SetThreadExecutionState` (Win32 API) |
|
|
| **Weblink** | chromium-browser (kiosk) | Chrome/Edge (--kiosk mode) |
|
|
| **Audio** | ALSA/PulseAudio | DirectSound |
|
|
| **Window Backend** | SDL2 (Wayland/X11) | SDL2 (Windows native) |
|
|
| **OpenGL** | Desktop GL | ANGLE (DirectX wrapper) |
|
|
|
|
### What works on Windows
|
|
- ✅ Media playback (images, videos via ffpyplayer)
|
|
- ✅ Playlist sync from DigiServer (HTTP/HTTPS)
|
|
- ✅ Touch & mouse controls
|
|
- ✅ Settings popup
|
|
- ✅ Image editing/annotation
|
|
- ✅ Password-protected exit
|
|
- ✅ Web links (opens in Chrome/Edge kiosk)
|
|
- ✅ Network monitoring
|
|
- ✅ Auto-update playlist
|
|
- ✅ Card reader authentication (Raw Input API — see below)
|
|
|
|
### What is disabled on Windows
|
|
- ❌ HDMI power management (tvservice is RPi-specific)
|
|
- ❌ WiFi restart (uses Linux `nmcli`)
|
|
|
|
## 🚀 Quick Start (Development)
|
|
|
|
### Prerequisites
|
|
1. **Python 3.12+** (64-bit) — [python.org](https://python.org)
|
|
- ⚠️ **Python 3.13+ is NOT supported** — Kivy 2.3.1 does not have pre-built wheels for it
|
|
- ⚠️ **Python 3.14 is NOT supported** — no Kivy wheels available
|
|
- ✅ **Python 3.12.9** is the recommended version (confirmed working)
|
|
2. **FFmpeg** — for video codec support
|
|
- Download from [ffmpeg.org](https://ffmpeg.org/download.html)
|
|
- Add `bin\` folder to your PATH
|
|
3. **Visual C++ Redistributable** — [latest](https://aka.ms/vs/17/release/vc_redist.x64.exe)
|
|
|
|
### Install & Run
|
|
```batch
|
|
cd windows
|
|
|
|
REM Create virtual environment with Python 3.12
|
|
py -3.12 -m venv venv
|
|
:: OR specify full path:
|
|
:: "C:\Users\Dell-PC\AppData\Local\Programs\Python\Python312\python.exe" -m venv venv
|
|
|
|
venv\Scripts\activate
|
|
|
|
REM Install dependencies
|
|
pip install -r requirements_win.txt
|
|
|
|
REM Run in development mode
|
|
python run_win.py
|
|
```
|
|
|
|
## 📦 Building the .exe
|
|
|
|
### One-Command Build
|
|
```batch
|
|
cd windows
|
|
build_win.bat
|
|
```
|
|
|
|
### Manual Build
|
|
```batch
|
|
cd windows
|
|
venv\Scripts\activate
|
|
pip install -r requirements_win.txt
|
|
pyinstaller build.spec --clean --noconfirm
|
|
```
|
|
|
|
### Output
|
|
```
|
|
windows\dist\KiwySignagePlayer\
|
|
├── KiwySignagePlayer.exe # Main executable
|
|
├── config/ # Config files (auto-copied)
|
|
├── resources/ # Icons, intro video
|
|
└── ... (supporting DLLs)
|
|
```
|
|
|
|
For a **single-file .exe**, edit `build.spec` — uncomment the `exe_onefile` section and comment out the `coll = COLLECT(...)` section.
|
|
|
|
## 🌐 Web Links (embedded WebView2)
|
|
|
|
Web links render with **WebView2**, embedded as a child window *inside* the
|
|
Kivy window. Because it is not a separate browser process, it cannot open
|
|
behind the player, cannot be handed off to an existing browser and exit, and
|
|
never leaves leaked `msedge.exe`/`chrome.exe` processes behind.
|
|
|
|
WebView2 has **two** parts, and they are handled differently:
|
|
|
|
| Part | What it is | How it ships |
|
|
|------|------------|--------------|
|
|
| **SDK** | `Microsoft.Web.WebView2.Core.dll` + `WebView2Loader.dll` — the API surface | Bundled in the exe (`windows\webview2_sdk\`, ~860 KB) |
|
|
| **Runtime** | `msedgewebview2.exe` — the actual Chromium engine | Microsoft's evergreen component. Ships with Windows 11 and nearly all Windows 10 machines. **Installed automatically on first start if missing.** |
|
|
|
|
### Automatic Runtime installation
|
|
|
|
On start-up the player checks for the Runtime (registry `pv` value under the
|
|
Edge Update client GUID, with a live SDK probe as fallback). If it is absent it
|
|
runs the installer silently:
|
|
|
|
```
|
|
MicrosoftEdgeWebview2Setup.exe /silent /install
|
|
```
|
|
|
|
Deliberately **not elevated** — an unelevated run performs a *per-user* install,
|
|
so no UAC dialog ever appears on the signage display.
|
|
|
|
Installers are looked up in this order, so you can drop a replacement next to
|
|
the `.exe` without rebuilding:
|
|
|
|
1. `KIWY_WEBVIEW2_INSTALLER` environment variable
|
|
2. `<exe dir>\webview2_runtime\`
|
|
3. `<exe dir>\` (next to the executable)
|
|
4. bundled copy inside the exe
|
|
|
|
| Installer | Size | Bundled? | Use when |
|
|
|-----------|------|----------|----------|
|
|
| `MicrosoftEdgeWebview2Setup.exe` | ~1.7 MB | ✅ yes | Machine has internet (downloads the Runtime) |
|
|
| `MicrosoftEdgeWebView2RuntimeInstallerX64.exe` | ~203 MB | ⬜ opt-in | Machine is **offline** |
|
|
|
|
To bundle the offline installer (adds ~200 MB to the exe):
|
|
|
|
```powershell
|
|
cd windows
|
|
.\webview2_runtime\download_runtime_installers.ps1 -Offline
|
|
venv\Scripts\python.exe -m PyInstaller build.spec --clean --noconfirm
|
|
```
|
|
|
|
Re-download the installers at any time (they are Microsoft-signed; the script
|
|
verifies the signature):
|
|
|
|
```powershell
|
|
.\webview2_runtime\download_runtime_installers.ps1
|
|
```
|
|
|
|
### If WebView2 is unavailable
|
|
|
|
Web links fall back to the Chrome/Edge subprocess engine (see
|
|
`src/weblink_session.py`). That engine still works, but reintroduces the old
|
|
drawbacks — a separate browser window, possible background/z-order behaviour,
|
|
and browser processes to clean up.
|
|
|
|
### Troubleshooting
|
|
|
|
| Problem | Solution |
|
|
|---------|----------|
|
|
| Web links show a black/blank page | Check `logs\` for `[WebView2]` lines. Confirm the Runtime version is reported. |
|
|
| Runtime install did not happen | A failed attempt is not retried for 6 hours (marker: `logs\.webview2_install_attempt`). Delete that file to retry immediately. |
|
|
| Need it working offline | Bundle the standalone installer (see above). |
|
|
|
|
## ⚙️ Configuration
|
|
|
|
**No configuration ships with the exe.** On a machine that has never been set
|
|
up, `config\app_config.json` is absent, so after the splash video the player
|
|
shows a *"Player is not configured"* notice, waits 5 seconds and then opens the
|
|
**Settings** screen automatically. Enter the server details and playback starts
|
|
right away — no restart required.
|
|
|
|
On a machine that already has a valid `config\app_config.json`, the first-run
|
|
flow is skipped entirely and the cached playlist plays immediately.
|
|
|
|
1. Config is created next to the **executable** (not in `%APPDATA%`)
|
|
- The .exe creates: `config/`, `media/`, `playlists/`, `logs/` locally
|
|
- This allows you to copy the entire `dist\KiwySignagePlayer\` folder anywhere and it works
|
|
2. The player is considered configured once `server_ip`, `screen_name` and
|
|
`quickconnect_key` all hold real values. Placeholder values
|
|
(`localhost`, `kivy-player`, `1234567`, `127.0.0.1`) count as unconfigured.
|
|
3. Edit `config\app_config.json` (next to the .exe) to set your server:
|
|
|
|
```json
|
|
{
|
|
"server_ip": "192.168.0.109",
|
|
"port": "8080",
|
|
"screen_name": "Birou_IT",
|
|
"quickconnect_key": "8887779",
|
|
"orientation": "Landscape",
|
|
"touch": "True",
|
|
"max_resolution": "1920x1080",
|
|
"edit_feature_enabled": true,
|
|
"use_https": false,
|
|
"verify_ssl": false
|
|
}
|
|
```
|
|
|
|
## 💳 Card Reader (Windows Edition)
|
|
|
|
The card reader now works on Windows via the **Raw Input API** (with a
|
|
low-level keyboard-hook fallback). It replaces the Linux-only `evdev`
|
|
implementation automatically when `run_win.py` starts.
|
|
|
|
- Detection mirrors the Linux logic:
|
|
1. A device named with `card` / `reader` / `rfid`
|
|
2. A USB HID keyboard (non-PS/2) — most card readers enumerate this way
|
|
3. Any remaining keyboard (excluding touchscreens/mice)
|
|
- Only keystrokes from the **selected device** are captured, so the
|
|
operator's real keyboard cannot pollute card data.
|
|
- Card data ends on **Enter** (same as Linux).
|
|
|
|
### Card reader config (optional)
|
|
|
|
Add any of these to `config\app_config.json` next to the .exe:
|
|
|
|
```json
|
|
{
|
|
"card_reader_mode": "auto", // "auto" | "raw" | "hook"
|
|
"card_reader_device": "", // e.g. "VID_08FF" to force a specific device
|
|
"card_reader_timeout": 5 // seconds
|
|
}
|
|
```
|
|
|
|
- `card_reader_mode`: `auto` (default, tries Raw Input then falls back),
|
|
`raw` (force Raw Input), or `hook` (force the low-level keyboard hook).
|
|
- `card_reader_device`: optional substring of the device name to pin the
|
|
reader (e.g. `VID_08FF`, `HID#VID_08FF`). Run the manual test below to see
|
|
the exact device names on your host.
|
|
- `card_reader_timeout`: how long the swipe popup waits (default 5 s).
|
|
|
|
### Manual card reader test (no GUI)
|
|
|
|
```batch
|
|
cd windows
|
|
venv\Scripts\activate
|
|
python win_card_reader.py
|
|
```
|
|
|
|
Swipe a card within 10 seconds — the tool prints the captured data, then
|
|
exits. The detected devices are listed in the console/log.
|
|
|
|
## 🧪 Testing
|
|
|
|
```batch
|
|
cd windows
|
|
venv\Scripts\activate
|
|
python run_win.py
|
|
```
|
|
|
|
## 🔧 Troubleshooting
|
|
|
|
| Problem | Solution |
|
|
|---------|----------|
|
|
| **"ffpyplayer not found"** | Install: `pip install ffpyplayer` |
|
|
| **"No video" / black screen** | Install FFmpeg and add to PATH. Try `KIVY_GL_BACKEND=angle_sdl2` or `KIVY_GL_BACKEND=gl` |
|
|
| **Kivy window doesn't open** | Run from command prompt to see error messages. Ensure GPU drivers are up to date. |
|
|
| **Weblinks not opening** | Install Google Chrome or Microsoft Edge |
|
|
| **Can't connect to server** | Check firewall. Try `use_https: false` and `verify_ssl: false` for testing |
|
|
| **Antivirus flags .exe** | Add the output folder to antivirus exclusions. This is a false positive common with PyInstaller. |
|
|
|
|
## 📁 Project Structure (Build)
|
|
|
|
```
|
|
Kiwy-Signage/
|
|
├── windows/
|
|
│ ├── run_win.py # Windows entry point (patches platform differences)
|
|
│ ├── build.spec # PyInstaller configuration
|
|
│ ├── build_win.bat # One-click build script
|
|
│ ├── pyi_runtime_hook.py # PyInstaller runtime hook
|
|
│ ├── requirements_win.txt # Windows Python dependencies
|
|
│ └── README_WINDOWS_BUILD.md # This file
|
|
├── src/
|
|
│ ├── main.py # Main application (original)
|
|
│ ├── get_playlists_v2.py # Playlist sync
|
|
│ ├── player_auth.py # Authentication
|
|
│ ├── ssl_utils.py # SSL/HTTPS
|
|
│ ├── keyboard_widget.py # On-screen keyboard
|
|
│ ├── network_monitor.py # Network monitoring
|
|
│ ├── edit_popup.py # Image editing
|
|
│ └── signage_player.kv # Kivy UI layout
|
|
├── config/
|
|
│ ├── app_config.json # Player configuration
|
|
│ └── resources/ # Icons, images, intro video
|
|
├── media/ # Downloaded media (created at runtime)
|
|
├── playlists/ # Playlist files (created at runtime)
|
|
└── logs/ # Log files (created at runtime)
|
|
```
|