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:
ske087
2026-07-24 14:39:12 +03:00
parent 7efc023327
commit 844e5eeebb
3 changed files with 421 additions and 109 deletions
+143 -90
View File
@@ -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/)