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)
|
## 🚀 Quick Start (Development)
|
||||||
|
|
||||||
### Prerequisites
|
### 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
|
2. **FFmpeg** — for video codec support
|
||||||
- Download from [ffmpeg.org](https://ffmpeg.org/download.html)
|
- Download from [ffmpeg.org](https://ffmpeg.org/download.html)
|
||||||
- Add `bin\` folder to your PATH
|
- Add `bin\` folder to your PATH
|
||||||
@@ -46,8 +49,11 @@ The original app was built for **Raspberry Pi (Linux)**, using these technologie
|
|||||||
```batch
|
```batch
|
||||||
cd windows
|
cd windows
|
||||||
|
|
||||||
REM Create virtual environment
|
REM Create virtual environment with Python 3.12
|
||||||
python -m venv venv
|
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
|
venv\Scripts\activate
|
||||||
|
|
||||||
REM Install dependencies
|
REM Install dependencies
|
||||||
@@ -86,8 +92,10 @@ For a **single-file .exe**, edit `build.spec` — uncomment the `exe_onefile` se
|
|||||||
|
|
||||||
## ⚙️ Configuration
|
## ⚙️ Configuration
|
||||||
|
|
||||||
1. On first run, config files are created in `%APPDATA%\KiwySignage\`
|
1. On first run, config files are created in the **same folder as the executable** (not in `%APPDATA%`)
|
||||||
2. Edit `%APPDATA%\KiwySignage\config\app_config.json` to set your server:
|
- 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
|
```json
|
||||||
{
|
{
|
||||||
|
|||||||
+199
-9
@@ -139,6 +139,128 @@ def _windows_find_browser():
|
|||||||
return None
|
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():
|
def _patch_main():
|
||||||
"""Patch the main module after import for Windows compatibility."""
|
"""Patch the main module after import for Windows compatibility."""
|
||||||
# ── CRITICAL: Override Linux env vars BEFORE importing main ─────
|
# ── CRITICAL: Override Linux env vars BEFORE importing main ─────
|
||||||
@@ -170,7 +292,14 @@ def _patch_main():
|
|||||||
_original_play_weblink = signage_main.SignagePlayer.play_weblink
|
_original_play_weblink = signage_main.SignagePlayer.play_weblink
|
||||||
|
|
||||||
def _windows_play_weblink(self, url, duration):
|
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()
|
browser = _windows_find_browser()
|
||||||
if not browser:
|
if not browser:
|
||||||
from kivy.logger import Logger
|
from kivy.logger import Logger
|
||||||
@@ -195,14 +324,38 @@ def _patch_main():
|
|||||||
from kivy.logger import Logger
|
from kivy.logger import Logger
|
||||||
from kivy.clock import Clock
|
from kivy.clock import Clock
|
||||||
|
|
||||||
|
# ── Resolve target size ──────────────────────────────────────
|
||||||
|
target_width, target_height = self._get_browser_target_size()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
Logger.info(f"SignagePlayer: Opening weblink: {url} (browser: {browser})")
|
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()
|
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([
|
self._weblink_proc = subprocess.Popen([
|
||||||
browser,
|
browser,
|
||||||
'--kiosk',
|
'--new-window',
|
||||||
|
'--start-maximized',
|
||||||
|
'--start-fullscreen',
|
||||||
|
'--app=' + url,
|
||||||
'--no-first-run',
|
'--no-first-run',
|
||||||
'--noerrdialogs',
|
'--noerrdialogs',
|
||||||
'--disable-infobars',
|
'--disable-infobars',
|
||||||
@@ -210,9 +363,24 @@ def _patch_main():
|
|||||||
'--no-default-browser-check',
|
'--no-default-browser-check',
|
||||||
'--disable-session-crashed-bubble',
|
'--disable-session-crashed-bubble',
|
||||||
'--disable-features=TranslateUI',
|
'--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,
|
url,
|
||||||
], shell=False)
|
], 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)
|
Clock.unschedule(self.next_media)
|
||||||
self._start_inactivity_watchdog(duration)
|
self._start_inactivity_watchdog(duration)
|
||||||
self.preload_next_media()
|
self.preload_next_media()
|
||||||
@@ -220,6 +388,7 @@ def _patch_main():
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
Logger.error(f"SignagePlayer: Error opening weblink: {e}")
|
Logger.error(f"SignagePlayer: Error opening weblink: {e}")
|
||||||
|
_Win32Overlay.hide()
|
||||||
self.consecutive_errors += 1
|
self.consecutive_errors += 1
|
||||||
self._skip_to_next_media()
|
self._skip_to_next_media()
|
||||||
return False
|
return False
|
||||||
@@ -279,22 +448,43 @@ def _patch_main():
|
|||||||
|
|
||||||
# Patch the _kill_weblink_after_frame to work without Linux-specific code
|
# Patch the _kill_weblink_after_frame to work without Linux-specific code
|
||||||
def _windows_kill_weblink_after_frame(self):
|
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
|
from kivy.clock import Clock
|
||||||
self._stop_inactivity_watchdog()
|
self._stop_inactivity_watchdog()
|
||||||
self._kill_weblink_preload()
|
self._kill_weblink_preload()
|
||||||
proc = self._weblink_proc
|
proc = self._weblink_proc
|
||||||
self._weblink_proc = None
|
self._weblink_proc = None
|
||||||
if proc is not None and proc.poll() is None:
|
|
||||||
|
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):
|
def _do_kill(dt):
|
||||||
if proc.poll() is None:
|
if proc.poll() is None:
|
||||||
|
_windows_kill_process_tree(proc)
|
||||||
|
Logger.debug("SignagePlayer: Closed weblink browser (deferred)")
|
||||||
|
# Bring Kivy window to front
|
||||||
try:
|
try:
|
||||||
proc.terminate()
|
from kivy.core.window import Window as KivyWindow
|
||||||
try:
|
KivyWindow.show()
|
||||||
proc.wait(timeout=3)
|
KivyWindow.raise_window()
|
||||||
except Exception:
|
|
||||||
proc.kill()
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
# Hide overlay — Kivy should be visible now
|
||||||
|
_Win32Overlay.hide()
|
||||||
|
|
||||||
Clock.schedule_once(_do_kill, 0)
|
Clock.schedule_once(_do_kill, 0)
|
||||||
signage_main.SignagePlayer._kill_weblink_after_frame = _windows_kill_weblink_after_frame
|
signage_main.SignagePlayer._kill_weblink_after_frame = _windows_kill_weblink_after_frame
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user