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:
@@ -25,6 +25,7 @@ wheels/
|
|||||||
venv/
|
venv/
|
||||||
ENV/
|
ENV/
|
||||||
env/
|
env/
|
||||||
|
windows/venv312/
|
||||||
|
|
||||||
# Kivy
|
# Kivy
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"server_ip": "192.168.0.109",
|
"server_ip": "192.168.0.107",
|
||||||
"port": "8080",
|
"port": "8080",
|
||||||
"screen_name": "Birou_IT",
|
"screen_name": "WINDOWS-PC",
|
||||||
"quickconnect_key": "8887779",
|
"quickconnect_key": "8887779",
|
||||||
"orientation": "Landscape",
|
"orientation": "Landscape",
|
||||||
"touch": "True",
|
"touch": "True",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Python 3.12.9
|
||||||
+19
-15
@@ -14,21 +14,21 @@ import asyncio
|
|||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
# Set environment variables for better video performance
|
# Set environment variables for better video performance
|
||||||
os.environ['KIVY_VIDEO'] = 'ffpyplayer' # Use ffpyplayer as video provider
|
# Use setdefault() so that a wrapper script (e.g. run_win.py) can pre-set
|
||||||
os.environ['FFPYPLAYER_CODECS'] = 'h264,h265,vp9,vp8' # Support common codecs
|
# Windows-compatible values before this module is imported.
|
||||||
os.environ['SDL_VIDEO_ALLOW_SCREENSAVER'] = '0' # Prevent screen saver
|
os.environ.setdefault('KIVY_VIDEO', 'ffpyplayer')
|
||||||
os.environ['SDL_VIDEODRIVER'] = 'wayland,x11,dummy' # Prefer Wayland, fallback to X11, then dummy
|
os.environ.setdefault('FFPYPLAYER_CODECS', 'h264,h265,vp9,vp8')
|
||||||
os.environ['SDL_AUDIODRIVER'] = 'alsa,pulse,dummy' # Prefer ALSA, fallback to pulse, then dummy
|
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
|
# Video playback optimizations
|
||||||
# Note: pygame backend requires X11/Wayland context; let Kivy auto-detect for better compatibility
|
os.environ.setdefault('KIVY_AUDIO', 'ffpyplayer')
|
||||||
# os.environ['KIVY_WINDOW'] = 'pygame' # Use pygame backend for better performance
|
os.environ.setdefault('KIVY_GL_BACKEND', 'gl')
|
||||||
os.environ['KIVY_AUDIO'] = 'ffpyplayer' # Use ffpyplayer for audio
|
os.environ.setdefault('KIVY_INPUTPROVIDERS', 'wayland,x11')
|
||||||
os.environ['KIVY_GL_BACKEND'] = 'gl' # Use OpenGL backend
|
os.environ.setdefault('FFMPEG_THREADS', '2')
|
||||||
os.environ['KIVY_INPUTPROVIDERS'] = 'wayland,x11' # Only use Wayland and X11 input providers, skip problematic ones
|
os.environ.setdefault('LIBPLAYER_BUFFER', '1048576')
|
||||||
os.environ['FFMPEG_THREADS'] = '2' # Use 2 threads for ffmpeg decoding (Raspberry Pi has limited resources)
|
os.environ.setdefault('SDL_AUDIODRIVER', 'alsa')
|
||||||
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
|
|
||||||
|
|
||||||
# Configure Kivy BEFORE importing any Kivy modules
|
# Configure Kivy BEFORE importing any Kivy modules
|
||||||
from kivy.config import Config
|
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.floatlayout import FloatLayout
|
||||||
from kivy.uix.slider import Slider
|
from kivy.uix.slider import Slider
|
||||||
|
|
||||||
# Load the KV file
|
# Load the KV file - resolve relative to this file's directory
|
||||||
Builder.load_file('signage_player.kv')
|
_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:
|
class CardReader:
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"hostname": "Birou_IT",
|
"hostname": "WINDOWS-PC",
|
||||||
"auth_code": "CZncd_2dlTZGieBEdqAbUTjf3qNyEUPDXr8jLVx7NLs",
|
"auth_code": "",
|
||||||
"player_id": 1,
|
"player_id": 2,
|
||||||
"player_name": "Test_player1",
|
"player_name": "Windows-Player1",
|
||||||
"playlist_id": 1,
|
"playlist_id": 1,
|
||||||
"orientation": "Landscape",
|
"orientation": "Landscape",
|
||||||
"authenticated": true,
|
"authenticated": true,
|
||||||
"server_url": "http://192.168.0.109:8080"
|
"server_url": "http://192.168.0.107:8080"
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
Executable → Regular
Reference in New Issue
Block a user