From 9f5409685df30adf1af60791ef496955ea50027a Mon Sep 17 00:00:00 2001 From: ske087 Date: Sun, 13 Sep 2026 10:14:18 +0300 Subject: [PATCH] Embedded WebView2 engine for web links (Windows) Web links previously launched a separate Chrome/Edge kiosk process, which caused the whole class of bugs in the tracker: the browser opening behind the player, being handed off to an already-running instance and exiting instantly, fighting for foreground/z-order, and leaking msedge.exe/chrome.exe processes that were never closed. WebView2 renders as a CHILD HWND of Kivy's own SDL window instead, so there is no separate top-level browser to open behind the player, nothing to hand the URL off to, no z-order contest, and no leaked browser process. Windows/webview2_browser.py - Environment -> controller -> navigate, driven through pythonnet. - Async .NET Tasks are polled from Kivy's Clock. Calling GetAwaiter() .GetResult() would deadlock: the continuation needs the same thread's message pump. - The controller is a .NET IntPtr, not a Python int (CreateAsync overloads do not match otherwise). - NavigationCompleted is tracked so a page that never loads can be told apart from one that did. This matters on a closed network: an unreachable host paints a Chromium error page, and without this the player would show a blank/error screen for the item's whole slot instead of skipping it. - is_alive() reports True while starting up. Start-up is async, so a controller that does not exist yet is not a dead browser; treating it as one made the first web link after a cold start be skipped instantly. Windows/webview2_runtime.py - Detects the Runtime (registry pv value, SDK probe as fallback) and installs it silently when missing, unelevated, which produces a per-user install and therefore never raises a UAC prompt on the signage display. - Success is decided by RE-READING the installed version, not by the installer exit code: Edge Update returns a non-zero HRESULT (-2147219416) when the Runtime is already current, which is not a failure. - On a closed network the online bootstrapper can never succeed, so it fails fast with an actionable message instead of hanging for the full timeout. - Failed attempts are cooldown-gated so a broken machine does not re-run an installer on every start. Offline hardening - Browser arguments disable component updates, field trials, safe-browsing list fetches, translate and other internet chatter. On an isolated LAN each of those would otherwise have to time out, costing start-up latency. Pages on the local server are unaffected. Engine order (best first): WebView2 -> CEF -> Chrome/Edge subprocess. CEF has no wheels past Python 3.9 so it is dormant on this build; the subprocess engine remains only as a last resort. Also fixes the reason the Windows adapters were never used at all: SignagePlayer.__init__ assigned self.weblink_adapter_factory = None, which shadowed the CLASS attribute that run_win.py injects. play_weblink() therefore fell back to the generic adapter, whose find_browser() uses shutil.which() and finds nothing on Windows because Chrome/Edge are not on PATH. The instance attribute is now only set when the class attribute is absent. Verified: windows/test_webview2_embed.py, test_webview2_navigation.py and test_webview2_offline.py all pass (a locally served page renders with all internet traffic disabled), and the packaged exe reports "weblink_launch engine=webview2-embedded" -> "weblink_launched" on every cycle with no leaked browser processes. --- PLAYER_WEBLINK_INTEGRATION.md | 57 +- src/main.py | 404 ++++++++-- src/weblink_session.py | 28 +- windows/build.spec | 72 +- windows/requirements_win.txt | 12 + windows/test_webview2_embed.py | 169 +++++ windows/test_webview2_navigation.py | 186 +++++ windows/test_webview2_offline.py | 221 ++++++ windows/test_webview2_runtime.py | 106 +++ windows/webview2_browser.py | 718 ++++++++++++++++++ windows/webview2_runtime.py | 446 +++++++++++ .../MicrosoftEdgeWebview2Setup.exe | Bin 0 -> 1783000 bytes .../download_runtime_installers.ps1 | 65 ++ .../Microsoft.Web.WebView2.Core.dll | Bin 0 -> 698208 bytes windows/webview2_sdk/WebView2Loader.dll | Bin 0 -> 163680 bytes 15 files changed, 2418 insertions(+), 66 deletions(-) create mode 100644 windows/test_webview2_embed.py create mode 100644 windows/test_webview2_navigation.py create mode 100644 windows/test_webview2_offline.py create mode 100644 windows/test_webview2_runtime.py create mode 100644 windows/webview2_browser.py create mode 100644 windows/webview2_runtime.py create mode 100644 windows/webview2_runtime/MicrosoftEdgeWebview2Setup.exe create mode 100644 windows/webview2_runtime/download_runtime_installers.ps1 create mode 100644 windows/webview2_sdk/Microsoft.Web.WebView2.Core.dll create mode 100644 windows/webview2_sdk/WebView2Loader.dll diff --git a/PLAYER_WEBLINK_INTEGRATION.md b/PLAYER_WEBLINK_INTEGRATION.md index a107df0..9ba7b53 100644 --- a/PLAYER_WEBLINK_INTEGRATION.md +++ b/PLAYER_WEBLINK_INTEGRATION.md @@ -6,9 +6,10 @@ playlist item type (display a live web page / URL instead of an uploaded media file). > **Status: implemented.** The player supports `weblink` items on both -> Raspberry Pi (`chromium` subprocess) and Windows (embedded CEF with a -> Chrome/Edge subprocess fallback). Sections 1–4 describe the original design -> plan; section 6 documents the shipped architecture and the interaction model. +> Raspberry Pi (`chromium` subprocess) and Windows (embedded **WebView2**, with +> the CEF and Chrome/Edge subprocess engines as fallbacks). Sections 1–4 +> describe the original design plan; section 6 documents the shipped +> architecture and the interaction model. --- @@ -256,18 +257,56 @@ platform: | Piece | Responsibility | |--------------------------|----------------| | `WeblinkSession` | Owns one weblink item: validate → launch → verify → watch → teardown. Generation-tokened so stale callbacks are ignored, and `atexit`-safe. | -| `WeblinkAdapter` | The only platform-specific part: launch / wait for the window / is it alive / tear it down / pre-warm. | -| `ChromiumSubprocessAdapter` | Default engine (Raspberry Pi `chromium`, Windows `chrome.exe`/`msedge.exe`). | +| `WeblinkAdapter` | The only platform-specific part: launch / wait for the window / is it alive / tear it down / pre-warm. `extra_launch_args()` lets a subclass add browser flags without copying `launch()`. | +| `ChromiumSubprocessAdapter` | Default engine (Raspberry Pi `chromium`; on Windows the Chrome/Edge fallback). | | `InteractionWatcher` | Decides when the item is finished (see the interaction model below). | -| `WebInputSources` | Reads `/dev/input/event*` (Linux) and does a pointer-position tap (Windows, needed for embedded CEF). | +| `WebInputSources` | Reads `/dev/input/event*` (Linux) and does a pointer-position tap (Windows, needed for embedded engines). | +| `webview2_browser.py` | **Windows, preferred**: embeds WebView2 as a child HWND of the Kivy window. | +| `webview2_runtime.py` | **Windows**: detects the WebView2 Runtime and installs it silently when missing. | Platform wrappers inject their engines through `SignagePlayer.weblink_adapter_factory`: * **Raspberry Pi / Linux** — built-in Chromium subprocess adapter. -* **Windows** (`windows/run_win.py`) — embedded CEF first (`cef_browser.py`, - renders inside the Kivy window: no z-order fights, no subprocess), then the - Chrome/Edge subprocess adapter as fallback. +* **Windows** (`windows/run_win.py`) — engines are tried in this order: + + | Order | Engine | Renders | Notes | + |-------|--------|---------|-------| + | 1 | **WebView2** (`webview2_browser.py`) | child window **inside** Kivy | Preferred. No subprocess, so no background/z-order/hand-off/leak problems. | + | 2 | CEF (`cef_browser.py`) | child window inside Kivy | Dormant: `cefpython3` has no wheels past Python 3.9. | + | 3 | Chrome/Edge subprocess | separate window | Last resort only; retains the old drawbacks. | + +`WeblinkAdapter.extra_launch_args()` is the hook subclasses use to add flags +without duplicating `launch()` — the Chrome adapter uses it for +`--user-data-dir` + `--kiosk`. + +> **Do not give the factory a class-level `None` default combined with an +> unconditional instance assignment.** `SignagePlayer.__init__` originally set +> `self.weblink_adapter_factory = None`, which shadowed the class attribute the +> Windows wrapper installs — so the platform adapters were silently ignored and +> every weblink fell back to the generic adapter and failed. It now only sets +> the instance attribute when the class attribute is absent. + +### 6.2 Windows: WebView2 Runtime + +WebView2 is two separate things, and they ship differently: + +* the **SDK** (`Microsoft.Web.WebView2.Core.dll`, `WebView2Loader.dll`) — the + API surface, bundled in the exe from `windows\webview2_sdk\` (~860 KB); +* the **Runtime** (`msedgewebview2.exe`) — the actual Chromium engine, shipped + by Microsoft and **verified/installed at start-up** by + `windows\webview2_runtime.py`. + +If the Runtime is absent the player runs an installer silently +(`/silent /install`) and **unelevated**, which produces a *per-user* install and +therefore never raises a UAC prompt on the signage display. A ~1.7 MB online +bootstrapper is bundled by default; a ~203 MB offline standalone installer can +be bundled instead (see `windows\webview2_runtime\download_runtime_installers.ps1`) +for machines with no internet. + +Install success is decided by **re-reading the installed version**, not by the +installer exit code — Edge Update returns a non-zero HRESULT (e.g. +`-2147219416`) when the Runtime is already current, which is not a failure. ### 6.1 Interaction model — web links are not passive media diff --git a/src/main.py b/src/main.py index 92274d8..11d8825 100644 --- a/src/main.py +++ b/src/main.py @@ -9,6 +9,8 @@ import os import json import platform import signal +import subprocess +import sys import threading import time import asyncio @@ -91,6 +93,7 @@ from kivy.graphics import Color, Line, Ellipse from kivy.uix.floatlayout import FloatLayout from kivy.uix.slider import Slider from playback_trace import trace # always-on playback transition logger +from video_safety import suppress_kivy_video_blocking_unload # bound Kivy's blocking video join from weblink_session import ( WeblinkSession, WeblinkSettings, @@ -627,12 +630,19 @@ class ExitPasswordPopup(Popup): Clock.schedule_once(lambda dt: self.dismiss(), 1) class SettingsPopup(Popup): - def __init__(self, player_instance, was_paused=False, **kwargs): + def __init__(self, player_instance, was_paused=False, first_run=False, **kwargs): super(SettingsPopup, self).__init__(**kwargs) self.player = player_instance self.was_paused = was_paused + self.first_run = bool(first_run) self.keyboard_widget = None - + + # First-run setup is the ONLY thing on screen and there is no playlist + # yet, so the popup must not be dismissable into a blank player. + if self.first_run: + self.auto_dismiss = False + self.title = 'Player Setup - Not Configured' + # Cancel all scheduled cursor/control hide events try: if self.player.controls_timer: @@ -649,15 +659,22 @@ class SettingsPopup(Popup): except: pass - # Populate current values - self.ids.server_input.text = self.player.config.get('server_ip', 'localhost') - self.ids.port_input.text = str(self.player.config.get('port', '')) - self.ids.screen_input.text = self.player.config.get('screen_name', 'kivy-player') - self.ids.quickconnect_input.text = self.player.config.get('quickconnect_key', '1234567') + # Populate current values. Fields are blank on a fresh install, so the + # hint text tells the operator what to enter. + self.ids.server_input.text = self.player.config.get('server_ip', '') + self.ids.port_input.text = str(self.player.config.get('port', '') or '') + self.ids.screen_input.text = self.player.config.get('screen_name', '') + self.ids.quickconnect_input.text = self.player.config.get('quickconnect_key', '') self.ids.orientation_input.text = self.player.config.get('orientation', 'Landscape') self.ids.touch_input.text = self.player.config.get('touch', 'True') - self.ids.resolution_input.text = self.player.config.get('max_resolution', 'auto') + self.ids.resolution_input.text = self.player.config.get('max_resolution', '1920x1080') self.ids.edit_enabled_checkbox.active = self.player.config.get('edit_feature_enabled', True) + + if self.first_run: + Logger.info( + "SettingsPopup: First-run setup opened (server_ip/screen_name/" + "quickconnect_key must be filled in)" + ) # Update status info self.ids.playlist_info.text = f'Playlist: v{self.player.playlist_version}' @@ -711,6 +728,11 @@ class SettingsPopup(Popup): """Handle popup dismissal - resume playback and restart cursor hide timer""" # Hide and remove keyboard self.hide_keyboard() + if self.first_run: + # Nothing was playing: there is no playlist to resume, and + # scheduling an advance here would race the first sync kicked off + # by on_first_run_config_saved(). + return # Resume playback and re-arm the media advance timer self.player.resume_after_popup(self.was_paused) @@ -919,27 +941,122 @@ class SettingsPopup(Popup): Clock.schedule_once(lambda dt: popup.dismiss(), 2) def save_and_close(self): - """Save configuration and close popup""" + """Save configuration and close popup. + + During first-run setup the play/pause guard does not apply (there is no + playlist yet) and the player is told to start once the details are in. + """ # Update config - self.player.config['server_ip'] = self.ids.server_input.text + self.player.config['server_ip'] = self.ids.server_input.text.strip() self.player.config['port'] = self.ids.port_input.text.strip() - self.player.config['screen_name'] = self.ids.screen_input.text - self.player.config['quickconnect_key'] = self.ids.quickconnect_input.text + self.player.config['screen_name'] = self.ids.screen_input.text.strip() + self.player.config['quickconnect_key'] = self.ids.quickconnect_input.text.strip() self.player.config['orientation'] = self.ids.orientation_input.text self.player.config['touch'] = self.ids.touch_input.text self.player.config['max_resolution'] = self.ids.resolution_input.text self.player.config['edit_feature_enabled'] = self.ids.edit_enabled_checkbox.active - + + # First-run validation: refuse to "configure" the player with blanks, + # otherwise it would be marked configured and then fail on every sync. + if self.first_run: + missing = [ + label for label, key in ( + ('Server IP', 'server_ip'), + ('Player Name', 'screen_name'), + ('Quick Connect Key', 'quickconnect_key'), + ) + if not str(self.player.config.get(key, '') or '').strip() + ] + if missing: + self._show_temp_message( + 'Required: ' + ', '.join(missing), (1, 0.7, 0, 1) + ) + return + # A brand-new install has no reason to keep HTTPS verification on. + self.player.config.setdefault('use_https', False) + self.player.config.setdefault('verify_ssl', False) + # Save to file self.player.save_config() # Notify user that resolution change requires restart if self.ids.resolution_input.text != self.player.config.get('max_resolution', 'auto'): Logger.info("SettingsPopup: Resolution changed - restart required") + + was_first_run = self.first_run # Close popup self.dismiss() + if was_first_run: + self.player.on_first_run_config_saved() + + +# ── First-run configuration ────────────────────────────────────────── +# The player is shipped WITHOUT any server credentials: `app_config.json` is +# not bundled into the exe (see windows/build.spec). On first start there is +# nothing to connect to, so the player shows a notice after the splash video +# and then opens Settings automatically. +# +# These are the values the player cannot work without, plus the placeholder +# values a fresh install used to be given. A config holding only placeholders +# counts as UNCONFIGURED, so a machine that has never been set up runs the +# first-run flow instead of silently trying to reach "localhost". +CONFIG_REQUIRED_KEYS = ('server_ip', 'screen_name', 'quickconnect_key') + +CONFIG_PLACEHOLDER_VALUES = { + 'server_ip': {'', 'localhost', '127.0.0.1'}, + 'screen_name': {'', 'kivy-player'}, + 'quickconnect_key': {'', '1234567'}, +} + +#: In-memory starting point when no usable config file exists. Deliberately +#: has EMPTY credentials so `config_is_configured()` reports False. +DEFAULT_CONFIG = { + 'server_ip': '', + 'port': '8080', + 'screen_name': '', + 'quickconnect_key': '', + 'orientation': 'Landscape', + 'touch': 'True', + 'max_resolution': '1920x1080', + 'edit_feature_enabled': True, + 'use_https': False, + 'verify_ssl': False, + 'production_mode': False, + 'weblink': { + 'engine': 'auto', + 'interaction_postpone': 10, + 'interaction_debounce': 0.5, + 'interaction_grace': 5.0, + 'max_dwell_factor': 6.0, + 'min_max_dwell': 300, + 'launch_timeout': 15, + 'prewarm': True, + }, +} + +#: Seconds the "not configured" notice stays up before Settings opens. +SETUP_NOTICE_SECONDS = 5 + + +def config_is_configured(config): + """True when ``config`` has enough real values to talk to a server. + + Missing file, empty file, unparseable JSON, missing keys and leftover + placeholder values all mean "not configured" — that is what triggers the + first-run setup screen instead of a playlist attempt. + """ + if not isinstance(config, dict) or not config: + return False + for key in CONFIG_REQUIRED_KEYS: + value = str(config.get(key, '') or '').strip() + if not value: + return False + if value.lower() in CONFIG_PLACEHOLDER_VALUES.get(key, set()): + return False + return True + class SignagePlayer(Widget): from kivy.properties import StringProperty @@ -968,8 +1085,16 @@ class SignagePlayer(Widget): # watching and teardown for one weblink item at a time. Platform wrappers # (e.g. windows/run_win.py) inject their adapters via # `weblink_adapter_factory` before playback starts. + # + # CRITICAL: the factory is injected as a *class* attribute, so do NOT + # assign None unconditionally here. An instance attribute would shadow + # it, play_weblink() would silently fall back to the generic adapter, + # and on Windows that adapter's find_browser() (shutil.which) finds no + # browser because Chrome/Edge are not on PATH — so every weblink failed + # with "launch() returned False" and the item was skipped. self._weblink_session = None - self.weblink_adapter_factory = None + if not callable(getattr(type(self), 'weblink_adapter_factory', None)): + self.weblink_adapter_factory = None self.is_playing = False self.is_paused = False self.auto_resume_event = None # Track scheduled auto-resume @@ -1238,31 +1363,141 @@ class SignagePlayer(Widget): # Start media playback Clock.schedule_interval(self.check_playlist_and_play, 30) # Check every 30 seconds - def load_config(self): - """Load configuration from file""" - Logger.debug("SignagePlayer: load_config() starting...") + def requires_setup(self): + """True when the player has no usable server configuration.""" + return not getattr(self, '_configured', config_is_configured(self.config)) + + def on_intro_finished(self): + """Splash video has ended — continue with either setup or playback. + + Single decision point for "intro done", so the first-run branch and the + normal branch cannot drift apart. + """ + self.intro_played = True + if self.requires_setup(): + Logger.warning( + "SignagePlayer: No server configuration - showing setup notice" + ) + self.show_setup_required_notice() + return + # Normal start: load whatever playlist is cached and begin playing. + self.check_playlist_and_play(None) + + def show_setup_required_notice(self): + """Show the "not configured" notice, then open Settings automatically. + + The player is shipped without credentials, so this is the expected + first-run experience on a brand-new .exe: tell the operator what is + missing, wait SETUP_NOTICE_SECONDS, then open the settings screen so + they can enter the server details. + """ + trace('setup_required_shown') try: - if os.path.exists(self.config_file): + self.ids.status_label.text = ( + 'Player is not configured\n\n' + 'No server settings found. Opening setup...' + ) + self.ids.status_label.opacity = 1 + except Exception: + pass + + self._setup_notice_event = Clock.schedule_once( + lambda dt: self._open_first_run_settings(), SETUP_NOTICE_SECONDS + ) + + def _open_first_run_settings(self): + """Open Settings for first-run configuration.""" + trace('setup_opening_settings') + try: + self.ids.status_label.opacity = 0 + except Exception: + pass + popup = SettingsPopup(player_instance=self, first_run=True) + popup.open() + + def on_first_run_config_saved(self): + """Called when Settings was used to configure the player. + + Applies the new values immediately and starts playback, so the operator + does not have to restart the .exe after entering the server details. + """ + self._configured = config_is_configured(self.config) + if not self._configured: + Logger.warning( + "SignagePlayer: Settings saved but the player is still not " + "configured - setup will be offered again" + ) + self.show_setup_required_notice() + return + + trace('setup_completed') + Logger.info("SignagePlayer: First-run configuration saved - starting playback") + try: + self.ids.status_label.text = 'Configuration saved - connecting...' + self.ids.status_label.opacity = 1 + except Exception: + pass + + # Re-arm the pieces that depend on the server details. + self.start_network_monitoring() + + # Pull the first playlist in the background, then play it. + def _fetch_and_play(dt): + try: + updated = update_playlist_if_needed( + self.config, self.playlists_dir, self.media_dir + ) + if updated: + Logger.info("SignagePlayer: Playlist fetched after setup") + except Exception as exc: + Logger.error(f"SignagePlayer: Playlist fetch after setup failed: {exc}") + self.load_playlist() + self.is_playing = False + self.is_paused = False + self.start_playback() + + threading.Thread(target=_fetch_and_play, args=(None,), daemon=True).start() + + def load_config(self): + """Load configuration from file. + + A missing/empty/unreadable file is NOT an error and is NOT written + back: the player starts with empty credentials and the first-run setup + flow asks the operator to fill them in. Writing a placeholder file here + would defeat that (and used to plant `localhost` as a fake server). + """ + Logger.debug("SignagePlayer: load_config() starting...") + self.config = dict(DEFAULT_CONFIG) + self._config_file_existed = os.path.exists(self.config_file) + try: + if self._config_file_existed: with open(self.config_file, 'r') as f: - self.config = json.load(f) - Logger.info(f"SignagePlayer: Configuration loaded from {self.config_file}") + loaded = json.load(f) + if isinstance(loaded, dict) and loaded: + # Keep defaults for anything the file omits. + self.config.update(loaded) + Logger.info( + f"SignagePlayer: Configuration loaded from {self.config_file}" + ) + else: + Logger.warning( + "SignagePlayer: Configuration file is empty or not a JSON " + "object - treating the player as unconfigured" + ) else: - # Create default configuration with HTTPS support - self.config = { - "server_ip": "localhost", - "port": "443", - "screen_name": "kivy-player", - "quickconnect_key": "1234567", - "max_resolution": "auto", - "use_https": True, - "verify_ssl": True, - "production_mode": False - } - self.save_config() - Logger.info("SignagePlayer: Created default configuration with HTTPS enabled") + Logger.warning( + f"SignagePlayer: No configuration file at {self.config_file} " + "- first-run setup will be shown" + ) except Exception as e: Logger.error(f"SignagePlayer: Error loading config: {e}") - self.show_error(f"Failed to load configuration: {e}") + self.config = dict(DEFAULT_CONFIG) + + self._configured = config_is_configured(self.config) + Logger.info( + "SignagePlayer: Configuration status: " + + ("configured" if self._configured else "NOT configured (setup required)") + ) def save_config(self): """Save configuration to file""" @@ -1380,14 +1615,18 @@ class SignagePlayer(Widget): if not os.path.exists(intro_path): Logger.warning(f"SignagePlayer: Intro video not found at {intro_path}") - # Skip intro and load playlist - self.intro_played = True - Clock.schedule_once(self.check_playlist_and_play, 0.1) + # No splash to show: go straight to setup or playback. + self.on_intro_finished() return try: Logger.info("SignagePlayer: Playing intro video...") self.ids.status_label.opacity = 0 # Hide status label + + # Same blocking-join hazard as playlist videos: Kivy's on_state + # teardown joins the ffpyplayer decode thread on the calling + # thread. Install the bound join before the widget exists. + suppress_kivy_video_blocking_unload() # Create video widget for intro intro_video = Video( @@ -1407,23 +1646,29 @@ class SignagePlayer(Widget): # Mark intro as played before removing video self.intro_played = True - # Stop and unload the video properly - try: - instance.state = 'stop' - instance.unload() - except Exception as e: - Logger.debug(f"SignagePlayer: Could not unload intro video: {e}") - - # Remove intro video + # Do NOT call instance.state = 'stop' / instance.unload() + # here: this runs during the Kivy MainThread dispatch of + # state, and Kivy's Video.unload() joins the ffpyplayer + # decode thread on the CALLING thread — a blocking call + # that froze the UI for 0.4s..100s+ and, when the thread + # never exited, hung the whole player. Detach the widget + # and let the normal video-teardown worker stop it. try: if intro_video in self.ids.content_area.children: self.ids.content_area.remove_widget(intro_video) except Exception as e: Logger.warning(f"SignagePlayer: Error removing intro video widget: {e}") + + threading.Thread( + target=self._teardown_video_async, + args=(intro_video,), + daemon=True, + name='intro-video-teardown', + ).start() # Start normal playlist immediately to reduce white screen Logger.debug("SignagePlayer: Triggering playlist check after intro") - self.check_playlist_and_play(None) + self.on_intro_finished() except Exception as e: Logger.error(f"SignagePlayer: Error in intro end callback: {e}") import traceback @@ -1438,9 +1683,8 @@ class SignagePlayer(Widget): Logger.error(f"SignagePlayer: Error playing intro video: {e}") import traceback Logger.error(f"SignagePlayer: Traceback: {traceback.format_exc()}") - # Skip intro and load playlist - self.intro_played = True - Clock.schedule_once(self.check_playlist_and_play, 0.1) + # Skip intro and continue with setup or playback + self.on_intro_finished() def check_playlist_and_play(self, dt): """Check for playlist updates and ensure playback is running""" @@ -1448,6 +1692,11 @@ class SignagePlayer(Widget): if not self.intro_played: return + # An unconfigured player has no server to sync from; leave the setup + # flow in charge (otherwise the 30s timer would fight the notice). + if self.requires_setup(): + return + if not self.playlist: self.load_playlist() @@ -1663,6 +1912,39 @@ class SignagePlayer(Widget): self.show_error(f"Error playing media: {e}") self._skip_to_next_media() + def _video_has_audio(self, video_path): + """True when the file actually contains an audio stream. + + Needed because ffpyplayer initialises SDL2_mixer from the FIRST audio + file it opens and reuses those parameters. Handing it a video with NO + audio track (integer division of rate/channels by zero internally) + crashes the process with an access violation in SDL2_mixer.dll + (0xc0000005) — observed when a silent 4K clip entered the playlist + after an AAC stereo clip had already been played. + + Uses ffprobe (bundled next to the app) and fails SAFE: if we cannot + determine the streams, we report True and let the normal path proceed. + """ + try: + ffprobe = os.path.join(os.path.dirname(sys.executable), '_internal', 'ffprobe.exe') + if not os.path.exists(ffprobe): + # Development run: fall back to PATH / the ffpyplayer bundle. + import shutil as _shutil + ffprobe = _shutil.which('ffprobe') + if not ffprobe: + return True # cannot tell -> assume it has audio + + result = subprocess.run( + [ffprobe, '-v', 'error', '-select_streams', 'a', + '-show_entries', 'stream=index', '-of', 'csv=p=0', video_path], + capture_output=True, text=True, timeout=10, + creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0), + ) + return bool((result.stdout or '').strip()) + except Exception as exc: + Logger.debug(f"SignagePlayer: audio probe failed for {video_path}: {exc}") + return True # fail safe: let Kivy try + def play_video(self, video_path, duration, muted=False): """Play a video file using Kivy's Video widget with optimizations. @@ -1682,9 +1964,29 @@ class SignagePlayer(Widget): self.consecutive_errors += 1 self._skip_to_next_media() return - + + # A video with no audio stream must never be handed to ffpyplayer + # with sound enabled: SDL2_mixer access-violates on a silent track. + # Signage has no audio output anyway, so mute it. + if not muted and not self._video_has_audio(video_path): + Logger.info( + "SignagePlayer: Video has no audio track - forcing mute " + "(avoids an SDL2_mixer crash)" + ) + muted = True + Logger.debug(f"SignagePlayer: Loading video {os.path.basename(video_path)} for {duration}s") - + + # Bound the blocking join() Kivy's VideoFFPy performs on the + # CALLING thread during unload. Kivy's own on_eos handler sets + # state='stop' (which joins the ffpyplayer decode thread) while the + # event is being dispatched — i.e. on the Kivy main thread. When + # that thread is slow to exit, the join parks the UI thread and + # Windows declares the app hung (AppHangB1, observed after ~30-45 + # minutes of looping). Must run BEFORE the widget is constructed: + # the decode thread is created during play(). See src/video_safety.py. + suppress_kivy_video_blocking_unload() + # Create Video widget with optimized settings for smooth playback. # Apply the server's audio:off/muted flag via 'volume' (0.0=mute). self._video_source = video_path diff --git a/src/weblink_session.py b/src/weblink_session.py index a5720f9..438da90 100644 --- a/src/weblink_session.py +++ b/src/weblink_session.py @@ -223,6 +223,13 @@ class WeblinkAdapter: #: The session then skips the "browser window appeared" requirement. embedded = False + #: Set by adapters that render in-window but can still *prove* the page + #: appeared (WebView2). Without this, an embedded engine is trusted + #: blindly, so a page that fails to load (unreachable host on a closed + #: network, DNS failure, 404) would sit on screen for the whole slot + #: instead of being skipped. + can_verify_visibility = False + #: The launched process, when the engine is subprocess based. The session #: mirrors this onto the player's historic ``_weblink_proc`` attribute. process = None @@ -705,6 +712,17 @@ class ChromiumSubprocessAdapter(WeblinkAdapter): # Nothing generic to do; the platform layer hooks in here (overlay). pass + def extra_launch_args(self): + """Extra flags appended to the browser command line. + + Subclasses override this instead of duplicating ``launch()``. The + Windows adapter uses it to inject a dedicated ``--user-data-dir``, + which is mandatory there: without it Chrome/Edge hands the URL to an + already-running instance, the process we launched exits immediately + and the weblink never becomes visible. + """ + return () + def launch(self, url, width, height): browser = self._browser or self.find_browser() if not browser: @@ -735,6 +753,7 @@ class ChromiumSubprocessAdapter(WeblinkAdapter): '--force-device-scale-factor=1', ] args += self._extra_flags + args += [str(arg) for arg in self.extra_launch_args()] self._proc = subprocess.Popen(args) return True @@ -958,6 +977,13 @@ class WeblinkSession: with self._lock: if self._watcher is not None: self._watcher.stop() + # An embedded engine normally cannot be verified (CEF), but some can + # (WebView2) — for those the visibility wait must still run, + # otherwise a page that never loads is shown as a blank screen for + # the whole duration instead of being skipped. + verify = adapter.wait_visible if ( + not adapter.embedded or adapter.can_verify_visibility + ) else None self._watcher = InteractionWatcher( duration=duration, alive_check=adapter.is_alive, @@ -972,7 +998,7 @@ class WeblinkSession: interaction_debounce=self.settings.interaction_debounce, interaction_grace=self.settings.interaction_grace, embedded=adapter.embedded, - wait_visible=None if adapter.embedded else adapter.wait_visible, + wait_visible=verify, visible_timeout=self.settings.launch_timeout, launched_at=launched_at, ) diff --git a/windows/build.spec b/windows/build.spec index 0aff137..41ba522 100644 --- a/windows/build.spec +++ b/windows/build.spec @@ -103,6 +103,8 @@ hidden_imports = [ 'tempfile', # Windows-specific 'cef_browser', + 'webview2_browser', + 'webview2_runtime', 'win32gui', 'win32con', # Unified web-link controller (launch / verified visibility / interaction @@ -139,11 +141,60 @@ for item in RESOURCES_DIR.iterdir(): target_dir = 'config/resources' resources_data.append((str(item), target_dir)) -# Config directory (app_config.json) +# Config directory. +# +# app_config.json is deliberately NOT bundled. Including it shipped the +# developer's own server_ip / screen_name inside the exe, so a fresh install +# silently connected to the wrong server (or to a placeholder) instead of +# asking the operator. The player now starts unconfigured, shows a notice after +# the splash video and opens Settings to collect the real values, which are +# then saved next to the .exe. config_data = [] config_file = CONFIG_DIR / 'app_config.json' if config_file.exists(): - config_data.append((str(config_file), 'config')) + print("[spec] app_config.json is NOT bundled (first-run setup collects it)") + +# --- Bundled web engines --------------------------------------------- +# Embedded WebView2 (Edge) SDK: the managed assembly plus the native loader +# DLL. The *runtime* itself is a Microsoft-shipped evergreen component and is +# deliberately NOT bundled (that is the point of using WebView2 — no ~150 MB +# Chromium payload inside our exe). +webview2_data = [] +webview2_sdk = BUILD_DIR / 'webview2_sdk' +if webview2_sdk.is_dir(): + for item in webview2_sdk.iterdir(): + if item.is_file(): + webview2_data.append((str(item), 'webview2_sdk')) + print(f"[spec] Bundling {len(webview2_data)} WebView2 SDK file(s) from {webview2_sdk}") + +# WebView2 Runtime installer, so a machine that ships WITHOUT the Runtime can +# install it on first start (see windows/webview2_runtime.py). +# +# Only the small bootstrapper (~1.7 MB) is bundled by default; it downloads the +# Runtime from Microsoft. Dropping the ~203 MB offline standalone installer +# into windows/webview2_runtime/ bundles it too, which is what you want for +# machines with no internet — but it triples the exe size, so it is opt-in. +webview2_runtime = BUILD_DIR / 'webview2_runtime' +_bootstrap = webview2_runtime / 'MicrosoftEdgeWebview2Setup.exe' +_standalone = webview2_runtime / 'MicrosoftEdgeWebView2RuntimeInstallerX64.exe' +if _bootstrap.is_file(): + webview2_data.append((str(_bootstrap), 'webview2_runtime')) + print(f"[spec] Bundling WebView2 Runtime bootstrapper ({_bootstrap.stat().st_size / 1024 / 1024:.1f} MB)") +if _standalone.is_file(): + webview2_data.append((str(_standalone), 'webview2_runtime')) + print(f"[spec] Bundling WebView2 offline standalone installer " + f"({_standalone.stat().st_size / 1024 / 1024:.0f} MB) — exe will be much larger") +if not _bootstrap.is_file() and not _standalone.is_file(): + print("=" * 70) + print("WARNING: no WebView2 Runtime installer in windows/webview2_runtime/.") + print("Machines without the Runtime cannot show web links (they fall back") + print("to the Chrome/Edge subprocess engine).") + print("=" * 70) +else: + print("=" * 70) + print("WARNING: windows/webview2_sdk/ not found.") + print("Web links will fall back to the Chrome/Edge subprocess engine.") + print("=" * 70) # Source files - .kv file kv_file = SRC_DIR / 'signage_player.kv' @@ -151,8 +202,19 @@ kv_data = [] 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='', excludes=['*.pyc', '__pycache__', '*.ini']) +# Bundle the entire src directory as a tree. +# +# EXCLUDE player_auth.json: it holds LIVE credentials (auth_code, player_id, +# server_url). Bundling it means the frozen app starts up in _internal/ and +# loads that snapshot as its auth state — so a freshly built exe boots +# "already authenticated" against whatever server the file happened to name, +# and plays a stale playlist. Auth must be created at runtime in the data dir +# next to the .exe (see run_win.py `_patch_auth_paths`). +source_tree = Tree( + str(SRC_DIR), + prefix='', + excludes=['*.pyc', '__pycache__', '*.ini', 'player_auth.json'], +) # --- Collect binary DLLs from kivy_deps and ffpyplayer ---------------- import importlib.util @@ -250,7 +312,7 @@ a = Analysis( ['run_win.py'], # Entry point (relative to this spec) pathex=[str(BUILD_DIR), str(SRC_DIR)], # Where to find modules binaries=_all_binaries, - datas=resources_data + config_data + kv_data, + datas=resources_data + config_data + kv_data + webview2_data, hiddenimports=hidden_imports, hookspath=[], hooksconfig={}, diff --git a/windows/requirements_win.txt b/windows/requirements_win.txt index da31bdd..b0145f6 100644 --- a/windows/requirements_win.txt +++ b/windows/requirements_win.txt @@ -23,6 +23,18 @@ bcrypt>=4.2.0,<5.0.0 # PyInstaller for building the .exe pyinstaller>=6.0 +# --- Embedded web engine (web links) --- +# pythonnet lets Python drive the WebView2 .NET SDK. WebView2 renders INSIDE +# the Kivy window as a child window, which is what removed the old subprocess +# browser bugs (window opening behind the player, instant hand-off exit, +# z-order/focus fights, leaked chrome.exe/msedge.exe processes). +# The WebView2 *runtime* is a free, Microsoft-shipped evergreen component and +# is intentionally NOT bundled; the small SDK DLLs live in windows/webview2_sdk/ +# and are added to the exe by build.spec. +# Without pythonnet the player silently falls back to the Chrome/Edge +# subprocess engine, so weblinks still work but with the old drawbacks. +pythonnet>=3.0.3 + # --- Windows-specific Libraries --- # cefpython3: Embedded Chromium browser (replaces subprocess Chrome/Edge) # Installed separately because it's a large package (69 MB): diff --git a/windows/test_webview2_embed.py b/windows/test_webview2_embed.py new file mode 100644 index 0000000..b06bea8 --- /dev/null +++ b/windows/test_webview2_embed.py @@ -0,0 +1,169 @@ +"""Standalone harness for windows/webview2_browser.py — no Kivy, no player. + +Creates a plain Win32 window, embeds WebView2 in it via the same +WebView2Browser class the player uses, navigates to a page, checks that the +page actually becomes visible, then resizes and tears down. + +Run: windows\\venv\\Scripts\\python.exe windows\\test_webview2_embed.py +Exit code 0 = embedded engine works. +""" + +import ctypes +import os +import sys +import time +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +URL = os.environ.get('KIWY_TEST_URL', 'https://example.com/') + +user32 = ctypes.windll.user32 +kernel32 = ctypes.windll.kernel32 + +WNDPROC = ctypes.WINFUNCTYPE( + ctypes.c_int64, + ctypes.c_void_p, # HWND + ctypes.c_uint, # UINT msg + ctypes.c_void_p, # WPARAM + ctypes.c_void_p, # LPARAM +) + +_messages = [] + + +@WNDPROC +def _wnd_proc(hwnd, msg, wparam, lparam): + _messages.append(msg) + if msg == 0x0002: # WM_DESTROY + user32.PostQuitMessage(0) + return 0 + user32.DefWindowProcW.restype = ctypes.c_int64 + user32.DefWindowProcW.argtypes = [ + ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p, ctypes.c_void_p, + ] + return user32.DefWindowProcW(hwnd, msg, wparam, lparam) + + +def _make_window(width=1280, height=720): + """Register a class and create a visible top-level window.""" + hinstance = kernel32.GetModuleHandleW(None) + class_name = 'KiwyWebView2Test' + + class WNDCLASSEX(ctypes.Structure): + _fields_ = [ + ('cbSize', ctypes.c_uint), + ('style', ctypes.c_uint), + ('lpfnWndProc', WNDPROC), + ('cbClsExtra', ctypes.c_int), + ('cbWndExtra', ctypes.c_int), + ('hInstance', ctypes.c_void_p), + ('hIcon', ctypes.c_void_p), + ('hCursor', ctypes.c_void_p), + ('hbrBackground', ctypes.c_void_p), + ('lpszMenuName', ctypes.c_wchar_p), + ('lpszClassName', ctypes.c_wchar_p), + ('hIconSm', ctypes.c_void_p), + ] + + wc = WNDCLASSEX() + wc.cbSize = ctypes.sizeof(WNDCLASSEX) + wc.style = 0x0002 | 0x0001 # CS_HREDRAW | CS_VREDRAW + wc.lpfnWndProc = _wnd_proc + wc.hInstance = hinstance + wc.hbrBackground = ctypes.c_void_p(6) # COLOR_WINDOW+1 + wc.lpszClassName = class_name + user32.RegisterClassExW(ctypes.byref(wc)) + + hwnd = user32.CreateWindowExW( + 0, + class_name, + 'Kiwy WebView2 Embed Test', + 0x00CF0000 | 0x10000000, # WS_OVERLAPPEDWINDOW | WS_VISIBLE + 100, 100, width, height, + 0, 0, hinstance, 0, + ) + if not hwnd: + raise RuntimeError(f'CreateWindowExW failed (err={kernel32.GetLastError()})') + user32.UpdateWindow(hwnd) + return hwnd + + +def _pump(seconds): + """Pump Win32 messages — WebView2 needs this to deliver its callbacks.""" + deadline = time.monotonic() + seconds + while time.monotonic() < deadline: + msg = ctypes.wintypes.MSG() if hasattr(ctypes, 'wintypes') else None + import ctypes.wintypes as wt + + msg = wt.MSG() + while user32.PeekMessageW(ctypes.byref(msg), None, 0, 0, 1): + user32.TranslateMessage(ctypes.byref(msg)) + user32.DispatchMessageW(ctypes.byref(msg)) + time.sleep(0.02) + + +def main(): + print('=' * 68) + print(' WebView2 embedded-engine test') + print('=' * 68) + + from webview2_browser import WebView2Browser + + print(f'SDK dir : {WebView2Browser.__module__}') + available = WebView2Browser.is_available() + print(f'available: {available}') + if not available: + print(f'REASON: {WebView2Browser._import_error}') + return 1 + + hwnd = _make_window() + print(f'window : hwnd=0x{hwnd:x}') + + browser = WebView2Browser(hwnd_provider=lambda: hwnd) + started = time.monotonic() + ok = browser.show(URL) + print(f'show() : {ok}') + if not ok: + print(f'FAILED : {browser.failed_reason}') + return 1 + + # Drive the Kivy-style Clock poll manually while pumping messages. + visible = False + while time.monotonic() - started < 25: + browser._tick(0) # consume the async task + _pump(0.1) + if browser.failed_reason: + print(f'FAILED : {browser.failed_reason}') + return 1 + if browser.is_showing(): + visible = True + break + print(f'visible : {visible} after {time.monotonic() - started:.1f}s') + if not visible: + print('FAILED : page never became visible') + return 1 + + # Resize to the signage resolution and confirm it is applied. + browser.resize(1920, 1080) + _pump(1.0) + print(f'resized : 1920x1080 (bounds={browser._size})') + + # Hide, then re-show to prove the controller survives a transition. + browser.hide() + _pump(0.5) + print(f'after hide -> is_showing={browser.is_showing()}') + browser.show(URL) + _pump(2.0) + print(f'after re-show -> is_showing={browser.is_showing()}') + + browser.shutdown() + print('shutdown: ok') + print('=' * 68) + print(' RESULT: PASS') + print('=' * 68) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/windows/test_webview2_navigation.py b/windows/test_webview2_navigation.py new file mode 100644 index 0000000..bac7290 --- /dev/null +++ b/windows/test_webview2_navigation.py @@ -0,0 +1,186 @@ +"""Does pythonnet fire NavigationCompleted for a real page load? + +This validates the mechanism the player relies on to tell "page loaded" apart +from "page failed" (e.g. unreachable host on a closed network). If the event +does not fire, the player cannot detect a failed weblink and would show +Chromium's error page for the full slot. + +Checks three things: + 1. the delegate can be constructed and subscribed, + 2. it fires for a GOOD page -> IsSuccess True, + 3. it fires for a BAD page -> IsSuccess False. + +Run: windows\\venv\\Scripts\\python.exe windows\\test_webview2_navigation.py +Exit code 0 = PASS. +""" + +import ctypes +import http.server +import socketserver +import sys +import threading +import time +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +PORT = 18766 +PAGE = '

KIWY-NAV-OK

' + + +class _Handler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + body = PAGE.encode() + self.send_response(200) + self.send_header('Content-Type', 'text/html') + self.send_header('Content-Length', str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, *args): + pass + + +def _start_server(): + socketserver.TCPServer.allow_reuse_address = True + httpd = socketserver.TCPServer(('127.0.0.1', PORT), _Handler) + threading.Thread(target=httpd.serve_forever, daemon=True).start() + return httpd + + +user32 = ctypes.windll.user32 +kernel32 = ctypes.windll.kernel32 +WNDPROC = ctypes.WINFUNCTYPE( + ctypes.c_int64, ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p, ctypes.c_void_p +) + + +@WNDPROC +def _wnd_proc(hwnd, msg, wparam, lparam): + if msg == 0x0002: + user32.PostQuitMessage(0) + return 0 + user32.DefWindowProcW.restype = ctypes.c_int64 + user32.DefWindowProcW.argtypes = [ + ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p, ctypes.c_void_p, + ] + return user32.DefWindowProcW(hwnd, msg, wparam, lparam) + + +def _make_window(width=1024, height=768): + hinstance = kernel32.GetModuleHandleW(None) + name = 'KiwyNavTest' + + class WNDCLASSEX(ctypes.Structure): + _fields_ = [ + ('cbSize', ctypes.c_uint), ('style', ctypes.c_uint), + ('lpfnWndProc', WNDPROC), ('cbClsExtra', ctypes.c_int), + ('cbWndExtra', ctypes.c_int), ('hInstance', ctypes.c_void_p), + ('hIcon', ctypes.c_void_p), ('hCursor', ctypes.c_void_p), + ('hbrBackground', ctypes.c_void_p), ('lpszMenuName', ctypes.c_wchar_p), + ('lpszClassName', ctypes.c_wchar_p), ('hIconSm', ctypes.c_void_p), + ] + + wc = WNDCLASSEX() + wc.cbSize = ctypes.sizeof(WNDCLASSEX) + wc.style = 0x0002 | 0x0001 + wc.lpfnWndProc = _wnd_proc + wc.hInstance = hinstance + wc.hbrBackground = ctypes.c_void_p(6) + wc.lpszClassName = name + user32.RegisterClassExW(ctypes.byref(wc)) + hwnd = user32.CreateWindowExW( + 0, name, 'Kiwy Nav Test', 0x00CF0000 | 0x10000000, + 40, 40, width, height, 0, 0, hinstance, 0, + ) + if not hwnd: + raise RuntimeError('CreateWindowExW failed') + user32.UpdateWindow(hwnd) + return hwnd + + +def _pump(seconds): + import ctypes.wintypes as wt + + deadline = time.monotonic() + seconds + while time.monotonic() < deadline: + msg = wt.MSG() + while user32.PeekMessageW(ctypes.byref(msg), None, 0, 0, 1): + user32.TranslateMessage(ctypes.byref(msg)) + user32.DispatchMessageW(ctypes.byref(msg)) + time.sleep(0.02) + + +def _drive(browser, seconds): + deadline = time.monotonic() + seconds + while time.monotonic() < deadline: + browser._tick(0) + _pump(0.05) + + +def _wait_nav(browser, timeout): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + browser._tick(0) + _pump(0.05) + if browser.navigation_succeeded() is not None: + return browser.navigation_succeeded() + return None + + +def main(): + print('=' * 68) + print(' WebView2 NavigationCompleted test') + print('=' * 68) + + from webview2_browser import WebView2Browser + + if not WebView2Browser.is_available(): + print('FAIL: WebView2 unavailable:', WebView2Browser._import_error) + return 1 + + httpd = _start_server() + good = f'http://127.0.0.1:{PORT}/index.html' + # A port nothing listens on: guarantees a real navigation failure. + bad = 'http://127.0.0.1:1/missing' + + hwnd = _make_window() + browser = WebView2Browser(hwnd_provider=lambda: hwnd) + + ok = True + + print(f'\n[1] good page: {good}') + browser.show(good) + _drive(browser, 1.0) + result = _wait_nav(browser, 20) + print(f' navigation_succeeded = {result}') + print(f' status = {browser.navigation_status()!r}') + if result is not True: + print(' FAIL: good page did not report success') + ok = False + + print(f'\n[2] bad page: {bad}') + browser.show(bad) + _drive(browser, 1.0) + result = _wait_nav(browser, 25) + print(f' navigation_succeeded = {result}') + print(f' status = {browser.navigation_status()!r}') + if result is not False: + print(' FAIL: bad page did not report failure') + ok = False + + browser.shutdown() + httpd.shutdown() + httpd.server_close() + + print('=' * 68) + print(' RESULT:', 'PASS' if ok else 'FAIL') + if ok: + print(' The player can tell a loaded page from a failed one,') + print(' so unreachable weblinks are skipped instead of shown blank.') + print('=' * 68) + return 0 if ok else 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/windows/test_webview2_offline.py b/windows/test_webview2_offline.py new file mode 100644 index 0000000..5278885 --- /dev/null +++ b/windows/test_webview2_offline.py @@ -0,0 +1,221 @@ +"""Closed-network test: does a WebView2 page still load with no internet? + +The signage player lives on an isolated LAN, so the important question is not +"does example.com load" but "does a page on a reachable *local* host still +render when there is no internet at all". + +This test simulates that properly: + +1. Start a tiny HTTP server on 127.0.0.1 serving a known marker page. +2. Create the WebView2 environment **with the same offline browser arguments + the player uses** (webview2_browser._build_environment_options). +3. Navigate to the local page and confirm the page's actual content arrives — + not merely that the controller came up. + +It also blocks real internet resolution for the browser by pointing it at the +local server only, so a pass here means offline playback genuinely works. + +Run: windows\\venv\\Scripts\\python.exe windows\\test_webview2_offline.py +Exit code 0 = PASS. +""" + +import ctypes +import http.server +import os +import socketserver +import sys +import threading +import time +import urllib.request +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +MARKER = 'KIWY-OFFLINE-LAN-OK' +PORT = 18765 + +PAGE = f""" +pc + +
{MARKER}
+""" + + +class _Handler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + body = PAGE.encode('utf-8') + self.send_response(200) + self.send_header('Content-Type', 'text/html; charset=utf-8') + self.send_header('Content-Length', str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, *args): + pass # keep the test output clean + + +def _start_server(): + socketserver.TCPServer.allow_reuse_address = True + httpd = socketserver.TCPServer(('127.0.0.1', PORT), _Handler) + thread = threading.Thread(target=httpd.serve_forever, daemon=True) + thread.start() + return httpd + + +# ── Win32 window (same approach as test_webview2_embed.py) ────────── +user32 = ctypes.windll.user32 +kernel32 = ctypes.windll.kernel32 + +WNDPROC = ctypes.WINFUNCTYPE( + ctypes.c_int64, ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p, ctypes.c_void_p +) + + +@WNDPROC +def _wnd_proc(hwnd, msg, wparam, lparam): + if msg == 0x0002: # WM_DESTROY + user32.PostQuitMessage(0) + return 0 + user32.DefWindowProcW.restype = ctypes.c_int64 + user32.DefWindowProcW.argtypes = [ + ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p, ctypes.c_void_p, + ] + return user32.DefWindowProcW(hwnd, msg, wparam, lparam) + + +def _make_window(width=1280, height=720): + hinstance = kernel32.GetModuleHandleW(None) + class_name = 'KiwyWebView2OfflineTest' + + class WNDCLASSEX(ctypes.Structure): + _fields_ = [ + ('cbSize', ctypes.c_uint), ('style', ctypes.c_uint), + ('lpfnWndProc', WNDPROC), ('cbClsExtra', ctypes.c_int), + ('cbWndExtra', ctypes.c_int), ('hInstance', ctypes.c_void_p), + ('hIcon', ctypes.c_void_p), ('hCursor', ctypes.c_void_p), + ('hbrBackground', ctypes.c_void_p), ('lpszMenuName', ctypes.c_wchar_p), + ('lpszClassName', ctypes.c_wchar_p), ('hIconSm', ctypes.c_void_p), + ] + + wc = WNDCLASSEX() + wc.cbSize = ctypes.sizeof(WNDCLASSEX) + wc.style = 0x0002 | 0x0001 + wc.lpfnWndProc = _wnd_proc + wc.hInstance = hinstance + wc.hbrBackground = ctypes.c_void_p(6) + wc.lpszClassName = class_name + user32.RegisterClassExW(ctypes.byref(wc)) + + hwnd = user32.CreateWindowExW( + 0, class_name, 'Kiwy Offline LAN Test', + 0x00CF0000 | 0x10000000, 60, 60, width, height, 0, 0, hinstance, 0, + ) + if not hwnd: + raise RuntimeError('CreateWindowExW failed') + user32.UpdateWindow(hwnd) + return hwnd + + +def _pump(seconds): + import ctypes.wintypes as wt + + deadline = time.monotonic() + seconds + while time.monotonic() < deadline: + msg = wt.MSG() + while user32.PeekMessageW(ctypes.byref(msg), None, 0, 0, 1): + user32.TranslateMessage(ctypes.byref(msg)) + user32.DispatchMessageW(ctypes.byref(msg)) + time.sleep(0.02) + + +def main(): + print('=' * 68) + print(' WebView2 closed-network (LAN-only) test') + print('=' * 68) + + from webview2_browser import ( + WebView2Browser, _build_environment_options, _offline_browser_arguments, + ) + + if not WebView2Browser.is_available(): + print('FAIL: WebView2 unavailable:', WebView2Browser._import_error) + return 1 + + print('offline browser args:') + for flag in _offline_browser_arguments().split(): + print(f' {flag}') + + options = _build_environment_options() + if options is None: + print('\nFAIL: could not build offline environment options') + return 1 + print(f'\nAdditionalBrowserArguments set: ' + f'{bool(options.AdditionalBrowserArguments)}') + + httpd = _start_server() + url = f'http://127.0.0.1:{PORT}/dashboard' + print(f'\nlocal server: {url}') + + hwnd = _make_window() + browser = WebView2Browser(hwnd_provider=lambda: hwnd) + + started = time.monotonic() + if not browser.show(url): + print('FAIL: show() returned False:', browser.failed_reason) + httpd.shutdown() + return 1 + + visible = False + deadline = time.monotonic() + 25 + while time.monotonic() < deadline: + browser._tick(0) + _pump(0.1) + if browser.failed_reason: + print('FAIL:', browser.failed_reason) + httpd.shutdown() + return 1 + if browser.is_showing(): + visible = True + break + print(f'page visible : {visible} after {time.monotonic() - started:.1f}s') + + # Confirm the page's REAL CONTENT arrived, not just the controller. + # + # NOTE: ExecuteScriptAsync also returns a .NET Task. Calling .Result here + # would deadlock — the continuation needs this thread's message pump, which + # is exactly the mistake this file otherwise exists to catch. Poll it while + # pumping messages instead. + body = '' + if visible: + try: + task = browser._webview.ExecuteScriptAsync( + "document.getElementById('m').innerText" + ) + deadline = time.monotonic() + 10 + while time.monotonic() < deadline: + _pump(0.05) + if task.IsCompleted: + body = task.Result + break + except Exception as exc: + print(f'note: script eval failed ({exc})') + got_marker = MARKER in (body or '') + print(f'page content : {"marker found" if got_marker else "MARKER MISSING"} ' + f'({(body or "")[:60]})') + + browser.shutdown() + httpd.shutdown() + httpd.server_close() + + ok = visible and got_marker + print('=' * 68) + print(' RESULT:', 'PASS' if ok else 'FAIL') + if ok: + print(' A local page renders with all internet traffic disabled —') + print(' web links work on a closed network.') + print('=' * 68) + return 0 if ok else 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/windows/test_webview2_runtime.py b/windows/test_webview2_runtime.py new file mode 100644 index 0000000..2f5db1e --- /dev/null +++ b/windows/test_webview2_runtime.py @@ -0,0 +1,106 @@ +"""Standalone test for windows/webview2_runtime.py — no Kivy, no player. + +Checks the runtime-detection logic and (optionally) a real silent install. + +Run: windows\\venv\\Scripts\\python.exe windows\\test_webview2_runtime.py + windows\\venv\\Scripts\\python.exe windows\\test_webview2_runtime.py --install + +Without --install this is read-only: it reports the detected version and which +installer would be used. With --install it forces the installer path to run +(useful on a machine that genuinely lacks the Runtime). +Exit code 0 = checks passed. +""" + +import os +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +import webview2_runtime as w # noqa: E402 + + +def main(): + force_install = '--install' in sys.argv + + print('=' * 68) + print(' WebView2 Runtime detection test') + print('=' * 68) + + version = w.get_runtime_version() + installed = w.is_runtime_installed() + print(f'registry/SDK version : {version or "(none)"}') + print(f'is_runtime_installed : {installed}') + + installer, kind = w.find_installer() + print(f'installer : {installer}') + print(f'installer kind : {kind}') + print(f'describe() : {w.describe()}') + + ok = True + + # Version parsing must be comparable and tolerant of junk. + cases = { + '152.0.4191.66': (152, 0, 4191, 66), + '1.2': (1, 2, 0, 0), + '': (0, 0, 0, 0), + None: (0, 0, 0, 0), + } + for raw, expected in cases.items(): + got = w._parse_version(raw) + flag = 'ok' if got == expected else 'FAIL' + if got != expected: + ok = False + print(f' parse({raw!r:16}) -> {got} [{flag}]') + + # An installer must be discoverable: without one, a Runtime-less machine + # has no way to recover. + if installer is None: + print('\nWARNING: no installer found — a machine without the Runtime ' + 'cannot self-heal.') + print('Run: .\\webview2_runtime\\download_runtime_installers.ps1') + else: + sig_status = 'n/a' + try: + import subprocess + + out = subprocess.run( + ['powershell', '-NoProfile', '-Command', + f'(Get-AuthenticodeSignature -LiteralPath "{installer}").Status'], + capture_output=True, text=True, timeout=60, + ) + sig_status = (out.stdout or '').strip() or 'unknown' + except Exception as exc: + sig_status = f'check failed: {exc}' + print(f'signature : {sig_status}') + + if force_install: + print('\n--install given: running the silent installer path...') + result = w.ensure_runtime(timeout=600) + print(f'ensure_runtime() -> {result}') + if not result.get('installed'): + ok = False + else: + # Read-only path: ensure_runtime must be a no-op that reports presence. + result = w.ensure_runtime(timeout=60) + print(f'\nensure_runtime() (read-only) -> {result}') + if installed and not result.get('installed'): + print('FAIL: Runtime present but ensure_runtime() disagreed') + ok = False + if result.get('action') not in ('already-present', 'installer-missing', + 'skipped-recent-failure', + 'installed-standalone', + 'installed-bootstrapper', + 'attempted-standalone', + 'attempted-bootstrapper'): + print(f'FAIL: unexpected action {result.get("action")!r}') + ok = False + + print('=' * 68) + print(' RESULT:', 'PASS' if ok else 'FAIL') + print('=' * 68) + return 0 if ok else 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/windows/webview2_browser.py b/windows/webview2_browser.py new file mode 100644 index 0000000..735fe66 --- /dev/null +++ b/windows/webview2_browser.py @@ -0,0 +1,718 @@ +"""webview2_browser.py — Embedded WebView2 (Edge/Chromium) INSIDE Kivy's window. + +Why this exists +--------------- +The old weblink engines launched a *separate* browser process (Chrome/Edge +kiosk subprocess, or the dormant `cef_browser.py`). That model caused every +weblink bug in the tracker: the browser opening behind the Kivy window, being +handed off to an existing instance and exiting instantly, fighting for +foreground/z-order, and leaking `msedge.exe`/`chrome.exe` processes that were +never closed. + +WebView2 renders as a **child HWND of Kivy's own SDL window**, so: + + * no separate top-level window → nothing can open "in the background", + * nothing to hand the URL off to → no instant-exit hand-off, + * no z-order/foreground fight → it is literally a child of our window, + * teardown is ours → no leaked browser processes, + * the page renders at exactly the rectangle we give it (1920x1080 or + whatever the Kivy window currently is). + +Licensing / distribution: the WebView2 **runtime** is a free, evergreen, +Microsoft-shipped component (already present on this host as +``152.0.4191.66``). We only ship the small managed SDK + native loader DLLs. + +Implementation notes +-------------------- +* We talk to the .NET SDK through **pythonnet** (``clr``). +* Every WebView2 API is async (returns a .NET ``Task``). We must NOT call + ``.GetAwaiter().GetResult()``: the continuation needs the *same* thread's + message pump, so blocking would deadlock. Instead each task is **polled from + Kivy's Clock** (the SDL thread, which pumps Win32 messages) and consumed when + ``IsCompleted``. This mirrors how the old CEF code pumped via the Clock. +* All public methods are safe to call from the Kivy main thread. +""" + +from __future__ import annotations + +import ctypes +import os +import sys +import threading +from pathlib import Path + +# ── SDK discovery ──────────────────────────────────────────────────── +# The managed Microsoft.Web.WebView2.Core.dll and the native +# WebView2Loader.dll must sit in a folder we can find both in development and +# inside the PyInstaller bundle. +_SDK_ENV_VAR = 'KIWY_WEBVIEW2_SDK' + + +def _sdk_candidates(): + here = Path(__file__).resolve().parent + yield here / 'webview2_sdk' + # PyInstaller one-folder layout: bundled data lands next to the exe + # (sys._MEIPASS points at the temporary _MEIxxx dir). + meipass = getattr(sys, '_MEIPASS', None) + if meipass: + yield Path(meipass) / 'webview2_sdk' + yield here + + +def _find_sdk_dir(): + env = os.environ.get(_SDK_ENV_VAR) + if env and (Path(env) / 'Microsoft.Web.WebView2.Core.dll').is_file(): + return Path(env) + for candidate in _sdk_candidates(): + try: + if (candidate / 'Microsoft.Web.WebView2.Core.dll').is_file(): + return candidate + except OSError: + continue + return None + + +# ── Win32 helpers ──────────────────────────────────────────────────── +_SW_HIDE = 0 +_SW_SHOWNORMAL = 1 + + +class WebView2Browser: + """One embedded WebView2 instance parented to the Kivy (SDL) window. + + Lifecycle:: + + show(url) -> is_showing() (True once painted) -> resize(w,h) -> hide() + -> shutdown() + + ``hide()`` only hides the controller (it stays alive), so switching back to + a weblink later is instant. ``shutdown()`` disposes it for good. + """ + + #: Set True by the integration layer when the SDK + runtime are usable. + _import_error = None + + def __init__(self, hwnd_provider=None, user_data_dir=None): + self._hwnd_provider = hwnd_provider + self._user_data_dir = user_data_dir or os.path.join( + os.environ.get('KIWY_DATA_DIR', os.getcwd()), '.webview2-profile' + ) + self._env = None + self._controller = None + self._webview = None + self._hwnd = None + self._showing = False + self._stage = 'idle' # idle | env | controller | ready + self._pending_url = None + self._failed_reason = '' + self._poll_event = None + self._lock = threading.RLock() + self._task = None + self._task_kind = None + self._size = (0, 0) + # Navigation outcome. `_showing` only means "the controller was told to + # be visible", which happens the instant Navigate() is called — it says + # nothing about whether the page actually loaded. On a closed network + # that distinction is the whole point: an unreachable host paints a + # Chromium error page, so without this the player would show a blank + # error for the full slot instead of skipping the item. + self._navigation_ok = None # None = pending/unknown + self._navigation_status = '' + self._navigation_handlers = [] # keep refs: .NET must not GC these + + # ── Availability ───────────────────────────────────────────────── + @staticmethod + def is_available(): + """True when pythonnet + the SDK DLLs + a runtime are all present.""" + if sys.platform != 'win32': + return False + sdk = _find_sdk_dir() + if sdk is None: + return False + try: + import clr # noqa: F401 (pythonnet) + except Exception as exc: + WebView2Browser._import_error = f'pythonnet unavailable: {exc}' + return False + try: + cls = _load_webview2_types(sdk) + version = cls['env'].GetAvailableBrowserVersionString() + return bool(version) + except Exception as exc: + WebView2Browser._import_error = f'WebView2 unavailable: {exc}' + return False + + # ── Public API (Kivy main thread) ──────────────────────────────── + def show(self, url): + """Begin displaying ``url``. Returns True once the request is accepted. + + Rendering is asynchronous: the caller should poll :meth:`is_showing` + (the session's ``wait_visible`` does this on the watcher thread). + """ + with self._lock: + self._failed_reason = '' + self._pending_url = url + + if self._stage == 'ready' and self._controller is not None: + return self._navigate(url) + + if self._stage in ('env', 'controller'): + return True # already starting up; URL is queued + + # Start-up order: environment → controller → navigate. + self._stage = 'env' + if not self._start_environment(): + self._stage = 'idle' + return False + if self._stage != 'env': + # The environment resolved synchronously (fast path). + return self._after_environment() + return True + + def hide(self): + """Hide the page without destroying the controller (fast re-show).""" + with self._lock: + self._showing = False + self._pending_url = None + controller = self._controller + if controller is not None: + try: + controller.IsVisible = False + except Exception: + pass + + def is_showing(self): + """True while the page is actually on screen.""" + with self._lock: + if self._failed_reason: + return False + if self._controller is None: + return False + return self._showing + + def is_starting(self): + """True while the environment/controller is still being created. + + WebView2 start-up is asynchronous. A controller that does not exist yet + is NOT the same as a browser that has gone away, and conflating the two + made the *first* weblink after a cold start be skipped instantly (the + watcher saw "not alive" and advanced). Callers should treat + ``is_starting()`` as "still alive, not yet painted". + """ + with self._lock: + if self._failed_reason: + return False + return self._stage in ('env', 'controller') + + def is_alive(self): + """True when the browser is starting up or showing. False only on failure.""" + return self.is_showing() or self.is_starting() + + @property + def failed_reason(self): + return self._failed_reason + + def resize(self, width, height): + """Fit the page to ``width`` x ``height`` physical pixels.""" + width, height = int(width), int(height) + if width <= 0 or height <= 0: + return + with self._lock: + self._size = (width, height) + controller = self._controller + if controller is None: + return + try: + from System.Drawing import Rectangle + + controller.Bounds = Rectangle(0, 0, width, height) + except Exception as exc: + _log(f'WebView2 resize failed (non-fatal): {exc}') + + def shutdown(self): + """Dispose the controller and environment. Never raises.""" + with self._lock: + self._showing = False + self._stop_poll_locked() + controller, self._controller = self._controller, None + webview, self._webview = self._webview, None + env, self._env = self._env, None + self._stage = 'idle' + for obj, label in ((webview, 'webview'), (controller, 'controller')): + if obj is None: + continue + try: + dispose = getattr(obj, 'Dispose', None) + if dispose is not None: + dispose() + except Exception as exc: + _log(f'WebView2 {label} dispose failed (non-fatal): {exc}') + if ctypes is not None: + try: + ctypes.windll.ole32.CoUninitialize() + except Exception: + pass + del env + + # ── Start-up ───────────────────────────────────────────────────── + def _start_environment(self): + sdk = _find_sdk_dir() + if sdk is None: + self._failed_reason = 'WebView2 SDK not found' + _log('WebView2: SDK DLLs not found (expected Microsoft.Web.WebView2.Core.dll)') + return False + try: + types = _load_webview2_types(sdk) + except Exception as exc: + self._failed_reason = f'WebView2 SDK load failed: {exc}' + _log(f'WebView2: SDK load failed: {exc}') + return False + + # The controller must live on a thread with a message pump; Kivy's SDL + # thread qualifies, and COM must be initialised on it first. + try: + ctypes.windll.ole32.CoInitializeEx(None, 0x2) # STA + except Exception: + pass + + try: + os.makedirs(self._user_data_dir, exist_ok=True) + except Exception as exc: + _log(f'WebView2: could not create profile dir ({exc}); using temp') + import tempfile + + self._user_data_dir = tempfile.mkdtemp(prefix='kiwy-wv2-') + + _log(f'WebView2: creating environment (profile={self._user_data_dir})') + try: + options = _build_environment_options() + task = _create_environment_async(types, self._user_data_dir, options) + except Exception as exc: + self._failed_reason = f'CreateAsync failed: {exc}' + _log(f'WebView2: environment creation failed: {exc}') + return False + + self._task = task + self._task_kind = 'env' + self._start_poll() + return True + + def _start_controller(self): + hwnd = 0 + if self._hwnd_provider is not None: + try: + hwnd = self._hwnd_provider() or 0 + except Exception as exc: + _log(f'WebView2: hwnd provider failed: {exc}') + if not hwnd: + self._failed_reason = 'Kivy window handle not found' + _log('WebView2: could not locate the Kivy SDL window handle') + return False + self._hwnd = int(hwnd) + + _log(f'WebView2: creating controller inside hwnd=0x{self._hwnd:x}') + try: + # The parent window must be a .NET IntPtr; a plain Python int does + # not match the overload and pythonnet raises "No method matches + # given arguments". + from System import IntPtr + + parent = IntPtr(self._hwnd) + # HWND hosting: WebView2 creates its own child window in `parent`. + task = self._env.CreateCoreWebView2ControllerAsync(parent) + except Exception as exc: + self._failed_reason = f'controller creation failed: {exc}' + _log(f'WebView2: controller creation failed: {exc}') + return False + + self._task = task + self._task_kind = 'controller' + self._stage = 'controller' + self._start_poll() + return True + + def _after_environment(self): + """Called once the environment resolved.""" + if self._env is None: + return False + started = self._start_controller() + if not started and self._stage == 'controller': + return True # still coming up asynchronously + return started + + # ── Async task polling (Kivy Clock) ────────────────────────────── + def _start_poll(self): + try: + from kivy.clock import Clock + + if self._poll_event is None: + self._poll_event = Clock.schedule_interval(self._tick, 0.05) + except Exception: + # No Kivy (or called off-thread): poll from a plain timer instead. + if self._poll_event is None: + self._poll_event = _ThreadTimer(0.05, self._tick, None) + + def _stop_poll_locked(self): + event, self._poll_event = self._poll_event, None + if event is None: + return + try: + cancel = getattr(event, 'cancel', None) + if cancel is not None: + cancel() + else: + event.stop() + except Exception: + pass + + def _tick(self, _dt): + """Consume the in-flight Task once it completes.""" + with self._lock: + task, kind = self._task, self._task_kind + if task is None: + self._stop_poll_locked() + return False + try: + done = bool(task.IsCompleted) + except Exception as exc: + self._failed_reason = f'task poll failed: {exc}' + self._task = None + self._stop_poll_locked() + return False + if not done: + return True + self._task, self._task_kind = None, None + self._stop_poll_locked() + + try: + if task.IsFaulted: + exc = task.Exception + detail = '' + try: + detail = exc.GetBaseException().Message + except Exception: + detail = str(exc) + self._failed_reason = f'{kind} failed: {detail}' + _log(f'WebView2: {kind} task faulted: {detail}') + return False + result = task.Result + except Exception as exc: + self._failed_reason = f'{kind} task error: {exc}' + _log(f'WebView2: {kind} task error: {exc}') + return False + + if kind == 'env': + self._env = result + _log('WebView2: environment ready') + if not self._start_controller(): + self._stage = 'idle' + return False + + if kind == 'controller': + self._controller = result + self._on_controller_ready() + return False + + return False + + def _on_controller_ready(self): + """Wire up the page: bounds, settings, first navigation.""" + controller = self._controller + try: + controller.IsVisible = False # stay hidden until navigated + except Exception: + pass + + webview = None + try: + webview = controller.CoreWebView2 + except Exception as exc: + _log(f'WebView2: CoreWebView2 unavailable: {exc}') + if webview is None: + self._failed_reason = 'CoreWebView2 was not created' + return + self._webview = webview + + # Chrome-less, kiosk-like surface: no context menu, no devtools, + # no accelerators that could let an operator escape the signage. + try: + settings = webview.Settings + settings.AreDefaultContextMenusEnabled = False + settings.AreDevToolsEnabled = False + settings.IsStatusBarEnabled = False + settings.AreBrowserAcceleratorKeysEnabled = False + settings.IsZoomControlEnabled = False + settings.AreDefaultScriptDialogsEnabled = False + except Exception as exc: + _log(f'WebView2: settings tweak failed (non-fatal): {exc}') + + self._hook_navigation_events(webview) + + width, height = self._size + if width > 0 and height > 0: + self.resize(width, height) + + self._stage = 'ready' + _log('WebView2: controller ready') + + with self._lock: + url, self._pending_url = self._pending_url, None + if url: + self._navigate(url) + + def _hook_navigation_events(self, webview): + """Track whether the page actually loaded. + + ``is_showing()`` alone is misleading: it becomes True the moment + ``Navigate()`` is called, before anything has been fetched. On a closed + network the weblink host is often unreachable, and Chromium then paints + an error page — which the player must treat as a failure so the item is + skipped rather than shown as a broken screen for its whole slot. + + Handlers are stored on the instance: if the delegate were only a local, + the .NET GC would collect it and the event would silently stop firing. + """ + try: + handler = _NavigationCompletedHandler(self) + webview.NavigationCompleted += handler + self._navigation_handlers.append(handler) + _log('WebView2: navigation tracking enabled') + except Exception as exc: + # Not fatal: without it we simply cannot distinguish a loaded page + # from an error page, and fall back to "visible means OK". + _log(f'WebView2: could not hook NavigationCompleted ({exc})') + + def navigation_succeeded(self): + """True / False once navigation finished, None while still pending.""" + with self._lock: + return self._navigation_ok + + def navigation_status(self): + with self._lock: + return self._navigation_status + + def _navigate(self, url): + webview = self._webview + if webview is None: + return False + with self._lock: + self._navigation_ok = None + self._navigation_status = '' + try: + webview.Navigate(url) + except Exception as exc: + self._failed_reason = f'navigate failed: {exc}' + _log(f'WebView2: navigate failed: {exc}') + return False + width, height = self._size + if width > 0 and height > 0: + self.resize(width, height) + try: + self._controller.IsVisible = True + except Exception as exc: + _log(f'WebView2: could not show controller: {exc}') + return False + with self._lock: + self._showing = True + _log(f'WebView2: navigated to {url[:80]}') + return True + + +# ── Module helpers ─────────────────────────────────────────────────── +_TYPES_CACHE = {} + + +def _load_webview2_types(sdk_dir): + """Import the managed SDK and return the types we need (cached).""" + key = str(sdk_dir) + cached = _TYPES_CACHE.get(key) + if cached: + return cached + + if hasattr(os, 'add_dll_directory'): + try: + os.add_dll_directory(str(sdk_dir)) # let the loader find WebView2Loader.dll + except Exception: + pass + if key not in sys.path: + sys.path.insert(0, key) + + import clr + + # Framework assemblies we rely on (Rectangle for Bounds). + try: + clr.AddReference('System.Drawing') + except Exception: + pass + clr.AddReference(str(sdk_dir / 'Microsoft.Web.WebView2.Core.dll')) + + from Microsoft.Web.WebView2.Core import CoreWebView2Environment + + types = {'env': CoreWebView2Environment} + _TYPES_CACHE[key] = types + return types + + +def _create_environment_async(types, user_data_dir, options=None): + """Call CreateAsync with the options object. + + The SDK exposes exactly one overload: + ``CreateAsync(string browserExecutableFolder, string userDataFolder, + CoreWebView2EnvironmentOptions options)``. + """ + env_type = types['env'] + last = None + # Preferred: explicit options (used to pass offline browser arguments). + if options is not None: + try: + return env_type.CreateAsync(None, user_data_dir, options) + except Exception as exc: + last = exc + attempts = ( + (None, user_data_dir, None), + (None, user_data_dir), + ) + for args in attempts: + try: + return env_type.CreateAsync(*args) + except Exception as exc: + last = exc + raise last if last is not None else RuntimeError('CreateAsync failed') + + +def _offline_browser_arguments(): + """Chromium flags that stop internet chatter on a closed network. + + A signage player normally lives on an isolated LAN. By default Chromium + still tries to reach the internet for component updates, field trials, + safe-browsing lists, translate, and Google services. On a closed network + every one of those attempts has to time out, which costs start-up latency + (and, if DNS resolves but routes black-hole, can stall for many seconds). + + These flags disable that background traffic. They do NOT affect loading + actual pages — a weblink pointing at the local server still works, and one + pointing at the public internet simply fails fast with a normal + ERR_INTERNET_DISCONNECTED instead of hanging. + """ + return ' '.join([ + '--disable-background-networking', + '--disable-component-update', + '--disable-domain-reliability', + '--disable-features=Translate,OptimizationHints,MediaRouter,' + 'CalculateNativeWinOcclusion', + '--disable-sync', + '--no-first-run', + '--no-default-browser-check', + '--no-pings', + '--disable-breakpad', + '--metrics-recording-only', + '--disable-client-side-phishing-detection', + ]) + + +def _build_environment_options(): + """Create a CoreWebView2EnvironmentOptions with offline flags applied.""" + try: + from Microsoft.Web.WebView2.Core import CoreWebView2EnvironmentOptions + + options = CoreWebView2EnvironmentOptions() + options.AdditionalBrowserArguments = _offline_browser_arguments() + # Don't phone home with crash reports. + try: + options.IsCustomCrashReportingEnabled = False + except Exception: + pass + _log('WebView2: offline browser arguments applied') + return options + except Exception as exc: + _log(f'WebView2: could not build environment options ({exc}); ' + 'continuing with defaults') + return None + + +class _ThreadTimer: + """Minimal fallback timer used only when Kivy's Clock is unavailable.""" + + def __init__(self, interval, func, _unused): + self._interval = float(interval) + self._func = func + self._stop = threading.Event() + self._thread = threading.Thread(target=self._run, daemon=True) + self._thread.start() + + def _run(self): + while not self._stop.wait(self._interval): + try: + if self._func(None) is False: + return + except Exception: + return + + def cancel(self): + self._stop.set() + + +class _NavigationCompletedHandler: + """Adapter for WebView2's ``NavigationCompleted`` event. + + The event is ``System.EventHandler`` + — there is no ``CoreWebView2NavigationCompletedEventHandler`` type to import + (attempting to import one fails). pythonnet converts a plain Python callable + to the generic delegate automatically, so that is what we pass. + + The callable is kept on the browser instance: a delegate referenced only by + a local would be collected by the .NET GC, after which the event silently + stops firing. + """ + + def __init__(self, browser): + self._browser = browser + + def __call__(self, sender, args): + """Fires on the WebView2 thread that owns the message loop.""" + try: + success = bool(args.IsSuccess) + status = _describe_navigation_error(args, success) if not success else '' + with self._browser._lock: + self._browser._navigation_ok = success + self._browser._navigation_status = status + if success: + _log('WebView2: page loaded') + else: + _log(f'WebView2: page failed to load ({status or "unknown"})') + except Exception as exc: + _log(f'WebView2: navigation handler error ({exc})') + + +def _log(message): + try: + from kivy.logger import Logger + + Logger.info(f'[WebView2] {message}') + except Exception: + print(f'[WebView2] {message}') + + +def _describe_navigation_error(args, success): + """Human-readable reason for a failed navigation. + + ``WebErrorStatus`` is an enum whose numeric value is not useful on its own; + when it reports ``Unknown`` (common for connection-level failures) the HTTP + status is more informative, so prefer whichever actually says something. + """ + parts = [] + try: + error_status = str(args.WebErrorStatus) + if error_status and error_status.lower() != 'unknown': + parts.append(error_status) + except Exception: + pass + try: + http_status = int(args.HttpStatusCode) + if http_status > 0: + parts.append(f'HTTP {http_status}') + except Exception: + pass + if parts: + return ', '.join(parts) + return 'connection failed (host unreachable or DNS failure)' diff --git a/windows/webview2_runtime.py b/windows/webview2_runtime.py new file mode 100644 index 0000000..4c2a25b --- /dev/null +++ b/windows/webview2_runtime.py @@ -0,0 +1,446 @@ +"""webview2_runtime.py — make sure the WebView2 Runtime is present. + +Why this exists +--------------- +WebView2 splits into two parts: + +* the **SDK** (the ``Microsoft.Web.WebView2.Core.dll`` + ``WebView2Loader.dll`` + we bundle in ``windows/webview2_sdk``), which is just the API surface, and +* the **Runtime** (``msedgewebview2.exe`` etc.), the actual Chromium engine. + +The SDK is useless without the Runtime. The Runtime ships with Windows 11 and +is present on the vast majority of Windows 10 machines, but Microsoft still +recommends checking for it and installing it when missing — so that is what +this module does. + +Deployment notes (per Microsoft's distribution guidance): + +* If the Runtime is missing we run an installer with ``/silent /install``. +* Run it **without elevation** → per-user install, which never shows a UAC + prompt. That matters for an unattended signage player: a UAC dialog on a + kiosk screen is a failure, not a prompt. +* Two installers are supported, in order of preference: + 1. ``MicrosoftEdgeWebView2RuntimeInstallerX64.exe`` — the ~203 MB offline + *standalone* installer. Works with no internet (drop it in + ``windows/webview2_runtime/`` to have it bundled). + 2. ``MicrosoftEdgeWebview2Setup.exe`` — the ~1.7 MB *bootstrapper*, which + downloads the Runtime from Microsoft. Bundled by default. + +Nothing here ever raises: a failure just means web links fall back to the +Chrome/Edge subprocess engine, which is far better than the player crashing. +""" + +from __future__ import annotations + +import os +import subprocess +import sys +import threading +import time +from pathlib import Path + +# Per Microsoft, the Runtime's presence/version lives in this registry value. +# (Edge Update client GUID for the Evergreen WebView2 Runtime.) +_CLIENT_GUID = '{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' + +_STANDALONE_NAME = 'MicrosoftEdgeWebView2RuntimeInstallerX64.exe' +_BOOTSTRAPPER_NAME = 'MicrosoftEdgeWebview2Setup.exe' + +#: Don't re-attempt a failing install on every single start-up. +_RETRY_COOLDOWN_SECONDS = 6 * 60 * 60 + +_install_lock = threading.Lock() +_install_state = { + 'attempted': False, + 'installing': False, + 'installed': None, # bool once known + 'version': '', + 'error': '', +} + +#: Set once a background install has finished, so a weblink can wait for it. +_install_done = threading.Event() + + +# ── Detection ──────────────────────────────────────────────────────── +def _parse_version(text): + """Return a comparable tuple from a version string like '152.0.4191.66'.""" + parts = [] + for chunk in str(text or '').split('.'): + digits = ''.join(c for c in chunk if c.isdigit()) + parts.append(int(digits) if digits else 0) + while len(parts) < 4: + parts.append(0) + return tuple(parts[:4]) + + +def _read_registry_version(): + """Read the installed Runtime version from the registry, or ''. + + Checks both install scopes: HKLM (per-machine) and HKCU (per-user). On + 64-bit Windows the per-machine value lives under WOW6432Node because the + Edge Updater is a 32-bit component. + """ + if sys.platform != 'win32': + return '' + try: + import winreg + except Exception: + return '' + + candidates = [ + # (hive, subkey, access flag) + (winreg.HKEY_LOCAL_MACHINE, + rf'SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{_CLIENT_GUID}', 0), + (winreg.HKEY_LOCAL_MACHINE, + rf'SOFTWARE\Microsoft\EdgeUpdate\Clients\{_CLIENT_GUID}', + getattr(winreg, 'KEY_WOW64_32KEY', 0)), + (winreg.HKEY_CURRENT_USER, + rf'SOFTWARE\Microsoft\EdgeUpdate\Clients\{_CLIENT_GUID}', 0), + ] + for hive, subkey, access in candidates: + try: + with winreg.OpenKey(hive, subkey, 0, + winreg.KEY_READ | access) as key: + value, _ = winreg.QueryValueEx(key, 'pv') + value = str(value or '').strip() + if value and _parse_version(value) > (0, 0, 0, 0): + return value + except Exception: + continue + return '' + + +def get_runtime_version(): + """Version of the installed Evergreen Runtime, or '' when absent.""" + version = _read_registry_version() + if version: + return version + # Fallback: ask the SDK itself (also covers preview channels). + try: + from webview2_browser import _find_sdk_dir, _load_webview2_types + + sdk = _find_sdk_dir() + if sdk is not None: + types = _load_webview2_types(sdk) + reported = types['env'].GetAvailableBrowserVersionString() + return str(reported).strip() if reported else '' + except Exception: + pass + return '' + + +def is_runtime_installed(): + """True when a usable WebView2 Runtime is present.""" + return bool(get_runtime_version()) + + +# ── Installer discovery ────────────────────────────────────────────── +def _search_dirs(): + """Folders that may hold an installer, best (standalone) first.""" + here = Path(__file__).resolve().parent + dirs = [here / 'webview2_runtime', here] + meipass = getattr(sys, '_MEIPASS', None) + if meipass: + dirs.append(Path(meipass) / 'webview2_runtime') + # Next to the .exe, so an operator can drop the offline installer in + # without rebuilding. + data_dir = os.environ.get('KIWY_DATA_DIR') + if data_dir: + dirs.append(Path(data_dir) / 'webview2_runtime') + dirs.append(Path(data_dir)) + env = os.environ.get('KIWY_WEBVIEW2_INSTALLER') + if env: + dirs.insert(0, Path(env).parent) + return dirs + + +def find_installer(): + """Locate a usable installer. Returns ``(path, kind)`` or ``(None, None)``. + + The standalone (offline) installer is preferred: it does not depend on the + target machine having internet access, which is the normal case for a + signage player on an isolated LAN. + """ + env = os.environ.get('KIWY_WEBVIEW2_INSTALLER') + if env and Path(env).is_file(): + return Path(env), 'explicit' + + found = {'standalone': None, 'bootstrapper': None} + for directory in _search_dirs(): + try: + if found['standalone'] is None: + candidate = directory / _STANDALONE_NAME + if candidate.is_file(): + found['standalone'] = candidate + if found['bootstrapper'] is None: + candidate = directory / _BOOTSTRAPPER_NAME + if candidate.is_file(): + found['bootstrapper'] = candidate + except OSError: + continue + + if found['standalone'] is not None: + return found['standalone'], 'standalone' + if found['bootstrapper'] is not None: + return found['bootstrapper'], 'bootstrapper' + return None, None + + +def _has_internet(timeout=4.0): + """Quick reachability probe. A closed network returns False fast.""" + try: + import socket + + with socket.create_connection(('www.msftconnecttest.com', 80), + timeout=timeout): + return True + except Exception: + return False + + +# ── Cooldown bookkeeping ───────────────────────────────────────────── +def _marker_path(): + data_dir = os.environ.get('KIWY_DATA_DIR') or os.getcwd() + return Path(data_dir) / 'logs' / '.webview2_install_attempt' + + +def _recent_failed_attempt(): + try: + marker = _marker_path() + if not marker.is_file(): + return False + age = time.time() - marker.stat().st_mtime + return age < _RETRY_COOLDOWN_SECONDS + except Exception: + return False + + +def _record_attempt(): + try: + marker = _marker_path() + marker.parent.mkdir(parents=True, exist_ok=True) + marker.write_text(str(int(time.time()))) + except Exception: + pass + + +def _clear_attempt_marker(): + try: + marker = _marker_path() + if marker.is_file(): + marker.unlink() + except Exception: + pass + + +# ── Install ────────────────────────────────────────────────────────── +def _create_no_window(): + """Keep the installer from flashing a console window on the signage.""" + try: + return subprocess.CREATE_NO_WINDOW + except AttributeError: + return 0x08000000 + + +def _run_installer(path, timeout): + """Run the installer silently. Returns (ok, detail).""" + # `/silent /install` is the documented silent invocation. Deliberately NOT + # elevated: a non-elevated run performs a per-user install, which never + # raises a UAC prompt on the kiosk display. + args = [str(path), '/silent', '/install'] + _log(f'WebView2: running installer {Path(path).name} /silent /install ' + f'(per-user, no elevation)') + try: + result = subprocess.run( + args, + timeout=timeout, + creationflags=_create_no_window(), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + except subprocess.TimeoutExpired: + return False, f'installer timed out after {int(timeout)}s' + except Exception as exc: + return False, f'could not run installer: {exc}' + + code = result.returncode + detail = (result.stdout or b'').decode('utf-8', 'replace').strip() + # Edge Update installers commonly report 0 (success) or 3010 (reboot + # required). They also return non-zero HRESULTs when the Runtime is already + # installed at an equal/newer version — which is why the caller decides + # success by re-reading the installed version rather than trusting this + # code. We only use it to explain a failure. + if code in (0, 3010): + return True, f'installer exit code {code}' + return False, f'installer exit code {code}{": " + detail if detail else ""}' + + +def ensure_runtime(timeout=600): + """Install the Runtime when missing. Blocking; never raises. + + Returns a dict describing the outcome (``installed``, ``version``, + ``error``, ``action``). + """ + with _install_lock: + if is_runtime_installed(): + version = get_runtime_version() + _install_state.update( + attempted=True, installing=False, installed=True, + version=version, error='', + ) + return dict(_install_state, action='already-present') + + if _recent_failed_attempt(): + _install_state.update( + attempted=True, installing=False, installed=False, error='', + ) + return dict(_install_state, action='skipped-recent-failure') + + path, kind = find_installer() + if path is None: + message = ('no WebView2 installer found (expected ' + f'{_STANDALONE_NAME} or {_BOOTSTRAPPER_NAME} in ' + 'windows/webview2_runtime/)') + _log(f'WebView2: {message}') + _install_state.update( + attempted=True, installing=False, installed=False, + error=message, + ) + return dict(_install_state, action='installer-missing') + + if kind == 'bootstrapper': + # The bootstrapper downloads the Runtime from Microsoft. On a closed + # network that can never succeed, so fail fast with an actionable + # message instead of hanging for the whole timeout. + if not _has_internet(): + message = ('the WebView2 Runtime is missing and this machine has ' + f'no internet access; only the ONLINE bootstrapper ' + f'({_BOOTSTRAPPER_NAME}) is available. Bundle the ' + f'offline installer ({_STANDALONE_NAME}, run ' + 'webview2_runtime/download_runtime_installers.ps1 ' + '-Offline) to run on a closed network.') + _log(f'WebView2: {message}') + _install_state.update( + attempted=True, installing=False, installed=False, + error=message, + ) + _install_done.set() + return dict(_install_state, action='offline-no-installer') + _log('WebView2: Runtime missing — using the ONLINE bootstrapper ' + '(downloads ~150 MB). Add the offline standalone installer to ' + 'avoid needing internet.') + + _install_state.update(attempted=True, installing=True, error='') + _record_attempt() + ok, detail = _run_installer(path, timeout) + if not ok and 'already installed' not in detail.lower(): + _log(f'WebView2: installer reported {detail}') + + # Decide success by RE-READING the installed version, not by the exit + # code: a non-zero HRESULT can simply mean "nothing to do". + version = '' + deadline = time.monotonic() + 30 + while time.monotonic() < deadline: + version = get_runtime_version() + if version: + break + time.sleep(1.0) + + if version: + _clear_attempt_marker() + _log(f'WebView2: Runtime available (v{version}) [{detail}]') + _install_state.update( + installing=False, installed=True, version=version, + error='', action=f'installed-{kind}', + ) + else: + message = detail or 'Runtime still not detected after install' + _log(f'WebView2: install did not take effect ({message})') + _install_state.update( + installing=False, installed=False, version='', error=message, + ) + _install_done.set() + return dict(_install_state, action=f'attempted-{kind}') + + +def ensure_runtime_async(timeout=600): + """Kick off :func:`ensure_runtime` on a background thread. + + Called at start-up so a missing Runtime installs while the player is still + syncing its playlist, instead of freezing the UI. + """ + if is_runtime_installed(): + _install_done.set() + _install_state.update(installed=True, version=get_runtime_version()) + return None + + def _worker(): + try: + ensure_runtime(timeout=timeout) + except Exception as exc: # defensive: never kill the player + _log(f'WebView2: background install failed: {exc}') + _install_state.update(installing=False, installed=False, error=str(exc)) + _install_done.set() + + thread = threading.Thread(target=_worker, name='webview2-install', daemon=True) + thread.start() + return thread + + +def wait_for_install(timeout): + """Wait (briefly, on the watcher thread) for a pending install. + + Returns True when a Runtime is available afterwards. + """ + if is_runtime_installed(): + return True + if not _install_state.get('installing'): + return False + _install_done.wait(timeout=max(0.0, float(timeout))) + return is_runtime_installed() + + +def get_state(): + """Snapshot of the installer state, for logging/diagnostics.""" + state = dict(_install_state) + if state.get('installed') is None: + state['installed'] = is_runtime_installed() + state['version'] = state['version'] or get_runtime_version() + return state + + +def describe(): + """One-line status for the startup log.""" + version = get_runtime_version() + if version: + return f'WebView2 Runtime present (v{version})' + path, kind = find_installer() + if path is None: + return 'WebView2 Runtime MISSING and no bundled installer found' + if kind == 'standalone': + return (f'WebView2 Runtime MISSING (will install OFFLINE via ' + f'{path.name} — no internet needed)') + return (f'WebView2 Runtime MISSING (will install via the ONLINE ' + f'bootstrapper {path.name}; needs internet)') + + +def is_offline_ready(): + """True when a Runtime is present, or can be installed without internet. + + This is the property that matters for a closed-network deployment: web + links will work on first start with no outbound connectivity. + """ + if is_runtime_installed(): + return True + path, kind = find_installer() + return path is not None and kind in ('standalone', 'explicit') + + +def _log(message): + try: + from kivy.logger import Logger + + Logger.info(f'[WebView2] {message}') + except Exception: + print(f'[WebView2] {message}') diff --git a/windows/webview2_runtime/MicrosoftEdgeWebview2Setup.exe b/windows/webview2_runtime/MicrosoftEdgeWebview2Setup.exe new file mode 100644 index 0000000000000000000000000000000000000000..e10abbc580a880a96f9c36356bcd4976012f94a0 GIT binary patch literal 1783000 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