864ea06996
_compy_bundled_resources() in the PyInstaller runtime hook copied config/app_config.json next to the .exe on first start. With the config no longer bundled (see the first-run setup commit) that copy would simply fail, but leaving the entry in place keeps the intent alive and would re-plant a stale config if the file were ever added back to the bundle. The hook now copies only the resource files (icons, intro video), and the comment explains why the config is deliberately absent: the player must start unconfigured so it asks the operator for the server details instead of inheriting the build machine's settings.
170 lines
6.8 KiB
Python
170 lines
6.8 KiB
Python
"""
|
|
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
|
|
|
|
|
|
def _set_process_dpi_awareness():
|
|
"""Declare per-monitor DPI awareness BEFORE SDL/Kivy initialize.
|
|
|
|
On a display scaled above 100% (e.g. 1920x1080 @ 125%), Windows
|
|
virtualizes a non-DPI-aware app to the scaled-down size (1536x864).
|
|
Kivy then sizes its content area to the virtualized resolution, leaving a
|
|
black strip on one side and making images/videos render at the wrong size.
|
|
Must run before any SDL window is created, so this lives in the runtime
|
|
hook (the first Python code that runs in the frozen app).
|
|
"""
|
|
try:
|
|
try:
|
|
aware = ctypes.c_int(2) # PROCESS_PER_MONITOR_DPI_AWARE_V2
|
|
ctypes.windll.shcore.SetProcessDpiAwareness(aware)
|
|
return
|
|
except Exception:
|
|
pass
|
|
try:
|
|
ctypes.windll.user32.SetProcessDPIAware()
|
|
return
|
|
except Exception:
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
if platform.system() == 'Windows':
|
|
import ctypes
|
|
_set_process_dpi_awareness()
|
|
|
|
# ── 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
|
|
# Use native physical pixels (fixes black strip on DPI-scaled displays).
|
|
os.environ.setdefault('SDL_VIDEO_HIGHDPI', '1')
|
|
|
|
# ── 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 files to the local folders on first run.
|
|
|
|
NOTE: app_config.json is intentionally absent. It is not bundled (see
|
|
build.spec) because shipping it would plant the build machine's server
|
|
settings into a fresh install. The player instead starts unconfigured and
|
|
runs its first-run setup, writing a real config next to the .exe.
|
|
"""
|
|
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/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
|