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:
+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