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

15 KiB

🧪 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.

  • 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).
  • 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
  • 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

# 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

  • Investigate [BUG-006] → merged into [BUG-007], fixed with CEF + win32gui
  • Test SetForegroundWindow_bring_kivy_to_front() uses win32gui
  • Install cefpython3 — embedded Chromium, no more subprocess
  • Verify CEF embedded browser actually works at runtime → CEF needs Python 3.10, falls back to Chrome/Edge
  • Test the subprocess fallback path when CEF is unavailable → Tested and working with _bring_kivy_to_front()
  • Check why AsyncImage error shows for intro1.mp4 (path issue) → Runtime hook copies resources to exe dir
  • Ensure media files are downloaded before playbackpyi_runtime_hook.py copies config/resources on first run
  • Add cef_browser.py to PyInstaller hidden imports → Already in build.spec
  • 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