_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.
Removes the tracked credential files. They are kept on disk, but they must not
be in the repository: player_auth.json holds a live auth_code, player_id and
server_url.
removed from tracking: player_auth.json, src/player_auth.json,
working_files/player_auth.json
Also fixes the ignore rules for the player's runtime state, which were being
reported as untracked and would have been committed:
- .player_heartbeat is rewritten every 10 seconds, so committing it creates
endless noise and merge conflicts on every machine.
- .player_stop_requested is transient session state.
- logs/startup_marker.txt and the various log outputs are runtime artifacts.
While doing this I found that the existing ignore entry was spelled
".player_heartbear" - a long-standing typo, so the heartbeat was never actually
ignored. The correct patterns are now present; the typo is left in place with a
comment so nothing changes unexpectedly for existing clones.
Note the deliberate exceptions, both tracked on purpose:
- windows/webview2_sdk/ - build.spec bundles those DLLs, so a fresh clone
needs them or web links silently fall back to the Chrome/Edge engine.
- windows/webview2_runtime/MicrosoftEdgeWebview2Setup.exe (~1.7 MB) - so a
machine without the WebView2 Runtime can install it on first start. The
~203 MB offline standalone installer stays ignored; fetch it with
webview2_runtime/download_runtime_installers.ps1 -Offline when building for
machines that have no internet.
The exe shipped config/app_config.json AND src/player_auth.json inside the
bundle, and because a frozen app runs with cwd = _internal/, the player loaded
that snapshot as its live auth state. A stale snapshot therefore made a fresh
build boot "already authenticated" against an old server and play an outdated
playlist. It also meant every install inherited the build machine's server_ip,
screen_name and auth_code.
Both files are now excluded from the bundle (app_config.json is no longer added
to datas, player_auth.json is excluded from Tree(src)), and the runtime hook no
longer copies a config into place on first run.
New behaviour, all in src/main.py so it applies to the Pi build too:
- The player starts with blank credentials. A missing file, an empty or
unparseable file, missing keys, and leftover placeholder values
(localhost, 127.0.0.1, kivy-player, 1234567) all count as UNCONFIGURED.
config_is_configured() is the single source of truth for that decision.
- After the splash video a notice appears ("Player is not configured"), and
after 5 seconds the Settings screen opens automatically so the operator can
enter the server details.
- Saving valid values writes config/app_config.json next to the .exe and
starts playback immediately - no restart needed.
- On a machine that IS configured, the notice and Settings are skipped and the
cached playlist plays straight away.
- Settings refuses to close while the three required fields are blank, so it
cannot be dismissed into a permanently blank screen with no way back.
- The 30s playlist timer does not fight the setup flow while unconfigured.
on_intro_finished() is the single decision point after the splash; both intro
paths (video end and "no intro file") go through it so they cannot drift apart.
Also loads config over DEFAULT_CONFIG rather than replacing it, so a partial or
older config file keeps working defaults instead of losing keys.
Note for future changes: when adding a new REQUIRED config key, add it to
CONFIG_REQUIRED_KEYS or the first-run flow will not ask for it.
Verified on the packaged exe by removing the config to simulate a fresh install:
setup_required_shown -> setup_opening_settings exactly 5s later ->
setup_completed, with the config written next to the exe and playback resuming.
Restarting with that config produced no setup events at all.
Covered by windows/test_first_run_setup.py.
Two independent failures were killing long unattended runs.
1. HANG at the end of a video (Windows AppHangB1)
The player froze after ~30-45 minutes of looping, always at a video item. The
playback trace stopped dead right after "video_loaded" with no "video_eos" and
no "advance_after_video_eos", and Windows logged AppHangB1 rather than a crash.
Cause, all inside Kivy and verified against the installed source:
1. ffpyplayer fires on_eos.
2. Kivy's Video widget binds its OWN handler first (kivy/uix/video.py
_do_video_load), and that handler sets state = 'stop' DURING the event
dispatch.
3. state = 'stop' -> VideoFFPy.stop() -> unload(), which calls
self._thread.join() with no timeout (the source even carries the comment
"TODO: use callback, don't block here").
4. When that decode thread is slow to exit, the Kivy/SDL main thread never
returns, so the window stops pumping messages.
It is a race, which is why it looked random and only appeared after many videos.
src/video_safety.py bounds that join. ffpyplayer has already been told to quit
and its thread woken before the join, so limiting the wait does not leak work;
it only stops an unresponsive thread from taking the whole player down. The
guard is installed before the Video widget is constructed, because the decode
thread is created during play().
The intro video had the same hazard on the main thread (state='stop' followed by
unload() inside the state callback) and is now torn down on a worker thread like
playlist videos.
2. CRASH in SDL2_mixer.dll (0xc0000005) on a video with no audio stream
Triggered when a silent 4K clip entered the playlist while the item was marked
audio: on. ffpyplayer initialises SDL2_mixer from the FIRST audio file it opens
and reuses those parameters, so a file with no audio stream (rate/channels 0)
makes SDL2_mixer dereference garbage. Muting via volume=0.0 does NOT avoid it -
the audio stream itself must be disabled.
play_video now probes the file with ffprobe and forces mute when it has no audio
track, so such a file can never reach ffpyplayer with sound enabled. The probe
fails safe (assumes audio present) if ffprobe is unavailable.
3. 24/7 supervision (solution A + C)
windows/watchdog.ps1 + start_player_watchdog.bat restart the player when it
crashes (process gone) or hangs (process alive but .player_heartbeat stale),
with a crash-loop breaker that backs off when it cannot stay up. This is the
Windows counterpart of the proven Linux start.sh watchdog.
The exit-screen password remains the only supported way to stop the player. On
success it writes .player_stop_requested next to the .exe and the watchdog
stands down instead of restarting. The flag is SESSION SCOPED: the watchdog
clears it on every start, so launching again begins a new session and there is
no file to delete by hand. Clearing on start also means a power cut cannot leave
the player permanently off.
Deliberately NOT done: a Windows service. A service runs in session 0 with no
desktop, so the player could not render to the screen at all. A login-triggered
startup entry is the correct Windows analogue of the Pi's systemd unit.
Important detail: the packaged player is TWO processes (PyInstaller bootloader
parent plus the child that owns the SDL window), so any kill uses taskkill /T or
the visible window survives and the next launch collides with it.
Verified in the packaged exe over a 7-hour run: 170 playlist restarts, 1365
items, 171 web links launched/visible/ended with zero failures, and no crashes,
no hangs and no leaked browser processes.
Tests: windows/test_video_hang.py and windows/test_watchdog.py. The hang test
deliberately holds the heartbeat open with an exclusive Windows lock (share mode
0) so the player's own write fails - backdating the file's mtime does NOT
simulate a hang, because the healthy player rewrites it immediately and the test
would then pass for the wrong reason.
Web links previously launched a separate Chrome/Edge kiosk process, which
caused the whole class of bugs in the tracker: the browser opening behind the
player, being handed off to an already-running instance and exiting instantly,
fighting for foreground/z-order, and leaking msedge.exe/chrome.exe processes
that were never closed.
WebView2 renders as a CHILD HWND of Kivy's own SDL window instead, so there is
no separate top-level browser to open behind the player, nothing to hand the
URL off to, no z-order contest, and no leaked browser process.
Windows/webview2_browser.py
- Environment -> controller -> navigate, driven through pythonnet.
- Async .NET Tasks are polled from Kivy's Clock. Calling GetAwaiter()
.GetResult() would deadlock: the continuation needs the same thread's
message pump.
- The controller is a .NET IntPtr, not a Python int (CreateAsync overloads
do not match otherwise).
- NavigationCompleted is tracked so a page that never loads can be told
apart from one that did. This matters on a closed network: an unreachable
host paints a Chromium error page, and without this the player would show
a blank/error screen for the item's whole slot instead of skipping it.
- is_alive() reports True while starting up. Start-up is async, so a
controller that does not exist yet is not a dead browser; treating it as
one made the first web link after a cold start be skipped instantly.
Windows/webview2_runtime.py
- Detects the Runtime (registry pv value, SDK probe as fallback) and
installs it silently when missing, unelevated, which produces a per-user
install and therefore never raises a UAC prompt on the signage display.
- Success is decided by RE-READING the installed version, not by the
installer exit code: Edge Update returns a non-zero HRESULT
(-2147219416) when the Runtime is already current, which is not a failure.
- On a closed network the online bootstrapper can never succeed, so it fails
fast with an actionable message instead of hanging for the full timeout.
- Failed attempts are cooldown-gated so a broken machine does not re-run an
installer on every start.
Offline hardening
- Browser arguments disable component updates, field trials, safe-browsing
list fetches, translate and other internet chatter. On an isolated LAN
each of those would otherwise have to time out, costing start-up latency.
Pages on the local server are unaffected.
Engine order (best first): WebView2 -> CEF -> Chrome/Edge subprocess. CEF has
no wheels past Python 3.9 so it is dormant on this build; the subprocess engine
remains only as a last resort.
Also fixes the reason the Windows adapters were never used at all:
SignagePlayer.__init__ assigned self.weblink_adapter_factory = None, which
shadowed the CLASS attribute that run_win.py injects. play_weblink() therefore
fell back to the generic adapter, whose find_browser() uses shutil.which() and
finds nothing on Windows because Chrome/Edge are not on PATH. The instance
attribute is now only set when the class attribute is absent.
Verified: windows/test_webview2_embed.py, test_webview2_navigation.py and
test_webview2_offline.py all pass (a locally served page renders with all
internet traffic disabled), and the packaged exe reports
"weblink_launch engine=webview2-embedded" -> "weblink_launched" on every cycle
with no leaked browser processes.
Document how to build, package and release the player so the knowledge is not
rediscovered on each machine or session.
New file: .github/instructions/kiwy-build-and-development.instructions.md
Contents:
- Entry points: src/main.py on Linux/Pi, windows/run_win.py on Windows (which
patches platform differences before running main.py).
- Verified toolchain: Python 3.12.9 x64 in windows\venv, Kivy 2.3.1,
PyInstaller 6.21.0. Python 3.13+/3.14 is unsupported for the Windows build
because Kivy 2.3.1 has no wheels for them.
- Build commands (pyinstaller build.spec, build_win.bat) and the real output
path, including the note that cefpython3 is not installed in this venv, so
the embedded-CEF engine is inactive and web links use the subprocess adapter.
- build.spec expectations: runtime hook for DPI awareness, console=True,
excludes, and the rule that lazily/dynamically imported modules must be listed
in hiddenimports or the packaged exe fails with ModuleNotFoundError.
- Code signing constraint: production hosts enforce Smart App Control, where an
unsigned (or self-signed) exe is blocked at kernel level, so a public CA cert
is required.
- Verification: py_compile gate, the need to close the running player before a
rebuild (output lock / "Access is denied"), the .exe reserved-device-name
pitfall that makes Test-Path report a false positive without -LiteralPath, and
the stale duplicate one-file exe at windows\dist\KiwySignagePlayer.exe that
must not be deployed.
- Release checklist and commit hygiene (do not track dist/build output or the
player_auth.json credential files).
- src/player_auth.json, player_auth.json: refreshed player credentials. The
untracked root copy is now added to version control.
- playlists/server_playlist.json: latest synced playlist state.
- windows/archive_list.txt, windows/build_last.txt: build output, committed by
request.
SECURITY: these files contain live player credentials (auth_code, player_id,
server_url). Anyone who can read the repository can use them. Consider rotating
the auth code and removing the credential files from version control.
- windows/win_card_reader.py: Windows-native card reader via the Raw Input API
with a low-level keyboard-hook fallback.
- windows/sign_exe.ps1: sign the built executable with a .pfx certificate.
- windows/create_self_signed_cert.ps1: generate a self-signed cert for local
testing (not trusted by Smart App Control).
- windows/verify_sendinput_fix.py: verification helper for the SendInput
foreground-unlock fix in run_win.py.
- documentation/CODE_SIGNING_SMART_APP_CONTROL.md: signing guidance.
- working_files/execute_playlist_retrieve.py,
working_files/raw_server_playlist.json: playlist retrieval diagnostics.
Note: windows/archive_list.txt and windows/build_last.txt are build output and
were committed by request rather than by convention.
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.
src/main.py:
- Delegate web-link playback to WeblinkSession (get_weblink_session), with
on_finished / on_failed callbacks driving the transition.
- Remove the in-file Chromium subprocess launching, the /dev/input watchdog and
the pre-warm implementation; keep thin deprecated shims for platform code.
- Add _item_is_weblink(), which also accepts type aliases and the
"no file extension + http(s) url" shape so a slightly different server
payload is not treated as a missing media file.
- Replace the 1.0s wall-clock advance guard with a generation token
(next_media's _token plus _schedule_advance). The old window silently dropped
deliberate fast transitions such as weblink -> weblink; the token still
discards stale/duplicated callbacks.
- toggle_pause is now a no-op while a web link is active: a web link is an
interactive surface, so pause/play does not apply to it and can no longer cut
a viewer's session short. Pause still works for images and videos.
- Preload/prewarm the next item through the session.
- _get_browser_target_size uses the real window size instead of hardcoding a
1920x1080 fallback.
windows/run_win.py:
- Replace the Windows play_weblink override, watchdog, kill_weblink_after_frame,
play_current_media wrapper and prewarm override with adapter injection via
weblink_adapter_factory.
- _WinCefAdapter: embedded CEF, preferred (no subprocess, no z-order fights).
Binds the Kivy resize handler once instead of rebinding a new closure every
weblink cycle, which grew the callback list without bound.
- _WinChromeAdapter: Chrome/Edge subprocess with a real HWND visibility check,
so a hand-off or a page that never paints is detected instead of leaving a
black screen. Teardown keeps the required order (hide overlay, then raise
Kivy) to avoid handing foreground to Explorer.
- Delete the now-dead _hide_overlay_when_chrome_ready and
_bring_chrome_to_front. The former leaked a Clock.schedule_interval on every
weblink cycle.
Web links were implemented three times (main.py subprocess, run_win.py
subprocess + Win32 overlay, cef_browser.py embedded CEF), each owning its own
process handle, watchdog and teardown. That ambiguity caused leaked browsers,
skipped items, lost foreground and blank screens when a page failed to load.
Replace all three with a single owner in src/weblink_session.py:
- WeblinkSession: validate -> launch -> verify -> watch -> teardown.
Generation-tokened so stale callbacks are ignored, idempotent close(),
atexit-safe, never more than one browser alive.
- WeblinkAdapter: the only platform-specific part (launch / wait_visible /
is_alive / teardown / prewarm). Platform layers inject engines through
SignagePlayer.weblink_adapter_factory.
- ChromiumSubprocessAdapter: default engine (Pi chromium, Windows chrome/msedge).
- InteractionWatcher: decides when an item is finished.
- WebInputSources: /dev/input/event* (Linux) plus a GetCursorPos pointer tap
(needed on Windows for embedded CEF, which has no child process).
Interaction model: web links are an interactive surface, not timed media.
The player advances only when the configured duration has elapsed AND the
viewer has not interacted for 10s, measured from the most recent interaction.
A touch in the final seconds of a slot therefore pushes the advance 10s past
that touch, and each further touch pushes it again, so a page is never pulled
out from under someone using it. An untouched page still advances on schedule.
A drag burst counts as one interaction but the countdown tracks its last event,
so an item cannot be cut off mid-gesture. max_dwell (duration x factor, floored
by min_max_dwell) is an absolute backstop against a wedged browser or a jammed
touchscreen.
Verified start-up: the visibility wait runs on the watcher thread, never on
Kivy's main thread. If the browser window never appears the item is reported
failed and skipped, instead of resetting the error counter and leaving a black
screen up for the whole duration.
Config: new "weblink" block in config/app_config.json (engine,
interaction_postpone, interaction_debounce, interaction_grace,
max_dwell_factor, min_max_dwell, launch_timeout, prewarm) with safe defaults,
so an absent block still works.
Also add weblink_session to the PyInstaller hiddenimports so the frozen exe
bundles the new module.
- Added kivy.lib.gstplayer to excluded_imports (we use ffpyplayer)
- Added all cefpython3_py{27,34-311}.pyd to excluded_imports
- Removes ~300 lines of library-not-found warnings from build output
- Reduces .exe size from 143 MB to 96 MB
v1 created a separate Win32 window — same problem as external Chrome.
v2 creates CEF as a CHILD WINDOW of Kivy's SDL_app window:
- No separate taskbar entry
- No z-order fighting (CEF is INSIDE Kivy)
- No desktop flash
- CEF message loop pumped via Kivy Clock (main thread)
- Resize handler attached so CEF follows Kivy window changes
- build.spec includes cef_browser in hidden imports
- delete_unused_media: normalize Windows backslashes to forward slashes
when comparing with playlist file_name (fixes file deleted after download)
- update_playlist_if_needed: download media even when playlist version matches
- _on_video_eos: now schedules next_media() — was an empty stub
BUG-009: _on_video_eos was empty (stub). Added Clock.schedule_once
for next_media() when video reaches end of stream.
BUG-008: download_media_files only ran when server_version >
local_version. Added download check in up-to-date branch so media
files are synced even when playlist version hasn't changed.
Windows-specific fixes:
- _windows_play_weblink: uses --start-maximized + --app=URL for true fullscreen
- Shows black Win32 overlay before opening/closing Chrome to mask desktop
- _windows_kill_process_tree: uses taskkill /F /T to kill all Chrome child processes
- _Win32Overlay class: fullscreen borderless always-on-top black window
- Updated README to note Python 3.12 requirement and local data dir behavior
- Added empty playlist guard in play_current_media() to return
early instead of calling restart_playlist()
- Added empty playlist guard in restart_playlist() to return
early instead of calling play_current_media()
- Wrapped SettingsPopup content in ScrollView so fields
are not cut off on smaller screens
- 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
- Rebuilt evdev wheel for cp313 aarch64 (was missing from repo)
- Updated install.sh: add --system-site-packages to venv, verify imports
- Updated start.sh: activate .venv before running player
- Updated run_player.sh: activate .venv before running player
- Added pip fallback for kivy/ffpyplayer if apt/pip install misses them
- Replaced all cp311 wheels with cp313 wheels for Python 3.13 compatibility
- Added new dependency wheels: aiohappyeyeballs, evdev, ffpyplayer
- Rebuilt install.sh with virtual environment + offline wheel installation
- Added --sudo-user/-U and --sudo-password/-W CLI arguments
- Added run_sudo() helper to avoid password prompts during installation
- Fixed 'Setting Up Player Directories' hang (cat->stdin) by using touch
- Updated start.sh to activate virtual environment before running player
- Updated run_player.sh to activate virtual environment
- Removed old cp311 incompatible wheels
- Added backup of previous install.sh as install.sh.bak
- Fix: Replace broad pkill with PID-specific kill -9 in start.sh
- Fix: Remove unreachable deactivate call in start.sh
- Fix: Add find_boot_config helper for RPi config file detection
- Fix: Update autostart status message (remove fake systemd service claim)
- Fix: Add ACTUAL_USER/ACTUAL_HOME to power management function
- Add: Auto-reboot at end of install.sh with 5s cancel window
- Add: Ctrl+C cleanup trap during installation
- Add Port field to Settings UI (was hidden in config, not editable)
- Fix double-port bug: skip appending :port if server_ip already contains one
- Weblink: replace --kiosk with --start-fullscreen (fixes Wayland/Labwc
window restoration after Chromium closes)
- Weblink: inactivity watchdog - advance to next media only after N seconds
of no touch (instead of fixed timer)
- Weblink: detect Chromium process exit and advance immediately
- Weblink: pre-warm Chromium in hidden off-screen window during previous
media item to eliminate cold-start delay on transition
- Weblink: Wayland-safe hide/show transition - hide content_area before
Chromium, 200ms delay after kill before showing next media
- Chromium flags: suppress GNOME keyring popup (--password-store=basic)
- Suppress --disable-sync, --disable-background-networking dialogs
- get_playlists_v2: pass through weblink items without download, preserve type
- main.py: render weblink items fullscreen via Chromium kiosk overlay with cleanup on next/pause/stop
- main.py: fix unbounded recursion when playlist media is missing/invalid by scheduling retries via Clock (keeps standard playlist cycling offline)
- docs: add PLAYER_WEBLINK_INTEGRATION.md
- Added configure_display_resolution() function to force 1920x1080 output
- Supports three methods: xrandr (X11), tvservice (RPi native), /boot/config.txt (persistent)
- Ensures player displays Full HD on screens larger than 1920x1080
- Configuration runs automatically on each startup
- Removed 6 duplicate cron entries that were spawning multiple instances
- Enabled systemd kiwy-player.service for proper startup management
- Service is now the single source of truth for app startup
- App runs stably with proper DISPLAY environment from systemd
- Fixed start.sh environment variable loading from systemctl
- Use here-document (<<<) instead of pipe for subshell to preserve exports
- Added better error handling for evdev device enumeration
- Added exception handling in intro video playback with detailed logging
- App now properly initializes with DISPLAY=:0 and WAYLAND_DISPLAY=wayland-0
Issue: Player crashes after 2-3 minutes during editing
Hypothesis: The background upload thread may be interfering with playback
even with the Clock.schedule_once fix.
Action: Temporarily disable the background upload to see if this is
the root cause of the crashes.
Edits will still be saved locally, just not uploaded to server during
this diagnostic test.
If player is stable without this thread, the issue is in the upload
logic or thread management.
BUG: Background upload thread was crashing the app
Problem:
- _upload_to_server() runs in daemon thread
- Was directly setting self.player.should_refresh_playlist from thread
- Kivy is NOT thread-safe for direct state modifications
- App crashed after 2-3 minutes during editing
Root cause:
Line 503: self.player.should_refresh_playlist = True
This directly modified Kivy object state from non-main thread
Caused race conditions and memory corruption
Solution:
- Use Clock.schedule_once() to schedule flag update on main thread
- Ensures all Kivy state modifications happen on main thread
- Thread-safe and proper Kivy API usage
- No more app crashes during editing
This was causing the app to crash every 10-20 seconds during edits.
Should now be stable!
CRITICAL FIX - This was the main issue preventing edited images from appearing!
Problem:
- Edited media was being uploaded to server successfully
- Server updated the playlist (new version returned: 34)
- BUT player never reloaded the playlist
- So edited images stayed invisible until restart
Solution:
1. EditPopup now sets should_refresh_playlist flag when upload succeeds
2. Main player checks this flag in check_playlist_and_play()
3. When flag is set, player immediately reloads playlist
4. Edited media appears instantly without needing restart
Testing:
- Created diagnostic script test_edited_media_upload.py
- Confirmed server accepts edited media and returns new playlist version
- Verified SSL fix works correctly (verify=False)
Now edited images should appear immediately after save!
Root cause identified from logs:
[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate
The server uses a self-signed certificate (like production), but the edited media
upload endpoint was not disabling SSL verification while other API calls do.
Solution:
- Add verify=False to requests.post() call in _upload_to_server()
- Matches the SSL verification handling in get_playlists_v2.py
- Add warning about SSL verification being disabled
- Now edited images can upload successfully to server
This fixes the upload failures that were preventing edited images from being
synced to the server.
Critical fixes for image editing workflow:
1. Keep local edited files as backup (don't delete after server upload)
- Server may not process upload immediately
- Keeps edits safe locally in case server fails
- Prevents loss of edited images
2. Include original filename in metadata sent to server
- Server needs to know which file was edited
- Allows proper tracking and versioning
3. Improved error logging for server upload
- Now logs detailed errors (404, 401, timeout, connection)
- Shows clear messages when server doesn't support endpoint
- Helps diagnose why edits aren't syncing to server
4. Better user feedback during save
- Shows 'Saved to device' status first
- Then 'Upload in progress' to show server sync happening
- Clarifies local vs server save status
Bug symptoms fixed:
- Edited images now persist locally after restart
- Server upload now sends correct file information
- Clear error messages if server upload fails
- User understands 'local save' vs 'server sync' steps
- Commented out forced pygame backend (causes issues with display initialization)
- Added SDL_VIDEODRIVER and SDL_AUDIODRIVER fallback chains (wayland,x11,dummy)
- Limited KIVY_INPUTPROVIDERS to wayland,x11 (avoids problematic input providers)
- Reduced FFMPEG_THREADS from 4 to 2 (conserves Raspberry Pi resources)
- Reduced LIBPLAYER_BUFFER from 2MB to 1MB (saves memory)
- Fixed asyncio event loop deprecation warning (use try/except for get_running_loop)
- Better exception handling for cursor hiding
These changes fix the app crashing after 30 seconds due to graphics provider issues.
- Replaced failing systemd user service with system-wide service
- System service more reliable on Wayland/Bookworm systems
- Service creates /etc/systemd/system/kiwy-player.service
- Runs as pi user with proper display environment variables
- Adds Restart=on-failure for robustness
- Keeps XDG and cron methods as additional fallback layers
This resolves autostart failures after recent system updates.
Added support for both X11 and Wayland environments:
Display Server Detection:
- Auto-detects Wayland via WAYLAND_DISPLAY environment variable
- Falls back to X11 commands if not Wayland
- Works seamlessly on both display servers
Wayland-specific tools:
- wlopm - Wayland output power management (keeps display on)
- wlr-randr - Output management for wlroots compositors
- ydotool - Mouse movement for Wayland (alternative to xdotool)
- systemd-inhibit integration for idle prevention
Enhanced display keep-alive script:
- Detects display server type on startup
- Uses appropriate commands based on environment
- Wayland: wlopm, wlr-randr, ydotool
- X11: xset, xdotool, xrandr
- Both: tvservice for HDMI power control
App-level improvements (main.py):
- Detects Wayland via os.environ check
- Executes Wayland-specific commands when detected
- Maintains X11 compatibility for older systems
Installation improvements:
- Auto-installs Wayland tools if Wayland is detected
- Attempts to install: wlopm, wlr-randr, ydotool
- Graceful fallback if packages unavailable
This ensures HDMI power management works correctly on:
- Raspberry Pi OS with X11 (older versions)
- Raspberry Pi OS with Wayland (Bookworm and newer)
- Any Linux system using either display server
- Added signal_screen_activity() method to SignagePlayer class
- Runs every 20 seconds automatically
- Also triggered on any touch/user input events
Multiple methods used to keep display awake:
- xset s reset - Resets screensaver timer
- xset dpms force on - Forces display on
- xdotool - Subtle mouse movement to trigger activity
This complements the system-level power management:
- Works alongside display power management settings
- Non-blocking and non-critical (fails gracefully)
- Signals every 20 seconds + on user input
- Prevents display from sleeping during playback
Screen should now remain active throughout media playback.
- Created .keep-screen-alive.sh wrapper script with multiple methods:
* systemd-inhibit (primary - prevents OS-level sleep/suspend)
* xset commands (prevents X11 screensaver)
* Mouse movement (prevents idle timeout)
- Added screen-keepalive.service systemd unit:
* Runs xset s reset every 30 seconds
* Auto-restarts on failure
* Integrated with graphical session
- Multiple layers of screen protection:
* HDMI blanking disabled
* CPU power saving disabled
* System sleep/suspend disabled
* X11 screensaver disabled
* DPMS (Display Power Management) disabled
* Display forced on periodically
Screen will now remain active while player is running, preventing lockups or blank screens during playback.
- Enhanced install.sh with comprehensive autostart workflow:
* XDG autostart entry (desktop environment)
* systemd user service (most reliable)
* LXDE autostart support (Raspberry Pi OS)
* Cron fallback (@reboot)
* Terminal mode enabled for debugging
- Added Raspberry Pi power management features:
* Disable HDMI screen blanking
* Prevent CPU power saving (performance mode)
* Disable system sleep/suspend
* X11 screensaver disabled
* Display power management (DPMS) disabled
- Fixed sudo compatibility:
* Properly detects actual user when run with sudo
* Correct file ownership for user configs
* systemctl --user works correctly
- Player launches in terminal for error visibility
- Autostart configured to use start.sh (watchdog with auto-restart)
- Modified ssl_utils.py to treat 404 errors as expected when server doesn't have /api/certificate endpoint
- Changed verify_ssl setting to false in app_config.json to allow HTTPS connections without certificate verification
- This allows the player to connect to servers that don't implement the certificate endpoint