Files
Kiwy-Signage/windows/development-track.md
T
ske087 31ad592e98 Fix Windows player: kiosk lockdown, robust video transitions, keep-awake
- Production/kiosk mode: exit_on_escape=0, window-close guard, Ctrl+C
  ignore, Alt+F4/Alt+Tab/Win/Ctrl+Esc keyboard hook (Windows)
- Robust video playback: async (non-blocking) ffpyplayer teardown,
  video progress watchdog (advance at true clip end), EOS re-entrancy
  guard, stale-advance guard, focus keeper for foreground retention
- Resume playback timer after Settings/exit popups close
- Windows keep-awake: SetThreadExecutionState + disable screensaver/
  lock screen (restored on exit)
- Always-on playback_trace.log for diagnosing transitions
- exe metadata: app_icon.ico + version_info.txt (publisher identity)
2026-08-04 15:57:33 +03:00

290 lines
15 KiB
Markdown

# 🧪 Development Track — Kiwy Signage Player (Windows Edition)
> This file tracks every change, bug fix, tested solution, build info, and
> pending issues for the Windows port. Read this FIRST before starting any
> debugging or coding session.
---
## 📅 Current Session — 2026-07-31
| Field | Value |
|-------|-------|
| **Branch** | `Windows-Player` |
| **Python** | 3.12.9 — `windows\venv\` (250 MB, all deps installed) |
| **Kivy** | 2.3.1 |
| **PyInstaller** | 6.21.0 |
| **Last .exe build** | 2026-07-26 16:53 — `windows\dist\KiwySignagePlayer\KiwySignagePlayer.exe` (96 MB) |
| **Build command** | `.\venv\Scripts\python.exe -m PyInstaller build.spec --clean --noconfirm` |
### 📋 Cross-platform audit — Linux commands → Windows handling
Every Linux-only command in `src/` was cross-referenced against the patches
in `windows/run_win.py`. All are covered except the one listed below:
| # | File / method | Linux commands | Windows handling |
|---|---------------|----------------|------------------|
| 1 | `main.py` `signal_screen_activity()` | `xset`, `xdotool`, `xrandr`, `tvservice`, `wlopm`, `wlr-randr`, `ydotool` | ✅ patched → `SetThreadExecutionState` (ctypes) in `run_win.py` |
| 2 | `main.py` `play_weblink()` | `chromium-browser` / `chromium` | ✅ patched → CEF embedded, then Chrome/Edge subprocess |
| 3 | `main.py` `_start_inactivity_watchdog()` | `/dev/input/event*`, `select` | ✅ patched → fixed timer watchdog |
| 4 | `main.py` `CardReader` | `evdev`, `/dev/input/event*` | ✅ fake `evdev` injected → falls back |
| 5 | `main.py` `SettingsPopup.test_connection` | `/tmp/temp_auth_test.json` | ✅ patched → `tempfile.gettempdir()` |
| 6 | `main.py` weblink kill/prewarm wrappers | `proc.terminate()` only | ✅ patched → `taskkill /F /T` + `_Win32Overlay` |
| 7 | `network_monitor.py` `_test_server_connection()` | `ping -c 3 -W 3` | ❌ **was unpatched** → ✅ **fixed 2026-07-31** |
| 8 | `network_monitor.py` `_restart_wifi()` | `sudo rfkill`, `sudo ifconfig`, `sudo dhclient` | ❌ **was unpatched** → ✅ **fixed 2026-07-31** |
| 9 | `get_playlists_v2.py`, `player_auth.py`, `ssl_utils.py`, `edit_popup.py`, `keyboard_widget.py` | none | ✅ no Linux commands |
---
## 🐛 Bug Tracker
### [BUG-010] NetworkMonitor uses Linux-only ping + rfkill commands
- **Status:** ✅ **Fixed — 2026-07-31**
- **Symptom:** `network_monitor.py` ran `ping -c 3 -W 3` (Linux flags) and on
connection failure invoked `sudo rfkill` / `sudo ifconfig wlan0` /
`sudo dhclient` — all fail or hang on Windows (`sudo` isn't even present).
- **Root cause:** This module was missed when the other Linux paths were
patched in `run_win.py`.
- **Fix:** Made `network_monitor.py` self-contained cross-platform:
1. Added `IS_WINDOWS = platform.system() == 'Windows'`
2. `_test_server_connection()` uses `ping -n 3 -w 3000` on Windows
3. `_restart_wifi()` dispatches to `_restart_wifi_windows()`
(`netsh wlan disconnect` → wait → `netsh wlan connect`) or
`_restart_wifi_linux()` (original rfkill/ifconfig/dhclient path kept intact)
- **Files:** `src/network_monitor.py`
- **Test:** Windows `ping -n 3 -w 3000 localhost` returns 0; AST parse OK.
---
### [BUG-011] Weblink never displays on Windows (opens behind Kivy / exits instantly)
- **Status:** ✅ **Fixed — 2026-07-31**
- **Symptom:** Web link items don't show. In the console log the weblink item
is reached but no browser appears, then playback moves on.
- **Root causes (two compounding):**
1. **Chrome re-used an existing instance.** `subprocess.Popen([chrome, '--new-window', url])`
delegates the URL to the already-running Chrome process and this launched
process **exits immediately** (`poll() != None`) → the watchdog fired
instantly and advanced to the next item, so the weblink never displayed.
2. **Overlay-hide raised Kivy over Chrome.** `_hide_overlay()` called
`_bring_kivy_to_front()`, so even when Chrome did open it sat *behind*
the borderless-fullscreen Kivy window.
- **Fix (in `windows/run_win.py`):**
1. Launch Chrome/Edge with a **dedicated `--user-data-dir`** (`<data>/.kiosk-profile`)
so a brand-new, trackable browser instance is created instead of
delegating to an existing one. Also guarantees a top-level window we can
enumerate, raise, and `taskkill` without touching the user's profile.
2. `_hide_overlay()` now calls **`_bring_chrome_to_front(proc)`** (new helper
that enumerates `Chrome_WidgetWin_1/0` windows owned by the launched PID)
instead of raising Kivy.
- **Update (2026-07-31 15:42):** added **`--kiosk`** flag to the weblink launch
args so the browser opens in true kiosk mode (no UI/chrome, locks to screen).
Safe with the dedicated `--user-data-dir` — does not affect the user's normal
browser session.
- **Update (2026-07-31 16:04):** replaced the fixed 1.0s overlay-hide timer with
**adaptive polling** (`_hide_overlay_when_chrome_ready`). The black overlay now
stays up until Chrome's window is actually detected on screen
(`_find_chrome_hwnd`), so the host desktop is never exposed during cold
starts / slow disk / GPU init. Falls back to Kivy after a 6s timeout.
- **Update (2026-07-31 16:19):** added a **persistent `_Win32Backdrop`** — a
fullscreen black window created at player startup (`_Win32Backdrop.show()`)
placed at `HWND_BOTTOM` (below Kivy & the kiosk browser, above the desktop),
destroyed only on clean exit. Any browser load/unload gap now reveals clean
black instead of the host desktop.
- **Test:** exe rebuilt 2026-07-31 16:19; DLL set intact (28 DLLs incl. FFmpeg).
### [BUG-012] Next widget never comes to foreground after weblink ends
- **Status:** ✅ **Fixed — 2026-07-31**
- **Symptom:** After a weblink finishes, the next media/widget renders but the
Kivy window stays behind (or the window focus is lost) — user sees the wrong
window / frozen view.
- **Root cause:** `_bring_kivy_to_front()` did `import win32con`, but
`win32con` is a pure-Python module in `win32\lib\` that is **only importable
via the `pywin32.pth` file**. `.pth` files are ignored in frozen PyInstaller
apps, so `win32con` was never bundled (confirmed via `pyi-archive_viewer`
only `win32gui.pyd` / `win32api.pyd` / `win32process.pyd` present). The
`import win32con` threw, the whole function silently fell back to
`Window.raise_window()`, and the Kivy window was never reliably raised.
- **Fix (in `windows/run_win.py`):**
1. Replaced the `win32con` dependency with **raw ctypes + numeric constants**
(`_SW_SHOWNORMAL`, `_SWP_*`, `_HWND_TOPMOST`, …).
2. New `_bring_hwnd_to_front(hwnd)` — ctypes-only `SetForegroundWindow` with
`AttachThreadInput` foreground-lock bypass + `IsIconic` restore + topmost
flash.
3. `_bring_kivy_to_front()` now uses `_find_kivy_hwnd()` (win32gui.EnumWindows
for `SDL_app`) + `_bring_hwnd_to_front()`, with Kivy `raise_window()` as
last-resort fallback.
- **Test:** exe rebuilt; no `win32con` import remains in `run_win.py`.
---
### [BUG-001] RecursionError: play_current_media ↔ restart_playlist
- **Status:** ✅ Fixed 2026-07-24
- **Symptom:** Pressing "Restart Player" in settings with empty playlist causes
infinite recursion: `play_current_media → restart_playlist → play_current_media → ...`
- **Fix:** Added empty-playlist guard in both `play_current_media()` and
`restart_playlist()` → they return early instead of calling each other.
- **Files:** `src/main.py` — lines ~1304 and ~2073
- **Test:** Verified no Python syntax errors via `ast.parse`.
### [BUG-002] Settings fields cut off on small screens
- **Status:** ✅ Fixed 2026-07-24
- **Symptom:** "Screen Name", "Quickconnect" and other fields at the top of
the settings popup are invisible on smaller resolutions because content
overflows the popup.
- **Fix:** Wrapped settings content in a `ScrollView`. Moved "Save & Close" /
"Cancel" buttons outside the scroll (always visible). Reduced row heights.
- **Files:** `src/signage_player.kv``<SettingsPopup@Popup>` block
### [BUG-003] Chromium not fullscreen on Windows
- **Status:** ✅ Fixed 2026-07-24
- **Symptom:** Web links open in a small window instead of fullscreen.
- **Fix:** Changed launch args from `--kiosk` to `--start-maximized --app=URL`
+ explicit `--window-size=WxH`. `--kiosk` uses Wayland exclusive-fullscreen
protocol which doesn't work on Windows.
- **Tested rejected solutions:**
-`--kiosk` alone → small window, no fullscreen
-`--start-fullscreen` alone → not reliable
-`--start-maximized --app=URL --window-size=...` → works
- **Files:** `windows/run_win.py``_windows_play_weblink()`
### [BUG-004] Desktop flash when switching between Chromium and Kivy
- **Status:** ✅ Fixed 2026-07-24
- **Symptom:** When Chrome closes, the desktop is briefly visible before Kivy
reappears. Also when Chrome opens, there's a flash.
- **Fix:** Added `_Win32Overlay` class — a fullscreen black Win32 window that
covers the screen during transitions. Shown BEFORE closing Chrome / opening
Chrome, hidden AFTER Kivy is ready.
- **Tested rejected solutions:**
-`Window.raise_window()` alone → still shows flash
- ✅ Win32 black overlay → smooth masking
- **Files:** `windows/run_win.py``_Win32Overlay` class
### [BUG-005] Chrome processes linger after closing weblink
- **Status:** ✅ Fixed 2026-07-24
- **Symptom:** After a weblink item ends, Chrome child processes (GPU,
renderer) remain running → blank windows accumulate.
- **Fix:** Use `taskkill /F /T /PID <pid>` to kill the entire process tree.
- **Tested rejected solutions:**
-`proc.terminate()` → leaves children running
-`proc.kill()` → same problem
-`taskkill /F /T` → kills everything
- **Files:** `windows/run_win.py``_windows_kill_process_tree()`
### [BUG-007] Video plays behind Chromium on weblink→media transition
- **Status:** ✅ **Fixed — 2026-07-26 (final)**
- **Symptom:** When a weblink ends and the next media starts, the media plays
*behind* Chromium. Audio is heard but user sees Chrome.
- **Root cause (Windows):** Linux renders Kivy widget UNDER Chromium → closes
Chrome → widget visible. On Windows Chrome stays ON TOP.
`Window.raise_window()` is unreliable. Three compounding issues:
1. `KivyWindow.minimize()` made Kivy impossible to bring back reliably
2. `_windows_play_current_media` killed the browser but never restored
`content_area.opacity = 1`, so next widget rendered invisible
3. `_bring_kivy_to_front()` failed because Windows `SetForegroundWindow`
refuses to let a background process steal focus
- **Fix applied (2026-07-26):**
1. **Removed `KivyWindow.minimize()`** in `_windows_play_weblink()` — Kivy
stays visible behind the overlay instead of being hidden
2. **Restored `content_area.opacity = 1`** in `_windows_play_current_media`
and `_windows_kill_weblink_after_frame()` — ensures next widget is visible
3. **`_bring_kivy_to_front()`** — added `AttachThreadInput()` to bypass
Windows foreground lock so Kivy can steal focus from Chrome
4. **Overlay hide** now calls `_bring_kivy_to_front()` instead of
`Window.raise_window()`
5. **CEF path** (`_windows_kill_weblink_after_frame`) now also calls
`_bring_kivy_to_front()` after hiding
- **Note:** `cefpython3` requires Python 3.10 — falls back to subprocess
Chrome/Edge on 3.12.9. Transition now works reliably with subprocess path.
- **Files:** `windows/run_win.py`
### [BUG-008] Intro video and media files not found at runtime
- **Status:** ✅ **Fixed** 2026-07-24
- **Symptom:** `[ERROR] [Image] Error loading <...intro1.mp4>` — intro
broken. Also `❌ Media file not found` for playlist items.
- **Root cause:** Media download only ran when `server_version > local_version`.
When versions matched (v16 == v16), `download_media_files` was never called
→ media folder stayed empty.
- **Fix:** Added download check in the "up to date" branch — now downloads
missing media files even when playlist version hasn't changed.
### [BUG-009] Video never advances to next item (EOS handler empty)
- **Status:** ✅ **Fixed** 2026-07-24
- **Symptom:** Video plays but never advances to the next playlist item.
- **Root cause:** `_on_video_eos()` callback was a stub — just logged
"Video finished playing (EOS)" but never called `next_media()`.
- **Fix:** Added `Clock.unschedule(self.next_media)` + `Clock.schedule_once`
to advance after 0.5s when a video reaches end of stream.
---
## 🧪 Tested & Rejected Solutions Log
> Keep a record of approaches that were tried and didn't work, so we don't
> waste time re-testing them.
| Date | What was tested | Result | Reason it failed |
|------|----------------|--------|-----------------|
| 2026-07-24 | Python 3.14 with Kivy | ❌ | `kivy_deps.sdl2_dev~=0.8.0` has no cp314 wheel |
| 2026-07-24 | `--kiosk` Chrome flag on Windows | ❌ | Not fullscreen, Wayland exclusive-fullscreen not available |
| 2026-07-24 | `--start-fullscreen` alone | ❌ | Inconsistent, sometimes not full |
| 2026-07-24 | `proc.terminate()` for Chrome | ❌ | Leaves child processes running |
| 2026-07-24 | `proc.kill()` for Chrome | ❌ | Same as terminate — children survive |
| 2026-07-24 | `Window.raise_window()` for transition | ❌ | Brief desktop flash visible |
---
## 📁 Data Directory Behaviour
When the .exe runs:
1. Runtime hook (`pyi_runtime_hook.py`) sets `KIWY_DATA_DIR = exe_dir`
2. `run_win.py` patches `SignagePlayer.__init__` to use `KIWY_DATA_DIR`
3. Local folders created next to the .exe:
```
KiwySignagePlayer.exe
config/
app_config.json
resources/ (icons, intro video)
certs/ (SSL certificates)
media/
edited_media/
playlists/
logs/
.kivy/ (Kivy home)
.player_heartbeat
```
---
## 🔧 Build Cheatsheet
```powershell
# Build the .exe (from windows/ directory)
Set-Location windows
& .\venv\Scripts\python.exe -m PyInstaller build.spec --clean --noconfirm
# Run in dev mode (no build needed)
& .\venv\Scripts\python.exe run_win.py
# Test imports only
& .\venv\Scripts\python.exe test_import_fix.py
```
---
## 📝 Notes for the Next Session
- [x] ~~Investigate [BUG-006]~~ → merged into [BUG-007], fixed with CEF + win32gui
- [x] ~~Test `SetForegroundWindow`~~ → `_bring_kivy_to_front()` uses `win32gui`
- [x] Install `cefpython3` — embedded Chromium, no more subprocess
- [x] ~~Verify CEF embedded browser actually works at runtime~~ → CEF needs Python 3.10, falls back to Chrome/Edge
- [x] ~~Test the subprocess fallback path when CEF is unavailable~~ → Tested and working with `_bring_kivy_to_front()`
- [x] ~~Check why `AsyncImage` error shows for intro1.mp4 (path issue)~~ → Runtime hook copies resources to exe dir
- [x] ~~Ensure media files are downloaded before playback~~ → `pyi_runtime_hook.py` copies config/resources on first run
- [x] ~~Add `cef_browser.py` to PyInstaller hidden imports~~ → Already in `build.spec`
- [x] ~~Make `network_monitor.py` Windows-compatible~~ → [BUG-010] fixed 2026-07-31 (`ping -n` / `netsh wlan` on Windows, rfkill path preserved on Linux)
- [ ] Rebuild the .exe to pick up the `network_monitor.py` fix
- [ ] Clean `cefpython3` from `venv/` (Python 3.12 won't use it anyway)
- [ ] Verify the .exe works on a fresh Windows machine (no Python installed)
- [ ] Test the `taskkill` fallback path on a machine without Chrome/Edge installed
- [ ] Add a standalone `.bat` launcher for development mode