From 3845830a86aa8096675011cc84361c96e11c7f47 Mon Sep 17 00:00:00 2001 From: ske087 Date: Fri, 24 Jul 2026 08:34:00 +0300 Subject: [PATCH] Add Windows Player support and related files - New windows/ directory with build scripts, specs, and configuration - Windows-specific requirements (requirements_win.txt) - Launch and runtime scripts for Windows (run_win.py, launch_player.bat) - PyInstaller build configuration (build.spec) - Updated .gitignore to exclude windows/venv312/ - Updated config and source files for Windows compatibility - Moved working_files to proper directory --- .gitignore | 1 + config/app_config.json | 4 +- python version | 1 + src/main.py | 34 +- src/player_auth.json | 10 +- windows/2.3.0 | 34 ++ windows/README_WINDOWS_BUILD.md | 152 +++++ windows/build.spec | 240 ++++++++ windows/build_win.bat | 126 ++++ windows/launch_player.bat | 27 + windows/pyi_runtime_hook.py | 131 ++++ windows/requirements_win.txt | 28 + windows/run_win.py | 574 ++++++++++++++++++ windows/test_import_fix.py | 39 ++ New.txt => working_files/New.txt | 0 .../install.sh.bak | 0 16 files changed, 1379 insertions(+), 22 deletions(-) create mode 100644 python version create mode 100644 windows/2.3.0 create mode 100644 windows/README_WINDOWS_BUILD.md create mode 100644 windows/build.spec create mode 100644 windows/build_win.bat create mode 100644 windows/launch_player.bat create mode 100644 windows/pyi_runtime_hook.py create mode 100644 windows/requirements_win.txt create mode 100644 windows/run_win.py create mode 100644 windows/test_import_fix.py rename New.txt => working_files/New.txt (100%) rename install.sh.bak => working_files/install.sh.bak (100%) mode change 100755 => 100644 diff --git a/.gitignore b/.gitignore index 578997f..d44a3dd 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ wheels/ venv/ ENV/ env/ +windows/venv312/ # Kivy *.pyc diff --git a/config/app_config.json b/config/app_config.json index e09c3f5..f5dbb63 100644 --- a/config/app_config.json +++ b/config/app_config.json @@ -1,7 +1,7 @@ { - "server_ip": "192.168.0.109", + "server_ip": "192.168.0.107", "port": "8080", - "screen_name": "Birou_IT", + "screen_name": "WINDOWS-PC", "quickconnect_key": "8887779", "orientation": "Landscape", "touch": "True", diff --git a/python version b/python version new file mode 100644 index 0000000..b0b4962 --- /dev/null +++ b/python version @@ -0,0 +1 @@ +Python 3.12.9 \ No newline at end of file diff --git a/src/main.py b/src/main.py index 0ecbbd5..68acaf6 100644 --- a/src/main.py +++ b/src/main.py @@ -14,21 +14,21 @@ import asyncio from concurrent.futures import ThreadPoolExecutor # Set environment variables for better video performance -os.environ['KIVY_VIDEO'] = 'ffpyplayer' # Use ffpyplayer as video provider -os.environ['FFPYPLAYER_CODECS'] = 'h264,h265,vp9,vp8' # Support common codecs -os.environ['SDL_VIDEO_ALLOW_SCREENSAVER'] = '0' # Prevent screen saver -os.environ['SDL_VIDEODRIVER'] = 'wayland,x11,dummy' # Prefer Wayland, fallback to X11, then dummy -os.environ['SDL_AUDIODRIVER'] = 'alsa,pulse,dummy' # Prefer ALSA, fallback to pulse, then dummy +# Use setdefault() so that a wrapper script (e.g. run_win.py) can pre-set +# Windows-compatible values before this module is imported. +os.environ.setdefault('KIVY_VIDEO', 'ffpyplayer') +os.environ.setdefault('FFPYPLAYER_CODECS', 'h264,h265,vp9,vp8') +os.environ.setdefault('SDL_VIDEO_ALLOW_SCREENSAVER', '0') +os.environ.setdefault('SDL_VIDEODRIVER', 'wayland,x11,dummy') +os.environ.setdefault('SDL_AUDIODRIVER', 'alsa,pulse,dummy') # Video playback optimizations -# Note: pygame backend requires X11/Wayland context; let Kivy auto-detect for better compatibility -# os.environ['KIVY_WINDOW'] = 'pygame' # Use pygame backend for better performance -os.environ['KIVY_AUDIO'] = 'ffpyplayer' # Use ffpyplayer for audio -os.environ['KIVY_GL_BACKEND'] = 'gl' # Use OpenGL backend -os.environ['KIVY_INPUTPROVIDERS'] = 'wayland,x11' # Only use Wayland and X11 input providers, skip problematic ones -os.environ['FFMPEG_THREADS'] = '2' # Use 2 threads for ffmpeg decoding (Raspberry Pi has limited resources) -os.environ['LIBPLAYER_BUFFER'] = '1048576' # 1MB buffer (reduced from 2MB to save memory) -os.environ['SDL_AUDIODRIVER'] = 'alsa' # Use ALSA for better audio on Pi +os.environ.setdefault('KIVY_AUDIO', 'ffpyplayer') +os.environ.setdefault('KIVY_GL_BACKEND', 'gl') +os.environ.setdefault('KIVY_INPUTPROVIDERS', 'wayland,x11') +os.environ.setdefault('FFMPEG_THREADS', '2') +os.environ.setdefault('LIBPLAYER_BUFFER', '1048576') +os.environ.setdefault('SDL_AUDIODRIVER', 'alsa') # Configure Kivy BEFORE importing any Kivy modules from kivy.config import Config @@ -86,8 +86,12 @@ from kivy.graphics import Color, Line, Ellipse from kivy.uix.floatlayout import FloatLayout from kivy.uix.slider import Slider -# Load the KV file -Builder.load_file('signage_player.kv') +# Load the KV file - resolve relative to this file's directory +_kv_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'signage_player.kv') +if not os.path.exists(_kv_path): + # Fallback: relative to cwd (for PyInstaller bundled runs) + _kv_path = 'signage_player.kv' +Builder.load_file(_kv_path) class CardReader: diff --git a/src/player_auth.json b/src/player_auth.json index 78fed64..acfbb50 100644 --- a/src/player_auth.json +++ b/src/player_auth.json @@ -1,10 +1,10 @@ { - "hostname": "Birou_IT", - "auth_code": "CZncd_2dlTZGieBEdqAbUTjf3qNyEUPDXr8jLVx7NLs", - "player_id": 1, - "player_name": "Test_player1", + "hostname": "WINDOWS-PC", + "auth_code": "", + "player_id": 2, + "player_name": "Windows-Player1", "playlist_id": 1, "orientation": "Landscape", "authenticated": true, - "server_url": "http://192.168.0.109:8080" + "server_url": "http://192.168.0.107:8080" } \ No newline at end of file diff --git a/windows/2.3.0 b/windows/2.3.0 new file mode 100644 index 0000000..e4e0eb0 --- /dev/null +++ b/windows/2.3.0 @@ -0,0 +1,34 @@ +Requirement already satisfied: ffpyplayer in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (4.5.3) +Requirement already satisfied: requests in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (2.34.2) +Requirement already satisfied: aiohttp in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (3.14.3) +Requirement already satisfied: bcrypt in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (5.0.0) +Requirement already satisfied: certifi in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (2026.7.22) +Requirement already satisfied: pyinstaller in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (6.21.0) +Requirement already satisfied: kivy[base] in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (2.3.1) +Requirement already satisfied: Kivy-Garden>=0.1.4 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from kivy[base]) (0.1.5) +Requirement already satisfied: docutils in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from kivy[base]) (0.23) +Requirement already satisfied: pygments in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from kivy[base]) (2.20.0) +Requirement already satisfied: filetype in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from kivy[base]) (1.2.0) +Requirement already satisfied: kivy-deps.angle~=0.4.0 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from kivy[base]) (0.4.0) +Requirement already satisfied: kivy-deps.sdl2~=0.8.0 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from kivy[base]) (0.8.0) +Requirement already satisfied: kivy-deps.glew~=0.3.1 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from kivy[base]) (0.3.1) +Requirement already satisfied: pypiwin32 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from kivy[base]) (223) +Requirement already satisfied: pillow<11,>=9.5.0 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from kivy[base]) (10.4.0) +Requirement already satisfied: charset_normalizer<4,>=2 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from requests) (3.4.9) +Requirement already satisfied: idna<4,>=2.5 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from requests) (3.18) +Requirement already satisfied: urllib3<3,>=1.26 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from requests) (2.7.0) +Requirement already satisfied: aiohappyeyeballs>=2.5.0 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from aiohttp) (2.7.1) +Requirement already satisfied: aiosignal>=1.4.0 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from aiohttp) (1.4.0) +Requirement already satisfied: attrs>=17.3.0 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from aiohttp) (26.1.0) +Requirement already satisfied: frozenlist>=1.1.1 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from aiohttp) (1.8.0) +Requirement already satisfied: multidict<7.0,>=4.5 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from aiohttp) (6.7.1) +Requirement already satisfied: propcache>=0.2.0 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from aiohttp) (0.5.2) +Requirement already satisfied: typing_extensions>=4.4 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from aiohttp) (4.16.0) +Requirement already satisfied: yarl<2.0,>=1.17.0 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from aiohttp) (1.24.5) +Requirement already satisfied: altgraph in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from pyinstaller) (0.17.5) +Requirement already satisfied: packaging>=22.0 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from pyinstaller) (26.2) +Requirement already satisfied: pefile>=2022.5.30 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from pyinstaller) (2024.8.26) +Requirement already satisfied: pyinstaller-hooks-contrib>=2026.6 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from pyinstaller) (2026.6) +Requirement already satisfied: pywin32-ctypes>=0.2.1 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from pyinstaller) (0.2.3) +Requirement already satisfied: setuptools>=42.0.0 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from pyinstaller) (83.0.0) +Requirement already satisfied: pywin32>=223 in c:\users\dell-pc\desktop\kiwy-signage\windows\venv\lib\site-packages (from pypiwin32->kivy[base]) (312) diff --git a/windows/README_WINDOWS_BUILD.md b/windows/README_WINDOWS_BUILD.md new file mode 100644 index 0000000..7f89203 --- /dev/null +++ b/windows/README_WINDOWS_BUILD.md @@ -0,0 +1,152 @@ +# Kiwy Signage Player - Windows Edition + +Build and run the Kiwy digital signage player on Windows as a standalone `.exe`. + +## ๐Ÿ“‹ Requirements Analysis + +The original app was built for **Raspberry Pi (Linux)**, using these technologies: + +| Component | Original (RPi/Linux) | Windows Equivalent | +|-----------|---------------------|-------------------| +| **GUI** | Kivy 2.3+ | Kivy 2.3+ (works cross-platform) | +| **Video** | ffpyplayer | ffpyplayer (needs FFmpeg DLLs) | +| **Card Reader** | evdev (Linux input) | **Not available** โ€” gracefully disabled | +| **Screen Keep-Awake** | xset, xdotool, Wayland | `SetThreadExecutionState` (Win32 API) | +| **Weblink** | chromium-browser (kiosk) | Chrome/Edge (--kiosk mode) | +| **Audio** | ALSA/PulseAudio | DirectSound | +| **Window Backend** | SDL2 (Wayland/X11) | SDL2 (Windows native) | +| **OpenGL** | Desktop GL | ANGLE (DirectX wrapper) | + +### What works on Windows +- โœ… Media playback (images, videos via ffpyplayer) +- โœ… Playlist sync from DigiServer (HTTP/HTTPS) +- โœ… Touch & mouse controls +- โœ… Settings popup +- โœ… Image editing/annotation +- โœ… Password-protected exit +- โœ… Web links (opens in Chrome/Edge kiosk) +- โœ… Network monitoring +- โœ… Auto-update playlist + +### What is disabled on Windows +- โŒ Card reader (evdev is Linux-only; `EVDEV_AVAILABLE = False`) +- โŒ HDMI power management (tvservice is RPi-specific) +- โŒ WiFi restart (uses Linux `nmcli`) + +## ๐Ÿš€ Quick Start (Development) + +### Prerequisites +1. **Python 3.10+** (64-bit) โ€” [python.org](https://python.org) +2. **FFmpeg** โ€” for video codec support + - Download from [ffmpeg.org](https://ffmpeg.org/download.html) + - Add `bin\` folder to your PATH +3. **Visual C++ Redistributable** โ€” [latest](https://aka.ms/vs/17/release/vc_redist.x64.exe) + +### Install & Run +```batch +cd windows + +REM Create virtual environment +python -m venv venv +venv\Scripts\activate + +REM Install dependencies +pip install -r requirements_win.txt + +REM Run in development mode +python run_win.py +``` + +## ๐Ÿ“ฆ Building the .exe + +### One-Command Build +```batch +cd windows +build_win.bat +``` + +### Manual Build +```batch +cd windows +venv\Scripts\activate +pip install -r requirements_win.txt +pyinstaller build.spec --clean --noconfirm +``` + +### Output +``` +windows\dist\KiwySignagePlayer\ + โ”œโ”€โ”€ KiwySignagePlayer.exe # Main executable + โ”œโ”€โ”€ config/ # Config files (auto-copied) + โ”œโ”€โ”€ resources/ # Icons, intro video + โ””โ”€โ”€ ... (supporting DLLs) +``` + +For a **single-file .exe**, edit `build.spec` โ€” uncomment the `exe_onefile` section and comment out the `coll = COLLECT(...)` section. + +## โš™๏ธ Configuration + +1. On first run, config files are created in `%APPDATA%\KiwySignage\` +2. Edit `%APPDATA%\KiwySignage\config\app_config.json` to set your server: + +```json +{ + "server_ip": "192.168.0.109", + "port": "8080", + "screen_name": "Birou_IT", + "quickconnect_key": "8887779", + "orientation": "Landscape", + "touch": "True", + "max_resolution": "1920x1080", + "edit_feature_enabled": true, + "use_https": false, + "verify_ssl": false +} +``` + +## ๐Ÿงช Testing + +```batch +cd windows +venv\Scripts\activate +python run_win.py +``` + +## ๐Ÿ”ง Troubleshooting + +| Problem | Solution | +|---------|----------| +| **"ffpyplayer not found"** | Install: `pip install ffpyplayer` | +| **"No video" / black screen** | Install FFmpeg and add to PATH. Try `KIVY_GL_BACKEND=angle_sdl2` or `KIVY_GL_BACKEND=gl` | +| **Kivy window doesn't open** | Run from command prompt to see error messages. Ensure GPU drivers are up to date. | +| **Weblinks not opening** | Install Google Chrome or Microsoft Edge | +| **Can't connect to server** | Check firewall. Try `use_https: false` and `verify_ssl: false` for testing | +| **Antivirus flags .exe** | Add the output folder to antivirus exclusions. This is a false positive common with PyInstaller. | + +## ๐Ÿ“ Project Structure (Build) + +``` +Kiwy-Signage/ +โ”œโ”€โ”€ windows/ +โ”‚ โ”œโ”€โ”€ run_win.py # Windows entry point (patches platform differences) +โ”‚ โ”œโ”€โ”€ build.spec # PyInstaller configuration +โ”‚ โ”œโ”€โ”€ build_win.bat # One-click build script +โ”‚ โ”œโ”€โ”€ pyi_runtime_hook.py # PyInstaller runtime hook +โ”‚ โ”œโ”€โ”€ requirements_win.txt # Windows Python dependencies +โ”‚ โ””โ”€โ”€ README_WINDOWS_BUILD.md # This file +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ main.py # Main application (original) +โ”‚ โ”œโ”€โ”€ get_playlists_v2.py # Playlist sync +โ”‚ โ”œโ”€โ”€ player_auth.py # Authentication +โ”‚ โ”œโ”€โ”€ ssl_utils.py # SSL/HTTPS +โ”‚ โ”œโ”€โ”€ keyboard_widget.py # On-screen keyboard +โ”‚ โ”œโ”€โ”€ network_monitor.py # Network monitoring +โ”‚ โ”œโ”€โ”€ edit_popup.py # Image editing +โ”‚ โ””โ”€โ”€ signage_player.kv # Kivy UI layout +โ”œโ”€โ”€ config/ +โ”‚ โ”œโ”€โ”€ app_config.json # Player configuration +โ”‚ โ””โ”€โ”€ resources/ # Icons, images, intro video +โ”œโ”€โ”€ media/ # Downloaded media (created at runtime) +โ”œโ”€โ”€ playlists/ # Playlist files (created at runtime) +โ””โ”€โ”€ logs/ # Log files (created at runtime) +``` diff --git a/windows/build.spec b/windows/build.spec new file mode 100644 index 0000000..0fb9211 --- /dev/null +++ b/windows/build.spec @@ -0,0 +1,240 @@ +# -*- mode: python ; coding: utf-8 -*- +""" +PyInstaller spec file for Kiwy Signage Player (Windows .exe) + +Build command (from windows/ directory): + pyinstaller build.spec --clean --noconfirm + +OR use the build script: + build_win.bat +""" + +import os +import sys +from pathlib import Path + +# --- Paths ----------------------------------------------------------- +# This spec file is in windows/build.spec, so the project root is +# always two levels up from this file's real location. +# __file__ may not be available in PyInstaller spec context fallback to cwd. +try: + _spec_dir = Path(__file__).resolve().parent +except NameError: + _spec_dir = Path(os.getcwd()).resolve() +# _spec_dir is now the absolute path to the windows/ directory +BUILD_DIR = _spec_dir +ROOT_DIR = BUILD_DIR.parent +SRC_DIR = ROOT_DIR / 'src' +CONFIG_DIR = ROOT_DIR / 'config' +RESOURCES_DIR = CONFIG_DIR / 'resources' + +# --- Determine hidden imports that PyInstaller might miss ------------- +hidden_imports = [ + # Kivy core modules + 'kivy.core.window', + 'kivy.core.video', + 'kivy.core.audio', + 'kivy.core.text', + 'kivy.core.image', + 'kivy.core.gl', + 'kivy.core.camera', + 'kivy.core.clipboard', + 'kivy.core.spelling', + 'kivy.core.text.markup', + 'kivy.core.window.window_sdl2', + 'kivy.core.image.img_sdl2', + 'kivy.core.video.video_ffpyplayer', + 'kivy.core.audio.audio_ffpyplayer', + # Kivy modules + 'kivy.uix.video', + 'kivy.uix.vkeyboard', + 'kivy.uix.popup', + 'kivy.uix.image', + 'kivy.uix.button', + 'kivy.uix.label', + 'kivy.uix.textinput', + 'kivy.uix.boxlayout', + 'kivy.uix.floatlayout', + 'kivy.uix.slider', + 'kivy.uix.widget', + 'kivy.uix.checkbox', + 'kivy.graphics', + 'kivy.graphics.texture', + 'kivy.graphics.vertex_instructions', + 'kivy.graphics.context_instructions', + 'kivy.clock', + 'kivy.loader', + 'kivy.animation', + 'kivy.lang', + 'kivy.logger', + 'kivy.config', + 'kivy.properties', + 'kivy.metrics', + 'kivy.factory', + # Graphics providers + 'kivy.graphics.opengl', + 'kivy.graphics.opengl_utils', + 'kivy.graphics.fbo', + 'kivy.graphics.gl_instructions', + 'kivy.graphics.stencil_instructions', + 'kivy.graphics.scissor_instructions', + 'kivy.graphics.buffer', + 'kivy.graphics.vbo', + 'kivy.graphics.shader', + 'kivy.graphics.compiler', + # ffpyplayer + 'ffpyplayer', + 'ffpyplayer.player', + 'ffpyplayer.pic', + 'ffpyplayer.writer', + # Networking + 'requests', + 'aiohttp', + 'urllib3', + 'certifi', + 'bcrypt', + # Platform + 'ctypes', + 'ctypes.wintypes', + 'subprocess', + 'shutil', + 'glob', + 'selectors', + 'tempfile', +] + +# Exclude Linux-only modules +excluded_imports = [ + 'gi', # GTK introspection (Linux) + 'gi.repository', + 'evdev', # We inject a fake evdev module in run_win.py +] + +# --- Application data files to bundle -------------------------------- +# Resources (icons, intro video, etc.) +resources_data = [] +for item in RESOURCES_DIR.iterdir(): + if item.is_file(): + target_dir = 'config/resources' + resources_data.append((str(item), target_dir)) + +# Config directory (app_config.json) +config_data = [] +config_file = CONFIG_DIR / 'app_config.json' +if config_file.exists(): + config_data.append((str(config_file), 'config')) + +# Source files - .kv file +kv_file = SRC_DIR / 'signage_player.kv' +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']) + +# --- Collect binary DLLs from kivy_deps and ffpyplayer ---------------- +import importlib.util +from pathlib import Path as _Path + +def _find_share_dlls(package_path, subdir='bin'): + """Find .dll files under a package's share/ directory.""" + if not package_path: + return [] + base = _Path(package_path).parent + # Check: share//bin/ relative to parent + share = base / 'share' + if share.is_dir(): + results = [] + for root, dirs, files in os.walk(share): + for f in files: + if f.endswith('.dll'): + results.append((os.path.join(root, f), '.')) + return results + return [] + +# SDL2 DLLs +_sdl2_spec = importlib.util.find_spec('kivy_deps.sdl2') +_sdl2_dlls = _find_share_dlls(_sdl2_spec.origin if _sdl2_spec else None) + +# ANGLE DLLs +_angle_spec = importlib.util.find_spec('kivy_deps.angle') +_angle_dlls = _find_share_dlls(_angle_spec.origin if _angle_spec else None) + +# GLEW DLLs +_glew_spec = importlib.util.find_spec('kivy_deps.glew') +_glew_dlls = _find_share_dlls(_glew_spec.origin if _glew_spec else None) + +# ffpyplayer FFmpeg DLLs +_ffpy_spec = importlib.util.find_spec('ffpyplayer') +_ffpy_dlls = _find_share_dlls(_ffpy_spec.origin if _ffpy_spec else None) + +_all_binaries = _sdl2_dlls + _angle_dlls + _glew_dlls + _ffpy_dlls + +if not _all_binaries: + print("=" * 70) + print("WARNING: No Kivy/ffpyplayer DLLs found via share/ directories.") + print("PyInstaller may still auto-detect them, but if the .exe") + print("fails with 'SDL2.dll not found' or similar, you will need") + print("to manually add the DLL paths to the spec file.") + print("=" * 70) + +# --- Build the .exe -------------------------------------------------- +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, + hiddenimports=hidden_imports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[str(BUILD_DIR / 'pyi_runtime_hook.py')], + excludes=excluded_imports, + noarchive=False, + module_collection_mode={ + 'kivy': 'pyz', + 'kivy.core': 'pyz', + 'kivy.uix': 'pyz', + 'kivy.graphics': 'pyz', + }, +) + +# Add the source tree (main.py, etc.) +a.datas += source_tree + +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='KiwySignagePlayer', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, # Show console for debugging startup errors + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=str(RESOURCES_DIR / 'app_icon.ico') if (RESOURCES_DIR / 'app_icon.ico').exists() else None, +) + +# --- COLLECT everything into a single folder ------------------------- +coll = COLLECT( + exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='KiwySignagePlayer', +) diff --git a/windows/build_win.bat b/windows/build_win.bat new file mode 100644 index 0000000..feef21c --- /dev/null +++ b/windows/build_win.bat @@ -0,0 +1,126 @@ +@echo off +REM ===================================================================== +REM Kiwy Signage Player - Windows Build Script +REM ===================================================================== +REM This script builds a standalone Windows .exe using PyInstaller. +REM +REM Prerequisites: +REM 1. Python 3.10+ installed (with "Add to PATH" checked) +REM 2. Visual C++ Redistributable (for ffpyplayer) +REM 3. FFmpeg binaries in PATH (optional, for video codec support) +REM +REM Steps: +REM 1. Run this script from the project root or the windows\ folder +REM 2. The .exe will be created in windows\dist\KiwySignagePlayer\ +REM ===================================================================== + +setlocal enabledelayedexpansion + +cd /d "%~dp0" + +echo ============================================ +echo Kiwy Signage Player - Windows Build +echo ============================================ +echo. + +REM ---- Check Python ---- +where python >nul 2>&1 +if %ERRORLEVEL% neq 0 ( + echo [ERROR] Python not found! Please install Python 3.10+ and add it to PATH. + pause + exit /b 1 +) + +echo [INFO] Using Python: +python --version + +REM ---- Create virtual environment (if not exists) ---- +if not exist "venv\Scripts\python.exe" ( + echo. + echo [STEP] Creating virtual environment... + python -m venv venv + if %ERRORLEVEL% neq 0 ( + echo [ERROR] Failed to create virtual environment. + pause + exit /b 1 + ) +) else ( + echo [INFO] Virtual environment already exists. +) + +REM ---- Activate virtual environment ---- +call venv\Scripts\activate.bat + +REM ---- Install/upgrade pip ---- +echo. +echo [STEP] Upgrading pip... +python -m pip install --upgrade pip + +REM ---- Install dependencies ---- +echo. +echo [STEP] Installing Windows dependencies... +pip install -r requirements_win.txt +if %ERRORLEVEL% neq 0 ( + echo [ERROR] Failed to install dependencies. + pause + exit /b 1 +) + +REM ---- Verify Kivy installation ---- +echo. +echo [STEP] Verifying Kivy installation... +python -c "import kivy; print(f'Kivy {kivy.__version__}')" 2>&1 +if %ERRORLEVEL% neq 0 ( + echo [WARNING] Kivy check failed. Build may still work but test carefully. +) + +REM ---- Check PyInstaller ---- +echo. +echo [STEP] Verifying PyInstaller... +python -c "import PyInstaller; print(f'PyInstaller {PyInstaller.__version__}')" 2>&1 +if %ERRORLEVEL% neq 0 ( + echo [ERROR] PyInstaller not found. + pause + exit /b 1 +) + +REM ---- Create app icon (from PNG if possible) ---- +echo. +echo [STEP] Checking for app icon... +if not exist "..\config\resources\app_icon.ico" ( + echo [INFO] No .ico icon found. Will use default PyInstaller icon. + echo [INFO] To add a custom icon, place app_icon.ico in config\resources\ +) + +REM ---- Run PyInstaller ---- +echo. +echo [STEP] Building executable with PyInstaller... +echo This may take several minutes. Please wait... +echo. + +pyinstaller build.spec --clean --noconfirm +if %ERRORLEVEL% neq 0 ( + echo. + echo [ERROR] PyInstaller build failed! + echo Check the output above for error details. + pause + exit /b 1 +) + +REM ---- Success ---- +echo. +echo ============================================ +echo BUILD COMPLETE! +echo ============================================ +echo. +echo Output: %~dp0dist\KiwySignagePlayer\ +echo. +echo The executable is: +echo %~dp0dist\KiwySignagePlayer\KiwySignagePlayer.exe +echo. +echo To run: Double-click KiwySignagePlayer.exe +echo. +echo Note: The first run may take a while as Windows Defender +echo scans the executable. This is normal. +echo. +pause diff --git a/windows/launch_player.bat b/windows/launch_player.bat new file mode 100644 index 0000000..846c358 --- /dev/null +++ b/windows/launch_player.bat @@ -0,0 +1,27 @@ +@echo off +REM ============================================================ +REM Kiwy Signage Player - Windows Launcher +REM ============================================================ +REM This batch file launches the Kiwy Signage Player executable. +REM It creates local folders for playlist, media, config, and logs +REM next to the executable. +REM ============================================================ + +cd /d "%~dp0dist\KiwySignagePlayer" + +echo ============================================ +echo Kiwy Signage Player - Windows Edition +echo ============================================ +echo. +echo Launching player... +echo. + +start "" "KiwySignagePlayer.exe" + +echo Player started. +echo. +echo If the player window does not appear, check: +echo dist\KiwySignagePlayer\logs\crash.log +echo dist\KiwySignagePlayer\logs\fatal_crash.log +echo. +pause diff --git a/windows/pyi_runtime_hook.py b/windows/pyi_runtime_hook.py new file mode 100644 index 0000000..312c171 --- /dev/null +++ b/windows/pyi_runtime_hook.py @@ -0,0 +1,131 @@ +""" +PyInstaller Runtime Hook for Kiwy Signage Player +------------------------------------------------ +Runs at startup of the packaged .exe to fix paths and environment. +Creates all necessary folders LOCAL to the executable's directory. +""" + +import os +import sys +import platform +from pathlib import Path + +# โ”€โ”€ IMPORTANT: Set Windows environment BEFORE any Kivy code runs โ”€โ”€ +# This must happen before main.py's top-level code executes, because +# main.py sets SDL_VIDEODRIVER=wayland,x11,dummy which would crash on Windows. +os.environ['SDL_VIDEODRIVER'] = 'windows' +os.environ['SDL_AUDIODRIVER'] = 'directsound' +os.environ['KIVY_WINDOW'] = 'sdl2' +os.environ['KIVY_GL_BACKEND'] = 'angle_sdl2' +os.environ['KIVY_VIDEO'] = 'ffpyplayer' +os.environ['KIVY_AUDIO'] = 'ffpyplayer' +os.environ['FFPYPLAYER_CODECS'] = 'h264,h265,vp9,vp8' +os.environ['SDL_VIDEO_ALLOW_SCREENSAVER'] = '0' +os.environ['KIVY_NO_FILELOG'] = '1' +os.environ['KIVY_INPUTPROVIDERS'] = '' # Let Kivy auto-detect on Windows + +# โ”€โ”€ Capture ALL early output to a crash log โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Ensure we catch any exception that happens before Logger is available. +_startup_log_path = None +try: + _exe_dir = Path(sys.executable).parent + _startup_log_path = _exe_dir / 'logs' / 'startup_crash.log' + (_startup_log_path.parent).mkdir(parents=True, exist_ok=True) + with open(_startup_log_path, 'w') as _f: + _f.write("pyi_runtime_hook.py started\n") +except Exception: + pass + + +def _setup_paths(): + """Ensure the app can find its bundled files at runtime. + + All data folders (config, media, playlists, logs) are created + LOCAL to the executable's directory โ€” NOT in %%APPDATA%%. + """ + # In PyInstaller, sys.executable is the .exe path. + # sys._MEIPASS is the extraction directory (i.e. _internal/ folder). + exe_dir = Path(sys.executable).parent + internal_dir = Path(getattr(sys, '_MEIPASS', exe_dir)) + + # โ”€โ”€ Change cwd to _internal so Builder.load_file('signage_player.kv') + # and other relative file references from main.py resolve โ”€โ”€โ”€โ”€โ”€ + os.chdir(str(internal_dir)) + + # Add bundled src directory to Python path + src_dir = str(internal_dir / 'src') + if os.path.isdir(src_dir) and src_dir not in sys.path: + sys.path.insert(0, src_dir) + + # Add internal directory for config/media/playlists access + if str(internal_dir) not in sys.path: + sys.path.insert(0, str(internal_dir)) + + # โ”€โ”€ Local folders next to the .exe โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + # All data lives in the SAME folder as the executable so the user + # can copy/move the whole directory and everything still works. + os.environ['KIWY_DATA_DIR'] = str(exe_dir) + + # Set KIVY_HOME to a local .kivy folder next to the .exe + kivy_home = exe_dir / '.kivy' + os.environ.setdefault('KIVY_HOME', str(kivy_home)) + kivy_home.mkdir(parents=True, exist_ok=True) + + # Create local data folders next to the .exe + for sub in ['config', 'config/resources', 'media', 'playlists', 'logs']: + (exe_dir / sub).mkdir(parents=True, exist_ok=True) + + +def _copy_bundled_resources(): + """Copy bundled resource/config files to the local folders on first run.""" + exe_dir = Path(sys.executable).parent + internal_dir = Path(getattr(sys, '_MEIPASS', exe_dir)) + + # Files to copy (source in bundle -> destination next to .exe) + files_to_copy = [ + ('config/app_config.json', 'config/app_config.json'), + ('config/resources/access-card.png', 'config/resources/access-card.png'), + ('config/resources/arrow.png', 'config/resources/arrow.png'), + ('config/resources/backward.png', 'config/resources/backward.png'), + ('config/resources/card-checked.png', 'config/resources/card-checked.png'), + ('config/resources/edit-pen.png', 'config/resources/edit-pen.png'), + ('config/resources/exit.png', 'config/resources/exit.png'), + ('config/resources/forward.png', 'config/resources/forward.png'), + ('config/resources/intro1.mp4', 'config/resources/intro1.mp4'), + ('config/resources/pause.png', 'config/resources/pause.png'), + ('config/resources/pencil.png', 'config/resources/pencil.png'), + ('config/resources/play.png', 'config/resources/play.png'), + ('config/resources/settings.png', 'config/resources/settings.png'), + ] + + for src_rel, dest_rel in files_to_copy: + src_path = internal_dir / src_rel + dest_path = exe_dir / dest_rel + if src_path.is_file() and not dest_path.exists(): + try: + dest_path.parent.mkdir(parents=True, exist_ok=True) + import shutil + shutil.copy2(str(src_path), str(dest_path)) + except Exception: + pass # Non-critical; app can still run + + +# โ”€โ”€ Wrap everything in try/except to capture early crashes โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +try: + _setup_paths() + _copy_bundled_resources() + # If we reach here, the hook finished successfully + try: + with open(_startup_log_path, 'a') as _f: + _f.write("pyi_runtime_hook.py completed successfully\n") + except Exception: + pass +except Exception as _hook_exc: + import traceback as _tb + try: + with open(_startup_log_path, 'a') as _f: + _f.write(f"pyi_runtime_hook.py CRASHED: {_hook_exc}\n") + _tb.print_exc(file=_f) + except Exception: + pass + raise # Re-raise so the .exe still fails visibly diff --git a/windows/requirements_win.txt b/windows/requirements_win.txt new file mode 100644 index 0000000..33e88dd --- /dev/null +++ b/windows/requirements_win.txt @@ -0,0 +1,28 @@ +# ===================================================================== +# Kiwy Signage Player - Windows Dependencies +# ===================================================================== +# Install with: pip install -r requirements_win.txt + +# --- Core GUI Framework --- +# Kivy 2.3+ with SDL2 backend (best for Windows) +kivy[base]>=2.3.0 + +# --- Video Playback --- +# ffpyplayer for video decoding +ffpyplayer>=4.5 + +# --- HTTP / Networking --- +requests>=2.32.0,<3.0.0 +aiohttp>=3.9.0,<4.0.0 +certifi>=2024.0.0 + +# --- Password / Auth --- +bcrypt>=4.2.0,<5.0.0 + +# --- Packaging --- +# PyInstaller for building the .exe +pyinstaller>=6.0 + +# --- Optional: DirectShow filters for better video on Windows --- +# ffmpeg (install via chocolatey or manual download) +# https://ffmpeg.org/download.html diff --git a/windows/run_win.py b/windows/run_win.py new file mode 100644 index 0000000..ad054b2 --- /dev/null +++ b/windows/run_win.py @@ -0,0 +1,574 @@ +""" +Kiwy Signage Player - Windows Entry Point +------------------------------------------ +Patches platform-specific code and environment for Windows before launching +the original Kivy-based signage player application. + +Usage: + python run_win.py (for development/testing) + run_win.exe (after PyInstaller build) +""" + +import ctypes +import os +import sys +import platform +import tempfile +import subprocess +import shutil +import logging +from pathlib import Path + + +def _show_error_box(title, message): + """Show a Windows message box with the error (visible even without console).""" + try: + ctypes.windll.user32.MessageBoxW(0, message, title, 0x10) # MB_ICONERROR + except Exception: + pass + +# --- Ensure we are on Windows; warn if not --- +if platform.system() != 'Windows': + print(f"WARNING: This entry point is designed for Windows. Detected: {platform.system()}") + +# ===================================================================== +# 1. Set Windows-compatible environment variables BEFORE Kivy imports +# ===================================================================== + +# Video driver: Use 'windib' or 'angle' (DirectX via ANGLE) for Windows +os.environ.setdefault('SDL_VIDEODRIVER', 'windows') +# Audio driver: DirectSound for Windows +os.environ.setdefault('SDL_AUDIODRIVER', 'directsound') +# Prevent screensaver +os.environ.setdefault('SDL_VIDEO_ALLOW_SCREENSAVER', '0') +# Video backend via ffpyplayer +os.environ.setdefault('KIVY_VIDEO', 'ffpyplayer') +os.environ.setdefault('KIVY_AUDIO', 'ffpyplayer') +os.environ.setdefault('FFPYPLAYER_CODECS', 'h264,h265,vp9,vp8') +# Kivy window backend: prefer SDL2 on Windows +os.environ.setdefault('KIVY_WINDOW', 'sdl2') +# OpenGL +os.environ.setdefault('KIVY_GL_BACKEND', 'angle_sdl2') + +# ===================================================================== +# 2. Patch the evdev import โ€” it is Linux-only. We provide a dummy +# module so that `from evdev import ...` will not crash on Windows. +# ===================================================================== +class _DummyEvdev: + """Fake evdev module that raises ImportError for all meaningful uses.""" + + class InputDevice: + def __init__(self, *a, **kw): + raise ImportError("evdev is not available on Windows") + + class ecodes: + EV_KEY = 1 + EV_ABS = 3 + + def categorize(self, *a, **kw): + raise ImportError("evdev is not available on Windows") + + def list_devices(self): + return [] + + +class _FakeEvdevInputDevice: + pass + + +# Inject the fake evdev module into sys.modules so that main.py's +# `try: import evdev` succeeds but EVDEV_AVAILABLE stays False. +_evdev_dummy = _DummyEvdev() +sys.modules['evdev'] = _evdev_dummy +sys.modules['evdev.InputDevice'] = _FakeEvdevInputDevice + +# ===================================================================== +# 3. Provide a Windows implementation of screen activity signaling +# We monkey-patch the SignagePlayer.signal_screen_activity method +# after the class is defined but before it's used, by hooking into +# the import machinery. +# ===================================================================== + +# We'll store a reference to the original module's signal_screen_activity +# so we can replace it after import. This is done inside _patch_main(). + +def _windows_screen_activity(self, dt): + """Windows alternative to Linux screen-keep-awake commands. + + Uses SetThreadExecutionState via ctypes to tell Windows to keep + the display and system awake. + """ + try: + # ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED | ES_CONTINUOUS + ES_CONTINUOUS = 0x80000000 + ES_SYSTEM_REQUIRED = 0x00000001 + ES_DISPLAY_REQUIRED = 0x00000002 + + ctypes.windll.kernel32.SetThreadExecutionState( + ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED + ) + except Exception: + pass # non-critical + + +def _windows_find_browser(): + """Find Chrome or Edge executable on Windows for weblink support. + + Returns the path to the browser or None. + """ + # Common install locations + candidates = [ + # Chrome + os.path.expandvars(r'%PROGRAMFILES%\Google\Chrome\Application\chrome.exe'), + os.path.expandvars(r'%PROGRAMFILES(X86)%\Google\Chrome\Application\chrome.exe'), + os.path.expandvars(r'%LOCALAPPDATA%\Google\Chrome\Application\chrome.exe'), + # Edge + os.path.expandvars(r'%PROGRAMFILES%\Microsoft\Edge\Application\msedge.exe'), + os.path.expandvars(r'%PROGRAMFILES(X86)%\Microsoft\Edge\Application\msedge.exe'), + os.path.expandvars(r'%LOCALAPPDATA%\Microsoft\Edge\Application\msedge.exe'), + ] + for path in candidates: + if os.path.isfile(path): + return path + + # Fallback: try PATH + which = shutil.which('chrome') or shutil.which('msedge') or shutil.which('google-chrome') + if which: + return which + + return None + + +def _patch_main(): + """Patch the main module after import for Windows compatibility.""" + # โ”€โ”€ CRITICAL: Override Linux env vars BEFORE importing main โ”€โ”€โ”€โ”€โ”€ + # main.py's top-level code sets SDL_VIDEODRIVER=wayland,x11,dummy + # and other Linux values. We MUST override these before main.py + # gets imported, otherwise Kivy will initialize with the wrong + # window provider and crash with SystemExit(1). + os.environ['SDL_VIDEODRIVER'] = 'windows' + os.environ['SDL_AUDIODRIVER'] = 'directsound' + os.environ['KIVY_WINDOW'] = 'sdl2' + os.environ['KIVY_GL_BACKEND'] = 'angle_sdl2' + os.environ['KIVY_INPUTPROVIDERS'] = '' # Let Kivy auto-detect + os.environ['KIVY_VIDEO'] = 'ffpyplayer' + os.environ['KIVY_AUDIO'] = 'ffpyplayer' + + # Now safe to import main.py โ€” env vars are already Windows-correct + import main as signage_main + + # Replace signal_screen_activity + # IMPORTANT: Assign under BOTH the attribute name AND the function's own name. + # Kivy's WeakMethod stores self.__func__.__name__ (= '_windows_screen_activity') + # and later does getattr(instance, '_windows_screen_activity'). If we only + # assign under 'signal_screen_activity', the weakref lookup fails with + # AttributeError: 'SignagePlayer' object has no attribute '_windows_screen_activity' + signage_main.SignagePlayer.signal_screen_activity = _windows_screen_activity + signage_main.SignagePlayer._windows_screen_activity = _windows_screen_activity + + # Store a reference to the original play_weblink so we can wrap it + _original_play_weblink = signage_main.SignagePlayer.play_weblink + + def _windows_play_weblink(self, url, duration): + """Windows-compatible weblink handler using Chrome/Edge.""" + browser = _windows_find_browser() + if not browser: + from kivy.logger import Logger + Logger.error( + "SignagePlayer: Chrome/Edge not found. " + "Cannot display weblink on Windows." + ) + self.consecutive_errors += 1 + self._skip_to_next_media() + return False + + from urllib.parse import urlparse + scheme = urlparse(url).scheme.lower() + if scheme not in ('http', 'https'): + from kivy.logger import Logger + Logger.warning(f"SignagePlayer: Refusing non-http(s) weblink: {url}") + self.consecutive_errors += 1 + self._skip_to_next_media() + return False + + import subprocess + from kivy.logger import Logger + from kivy.clock import Clock + + try: + Logger.info(f"SignagePlayer: Opening weblink: {url} (browser: {browser})") + self._kill_weblink_preload() + + # Launch in fullscreen / kiosk-like mode + self._weblink_proc = subprocess.Popen([ + browser, + '--kiosk', + '--no-first-run', + '--noerrdialogs', + '--disable-infobars', + '--incognito', + '--no-default-browser-check', + '--disable-session-crashed-bubble', + '--disable-features=TranslateUI', + url, + ], shell=False) + + Clock.unschedule(self.next_media) + self._start_inactivity_watchdog(duration) + self.preload_next_media() + return True + + except Exception as e: + Logger.error(f"SignagePlayer: Error opening weblink: {e}") + self.consecutive_errors += 1 + self._skip_to_next_media() + return False + + # Replace weblink handling + signage_main.SignagePlayer.play_weblink = _windows_play_weblink + + # Patch the _get_browser_target_size to always return a reasonable size on Windows + def _windows_get_browser_target_size(self): + try: + from kivy.core.window import Window + width, height = Window.size + if width >= 1280 and height >= 720: + return int(width), int(height) + except Exception: + pass + return 1920, 1080 + + signage_main.SignagePlayer._get_browser_target_size = _windows_get_browser_target_size + + # Patch _start_inactivity_watchdog for Windows โ€” /dev/input does not exist + _original_watchdog = signage_main.SignagePlayer._start_inactivity_watchdog + + def _windows_watchdog(self, duration): + """Windows watchdog: uses a simple timer since /dev/input is not available. + + Falls back to a fixed timer that advances after 'duration' seconds. + """ + from kivy.clock import Clock + import threading + + self._stop_inactivity_watchdog() + stop_event = threading.Event() + self._watchdog_stop = stop_event + weblink_proc = self._weblink_proc + + def watchdog(): + import time + # Simply wait for the duration, checking if Chromium exited early + elapsed = 0.0 + step = 0.5 + while elapsed < duration and not stop_event.is_set(): + if weblink_proc is not None and weblink_proc.poll() is not None: + Clock.schedule_once(self.next_media, 0) + return + time.sleep(step) + elapsed += step + if not stop_event.is_set(): + Clock.schedule_once(self.next_media, 0) + + self._weblink_watchdog_thread = threading.Thread( + target=watchdog, daemon=True, name='weblink-watchdog-win' + ) + self._weblink_watchdog_thread.start() + + signage_main.SignagePlayer._start_inactivity_watchdog = _windows_watchdog + + # Patch the _kill_weblink_after_frame to work without Linux-specific code + def _windows_kill_weblink_after_frame(self): + from kivy.clock import Clock + self._stop_inactivity_watchdog() + self._kill_weblink_preload() + proc = self._weblink_proc + self._weblink_proc = None + if proc is not None and proc.poll() is None: + def _do_kill(dt): + if proc.poll() is None: + try: + proc.terminate() + try: + proc.wait(timeout=3) + except Exception: + proc.kill() + except Exception: + pass + Clock.schedule_once(_do_kill, 0) + signage_main.SignagePlayer._kill_weblink_after_frame = _windows_kill_weblink_after_frame + + # Patch _prewarm_weblink for Windows + def _windows_prewarm_weblink(self, url): + from urllib.parse import urlparse + if not url: + return + scheme = urlparse(url).scheme.lower() + if scheme not in ('http', 'https'): + return + browser = _windows_find_browser() + if not browser: + return + import subprocess + from kivy.logger import Logger + self._kill_weblink_preload() + try: + Logger.debug(f"SignagePlayer: Pre-warming weblink off-screen: {url}") + self._weblink_preload_proc = subprocess.Popen([ + browser, + '--incognito', + '--no-first-run', + '--window-position=-9999,-9999', + url, + ], shell=False) + except Exception as exc: + Logger.debug(f"SignagePlayer: Pre-warm failed (non-fatal): {exc}") + self._weblink_preload_proc = None + signage_main.SignagePlayer._prewarm_weblink = _windows_prewarm_weblink + + # Patch cleanup of temp auth file (was using /tmp/) + _original_connection_test = signage_main.SettingsPopup.test_connection + + def _windows_test_connection(self): + """Test connection with Windows-safe temp file path.""" + import tempfile as _tf + import os as _os + from player_auth import PlayerAuth + import re + import threading + from kivy.clock import Clock + + self.ids.connection_status.text = 'Testing connection...' + self.ids.connection_status.color = (1, 0.7, 0, 1) + + def run_test(): + try: + server_ip = self.ids.server_input.text.strip() + screen_name = self.ids.screen_input.text.strip() + quickconnect = self.ids.quickconnect_input.text.strip() + port = self.ids.port_input.text.strip() or self.player.config.get('port', '') + use_https = self.player.config.get('use_https', True) + verify_ssl = self.player.config.get('verify_ssl', True) + + if not all([server_ip, screen_name, quickconnect]): + Clock.schedule_once(lambda dt: self.update_connection_status('Error: Fill all fields', False)) + return + + if server_ip.startswith('http://') or server_ip.startswith('https://'): + server_url = server_ip + if ':' not in server_ip.replace('https://', '').replace('http://', ''): + if port and port not in ('443', '80'): + server_url = f"{server_ip}:{port}" + else: + protocol = "https" if use_https else "http" + if ':' in server_ip: + server_url = f"{protocol}://{server_ip}" + else: + server_url = f"{protocol}://{server_ip}:{port}" if port else f"{protocol}://{server_ip}" + + # Use Windows temp path + temp_file = _os.path.join(_tf.gettempdir(), 'temp_auth_test.json') + auth = PlayerAuth(temp_file, use_https=use_https, verify_ssl=verify_ssl) + success, error = auth.authenticate(server_url=server_url, hostname=screen_name, quickconnect_code=quickconnect) + + try: + if _os.path.exists(temp_file): + _os.remove(temp_file) + except Exception: + pass + + if success: + player_name = auth.get_player_name() + Clock.schedule_once(lambda dt: self.update_connection_status(f'โœ“ Connected: {player_name}', True)) + else: + Clock.schedule_once(lambda dt: self.update_connection_status(f'โœ— Failed: {error}', False)) + + except Exception as e: + Clock.schedule_once(lambda dt: self.update_connection_status(f'โœ— Error: {str(e)}', False)) + + threading.Thread(target=run_test, daemon=True).start() + + signage_main.SettingsPopup.test_connection = _windows_test_connection + + return signage_main + + +# ===================================================================== +# 4. Adjust path so we can import the src modules +# ===================================================================== +# When run from PyInstaller .exe: the runtime hook inserts paths. +# When run as plain python, we add src/ relative to this file. +_script_dir = Path(__file__).resolve().parent +_project_root = _script_dir.parent +_src_dir = _project_root / 'src' + +for p in [str(_src_dir), str(_project_root)]: + if p not in sys.path: + sys.path.insert(0, p) + +# ===================================================================== +# 5. Determine the local data directory (next to the executable) +# ===================================================================== +# The pyi_runtime_hook.py (when packaged) sets KIWY_DATA_DIR. +# When running in dev mode from python, use the project root. +# The executable will create its own local folders for playlist, +# media, config, and logs where the executable is launched. +DATA_DIR = os.environ.get('KIWY_DATA_DIR', str(_project_root)) + +# Create local data folders NEXT TO the executable +os.makedirs(os.path.join(DATA_DIR, 'config', 'resources'), exist_ok=True) +os.makedirs(os.path.join(DATA_DIR, 'media'), exist_ok=True) +os.makedirs(os.path.join(DATA_DIR, 'media', 'edited_media'), exist_ok=True) +os.makedirs(os.path.join(DATA_DIR, 'playlists'), exist_ok=True) +os.makedirs(os.path.join(DATA_DIR, 'logs'), exist_ok=True) +os.makedirs(os.path.join(DATA_DIR, 'config', 'certs'), exist_ok=True) + +# ===================================================================== +# 6. Set Kivy config BEFORE importing Kivy +# ===================================================================== +os.environ['KIVY_NO_FILELOG'] = '1' # Avoid file logging issues on Windows +os.environ['KIVY_HOME'] = os.path.join(DATA_DIR, '.kivy') + +from kivy.config import Config +Config.set('kivy', 'keyboard_mode', '') # Disable default virtual keyboard +Config.set('graphics', 'fullscreen', '0') +Config.set('graphics', 'window_state', 'maximized') +Config.set('graphics', 'multisampling', '0') +Config.set('graphics', 'fast_rgba', '1') +Config.set('kivy', 'log_level', 'warning') + +# ===================================================================== +# 7. Patch the main module, then run the app +# ===================================================================== +if __name__ == '__main__': + try: + # Write a startup marker so we know the .exe at least launched + try: + os.makedirs(os.path.join(DATA_DIR, 'logs'), exist_ok=True) + marker = os.path.join(DATA_DIR, 'logs', 'startup_marker.txt') + with open(marker, 'w') as f: + f.write(f"run_win.py started at {__import__('time').time()}\n") + except Exception: + pass + + # Apply all Windows patches before launching + try: + patched_main = _patch_main() + except Exception as e: + # Catch early import errors (pre-Logger) to a file + import traceback + try: + err_log = os.path.join(DATA_DIR, 'logs', 'startup_error.log') + with open(err_log, 'w') as f: + f.write(f"Error in _patch_main(): {e}\n") + traceback.print_exc(file=f) + except Exception: + pass + raise # Re-raise so console shows it too + + from kivy.logger import Logger + Logger.info("=" * 80) + Logger.info("Kiwy Signage Player - Windows Edition") + Logger.info(f"Python: {sys.version}") + Logger.info(f"Platform: {platform.platform()}") + Logger.info(f"Data directory: {DATA_DIR}") + Logger.info("=" * 80) + + # Patch base_dir in SignagePlayer instances to point to local data folder + _original_init = patched_main.SignagePlayer.__init__ + + def _patched_init(self, **kwargs): + """Override SignagePlayer.__init__ to use local data folders. + + Creates all necessary folders (config, media, playlists, logs) + in the same directory where the executable is launched. + """ + # Call parent Widget.__init__ + _original_init(self, **kwargs) + # Now override ALL paths to point to local data directory + # (where the .exe is located) + self.base_dir = DATA_DIR + self.config_dir = os.path.join(DATA_DIR, 'config') + self.media_dir = os.path.join(DATA_DIR, 'media') + self.playlists_dir = os.path.join(DATA_DIR, 'playlists') + self.config_file = os.path.join(self.config_dir, 'app_config.json') + self.resources_path = os.path.join(self.config_dir, 'resources') + self.heartbeat_file = os.path.join(DATA_DIR, '.player_heartbeat') + # Ensure all required folders exist locally + for directory in [ + self.config_dir, + self.resources_path, + os.path.join(self.media_dir, 'edited_media'), + self.playlists_dir, + os.path.join(DATA_DIR, 'logs'), + os.path.join(DATA_DIR, 'config', 'certs'), + ]: + os.makedirs(directory, exist_ok=True) + + patched_main.SignagePlayer.__init__ = _patched_init + + # Patch SSLManager cert directory to use local data folder + # ssl_utils is imported by player_auth.py and get_playlists_v2.py, not main.py + import ssl_utils + ssl_utils.SSLManager.CERT_DIR = os.path.join(DATA_DIR, 'config', 'certs') + ssl_utils.SSLManager.CERT_FILE = os.path.join( + ssl_utils.SSLManager.CERT_DIR, 'server_cert.pem' + ) + ssl_utils.SSLManager.CERT_INFO_FILE = os.path.join( + ssl_utils.SSLManager.CERT_DIR, 'cert_info.json' + ) + + # Run the app + try: + app = patched_main.SignagePlayerApp() + app.run() + except KeyboardInterrupt: + Logger.info("Application stopped by user (Ctrl+C)") + except SystemExit as _se: + Logger.critical(f"Kivy SystemExit (likely window provider missing): {_se}") + try: + crash_log = os.path.join(DATA_DIR, 'logs', 'crash.log') + with open(crash_log, 'w') as f: + f.write(f"Kivy SystemExit: {_se}\n") + f.write("This usually means Kivy could not find a window provider on this system.\n") + except Exception: + pass + _show_error_box( + "Kiwy Signage Player - Kivy Error", + f"Kivy exited: {_se}\n\n" + "This usually means Kivy could not create a window.\n" + "Check your GPU drivers and DirectX installation.\n\n" + "See logs/crash.log for details." + ) + sys.exit(1) + except Exception as e: + Logger.critical(f"Fatal error: {e}") + Logger.exception("Full traceback:") + # Also write to a crash log next to the executable + try: + import traceback + crash_log = os.path.join(DATA_DIR, 'logs', 'crash.log') + with open(crash_log, 'w') as f: + f.write(f"Fatal error: {e}\n") + traceback.print_exc(file=f) + except Exception: + pass + sys.exit(1) + finally: + Logger.info("Application shutdown complete") + except BaseException as _top_e: + # Catch any error BEFORE Logger is available (including SystemExit) + import traceback as _tb + _trace = _tb.format_exc() + try: + _crash_log = os.path.join(DATA_DIR, 'logs', 'fatal_crash.log') + with open(_crash_log, 'w') as _f: + _f.write(f"FATAL (pre-Logger): {_top_e}\n") + _f.write(_trace) + except Exception: + pass + _show_error_box( + "Kiwy Signage Player - Startup Error", + f"{_top_e}\n\nSee logs/fatal_crash.log for details." + ) + raise # Re-raise so .exe still shows the error diff --git a/windows/test_import_fix.py b/windows/test_import_fix.py new file mode 100644 index 0000000..f4d0277 --- /dev/null +++ b/windows/test_import_fix.py @@ -0,0 +1,39 @@ +"""Test that setting env vars before importing main.py fixes the crash.""" +import os +import sys + +# This is the KEY fix: set Windows env vars BEFORE main.py is imported +os.environ['SDL_VIDEODRIVER'] = 'windows' +os.environ['SDL_AUDIODRIVER'] = 'directsound' +os.environ['KIVY_WINDOW'] = 'sdl2' +# Use 'angle_sdl2' on Windows for better DirectX compatibility +os.environ['KIVY_GL_BACKEND'] = 'angle_sdl2' +# Let Kivy auto-detect input providers on Windows +os.environ['KIVY_INPUTPROVIDERS'] = '' +os.environ['KIVY_VIDEO'] = 'ffpyplayer' +os.environ['KIVY_AUDIO'] = 'ffpyplayer' +os.environ['FFPYPLAYER_CODECS'] = 'h264,h265,vp9,vp8' +os.environ['SDL_VIDEO_ALLOW_SCREENSAVER'] = '0' + +# Add src to path +sys.path.insert(0, r'C:\Users\Dell-PC\Desktop\Kiwy-Signage\src') + +print("=" * 60) +print("Testing main.py import with Windows env vars...") +print("=" * 60) + +try: + import main + print("SUCCESS: main.py imported without crashing!") + print(f" SDL_VIDEODRIVER = {os.environ.get('SDL_VIDEODRIVER')}") + print(f" KIVY_WINDOW = {os.environ.get('KIVY_WINDOW')}") + print(f" KIVY_GL_BACKEND = {os.environ.get('KIVY_GL_BACKEND')}") + print(f" KIVY_INPUTPROVIDERS = {os.environ.get('KIVY_INPUTPROVIDERS')}") +except SystemExit as e: + print(f"FAILED: SystemExit({e}) - Kivy window provider still not loading") + sys.exit(1) +except Exception as e: + print(f"FAILED with exception: {e}") + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/New.txt b/working_files/New.txt similarity index 100% rename from New.txt rename to working_files/New.txt diff --git a/install.sh.bak b/working_files/install.sh.bak old mode 100755 new mode 100644 similarity index 100% rename from install.sh.bak rename to working_files/install.sh.bak