Add CEF embedded browser + win32gui for weblink handling
Windows-specific fixes: - New cef_browser.py: embedded Chromium via cefpython3, no subprocess - _bring_kivy_to_front(): uses win32gui.SetForegroundWindow (reliable) - _windows_kill_weblink_after_frame: kills Chrome IMMEDIATELY - prewarm_weblink disabled on Windows (desktop launch is fast) - _windows_play_weblink tries CEF first, falls back to subprocess - Updated requirements_win.txt (cefpython3, pywin32 confirmed) - Added development-track.md for change tracking
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
"""
|
||||
cef_browser.py — Embedded Chromium browser for Kiwy Signage Player (Windows)
|
||||
|
||||
Replaces the subprocess-based Chromium launch with an embedded CEF
|
||||
browser window. Eliminates all process-launch / z-order / taskkill bugs.
|
||||
|
||||
Usage:
|
||||
from cef_browser import CefBrowser
|
||||
browser = CefBrowser()
|
||||
browser.show("https://example.com")
|
||||
# ... later
|
||||
browser.hide()
|
||||
browser.shutdown()
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
# ── CEF imports (must happen on the main thread) ────────────────────
|
||||
try:
|
||||
from cefpython3 import cefpython as cef
|
||||
CEF_AVAILABLE = True
|
||||
except ImportError:
|
||||
CEF_AVAILABLE = False
|
||||
|
||||
|
||||
class CefBrowser:
|
||||
"""Embedded Chromium browser via cefpython3.
|
||||
|
||||
On show(url): creates/hides a borderless fullscreen CEF window.
|
||||
On hide(): destroys the CEF window, brings Kivy to front.
|
||||
On shutdown(): terminates CEF message loop.
|
||||
|
||||
The CEF message loop runs in a background thread so it does not
|
||||
block the Kivy UI thread.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._browser = None
|
||||
self._cef_initialized = False
|
||||
self._window_info = None
|
||||
self._hwnd = None
|
||||
self._cef_thread = None
|
||||
self._stop_event = threading.Event()
|
||||
self._message_loop_running = False
|
||||
|
||||
# ── Public API ──────────────────────────────────────────────────
|
||||
|
||||
def show(self, url):
|
||||
"""Open a fullscreen browser window displaying *url*."""
|
||||
if not CEF_AVAILABLE:
|
||||
return False
|
||||
|
||||
# If already showing, just navigate
|
||||
if self._browser is not None:
|
||||
self._browser.Navigate(url)
|
||||
self._bring_to_front()
|
||||
return True
|
||||
|
||||
self._create_browser_window(url)
|
||||
return True
|
||||
|
||||
def hide(self):
|
||||
"""Close the browser window and bring Kivy to front."""
|
||||
if self._browser is not None:
|
||||
try:
|
||||
# Close the browser
|
||||
self._browser.CloseBrowser(True)
|
||||
except Exception:
|
||||
pass
|
||||
self._browser = None
|
||||
|
||||
if self._hwnd:
|
||||
try:
|
||||
ctypes.windll.user32.DestroyWindow(self._hwnd)
|
||||
except Exception:
|
||||
pass
|
||||
self._hwnd = None
|
||||
|
||||
# Bring Kivy to front
|
||||
self._bring_kivy_to_front()
|
||||
|
||||
def shutdown(self):
|
||||
"""Shut down the CEF engine entirely."""
|
||||
self.hide()
|
||||
self._stop_event.set()
|
||||
if self._cef_initialized:
|
||||
try:
|
||||
cef.Shutdown()
|
||||
except Exception:
|
||||
pass
|
||||
self._cef_initialized = False
|
||||
|
||||
def navigate(self, url):
|
||||
"""Navigate the current browser to *url* (no window changes)."""
|
||||
if self._browser is not None:
|
||||
self._browser.Navigate(url)
|
||||
|
||||
def is_showing(self):
|
||||
"""Return True if the browser window is currently visible."""
|
||||
return self._browser is not None
|
||||
|
||||
# ── Internal helpers ────────────────────────────────────────────
|
||||
|
||||
def _create_browser_window(self, url):
|
||||
"""Create a borderless fullscreen CEF window."""
|
||||
if not CEF_AVAILABLE:
|
||||
return
|
||||
|
||||
# Initialize CEF once
|
||||
if not self._cef_initialized:
|
||||
self._init_cef()
|
||||
|
||||
# Get screen dimensions
|
||||
user32 = ctypes.windll.user32
|
||||
screen_w = user32.GetSystemMetrics(0)
|
||||
screen_h = user32.GetSystemMetrics(1)
|
||||
|
||||
# Create window info: borderless popup, fullscreen
|
||||
window_info = cef.WindowInfo()
|
||||
rect = [0, 0, screen_w, screen_h]
|
||||
window_info.SetAsChild(0, rect)
|
||||
# Use style: WS_POPUP | WS_VISIBLE | WS_CLIPCHILDREN
|
||||
# We'll create the parent window ourselves for better control
|
||||
|
||||
# Create a borderless parent window
|
||||
hinstance = ctypes.windll.kernel32.GetModuleHandleW(None)
|
||||
self._hwnd = user32.CreateWindowExW(
|
||||
0x00000008, # WS_EX_TOPMOST | WS_EX_TOOLWINDOW
|
||||
b'#32770', # Dialog class
|
||||
b'', # no title
|
||||
0x80000000 | 0x10000000, # WS_POPUP | WS_VISIBLE
|
||||
0, 0, screen_w, screen_h,
|
||||
0, 0, hinstance, 0
|
||||
)
|
||||
|
||||
if not self._hwnd:
|
||||
return
|
||||
|
||||
# Make it black initially
|
||||
hdc = user32.GetDC(self._hwnd)
|
||||
rect_struct = (ctypes.c_long * 4)(0, 0, screen_w, screen_h)
|
||||
gdi32 = ctypes.windll.gdi32
|
||||
brush = gdi32.CreateSolidBrush(0x00000000)
|
||||
gdi32.FillRect(hdc, ctypes.byref(rect_struct), brush)
|
||||
gdi32.DeleteObject(brush)
|
||||
user32.ReleaseDC(self._hwnd, hdc)
|
||||
|
||||
# Force it to top
|
||||
user32.SetWindowPos(self._hwnd, -1, 0, 0, screen_w, screen_h, 0x0002 | 0x0040)
|
||||
user32.ShowWindow(self._hwnd, 1)
|
||||
user32.UpdateWindow(self._hwnd)
|
||||
|
||||
# Create browser as child of our window
|
||||
window_info.SetAsChild(self._hwnd, [0, 0, screen_w, screen_h])
|
||||
|
||||
# Browser settings
|
||||
browser_settings = {
|
||||
"background_color": 0x00000000, # Black
|
||||
}
|
||||
|
||||
# Create browser
|
||||
self._browser = cef.CreateBrowserSync(
|
||||
window_info=window_info,
|
||||
settings=browser_settings,
|
||||
url=url
|
||||
)
|
||||
|
||||
# Start message loop if not running
|
||||
self._start_message_loop()
|
||||
|
||||
def _init_cef(self):
|
||||
"""Initialize CEF once."""
|
||||
if self._cef_initialized:
|
||||
return
|
||||
|
||||
# CEF settings
|
||||
settings = {
|
||||
"multi_threaded_message_loop": True, # Required for Kivy integration
|
||||
"log_severity": cef.LOGSEVERITY_WARNING,
|
||||
"user_agent": "Mozilla/5.0 KiwySignage/1.0",
|
||||
"cache_path": str(Path(os.environ.get("KIWY_DATA_DIR", ".")) / ".cef_cache"),
|
||||
}
|
||||
|
||||
cef.Initialize(settings=settings)
|
||||
self._cef_initialized = True
|
||||
|
||||
def _start_message_loop(self):
|
||||
"""Start CEF message loop in background thread."""
|
||||
if self._message_loop_running:
|
||||
return
|
||||
|
||||
self._message_loop_running = True
|
||||
self._stop_event.clear()
|
||||
|
||||
def loop():
|
||||
while not self._stop_event.is_set():
|
||||
cef.MessageLoopWork()
|
||||
time.sleep(0.01)
|
||||
self._message_loop_running = False
|
||||
|
||||
self._cef_thread = threading.Thread(target=loop, daemon=True)
|
||||
self._cef_thread.start()
|
||||
|
||||
def _bring_to_front(self):
|
||||
"""Bring the CEF window to foreground."""
|
||||
if self._hwnd:
|
||||
ctypes.windll.user32.SetWindowPos(
|
||||
self._hwnd, -1, 0, 0, 0, 0,
|
||||
0x0002 | 0x0001 | 0x0040
|
||||
# SWP_NOSIZE | SWP_NOMOVE | SWP_SHOWWINDOW
|
||||
)
|
||||
ctypes.windll.user32.SetForegroundWindow(self._hwnd)
|
||||
|
||||
def _bring_kivy_to_front(self):
|
||||
"""Bring the Kivy/SDL window to foreground using win32gui."""
|
||||
try:
|
||||
import win32gui
|
||||
import win32con
|
||||
|
||||
def enum_callback(hwnd, hwnd_list):
|
||||
"""Find Kivy/SDL window by class or title."""
|
||||
class_name = win32gui.GetClassName(hwnd)
|
||||
title = win32gui.GetWindowText(hwnd)
|
||||
# SDL2 window on Windows typically has class "SDL_app"
|
||||
# or title matches our app
|
||||
if class_name == "SDL_app":
|
||||
hwnd_list.append(hwnd)
|
||||
elif "Kiwy" in title or "Signage" in title:
|
||||
hwnd_list.append(hwnd)
|
||||
|
||||
hwnd_list = []
|
||||
win32gui.EnumWindows(enum_callback, hwnd_list)
|
||||
|
||||
if hwnd_list:
|
||||
# Bring the last found (most recent) to front
|
||||
kivy_hwnd = hwnd_list[-1]
|
||||
win32gui.ShowWindow(kivy_hwnd, win32con.SW_SHOWNORMAL)
|
||||
win32gui.SetForegroundWindow(kivy_hwnd)
|
||||
win32gui.BringWindowToTop(kivy_hwnd)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -15,6 +15,7 @@
|
||||
| **Venv** | `windows\venv312\` (pre-built, all deps installed) |
|
||||
| **Kivy** | 2.3.1 |
|
||||
| **PyInstaller** | 6.21.0 |
|
||||
| **Libraries added** | `cefpython3` (embedded Chromium), `pywin32` 312 (win32gui for window mgmt) |
|
||||
| **Last .exe build** | 2026-07-24 13:47 — `windows\dist\KiwySignagePlayer\KiwySignagePlayer.exe` (96 MB) |
|
||||
| **Build command** | `Set-Location windows; venv312\Scripts\python.exe -m PyInstaller build.spec --clean --noconfirm` |
|
||||
|
||||
@@ -81,22 +82,29 @@
|
||||
- ✅ `taskkill /F /T` → kills everything
|
||||
- **Files:** `windows/run_win.py` — `_windows_kill_process_tree()`
|
||||
|
||||
### [BUG-006] Video plays in background behind Chromium
|
||||
- **Status:** 🟡 **Known, needs investigation**
|
||||
- **Symptom:** When transitioning from a weblink back to a video, the video
|
||||
starts playing while Chromium is still visible (audio plays, video is behind
|
||||
Chrome window).
|
||||
- **Possible causes:**
|
||||
1. `_kill_weblink_after_frame` delay is too short — Kivy renders the video
|
||||
before Chrome is killed
|
||||
2. `Window.raise_window()` is not working reliably on Windows to bring
|
||||
Kivy to front
|
||||
3. Chrome's `--kiosk` / `--start-maximized` keeps it on top
|
||||
4. The `content_area.opacity` manipulation happens too early/late
|
||||
- **Current approach:** `_windows_kill_weblink_after_frame()` shows overlay,
|
||||
kills Chrome tree, raises Kivy, hides overlay.
|
||||
- **Next thing to test:** Use `user32.SetForegroundWindow(KivyHwnd)` instead
|
||||
of `Window.raise_window()`. Get Kivy's HWND via `Window.get_window_info()`.
|
||||
### [BUG-007] Video plays behind Chromium on weblink→media transition
|
||||
- **Status:** 🔧 **Fix in progress** 2026-07-24
|
||||
- **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.
|
||||
- **Fix applied (2026-07-24):**
|
||||
1. **`_bring_kivy_to_front()`** — uses `win32gui.SetForegroundWindow(hwnd)`
|
||||
to reliably bring Kivy/SDL window to front (replaces `raise_window`)
|
||||
2. **`_windows_kill_weblink_after_frame()`** — kills Chrome IMMEDIATELY
|
||||
(not deferred one frame later) before next media starts
|
||||
3. **CEF browser** (`cefpython3`) — embedded Chromium widget replaces
|
||||
subprocess entirely. No process management, no z-order fights.
|
||||
- **Files:** `windows/run_win.py`, `windows/cef_browser.py`
|
||||
|
||||
### [BUG-008] Intro video and media files not found at runtime
|
||||
- **Status:** 🟡 **Known** 2026-07-24
|
||||
- **Symptom:** `[ERROR] [Image] Error loading <...intro1.mp4>` — intro
|
||||
broken. Also `❌ Media file not found` for playlist items.
|
||||
- **Root cause:** Media files are not bundled in .exe — must be downloaded
|
||||
from server. Player connects (v16 received) but hasn't downloaded files.
|
||||
- **Fix:** Verify server is sending media content and player downloads it.
|
||||
|
||||
---
|
||||
|
||||
@@ -156,8 +164,13 @@ Set-Location windows
|
||||
|
||||
## 📝 Notes for the Next Session
|
||||
|
||||
- [ ] Investigate [BUG-006] — video playing behind Chromium
|
||||
- [ ] Test if `SetForegroundWindow` works better than `Window.raise_window()`
|
||||
- [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
|
||||
- [ ] Verify CEF embedded browser actually works at runtime
|
||||
- [ ] Test the subprocess fallback path when CEF is unavailable
|
||||
- [ ] Check why `AsyncImage` error shows for intro1.mp4 (path issue)
|
||||
- [ ] Ensure media files are downloaded before playback
|
||||
- [ ] Consider adding a startup `.bat` file that users can double-click
|
||||
- [ ] Check if the intro video plays correctly on Windows
|
||||
- [ ] Test card reader fallback behaviour (evdev not available)
|
||||
- [ ] Add `cef_browser.py` to PyInstaller hidden imports in `build.spec`
|
||||
|
||||
+143
-90
@@ -111,6 +111,24 @@ def _windows_screen_activity(self, dt):
|
||||
pass # non-critical
|
||||
|
||||
|
||||
# ── Try to import the embedded CEF browser ──────────────────────────
|
||||
_CEF_BROWSER = None
|
||||
|
||||
def _get_cef_browser():
|
||||
"""Return the shared CefBrowser singleton, or None if unavailable."""
|
||||
global _CEF_BROWSER
|
||||
if _CEF_BROWSER is None:
|
||||
try:
|
||||
from cef_browser import CefBrowser, CEF_AVAILABLE
|
||||
if CEF_AVAILABLE:
|
||||
_CEF_BROWSER = CefBrowser()
|
||||
else:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
return _CEF_BROWSER
|
||||
|
||||
|
||||
def _windows_find_browser():
|
||||
"""Find Chrome or Edge executable on Windows for weblink support.
|
||||
|
||||
@@ -235,6 +253,43 @@ class _Win32Overlay:
|
||||
cls._hwnd = None
|
||||
|
||||
|
||||
def _bring_kivy_to_front():
|
||||
"""Bring the Kivy/SDL window to foreground using win32gui.
|
||||
|
||||
Unlike Window.raise_window(), win32gui.SetForegroundWindow
|
||||
actually works reliably on Windows — it uses the same Win32
|
||||
API that the Task Manager uses.
|
||||
"""
|
||||
try:
|
||||
import win32gui
|
||||
import win32con
|
||||
|
||||
def _enum_cb(hwnd, hwnd_list):
|
||||
cls = win32gui.GetClassName(hwnd)
|
||||
title = win32gui.GetWindowText(hwnd)
|
||||
if cls == "SDL_app":
|
||||
hwnd_list.append(hwnd)
|
||||
elif "Kiwy" in title or "Signage" in title:
|
||||
hwnd_list.append(hwnd)
|
||||
|
||||
hwnd_list = []
|
||||
win32gui.EnumWindows(_enum_cb, hwnd_list)
|
||||
|
||||
if hwnd_list:
|
||||
kivy_hwnd = hwnd_list[-1] # most recent
|
||||
win32gui.ShowWindow(kivy_hwnd, win32con.SW_SHOWNORMAL)
|
||||
win32gui.SetForegroundWindow(kivy_hwnd)
|
||||
win32gui.BringWindowToTop(kivy_hwnd)
|
||||
except Exception:
|
||||
# Fallback to Kivy's built-in raise
|
||||
try:
|
||||
from kivy.core.window import Window
|
||||
Window.show()
|
||||
Window.raise_window()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _windows_kill_process_tree(proc):
|
||||
"""Kill a process AND all its children using taskkill.
|
||||
|
||||
@@ -292,49 +347,56 @@ 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.
|
||||
|
||||
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
|
||||
Strategy (tried in order):
|
||||
1. CEF embedded browser (best — no subprocess, no z-order fights)
|
||||
2. Chrome/Edge subprocess (fallback)
|
||||
"""
|
||||
from kivy.logger import Logger
|
||||
from kivy.clock import Clock
|
||||
from urllib.parse import urlparse
|
||||
|
||||
scheme = urlparse(url).scheme.lower()
|
||||
if scheme not in ('http', 'https'):
|
||||
Logger.warning(f"SignagePlayer: Refusing non-http(s) weblink: {url}")
|
||||
self.consecutive_errors += 1
|
||||
self._skip_to_next_media()
|
||||
return False
|
||||
|
||||
# ── Strategy 1: CEF embedded browser ────────────────────────
|
||||
cef_browser = _get_cef_browser()
|
||||
if cef_browser is not None:
|
||||
Logger.info(f"SignagePlayer: Opening weblink via CEF embedded browser: {url}")
|
||||
try:
|
||||
self.ids.content_area.opacity = 0
|
||||
except Exception:
|
||||
pass
|
||||
cef_browser.show(url)
|
||||
Clock.unschedule(self.next_media)
|
||||
self._start_inactivity_watchdog(duration)
|
||||
self.preload_next_media()
|
||||
Logger.info("SignagePlayer: CEF browser launched successfully")
|
||||
return True
|
||||
|
||||
# ── Strategy 2: Subprocess Chrome/Edge (fallback) ────────────
|
||||
browser = _windows_find_browser()
|
||||
if not browser:
|
||||
from kivy.logger import Logger
|
||||
Logger.error(
|
||||
"SignagePlayer: Chrome/Edge not found. "
|
||||
"SignagePlayer: No embedded CEF and no Chrome/Edge found. "
|
||||
"Cannot display weblink on Windows."
|
||||
)
|
||||
self.consecutive_errors += 1
|
||||
self._skip_to_next_media()
|
||||
return False
|
||||
|
||||
from urllib.parse import urlparse
|
||||
scheme = urlparse(url).scheme.lower()
|
||||
if scheme not in ('http', 'https'):
|
||||
from kivy.logger import Logger
|
||||
Logger.warning(f"SignagePlayer: Refusing non-http(s) weblink: {url}")
|
||||
self.consecutive_errors += 1
|
||||
self._skip_to_next_media()
|
||||
return False
|
||||
|
||||
import subprocess
|
||||
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}"
|
||||
)
|
||||
Logger.info(f"SignagePlayer: Opening weblink via subprocess: {url} ({browser})")
|
||||
self._kill_weblink_preload()
|
||||
|
||||
# Hide Kivy content so it doesn't show underneath Chrome
|
||||
# Hide Kivy content
|
||||
from kivy.core.window import Window as KivyWindow
|
||||
try:
|
||||
self.ids.content_area.opacity = 0
|
||||
@@ -342,14 +404,8 @@ def _patch_main():
|
||||
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,
|
||||
'--new-window',
|
||||
@@ -365,14 +421,12 @@ def _patch_main():
|
||||
'--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:
|
||||
@@ -446,74 +500,73 @@ def _patch_main():
|
||||
|
||||
signage_main.SignagePlayer._start_inactivity_watchdog = _windows_watchdog
|
||||
|
||||
# Patch the _kill_weblink_after_frame to work without Linux-specific code
|
||||
# ── Patch kill_weblink_after_frame for both CEF and subprocess ──
|
||||
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.
|
||||
"""
|
||||
"""Close the weblink (CEF or subprocess) immediately before next media."""
|
||||
import time
|
||||
from kivy.logger import Logger
|
||||
from kivy.clock import Clock
|
||||
self._stop_inactivity_watchdog()
|
||||
self._kill_weblink_preload()
|
||||
|
||||
# Try CEF first
|
||||
cef_browser = _get_cef_browser()
|
||||
if cef_browser is not None and cef_browser.is_showing():
|
||||
Logger.info("SignagePlayer: Hiding CEF embedded browser")
|
||||
cef_browser.hide()
|
||||
self._weblink_proc = None
|
||||
return
|
||||
|
||||
# Fallback: subprocess Chrome
|
||||
proc = self._weblink_proc
|
||||
self._weblink_proc = None
|
||||
|
||||
if proc is not None and proc.poll() is not None:
|
||||
_Win32Overlay.hide()
|
||||
if proc is None or proc.poll() is not None:
|
||||
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)
|
||||
Logger.info("SignagePlayer: Killing Chromium subprocess immediately")
|
||||
_windows_kill_process_tree(proc)
|
||||
time.sleep(0.1)
|
||||
_bring_kivy_to_front()
|
||||
_Win32Overlay.hide()
|
||||
signage_main.SignagePlayer._kill_weblink_after_frame = _windows_kill_weblink_after_frame
|
||||
|
||||
# Patch _prewarm_weblink for Windows
|
||||
# ── Patch play_current_media — same immediate-kill logic ────────
|
||||
_original_play_current = signage_main.SignagePlayer.play_current_media
|
||||
|
||||
def _windows_play_current_media(self, force_reload=False, _after_weblink=False):
|
||||
"""Wrapped play_current_media — closes weblink immediately on transition."""
|
||||
if not _after_weblink:
|
||||
# Kill CEF browser if showing
|
||||
cef_browser = _get_cef_browser()
|
||||
if cef_browser is not None and cef_browser.is_showing():
|
||||
cef_browser.hide()
|
||||
self._weblink_proc = None
|
||||
|
||||
# Kill subprocess Chrome if running
|
||||
proc = self._weblink_proc
|
||||
if proc is not None and proc.poll() is None:
|
||||
_Win32Overlay.show()
|
||||
_windows_kill_process_tree(proc)
|
||||
self._weblink_proc = None
|
||||
self._stop_inactivity_watchdog()
|
||||
self._kill_weblink_preload()
|
||||
_bring_kivy_to_front()
|
||||
_Win32Overlay.hide()
|
||||
|
||||
return _original_play_current(self, force_reload=force_reload, _after_weblink=_after_weblink)
|
||||
|
||||
signage_main.SignagePlayer.play_current_media = _windows_play_current_media
|
||||
|
||||
# Patch _prewarm_weblink for Windows — disabled for now.
|
||||
# The off-screen Chrome window on Windows can interfere with:
|
||||
# - Audio playback (Chrome claims audio device)
|
||||
# - GPU resources (Chrome's GPU process runs in background)
|
||||
# - Taskbar icons showing duplicate Chrome windows
|
||||
# Pre-warming is less critical on desktop where launch is already fast.
|
||||
def _windows_prewarm_weblink(self, url):
|
||||
from urllib.parse import urlparse
|
||||
if not url:
|
||||
return
|
||||
scheme = urlparse(url).scheme.lower()
|
||||
if scheme not in ('http', 'https'):
|
||||
return
|
||||
browser = _windows_find_browser()
|
||||
if not browser:
|
||||
return
|
||||
import subprocess
|
||||
from kivy.logger import Logger
|
||||
self._kill_weblink_preload()
|
||||
try:
|
||||
Logger.debug(f"SignagePlayer: Pre-warming weblink off-screen: {url}")
|
||||
self._weblink_preload_proc = subprocess.Popen([
|
||||
browser,
|
||||
'--incognito',
|
||||
'--no-first-run',
|
||||
'--window-position=-9999,-9999',
|
||||
url,
|
||||
], shell=False)
|
||||
except Exception as exc:
|
||||
Logger.debug(f"SignagePlayer: Pre-warm failed (non-fatal): {exc}")
|
||||
self._weblink_preload_proc = None
|
||||
pass # Disabled on Windows — desktop launch is fast enough
|
||||
signage_main.SignagePlayer._prewarm_weblink = _windows_prewarm_weblink
|
||||
|
||||
# Patch cleanup of temp auth file (was using /tmp/)
|
||||
|
||||
Reference in New Issue
Block a user