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:
ske087
2026-09-13 21:57:49 +03:00
parent f437aba1fc
commit 3ac7f836c4
68 changed files with 5604 additions and 9326 deletions
+70
View File
@@ -0,0 +1,70 @@
"""Ad-hoc video playback probe (not part of the test suite).
Plays the intro video in a Kivy window and reports whether it decodes and
advances. Used to verify the ffpyplayer path on Raspberry Pi OS Trixie.
.venv/bin/python linux/_probe_video.py [path/to/video.mp4]
"""
import os
import sys
import time
# Mirror the real entry point: SDL2 does NOT discover the Wayland socket on its
# own, so WAYLAND_DISPLAY must be filled in first. Without this the probe falls
# back to x11 and dies with "Couldn't connect to X server".
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from linux_display import ensure_session_environment # noqa: E402
ensure_session_environment()
os.environ.setdefault('SDL_VIDEODRIVER', 'wayland,x11,dummy')
os.environ.setdefault('KIVY_GL_BACKEND', 'gl')
os.environ.setdefault('KIVY_VIDEO', 'ffpyplayer')
os.environ.setdefault('KIVY_AUDIO', 'ffpyplayer')
from kivy.config import Config # noqa: E402
Config.set('graphics', 'window_state', 'hidden')
Config.set('graphics', 'fullscreen', '0')
from kivy.app import App # noqa: E402
from kivy.clock import Clock # noqa: E402
from kivy.uix.video import Video # noqa: E402
SOURCE = sys.argv[1] if len(sys.argv) > 1 else 'config/resources/intro1.mp4'
DURATION = 12.0
results = {}
class Probe(App):
def build(self):
self.video = Video(
source=SOURCE, state='play', options={'eos': 'stop'},
allow_stretch=True, keep_ratio=True,
)
self.video.bind(on_eos=lambda *a: results.setdefault('eos', True))
return self.video
def on_start(self):
self.t0 = time.monotonic()
Clock.schedule_interval(self.tick, 1.5)
Clock.schedule_once(lambda dt: self.stop(), DURATION)
def tick(self, dt):
elapsed = time.monotonic() - self.t0
core = self.video._video
position = getattr(core, 'position', None) if core else None
if position is not None:
results['last_position'] = position
print(
f' t={elapsed:5.1f}s state={self.video.state} '
f'duration={self.video.duration:.1f} position={position} '
f'texture={"yes" if self.video.texture else "no"}',
flush=True,
)
Probe().run()
print(f'RESULT duration={results.get("duration")} '
f'last_position={results.get("last_position")} eos={results.get("eos")}')