3ac7f836c4
Replaces the Windows port with a Raspberry Pi / Linux implementation on Raspberry Pi OS "Trixie" (Debian 13, aarch64, Wayland/labwc). The Windows code is removed here but preserved on the Windows-Player branch. Entry point ----------- linux/run_linux.py replaces windows/run_win.py. src/main.py stays platform-neutral; all Pi-specific behaviour is injected from linux/. Five bugs that prevented the port (all measured on real hardware) ---------------------------------------------------------------- 1. Kivy's PyPI wheel bundles an SDL2 built WITHOUT the wayland driver, so no window could be created (Trixie has no X server). linux/fix_kivy_sdl2.sh symlinks the system SDL2 over the bundled filename. 2. SDL2 requires WAYLAND_DISPLAY to be *set* - the socket alone is not enough, unlike wlopm. This broke every systemd/cron/autostart launch. linux_display.ensure_session_environment() detects and exports it. 3. Kivy's Clock resolves callbacks via func.__name__; a patch assigned under a different name crashed the player ~20s after a successful start. 4. The inherited signal_screen_activity() shelled out to tvservice, xdotool and ydotool - none exist on Trixie - and mis-escaped 'wlopm --on \*', so the display blanked after 10 minutes. 5. The launchers ran src/main.py directly, bypassing every platform patch and resolving the data directory one level too high. Web links --------- - --ozone-platform-hint=auto does NOT fall back to Wayland on Chromium 152; it aborts. The platform is now chosen explicitly. - The keyring password prompt is suppressed via the ENVIRONMENT, not the flags: launch_env() strips DBUS_SESSION_BUS_ADDRESS for the child so Chromium cannot reach gnome-keyring-daemon. - Teardown kills the whole process group (needs start_new_session=True); previously it silently fell back to terminate() and orphaned children. Video normalisation ------------------- A 4K video cannot play on a Pi 4: ffpyplayer decodes in software, measured at 0.90x realtime (1080p is 3.03x). Oversized media is downscaled to 1920x1080 at sync time using the hardware h264_v4l2m2m encoder (~31s for an 18s clip), triggered by resolution only so already-playable files are untouched. src/media_state.py owns the shared on-disk contract: a .kiwy-converting marker makes the player skip the item while it is being rebuilt, then the converted file is played instead. If nothing is playable at all (a single-item playlist whose only video is converting), the player loops the intro video rather than leaving a blank screen. Also fixed ---------- - network_monitor: replaced netsh/ifconfig/dhclient with nmcli (Trixie uses NetworkManager; ifconfig and dhclient are not even installed). - Removed the Windows-only focus keeper/guardian from main.py. - main.py: duplicate SDL_AUDIODRIVER setdefault (a silent no-op); Settings "Test connection" now uses tempfile.gettempdir(). - config/app_config.json: credentials blanked so a fresh clone runs the first-run setup flow. Verification ------------ linux/test_media_state.py 18/18, test_linux_patches.py 21/21, test_linux_browser_flags.py 27/27. Verified live against a real DigiServer: image -> weblink -> image -> video with correct durations, zero leaked Chromium processes, and no throttling over a 10 minute monitored run.
93 lines
2.7 KiB
Python
93 lines
2.7 KiB
Python
"""Ad-hoc weblink engine probe — drives the real LinuxChromiumAdapter.
|
|
|
|
Verifies the full launch → visible → teardown cycle outside the player, so
|
|
weblink behaviour can be tested without a server-provided playlist.
|
|
|
|
.venv/bin/python linux/_probe_weblink.py [url]
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, _HERE)
|
|
sys.path.insert(0, os.path.join(os.path.dirname(_HERE), 'src'))
|
|
|
|
from linux_display import ensure_session_environment # noqa: E402
|
|
|
|
ensure_session_environment()
|
|
|
|
URL = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
|
|
REQUIRED = os.environ.get('KIWY_REQUIRE_SUBSTRING', '')
|
|
|
|
from linux_browser import LinuxChromiumAdapter, find_linux_browser # noqa: E402
|
|
|
|
|
|
def chromium_count():
|
|
import subprocess
|
|
out = subprocess.run(['pgrep', '-c', 'chromium'], capture_output=True, text=True)
|
|
try:
|
|
return int(out.stdout.strip())
|
|
except ValueError:
|
|
return 0
|
|
|
|
|
|
def rss_mb():
|
|
import subprocess
|
|
out = subprocess.run(
|
|
['ps', '-o', 'rss=', '-C', 'chromium'], capture_output=True, text=True
|
|
)
|
|
total = sum(int(x) for x in out.stdout.split() if x.isdigit())
|
|
return total / 1024.0
|
|
|
|
|
|
def main():
|
|
print(f'URL: {URL}')
|
|
print(f'baseline chromium procs: {chromium_count()}')
|
|
|
|
adapter = LinuxChromiumAdapter(
|
|
browser_path=find_linux_browser(),
|
|
kiosk=True,
|
|
profile_dir=os.path.join(os.getcwd(), '.kiosk-profile-probe'),
|
|
)
|
|
|
|
print(f'flags: {adapter.extra_launch_args()}')
|
|
|
|
launched = adapter.launch(URL, 1920, 1080)
|
|
print(f'launch() -> {launched}')
|
|
if not launched:
|
|
print('FAIL: launch returned False')
|
|
return 1
|
|
|
|
visible, reason = adapter.wait_visible(15.0)
|
|
print(f'wait_visible() -> {visible} ({reason})')
|
|
|
|
time.sleep(6)
|
|
print(f'during: {chromium_count()} procs, {rss_mb():.0f} MB')
|
|
|
|
if REQUIRED:
|
|
# Fetch the same URL over HTTP and confirm it resolves, so a blank page
|
|
# can be attributed to rendering rather than to the network.
|
|
import urllib.request
|
|
try:
|
|
body = urllib.request.urlopen(URL, timeout=10).read(400_000).decode(
|
|
'utf-8', 'replace'
|
|
)
|
|
print(f'server reachable; page contains {REQUIRED!r}: {REQUIRED in body}')
|
|
except Exception as exc:
|
|
print(f'server fetch failed (network/host issue, not the engine): {exc}')
|
|
|
|
adapter.teardown()
|
|
time.sleep(3)
|
|
|
|
left = chromium_count()
|
|
print(f'after teardown: {left} chromium procs (0 = no leak)')
|
|
ok = visible and left == 0
|
|
print('RESULT:', 'PASS' if ok else 'FAIL')
|
|
return 0 if ok else 1
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|