Fix pre-existing player issues: edit upload paths, playlist sync, DPI, console
src/edit_popup.py: pass and reproduce the server-side edited-media layout
('edited_media/<media_id>/') when saving an edit, falling back to the flat
folder when no media id is available, so uploads land where the server expects.
src/get_playlists_v2.py: preserve every server field (audio, muted, description,
id, position, ...) when rewriting playlist items, instead of rebuilding fixed
dicts that silently dropped them. Web-link items keep their original http(s) url
and are never downloaded.
windows/pyi_runtime_hook.py: declare per-monitor DPI awareness before SDL/Kivy
initialise, so on a scaled display the window is not virtualised to a smaller
resolution (which left a black strip and mis-scaled media).
windows/build_win.bat: optional code signing step for PCs with Smart App Control
enabled, driven by KIWY_SIGN_PFX / KIWY_SIGN_PFX_PASSWORD or a local
kiwy_signing.pfx.
windows/build.spec, windows/README_WINDOWS_BUILD.md, windows/development-track.md:
build notes and manifest updates.
This commit is contained in:
+75
-13
@@ -82,11 +82,17 @@ class DrawingLayer(Widget):
|
|||||||
|
|
||||||
class EditPopup(Popup):
|
class EditPopup(Popup):
|
||||||
"""Popup for editing/annotating images"""
|
"""Popup for editing/annotating images"""
|
||||||
def __init__(self, player_instance, image_path, user_card_data=None, **kwargs):
|
def __init__(self, player_instance, image_path, user_card_data=None,
|
||||||
|
media_id=None, original_filename=None, **kwargs):
|
||||||
super(EditPopup, self).__init__(**kwargs)
|
super(EditPopup, self).__init__(**kwargs)
|
||||||
self.player = player_instance
|
self.player = player_instance
|
||||||
self.image_path = image_path
|
self.image_path = image_path
|
||||||
self.user_card_data = user_card_data # Store card data to send to server on save
|
self.user_card_data = user_card_data # Store card data to send to server on save
|
||||||
|
# Server naming context: which media item (id) is being edited and what
|
||||||
|
# its original file name is on the server. The server stores edited
|
||||||
|
# media under 'edited_media/<media_id>/', so we must reproduce that.
|
||||||
|
self.media_id = media_id
|
||||||
|
self.original_filename = original_filename # server-side file_name
|
||||||
|
|
||||||
# Auto-close timer (5 minutes)
|
# Auto-close timer (5 minutes)
|
||||||
self.auto_close_timeout = 300 # 5 minutes in seconds
|
self.auto_close_timeout = 300 # 5 minutes in seconds
|
||||||
@@ -259,8 +265,15 @@ class EditPopup(Popup):
|
|||||||
def save_image(self, instance):
|
def save_image(self, instance):
|
||||||
"""Save the edited image"""
|
"""Save the edited image"""
|
||||||
try:
|
try:
|
||||||
# Create edited_media directory if it doesn't exist
|
# Edited media is stored on the server under
|
||||||
edited_dir = os.path.join(self.player.base_dir, 'media', 'edited_media')
|
# 'edited_media/<media_id>/'. Reproduce that subfolder locally so
|
||||||
|
# the upload naming matches what the server expects. Fall back to
|
||||||
|
# the flat 'edited_media/' folder when no media_id is available.
|
||||||
|
edited_base = os.path.join(self.player.base_dir, 'media', 'edited_media')
|
||||||
|
if self.media_id is not None:
|
||||||
|
edited_dir = os.path.join(edited_base, str(self.media_id))
|
||||||
|
else:
|
||||||
|
edited_dir = edited_base
|
||||||
os.makedirs(edited_dir, exist_ok=True)
|
os.makedirs(edited_dir, exist_ok=True)
|
||||||
|
|
||||||
# Get original filename
|
# Get original filename
|
||||||
@@ -310,8 +323,22 @@ class EditPopup(Popup):
|
|||||||
# Overwrite the file
|
# Overwrite the file
|
||||||
shutil.copy2(output_path, self.image_path)
|
shutil.copy2(output_path, self.image_path)
|
||||||
|
|
||||||
# Force file system sync to ensure data is written to disk
|
# Force file system sync to ensure data is written to disk.
|
||||||
os.sync()
|
# NOTE: os.sync() is Linux-only and raises AttributeError on
|
||||||
|
# Windows — that used to abort the whole pipeline before the
|
||||||
|
# metadata/upload steps. Use a cross-platform fsync that is
|
||||||
|
# best-effort and can never break the save/upload flow.
|
||||||
|
try:
|
||||||
|
if hasattr(os, 'sync'):
|
||||||
|
os.sync()
|
||||||
|
else:
|
||||||
|
with open(output_path, 'rb') as _f:
|
||||||
|
try:
|
||||||
|
os.fsync(_f.fileno())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception as _sync_err:
|
||||||
|
Logger.warning(f"EditPopup: File sync skipped ({_sync_err})")
|
||||||
|
|
||||||
# Verify the overwrite
|
# Verify the overwrite
|
||||||
new_size = os.path.getsize(self.image_path)
|
new_size = os.path.getsize(self.image_path)
|
||||||
@@ -326,9 +353,15 @@ class EditPopup(Popup):
|
|||||||
self.ids.top_toolbar.opacity = 1
|
self.ids.top_toolbar.opacity = 1
|
||||||
self.ids.right_sidebar.opacity = 1
|
self.ids.right_sidebar.opacity = 1
|
||||||
|
|
||||||
# Create and save metadata
|
# Create and save metadata. This runs in its own guarded
|
||||||
json_filename = self._save_metadata(edited_dir, new_name, base_name,
|
# block so that a failure here cannot silently stop the
|
||||||
new_version if version_match else 1, output_filename)
|
# upload — the two steps are intentionally decoupled.
|
||||||
|
json_filename = None
|
||||||
|
try:
|
||||||
|
json_filename = self._save_metadata(edited_dir, new_name, base_name,
|
||||||
|
new_version if version_match else 1, output_filename)
|
||||||
|
except Exception as meta_err:
|
||||||
|
Logger.error(f"EditPopup: Metadata save failed: {meta_err}")
|
||||||
|
|
||||||
# Upload to server in background (continues after popup closes)
|
# Upload to server in background (continues after popup closes)
|
||||||
upload_thread = threading.Thread(
|
upload_thread = threading.Thread(
|
||||||
@@ -414,6 +447,14 @@ class EditPopup(Popup):
|
|||||||
'version': version,
|
'version': version,
|
||||||
'user_card_data': self.user_card_data # Card data from reader (or None)
|
'user_card_data': self.user_card_data # Card data from reader (or None)
|
||||||
}
|
}
|
||||||
|
# Include the server-side file name and media id so the server can
|
||||||
|
# attach the edit to the correct media item.
|
||||||
|
if self.original_filename:
|
||||||
|
metadata['original_filename'] = self.original_filename
|
||||||
|
else:
|
||||||
|
metadata['original_filename'] = os.path.basename(self.image_path)
|
||||||
|
if self.media_id is not None:
|
||||||
|
metadata['media_id'] = self.media_id
|
||||||
|
|
||||||
# Save metadata JSON
|
# Save metadata JSON
|
||||||
json_filename = f"{new_name}_metadata.json"
|
json_filename = f"{new_name}_metadata.json"
|
||||||
@@ -444,16 +485,37 @@ class EditPopup(Popup):
|
|||||||
Logger.warning("EditPopup: Missing server URL or auth code (upload skipped)")
|
Logger.warning("EditPopup: Missing server URL or auth code (upload skipped)")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Load metadata from file
|
# Load metadata from file (or build it in memory if the metadata
|
||||||
with open(metadata_path, 'r') as meta_file:
|
# file was not written — the upload must still go through).
|
||||||
metadata = json.load(meta_file)
|
metadata = None
|
||||||
|
if metadata_path and os.path.exists(metadata_path):
|
||||||
|
try:
|
||||||
|
with open(metadata_path, 'r') as meta_file:
|
||||||
|
metadata = json.load(meta_file)
|
||||||
|
except Exception as e:
|
||||||
|
Logger.warning(f"EditPopup: Could not read metadata file: {e}")
|
||||||
|
if not metadata:
|
||||||
|
metadata = {
|
||||||
|
'time_of_modification': datetime.now().isoformat(),
|
||||||
|
'original_name': os.path.basename(image_path),
|
||||||
|
'new_name': os.path.basename(image_path),
|
||||||
|
'version': 1,
|
||||||
|
'user_card_data': self.user_card_data,
|
||||||
|
}
|
||||||
|
if self.original_filename:
|
||||||
|
metadata['original_filename'] = self.original_filename
|
||||||
|
if self.media_id is not None:
|
||||||
|
metadata['media_id'] = self.media_id
|
||||||
|
|
||||||
# Prepare upload URL - send to the original file endpoint
|
# Prepare upload URL - send to the original file endpoint
|
||||||
upload_url = f"{server_url}/api/player-edit-media"
|
upload_url = f"{server_url}/api/player-edit-media"
|
||||||
headers = {'Authorization': f'Bearer {auth_code}'}
|
headers = {'Authorization': f'Bearer {auth_code}'}
|
||||||
|
|
||||||
# Add the original filename to metadata so server knows which file was edited
|
# Ensure the original filename (server-side name) is present so the
|
||||||
metadata['original_filename'] = os.path.basename(metadata['original_path'])
|
# server knows which file was edited. Prefer the media context we
|
||||||
|
# captured when the edit popup opened.
|
||||||
|
if not metadata.get('original_filename'):
|
||||||
|
metadata['original_filename'] = os.path.basename(metadata.get('original_path', image_path))
|
||||||
|
|
||||||
# Disable SSL verification for self-signed certificates (like main code does)
|
# Disable SSL verification for self-signed certificates (like main code does)
|
||||||
# Note: This is NOT recommended for production with untrusted servers
|
# Note: This is NOT recommended for production with untrusted servers
|
||||||
|
|||||||
+12
-15
@@ -248,13 +248,11 @@ def download_media_files(playlist, media_dir, ssl_manager=None, server_url=None)
|
|||||||
# Web-link items have no file to download — pass the link through unchanged.
|
# Web-link items have no file to download — pass the link through unchanged.
|
||||||
if item_type == 'weblink':
|
if item_type == 'weblink':
|
||||||
logger.info(f"🔗 Web link item (no download): {file_url}")
|
logger.info(f"🔗 Web link item (no download): {file_url}")
|
||||||
updated_playlist.append({
|
# Preserve every server field (audio/muted/description/id/position/...)
|
||||||
'file_name': file_name,
|
# instead of rebuilding a fixed dict, so nothing is silently dropped.
|
||||||
'type': 'weblink',
|
weblink_item = dict(media)
|
||||||
'url': file_url, # keep the original web address (not a local path)
|
weblink_item['type'] = 'weblink'
|
||||||
'duration': duration,
|
updated_playlist.append(weblink_item)
|
||||||
'edit_on_player': False,
|
|
||||||
})
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
local_path = os.path.join(media_dir, file_name)
|
local_path = os.path.join(media_dir, file_name)
|
||||||
@@ -318,14 +316,13 @@ def download_media_files(playlist, media_dir, ssl_manager=None, server_url=None)
|
|||||||
# Don't skip - may still add to playlist
|
# Don't skip - may still add to playlist
|
||||||
|
|
||||||
# Always add the media item to the playlist, even if download failed
|
# Always add the media item to the playlist, even if download failed
|
||||||
# (it might already exist or be available later)
|
# (it might already exist or be available later).
|
||||||
updated_media = {
|
# Preserve EVERY server field (audio/muted/description/id/position/...)
|
||||||
'file_name': file_name,
|
# by copying the original dict and only overriding the URL with the
|
||||||
'type': item_type, # Preserve media type (image/video/...)
|
# local path — previously the fixed dict below dropped `audio`, `muted`,
|
||||||
'url': os.path.relpath(local_path, os.path.dirname(media_dir)),
|
# `description`, `id` and `position` from the saved playlist.
|
||||||
'duration': duration,
|
updated_media = dict(media)
|
||||||
'edit_on_player': media.get('edit_on_player', False) # Preserve edit_on_player flag
|
updated_media['url'] = os.path.relpath(local_path, os.path.dirname(media_dir))
|
||||||
}
|
|
||||||
updated_playlist.append(updated_media)
|
updated_playlist.append(updated_media)
|
||||||
|
|
||||||
return updated_playlist
|
return updated_playlist
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ The original app was built for **Raspberry Pi (Linux)**, using these technologie
|
|||||||
|-----------|---------------------|-------------------|
|
|-----------|---------------------|-------------------|
|
||||||
| **GUI** | Kivy 2.3+ | Kivy 2.3+ (works cross-platform) |
|
| **GUI** | Kivy 2.3+ | Kivy 2.3+ (works cross-platform) |
|
||||||
| **Video** | ffpyplayer | ffpyplayer (needs FFmpeg DLLs) |
|
| **Video** | ffpyplayer | ffpyplayer (needs FFmpeg DLLs) |
|
||||||
| **Card Reader** | evdev (Linux input) | **Not available** — gracefully disabled |
|
| **Card Reader** | evdev (Linux input) | ✅ Raw Input API + LL-hook fallback |
|
||||||
| **Screen Keep-Awake** | xset, xdotool, Wayland | `SetThreadExecutionState` (Win32 API) |
|
| **Screen Keep-Awake** | xset, xdotool, Wayland | `SetThreadExecutionState` (Win32 API) |
|
||||||
| **Weblink** | chromium-browser (kiosk) | Chrome/Edge (--kiosk mode) |
|
| **Weblink** | chromium-browser (kiosk) | Chrome/Edge (--kiosk mode) |
|
||||||
| **Audio** | ALSA/PulseAudio | DirectSound |
|
| **Audio** | ALSA/PulseAudio | DirectSound |
|
||||||
@@ -27,9 +27,9 @@ The original app was built for **Raspberry Pi (Linux)**, using these technologie
|
|||||||
- ✅ Web links (opens in Chrome/Edge kiosk)
|
- ✅ Web links (opens in Chrome/Edge kiosk)
|
||||||
- ✅ Network monitoring
|
- ✅ Network monitoring
|
||||||
- ✅ Auto-update playlist
|
- ✅ Auto-update playlist
|
||||||
|
- ✅ Card reader authentication (Raw Input API — see below)
|
||||||
|
|
||||||
### What is disabled on Windows
|
### What is disabled on Windows
|
||||||
- ❌ Card reader (evdev is Linux-only; `EVDEV_AVAILABLE = False`)
|
|
||||||
- ❌ HDMI power management (tvservice is RPi-specific)
|
- ❌ HDMI power management (tvservice is RPi-specific)
|
||||||
- ❌ WiFi restart (uses Linux `nmcli`)
|
- ❌ WiFi restart (uses Linux `nmcli`)
|
||||||
|
|
||||||
@@ -112,6 +112,50 @@ For a **single-file .exe**, edit `build.spec` — uncomment the `exe_onefile` se
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 💳 Card Reader (Windows Edition)
|
||||||
|
|
||||||
|
The card reader now works on Windows via the **Raw Input API** (with a
|
||||||
|
low-level keyboard-hook fallback). It replaces the Linux-only `evdev`
|
||||||
|
implementation automatically when `run_win.py` starts.
|
||||||
|
|
||||||
|
- Detection mirrors the Linux logic:
|
||||||
|
1. A device named with `card` / `reader` / `rfid`
|
||||||
|
2. A USB HID keyboard (non-PS/2) — most card readers enumerate this way
|
||||||
|
3. Any remaining keyboard (excluding touchscreens/mice)
|
||||||
|
- Only keystrokes from the **selected device** are captured, so the
|
||||||
|
operator's real keyboard cannot pollute card data.
|
||||||
|
- Card data ends on **Enter** (same as Linux).
|
||||||
|
|
||||||
|
### Card reader config (optional)
|
||||||
|
|
||||||
|
Add any of these to `config\app_config.json` next to the .exe:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"card_reader_mode": "auto", // "auto" | "raw" | "hook"
|
||||||
|
"card_reader_device": "", // e.g. "VID_08FF" to force a specific device
|
||||||
|
"card_reader_timeout": 5 // seconds
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `card_reader_mode`: `auto` (default, tries Raw Input then falls back),
|
||||||
|
`raw` (force Raw Input), or `hook` (force the low-level keyboard hook).
|
||||||
|
- `card_reader_device`: optional substring of the device name to pin the
|
||||||
|
reader (e.g. `VID_08FF`, `HID#VID_08FF`). Run the manual test below to see
|
||||||
|
the exact device names on your host.
|
||||||
|
- `card_reader_timeout`: how long the swipe popup waits (default 5 s).
|
||||||
|
|
||||||
|
### Manual card reader test (no GUI)
|
||||||
|
|
||||||
|
```batch
|
||||||
|
cd windows
|
||||||
|
venv\Scripts\activate
|
||||||
|
python win_card_reader.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Swipe a card within 10 seconds — the tool prints the captured data, then
|
||||||
|
exits. The detected devices are listed in the console/log.
|
||||||
|
|
||||||
## 🧪 Testing
|
## 🧪 Testing
|
||||||
|
|
||||||
```batch
|
```batch
|
||||||
|
|||||||
@@ -107,6 +107,38 @@ if %ERRORLEVEL% neq 0 (
|
|||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
REM ---- Optional code signing ----------------------------------------
|
||||||
|
REM For production PCs with Smart App Control ON, the exe MUST be signed
|
||||||
|
REM by a cert from a reputable public CA. If you have a .pfx, put its path
|
||||||
|
REM in the env var KIWY_SIGN_PFX (and optionally KIWY_SIGN_PFX_PASSWORD),
|
||||||
|
REM or drop a pfx named "kiwy_signing.pfx" in this folder. The build will
|
||||||
|
REM then auto-sign via sign_exe.ps1.
|
||||||
|
echo.
|
||||||
|
echo [STEP] Checking for code-signing certificate...
|
||||||
|
|
||||||
|
set "SIGN_PFX=%KIWY_SIGN_PFX%"
|
||||||
|
if not defined SIGN_PFX if exist "%~dp0kiwy_signing.pfx" set "SIGN_PFX=%~dp0kiwy_signing.pfx"
|
||||||
|
|
||||||
|
if defined SIGN_PFX (
|
||||||
|
echo [INFO ] Code-signing cert found: %SIGN_PFX%
|
||||||
|
if defined KIWY_SIGN_PFX_PASSWORD (
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0sign_exe.ps1" -CertPath "%SIGN_PFX%" -CertPassword "%KIWY_SIGN_PFX_PASSWORD%"
|
||||||
|
) else (
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0sign_exe.ps1" -CertPath "%SIGN_PFX%"
|
||||||
|
)
|
||||||
|
if not "!ERRORLEVEL!"=="0" (
|
||||||
|
echo [WARNING] Signing failed or signtool missing - the exe is NOT signed.
|
||||||
|
echo Smart App Control machines will still block it.
|
||||||
|
) else (
|
||||||
|
echo [OK] Executable signed successfully.
|
||||||
|
)
|
||||||
|
) else (
|
||||||
|
echo [INFO ] No signing cert found - skipping signing.
|
||||||
|
echo [INFO ] To sign automatically, set KIWY_SIGN_PFX to your .pfx path
|
||||||
|
echo or place "kiwy_signing.pfx" in this folder.
|
||||||
|
echo [INFO ] NOTE: Unsigned exe will be BLOCKED on PCs with Smart App Control ON.
|
||||||
|
)
|
||||||
|
|
||||||
REM ---- Success ----
|
REM ---- Success ----
|
||||||
echo.
|
echo.
|
||||||
echo ============================================
|
echo ============================================
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📅 Current Session — 2026-07-31
|
## 📅 Current Session — 2026-08-07
|
||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
|-------|-------|
|
|-------|-------|
|
||||||
@@ -14,9 +14,37 @@
|
|||||||
| **Python** | 3.12.9 — `windows\venv\` (250 MB, all deps installed) |
|
| **Python** | 3.12.9 — `windows\venv\` (250 MB, all deps installed) |
|
||||||
| **Kivy** | 2.3.1 |
|
| **Kivy** | 2.3.1 |
|
||||||
| **PyInstaller** | 6.21.0 |
|
| **PyInstaller** | 6.21.0 |
|
||||||
| **Last .exe build** | 2026-07-26 16:53 — `windows\dist\KiwySignagePlayer\KiwySignagePlayer.exe` (96 MB) |
|
| **Last .exe build** | 2026-08-07 08:23 — `windows\dist\KiwySignagePlayer\KiwySignagePlayer.exe` (98.8 MB) |
|
||||||
| **Build command** | `.\venv\Scripts\python.exe -m PyInstaller build.spec --clean --noconfirm` |
|
| **Build command** | `.\venv\Scripts\python.exe -m PyInstaller build.spec --clean --noconfirm` |
|
||||||
|
|
||||||
|
### Overnight soak test findings (2026-08-07 morning)
|
||||||
|
|
||||||
|
- **Symptom:** player froze on the last video widget (never advanced past
|
||||||
|
`video_loaded`); heartbeat stopped; 7 leaked `msedge.exe` processes left
|
||||||
|
running in the background.
|
||||||
|
- **Root cause (two compounding bugs in `run_win.py`):**
|
||||||
|
1. **Leaked browser + instant-exit handoff.** A previously leaked Chrome/Edge
|
||||||
|
process held the `.kiosk-profile` lock. The next weblink launch handed
|
||||||
|
the URL to that leaked instance and **exited in ~2s** (`07:04:57` launch →
|
||||||
|
`07:04:59 next_media_called`). The watchdog advanced instantly, so the
|
||||||
|
weblink never showed AND the leaked browser window was never killed →
|
||||||
|
`msedge.exe` processes accumulated overnight.
|
||||||
|
2. **Main-thread freeze.** The focus keeper ran heavy Win32 work
|
||||||
|
(`EnumWindows` + `AttachThreadInput` + `SetForegroundWindow` + `SendInput`)
|
||||||
|
synchronously on the Kivy thread every second. With leaked Edge windows
|
||||||
|
fighting back, this wedged the event loop → video never advanced, heartbeat
|
||||||
|
stopped (`08-07 07:09`).
|
||||||
|
- **Fix (in `run_win.py`, rebuilt 08:23):**
|
||||||
|
1. New `_windows_kill_browsers_on_profile()` — scans `chrome/msedge/chromium`
|
||||||
|
command lines (WMIC, PowerShell fallback), taskkills any browser holding
|
||||||
|
the `.kiosk-profile` lock. Called **before every weblink launch**.
|
||||||
|
2. Watchdog now has `MIN_ALIVE_BEFORE_EARLY_ADVANCE = 8s` — an instant
|
||||||
|
(~2s) handoff exit no longer advances/skips the weblink.
|
||||||
|
3. `_bring_kivy_to_front(async_ok=True)` runs the heavy Win32 bring-to-front
|
||||||
|
on a **background worker thread** guarded by a lock, so the Kivy main
|
||||||
|
thread is never blocked. Synchronous `async_ok=False` still available for
|
||||||
|
explicit transitions.
|
||||||
|
|
||||||
### 📋 Cross-platform audit — Linux commands → Windows handling
|
### 📋 Cross-platform audit — Linux commands → Windows handling
|
||||||
|
|
||||||
Every Linux-only command in `src/` was cross-referenced against the patches
|
Every Linux-only command in `src/` was cross-referenced against the patches
|
||||||
|
|||||||
@@ -10,6 +10,37 @@ import sys
|
|||||||
import platform
|
import platform
|
||||||
from pathlib import Path
|
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 ──
|
# ── IMPORTANT: Set Windows environment BEFORE any Kivy code runs ──
|
||||||
# This must happen before main.py's top-level code executes, because
|
# 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.
|
# main.py sets SDL_VIDEODRIVER=wayland,x11,dummy which would crash on Windows.
|
||||||
@@ -23,6 +54,8 @@ os.environ['FFPYPLAYER_CODECS'] = 'h264,h265,vp9,vp8'
|
|||||||
os.environ['SDL_VIDEO_ALLOW_SCREENSAVER'] = '0'
|
os.environ['SDL_VIDEO_ALLOW_SCREENSAVER'] = '0'
|
||||||
os.environ['KIVY_NO_FILELOG'] = '1'
|
os.environ['KIVY_NO_FILELOG'] = '1'
|
||||||
os.environ['KIVY_INPUTPROVIDERS'] = '' # Let Kivy auto-detect on Windows
|
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 ─────────────────────────
|
# ── Capture ALL early output to a crash log ─────────────────────────
|
||||||
# Ensure we catch any exception that happens before Logger is available.
|
# Ensure we catch any exception that happens before Logger is available.
|
||||||
|
|||||||
Reference in New Issue
Block a user