""" 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