Fix Windows weblink handling: fullscreen Chrome, black overlay masking, proper process tree kill
Windows-specific fixes: - _windows_play_weblink: uses --start-maximized + --app=URL for true fullscreen - Shows black Win32 overlay before opening/closing Chrome to mask desktop - _windows_kill_process_tree: uses taskkill /F /T to kill all Chrome child processes - _Win32Overlay class: fullscreen borderless always-on-top black window - Updated README to note Python 3.12 requirement and local data dir behavior
This commit is contained in:
@@ -36,7 +36,10 @@ The original app was built for **Raspberry Pi (Linux)**, using these technologie
|
||||
## 🚀 Quick Start (Development)
|
||||
|
||||
### Prerequisites
|
||||
1. **Python 3.10+** (64-bit) — [python.org](https://python.org)
|
||||
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
|
||||
@@ -46,8 +49,11 @@ The original app was built for **Raspberry Pi (Linux)**, using these technologie
|
||||
```batch
|
||||
cd windows
|
||||
|
||||
REM Create virtual environment
|
||||
python -m venv venv
|
||||
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
|
||||
@@ -86,8 +92,10 @@ For a **single-file .exe**, edit `build.spec` — uncomment the `exe_onefile` se
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
1. On first run, config files are created in `%APPDATA%\KiwySignage\`
|
||||
2. Edit `%APPDATA%\KiwySignage\config\app_config.json` to set your server:
|
||||
1. On first run, config files are created in the **same folder as the executable** (not in `%APPDATA%`)
|
||||
- The .exe creates: `config/`, `media/`, `playlists/`, `logs/` directories locally
|
||||
- This allows you to copy the entire `dist\KiwySignagePlayer\` folder anywhere and it works
|
||||
2. Edit `config\app_config.json` (next to the .exe) to set your server:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
+205
-15
@@ -139,6 +139,128 @@ def _windows_find_browser():
|
||||
return None
|
||||
|
||||
|
||||
# ── Win32 API helpers via ctypes ─────────────────────────────────────
|
||||
class _Win32Overlay:
|
||||
"""Fullscreen black overlay window to mask desktop during transitions.
|
||||
|
||||
When switching away from Chromium, the browser window disappears and
|
||||
there is a brief moment where the desktop is visible before Kivy
|
||||
manages to bring its window to the front. This overlay covers that
|
||||
flash with a pure-black borderless always-on-top Win32 window.
|
||||
"""
|
||||
|
||||
_hwnd = None
|
||||
_class_atom = None
|
||||
|
||||
@classmethod
|
||||
def show(cls):
|
||||
"""Create a fullscreen black overlay on top of everything."""
|
||||
if cls._hwnd is not None:
|
||||
return # already showing
|
||||
try:
|
||||
user32 = ctypes.windll.user32
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
|
||||
# Register a simple window class
|
||||
WNDPROC = ctypes.WINFUNCTYPE(
|
||||
ctypes.c_int64, ctypes.c_int64, ctypes.c_uint,
|
||||
ctypes.c_uint64, ctypes.c_int64
|
||||
)
|
||||
|
||||
@WNDPROC
|
||||
def wnd_proc(hwnd, msg, wparam, lparam):
|
||||
if msg == 0x0002: # WM_DESTROY
|
||||
user32.PostQuitMessage(0)
|
||||
if msg == 0x0014: # WM_ERASEBKGND
|
||||
return 1 # tell Windows we erased it
|
||||
return user32.DefWindowProcW(hwnd, msg, wparam, lparam)
|
||||
|
||||
hinstance = kernel32.GetModuleHandleW(None)
|
||||
|
||||
# Register class
|
||||
class_name = 'KiwyOverlay_' + str(ctypes.c_uint64(int(kernel32.GetTickCount64())).value)
|
||||
wc = ctypes.create_unicode_buffer(256)
|
||||
|
||||
_WNDCLASS = ctypes.c_byte * (6 * 8) # rough size
|
||||
buf = _WNDCLASS()
|
||||
# Simple approach: use RegisterClassExW
|
||||
user32.RegisterClassExW.restype = ctypes.c_uint16
|
||||
user32.RegisterClassExW.argtypes = [ctypes.c_void_p]
|
||||
|
||||
# We'll use a simpler method: just create a MessageBox-style window
|
||||
# Actually, let's use the simplest possible approach:
|
||||
|
||||
# Get screen dimensions
|
||||
screen_w = user32.GetSystemMetrics(0) # SM_CXSCREEN
|
||||
screen_h = user32.GetSystemMetrics(1) # SM_CYSCREEN
|
||||
|
||||
# Create a borderless always-on-top window
|
||||
cls._hwnd = user32.CreateWindowExW(
|
||||
0x00000008, # WS_EX_TOPMOST | WS_EX_TOOLWINDOW
|
||||
b'#32770', # Dialog class - always available
|
||||
b'', # no title
|
||||
0x80000000 | 0x10000000, # WS_POPUP | WS_VISIBLE
|
||||
0, 0, screen_w, screen_h,
|
||||
0, 0, hinstance, 0
|
||||
)
|
||||
|
||||
if cls._hwnd:
|
||||
# Make it black
|
||||
from ctypes import wintypes
|
||||
gdi32 = ctypes.windll.gdi32
|
||||
hdc = user32.GetDC(cls._hwnd)
|
||||
rect = (ctypes.c_long * 4)(0, 0, screen_w, screen_h)
|
||||
brush = gdi32.CreateSolidBrush(0x00000000) # black brush
|
||||
gdi32.FillRect(hdc, ctypes.byref(rect), brush)
|
||||
gdi32.DeleteObject(brush)
|
||||
user32.ReleaseDC(cls._hwnd, hdc)
|
||||
|
||||
# Force it to the top
|
||||
user32.SetWindowPos(cls._hwnd, -1, 0, 0, screen_w, screen_h, 0x0002 | 0x0040)
|
||||
user32.ShowWindow(cls._hwnd, 1) # SW_SHOWNORMAL
|
||||
user32.UpdateWindow(cls._hwnd)
|
||||
except Exception:
|
||||
cls._hwnd = None # failed gracefully
|
||||
|
||||
@classmethod
|
||||
def hide(cls):
|
||||
"""Destroy the overlay window."""
|
||||
if cls._hwnd is None:
|
||||
return
|
||||
try:
|
||||
user32 = ctypes.windll.user32
|
||||
user32.DestroyWindow(cls._hwnd)
|
||||
except Exception:
|
||||
pass
|
||||
cls._hwnd = None
|
||||
|
||||
|
||||
def _windows_kill_process_tree(proc):
|
||||
"""Kill a process AND all its children using taskkill.
|
||||
|
||||
Chrome/Edge spawns many child processes (GPU, renderer, network,
|
||||
etc.). A simple proc.terminate() leaves children running, causing
|
||||
lingering browser windows or zombie processes.
|
||||
"""
|
||||
if proc is None or proc.poll() is not None:
|
||||
return
|
||||
try:
|
||||
subprocess.run(
|
||||
['taskkill', '/F', '/T', '/PID', str(proc.pid)],
|
||||
capture_output=True, timeout=5
|
||||
)
|
||||
except Exception:
|
||||
# Fallback: try terminate + kill
|
||||
try:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=3)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _patch_main():
|
||||
"""Patch the main module after import for Windows compatibility."""
|
||||
# ── CRITICAL: Override Linux env vars BEFORE importing main ─────
|
||||
@@ -170,7 +292,14 @@ def _patch_main():
|
||||
_original_play_weblink = signage_main.SignagePlayer.play_weblink
|
||||
|
||||
def _windows_play_weblink(self, url, duration):
|
||||
"""Windows-compatible weblink handler using Chrome/Edge."""
|
||||
"""Windows-compatible weblink handler using Chrome/Edge.
|
||||
|
||||
Fixes on Windows:
|
||||
- Uses --start-maximized + --window-size to ensure fullscreen
|
||||
- Uses --app=URL instead of bare URL for app-like fullscreen
|
||||
- Kills the entire Chrome process tree to prevent lingering
|
||||
- Shows a black overlay BEFORE closing Chrome to mask desktop
|
||||
"""
|
||||
browser = _windows_find_browser()
|
||||
if not browser:
|
||||
from kivy.logger import Logger
|
||||
@@ -195,14 +324,38 @@ def _patch_main():
|
||||
from kivy.logger import Logger
|
||||
from kivy.clock import Clock
|
||||
|
||||
# ── Resolve target size ──────────────────────────────────────
|
||||
target_width, target_height = self._get_browser_target_size()
|
||||
|
||||
try:
|
||||
Logger.info(f"SignagePlayer: Opening weblink: {url} (browser: {browser})")
|
||||
Logger.info(
|
||||
f"SignagePlayer: Weblink target size: {target_width}x{target_height}"
|
||||
)
|
||||
self._kill_weblink_preload()
|
||||
|
||||
# Launch in fullscreen / kiosk-like mode
|
||||
# Hide Kivy content so it doesn't show underneath Chrome
|
||||
from kivy.core.window import Window as KivyWindow
|
||||
try:
|
||||
self.ids.content_area.opacity = 0
|
||||
KivyWindow.minimize()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Show a black overlay BEFORE Chrome opens — masks any
|
||||
# desktop flash during the transition.
|
||||
_Win32Overlay.show()
|
||||
|
||||
# Launch in fullscreen / app mode on Windows
|
||||
# --start-maximized ensures it fills the screen on first paint
|
||||
# --app=URL gives a window without address bar
|
||||
# --window-size ensures the browser targets the correct resolution
|
||||
self._weblink_proc = subprocess.Popen([
|
||||
browser,
|
||||
'--kiosk',
|
||||
'--new-window',
|
||||
'--start-maximized',
|
||||
'--start-fullscreen',
|
||||
'--app=' + url,
|
||||
'--no-first-run',
|
||||
'--noerrdialogs',
|
||||
'--disable-infobars',
|
||||
@@ -210,9 +363,24 @@ def _patch_main():
|
||||
'--no-default-browser-check',
|
||||
'--disable-session-crashed-bubble',
|
||||
'--disable-features=TranslateUI',
|
||||
'--disable-sync',
|
||||
'--disable-background-networking',
|
||||
'--no-default-browser-check',
|
||||
'--window-position=0,0',
|
||||
f'--window-size={target_width},{target_height}',
|
||||
'--force-device-scale-factor=1',
|
||||
url,
|
||||
], shell=False)
|
||||
|
||||
# Give Chrome a moment to cover the screen, then hide overlay
|
||||
def _hide_overlay(dt):
|
||||
_Win32Overlay.hide()
|
||||
try:
|
||||
KivyWindow.raise_window()
|
||||
except Exception:
|
||||
pass
|
||||
Clock.schedule_once(_hide_overlay, 1.5)
|
||||
|
||||
Clock.unschedule(self.next_media)
|
||||
self._start_inactivity_watchdog(duration)
|
||||
self.preload_next_media()
|
||||
@@ -220,6 +388,7 @@ def _patch_main():
|
||||
|
||||
except Exception as e:
|
||||
Logger.error(f"SignagePlayer: Error opening weblink: {e}")
|
||||
_Win32Overlay.hide()
|
||||
self.consecutive_errors += 1
|
||||
self._skip_to_next_media()
|
||||
return False
|
||||
@@ -279,23 +448,44 @@ def _patch_main():
|
||||
|
||||
# Patch the _kill_weblink_after_frame to work without Linux-specific code
|
||||
def _windows_kill_weblink_after_frame(self):
|
||||
"""Gracefully transition away from a weblink item on Windows.
|
||||
|
||||
Shows a black overlay FIRST, THEN kills Chrome, THEN brings
|
||||
Kivy to the front. This masks the desktop flash that happens
|
||||
between Chrome closing and Kivy reappearing.
|
||||
"""
|
||||
from kivy.clock import Clock
|
||||
self._stop_inactivity_watchdog()
|
||||
self._kill_weblink_preload()
|
||||
proc = self._weblink_proc
|
||||
self._weblink_proc = None
|
||||
if proc is not None and proc.poll() is None:
|
||||
def _do_kill(dt):
|
||||
if proc.poll() is None:
|
||||
try:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=3)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
Clock.schedule_once(_do_kill, 0)
|
||||
|
||||
if proc is not None and proc.poll() is not None:
|
||||
_Win32Overlay.hide()
|
||||
return
|
||||
|
||||
# Show black overlay to mask desktop while Chrome closes
|
||||
_Win32Overlay.show()
|
||||
|
||||
if proc is None:
|
||||
_Win32Overlay.hide()
|
||||
return
|
||||
|
||||
def _do_kill(dt):
|
||||
if proc.poll() is None:
|
||||
_windows_kill_process_tree(proc)
|
||||
Logger.debug("SignagePlayer: Closed weblink browser (deferred)")
|
||||
# Bring Kivy window to front
|
||||
try:
|
||||
from kivy.core.window import Window as KivyWindow
|
||||
KivyWindow.show()
|
||||
KivyWindow.raise_window()
|
||||
except Exception:
|
||||
pass
|
||||
# Hide overlay — Kivy should be visible now
|
||||
_Win32Overlay.hide()
|
||||
|
||||
Clock.schedule_once(_do_kill, 0)
|
||||
signage_main.SignagePlayer._kill_weblink_after_frame = _windows_kill_weblink_after_frame
|
||||
|
||||
# Patch _prewarm_weblink for Windows
|
||||
|
||||
Reference in New Issue
Block a user