diff --git a/windows/build.spec b/windows/build.spec index f14bd20..4660489 100644 --- a/windows/build.spec +++ b/windows/build.spec @@ -102,6 +102,7 @@ hidden_imports = [ 'selectors', 'tempfile', # Windows-specific + 'cef_browser', 'win32gui', 'win32con', ] @@ -134,9 +135,7 @@ if kv_file.exists(): kv_data.append((str(kv_file), '.')) # Bundle the entire src directory as a tree -source_tree = Tree(str(SRC_DIR), prefix='') -# Filter out pyc files, pycache dirs, and ini files -source_tree.excludes = ['*.pyc', '__pycache__', '*.ini'] +source_tree = Tree(str(SRC_DIR), prefix='', excludes=['*.pyc', '__pycache__', '*.ini']) # --- Collect binary DLLs from kivy_deps and ffpyplayer ---------------- import importlib.util diff --git a/windows/cef_browser.py b/windows/cef_browser.py index 27f781f..d6a978b 100644 --- a/windows/cef_browser.py +++ b/windows/cef_browser.py @@ -1,93 +1,83 @@ """ -cef_browser.py — Embedded Chromium browser for Kiwy Signage Player (Windows) +cef_browser.py v2 — Embedded Chromium INSIDE Kivy's SDL2 window -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() +v1 created a separate Win32 window (same as external Chrome). +v2 creates CEF as a **child window** of Kivy's SDL_app window: + - No separate taskbar entry + - No z-order fighting + - No desktop flash + - CEF message loop pumped via Kivy Clock (main thread) """ 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 +WS_CHILD = 0x40000000 +WS_VISIBLE = 0x10000000 +WS_CLIPSIBLINGS = 0x04000000 +WS_CLIPCHILDREN = 0x02000000 +SW_HIDE = 0 +SW_SHOWNORMAL = 1 + 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 + self._child_hwnd = None + self._kivy_hwnd = None + self._clock_event = None + self._showing = 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 not self._cef_initialized: + self._init_cef() if self._browser is not None: self._browser.Navigate(url) - self._bring_to_front() + self._show_in_kivy() return True - - self._create_browser_window(url) - return True + return self._create_embedded(url) def hide(self): - """Close the browser window and bring Kivy to front.""" + self._showing = False + if self._clock_event is not None: + try: + from kivy.clock import Clock + Clock.unschedule(self._clock_event) + except Exception: + pass + self._clock_event = None + if self._child_hwnd: + try: + ctypes.windll.user32.ShowWindow(self._child_hwnd, SW_HIDE) + except Exception: + pass if self._browser is not None: try: - # Close the browser self._browser.CloseBrowser(True) except Exception: pass self._browser = None - - if self._hwnd: + if self._child_hwnd: try: - ctypes.windll.user32.DestroyWindow(self._hwnd) + ctypes.windll.user32.DestroyWindow(self._child_hwnd) except Exception: pass - self._hwnd = None - - # Bring Kivy to front - self._bring_kivy_to_front() + self._child_hwnd = None def shutdown(self): - """Shut down the CEF engine entirely.""" self.hide() - self._stop_event.set() if self._cef_initialized: try: cef.Shutdown() @@ -96,151 +86,107 @@ class CefBrowser: 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 + return self._showing - # ── Internal helpers ──────────────────────────────────────────── + def resize(self, width, height): + """Called when Kivy window resizes — repositions CEF child.""" + if self._child_hwnd: + ctypes.windll.user32.SetWindowPos( + self._child_hwnd, 0, 0, 0, width, height, 0x0004 + ) + if self._browser: + self._browser.SetBounds(0, 0, width, height) - 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() + # ── Internal ──────────────────────────────────────────────────── def _init_cef(self): - """Initialize CEF once.""" - if self._cef_initialized: - return - - # CEF settings settings = { - "multi_threaded_message_loop": True, # Required for Kivy integration + "multi_threaded_message_loop": False, + "single_process": True, "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"), + "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.""" + def _get_kivy_hwnd(self): + if self._kivy_hwnd is not None: + return self._kivy_hwnd 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) + hwnd = win32gui.FindWindow("SDL_app", None) + if hwnd: + self._kivy_hwnd = hwnd + return hwnd except Exception: pass + return None + + def _create_embedded(self, url): + kivy_hwnd = self._get_kivy_hwnd() + if not kivy_hwnd: + return False + + user32 = ctypes.windll.user32 + rect = (ctypes.c_long * 4)() + user32.GetClientRect(kivy_hwnd, ctypes.byref(rect)) + w, h = rect[2], rect[3] + + hinstance = ctypes.windll.kernel32.GetModuleHandleW(None) + self._child_hwnd = user32.CreateWindowExW( + 0, b'#32770', b'', + WS_CHILD | WS_CLIPSIBLINGS | WS_CLIPCHILDREN, + 0, 0, w, h, kivy_hwnd, 0, hinstance, 0, + ) + if not self._child_hwnd: + return False + + winfo = cef.WindowInfo() + winfo.SetAsChild(self._child_hwnd, [0, 0, w, h]) + self._browser = cef.CreateBrowserSync( + window_info=winfo, + settings={"background_color": 0x00000000}, + url=url, + ) + self._showing = True + self._show_in_kivy() + self._start_clock_pump() + return True + + def _show_in_kivy(self): + if not self._child_hwnd: + return + kivy_hwnd = self._get_kivy_hwnd() + if kivy_hwnd: + user32 = ctypes.windll.user32 + rect = (ctypes.c_long * 4)() + user32.GetClientRect(kivy_hwnd, ctypes.byref(rect)) + user32.SetWindowPos( + self._child_hwnd, 0, 0, 0, rect[2], rect[3], 0x0004 + ) + ctypes.windll.user32.ShowWindow(self._child_hwnd, SW_SHOWNORMAL) + self._showing = True + + def _start_clock_pump(self): + if self._clock_event is not None: + return + + def _pump(dt): + if self._cef_initialized: + try: + cef.MessageLoopWork() + except Exception: + pass + if self._showing: + from kivy.clock import Clock + self._clock_event = Clock.schedule_once(_pump, 0.01) + + from kivy.clock import Clock + self._clock_event = Clock.schedule_once(_pump, 0) diff --git a/windows/run_win.py b/windows/run_win.py index 2e2838c..cd41966 100644 --- a/windows/run_win.py +++ b/windows/run_win.py @@ -115,7 +115,12 @@ def _windows_screen_activity(self, dt): _CEF_BROWSER = None def _get_cef_browser(): - """Return the shared CefBrowser singleton, or None if unavailable.""" + """Return the shared CefBrowser singleton, or None if unavailable. + + v2 embeds CEF as a CHILD WINDOW inside Kivy's SDL_app window. + This means: no separate taskbar entry, no z-order fighting, + no desktop flash, no taskkill needed. + """ global _CEF_BROWSER if _CEF_BROWSER is None: try: @@ -367,16 +372,37 @@ def _patch_main(): # ── 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}") + Logger.info(f"SignagePlayer: Opening weblink via CEF (embedded in Kivy): {url}") try: self.ids.content_area.opacity = 0 except Exception: pass + # Attach resize handler so CEF follows Kivy window resizes + from kivy.core.window import Window as KivyWindow + _orig_on_resize = getattr(KivyWindow, '_on_resize', None) + def _cef_resize(*args): + try: + w, h = KivyWindow.size + cef_browser.resize(int(w), int(h)) + except Exception: + pass + if _orig_on_resize: + try: + return _orig_on_resize(*args) + except Exception: + pass + KivyWindow._on_resize = _cef_resize + # Bind to size event as well + try: + KivyWindow.bind(size=_cef_resize) + 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") + Logger.info("SignagePlayer: CEF embedded browser visible (inside Kivy window)") return True # ── Strategy 2: Subprocess Chrome/Edge (fallback) ────────────