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
This commit is contained in:
@@ -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)
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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/<pkg>/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',
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user