Port the player to Raspberry Pi OS Trixie 64-bit (Linux-only branch)
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.
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
"""_probe_chromium_footprint.py — measure Chromium's footprint for one page.
|
||||
|
||||
Compares the flag profiles so the memory trade-off is measured rather than
|
||||
guessed. Run:
|
||||
|
||||
.venv/bin/python linux/_probe_chromium_footprint.py [url]
|
||||
|
||||
Reports process count and total resident memory for the browser tree, using the
|
||||
same flags the player uses (so the numbers match production).
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from linux_display import ensure_session_environment # noqa: E402
|
||||
|
||||
ensure_session_environment()
|
||||
|
||||
import linux_browser # noqa: E402
|
||||
|
||||
URL = sys.argv[1] if len(sys.argv) > 1 else 'about:blank'
|
||||
|
||||
|
||||
def tree_memory(root_pid):
|
||||
"""(PSS kB, process count) for root_pid and its descendants.
|
||||
|
||||
PSS (proportional set size) from ``/proc/<pid>/smaps_rollup`` is the right
|
||||
measure here, **not** RSS. Chromium forks many processes that share the same
|
||||
libraries and file-backed pages; summing RSS counts every shared page once
|
||||
per process, which inflated earlier measurements by roughly 2x and even made
|
||||
a smaller configuration look larger.
|
||||
"""
|
||||
try:
|
||||
out = subprocess.run(
|
||||
['ps', '-eo', 'pid,ppid'], capture_output=True, text=True,
|
||||
timeout=10, check=False,
|
||||
).stdout
|
||||
except Exception:
|
||||
return 0, 0
|
||||
|
||||
parents = {}
|
||||
for line in out.splitlines()[1:]:
|
||||
parts = line.split()
|
||||
if len(parts) >= 2 and parts[0].isdigit():
|
||||
parents[int(parts[0])] = int(parts[1])
|
||||
|
||||
family = [root_pid]
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for pid, parent in parents.items():
|
||||
if parent in family and pid not in family:
|
||||
family.append(pid)
|
||||
changed = True
|
||||
|
||||
total_kb = 0
|
||||
for pid in family:
|
||||
try:
|
||||
with open(f'/proc/{pid}/smaps_rollup') as fh:
|
||||
for line in fh:
|
||||
if line.startswith('Pss:'):
|
||||
total_kb += int(line.split()[1])
|
||||
break
|
||||
except OSError:
|
||||
# Process exited between listing and reading.
|
||||
continue
|
||||
return total_kb, len(family)
|
||||
|
||||
|
||||
def measure(mode):
|
||||
os.environ['KIWY_CHROMIUM_MODE'] = mode
|
||||
profile = f'/tmp/kiwy-footprint-{mode}'
|
||||
subprocess.run(['rm', '-rf', profile], check=False)
|
||||
|
||||
adapter = linux_browser.LinuxChromiumAdapter(
|
||||
browser_path=linux_browser.find_linux_browser(), kiosk=True,
|
||||
)
|
||||
adapter._profile_dir = profile
|
||||
|
||||
ok = adapter.launch(URL, 1280, 720)
|
||||
if not ok or adapter._proc is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Let Chromium finish spawning helpers before sampling.
|
||||
time.sleep(8)
|
||||
if adapter._proc.poll() is not None:
|
||||
return {'crashed': True, 'rc': adapter._proc.returncode}
|
||||
pss_kb, procs = tree_memory(adapter._proc.pid)
|
||||
return {"pss_mb": pss_kb / 1024.0, "procs": procs}
|
||||
finally:
|
||||
adapter.teardown()
|
||||
time.sleep(1.5)
|
||||
|
||||
|
||||
print(f'url = {URL}\n')
|
||||
print(f'{"mode":<10} {"procs":>6} {"PSS (MB)":>10}')
|
||||
print('-' * 30)
|
||||
|
||||
results = {}
|
||||
for mode in ('safe', 'light', 'minimal'):
|
||||
result = measure(mode)
|
||||
results[mode] = result
|
||||
if result is None:
|
||||
note = 'launch failed'
|
||||
elif result.get('crashed'):
|
||||
note = f'CRASHED rc={result["rc"]}'
|
||||
else:
|
||||
note = ''
|
||||
if note:
|
||||
print(f'{mode:<10} {"-":>6} {note:>10}')
|
||||
else:
|
||||
print(f'{mode:<10} {result["procs"]:>6} {result["pss_mb"]:>10.0f}')
|
||||
|
||||
print()
|
||||
base = results.get('safe', {}) or {}
|
||||
light = results.get('light', {}) or {}
|
||||
if base.get('pss_mb') and light.get('pss_mb'):
|
||||
saved = base['pss_mb'] - light['pss_mb']
|
||||
pct = 100.0 * saved / base['pss_mb']
|
||||
print(f'light mode saves {saved:.0f} MB ({pct:.0f}%) vs the default flag set')
|
||||
print(f' processes: {base["procs"]} -> {light["procs"]}')
|
||||
Reference in New Issue
Block a user