Files
Anthias/bin/test_webview_cpp.sh
Viktor Petersson cc92a714e4 feat(viewer,webview): embed QtMultimedia in AnthiasWebview, eliminate two-process DRM contention + Pi 4 drops (#2905)
* feat(viewer,webview): embed QtMultimedia in AnthiasWebview, eliminate Pi 4 frame drops (#2904)

Move video playback inside AnthiasWebview's Qt 6 process via
QtMultimedia (QMediaPlayer + QGraphicsVideoItem). The libmpv
subprocess goes away — a single Qt process owns the eglfs/wayland
surface, so the two-process DRM-master contention #2885 documented
(600-2800 vo drops per 60 s clip on Pi 4) no longer applies. The
D-Bus contract on MainWindow (playVideo / stopVideo / videoEnded)
is preserved so Python still calls a stable interface even though
the playback engine swapped underneath.

Architecture

* src/anthias_webview/src/videoview.{cpp,h} — new VideoView wraps
  QMediaPlayer + QGraphicsVideoItem + QAudioOutput. Qt 6.5 dropped
  the upstream gstreamer media backend so Debian Trixie ships only
  the ffmpeg-backed libffmpegmediaplugin.so; decode runs through
  libavcodec against the +rpt1 libav* packages already pinned in
  docker/_rpt1-ffmpeg-pin.j2 (which carry --enable-v4l2-request /
  --enable-v4l2-m2m so rpi-hevc-dec, bcm2835-codec, Hantro G2,
  rkvdec all engage automatically).
* QGraphicsView + QGraphicsScene + QGraphicsVideoItem (not
  QVideoWidget) is the rendering substrate so video-rotate actually
  rotates the displayed frames — QGraphicsItem::setRotation is
  honoured by the painter, whereas QVideoWidget has no rotation
  property and a setProperty("rotation", angle) shortcut would
  store a dynamic value nothing reads.
* src/anthias_webview/src/view.cpp — adds playVideo / stopVideo
  surface-switching alongside loadPage / loadImage; loadImage skips
  hideVideoSurface() for the 'null' sentinel so a freshly-started
  video isn't torn down ~66 ms after the first PLAYING event by the
  view_image('null') call that follows media_player.play() in
  asset_loop.
* src/anthias_viewer/media_player.py — MPVMediaPlayer.play() routes
  through pydbus to the AnthiasWebview proxy. Per-codec hwdec
  dispatch + ffprobe codec sniff are gone; libavcodec auto-engages
  the right decoder. _marshal_dbus_options picks the GLib.Variant
  signature by Python type so int / bool / float options round-trip
  cleanly. video-rotate is sent as int.

Operational

* Pi 4 switches QT_QPA_PLATFORM from linuxfb to eglfs (QtMultimedia
  needs a GL context for the QGraphicsVideoItem painter).
  QT_QPA_EGLFS_KMS_CONFIG pins 1080p so V3D 6.0 doesn't have to
  composite Chromium + the video graphics view on top of the
  connector's native 4K. QT_SCALE_FACTOR=1 pins CSS-px to
  physical-px on the 1080p surface.
* tools/image_builder/utils.py — drops libmpv2 / mpv from the viewer
  image, adds libqt6multimedia6 / libqt6multimediawidgets6 /
  qt6-multimedia-dev / qt6-image-formats-plugins.
* /data/.anthias/playback-stats.log (renamed from mpv-stats.log) is
  capped at 8 MB; truncate on viewer start past the cap so a long-
  running 15 GB SD-card device can't fill up with 1 Hz SAMPLE rows.
* VideoView::resolveAlsaDevice extracts CARD=<name> from the ALSA
  spec and matches the QAudioDevice id on that segment; logs the
  resolved id at INFO so multi-HDMI Pi 4 / Pi 5 mismatches are
  visible from journalctl.

Validation

Real-device measurements via /data/.anthias/playback-stats.log on
the BBB pack (1080p / 4K, 30 / 60 fps, H.264 + HEVC), median across
multi-cycle plays in the PR comments. Pi 4 BBB 1080p60 H.264 dropped
from 2973 frames/min on the libmpv subprocess baseline to 0 with
QtMultimedia. 12 h mixed-media burn-in: zero crashes, zero early-
stops, no RSS leak across x86 / Pi 4 / Pi 5. 3 h asset-churn (120
toggles × 3 boards): zero <100 ms stops, drops stable. Rock Pi 4
arm64 image is built and identical to the validated set; the
testbed itself is SSH-unreliable so its end-to-end run is deferred.

C++ QtTest suite (8 cases) covers VideoView construction, stop
idempotency, empty / unknown audio device handling, and
QGraphicsItem::rotation() actually receiving the angle for cardinal
rotations and snapping non-cardinal angles to 0. Python suite
(63 cases) covers options-dict composition, D-Bus marshalling for
str / int / bool / float, settings reload, codec gate symmetry,
proxy reset, and VLC fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(viewer,webview): polish stale comments from second PR review

* MPVMediaPlayer.__init__ comment no longer says the C++ side owns
  a libmpv handle — it owns QMediaPlayer + QGraphicsVideoItem.
* Rename _build_mpv_options to _build_video_options. The function
  composes options for QtMultimedia now; the "mpv" in the name is
  vestigial. Class names (MPVMediaPlayer / MediaPlayerProxy) are
  left alone — those are the public D-Bus contract.
* LoadedMedia comment in videoview.cpp now reflects Qt 6's actual
  semantics: "metadata available, playback can start" — first
  decoded frame lands a hair later via videoFrameChanged. Starting
  the elapsed-ms clock here is still a few-ms approximation of
  "first frame on screen", which is the intent.
* _marshal_dbus_options return type tightened from bare ``dict`` to
  ``dict[str, Any]`` for symmetry with the input annotation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(tests): marshal test works with real PyGObject + tighten typing

CI ships PyGObject so ``gi.repository.GLib`` is the real module — the
prior test relied on conftest's MagicMock stub (which only kicks in
when ``gi`` is missing) to invoke ``assert_any_call`` on GLib.Variant.
On the real Variant class that's an AttributeError.

Patch ``gi.repository.GLib.Variant`` to a sentinel-returning callable
inside the test scope so the assertions work with either the stub
host or the real PyGObject host. The marshal still picks signatures
by Python type (``s`` / ``i`` / ``b`` / ``d``); the test now asserts
on the per-key tuple rather than the spy.

mypy errors:
* Narrow ``_last_play_options`` / ``_last_play_uri`` return values
  via ``isinstance`` so they don't fall through Any (no ``# type:
  ignore``, no ``cast``).
* Add ``gi`` / ``gi.*`` to the mypy-overrides ``ignore_missing_imports``
  set so the conftest stub doesn't break the type-check on hosts
  without PyGObject.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:38:16 +02:00

39 lines
1.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build and run AnthiasWebview's QtTest unit tests.
#
# Requires Qt 6 (qt6-base-dev, qt6-multimedia-dev). The viewer
# Docker image already ships those for the per-board builder stage
# — running this inside that container is the canonical
# environment.
#
# Usage: bin/test_webview_cpp.sh
#
# The tests run under ``QT_QPA_PLATFORM=offscreen`` so no real
# display server is needed; QtMultimedia's playback pipeline won't
# fully initialise (no rendering target) but the API surface plus
# the QGraphicsVideoItem rotation transform are testable that way.
# Decoder engagement + drop counts are exercised on real devices
# via the BBB test bed. CI integration is a follow-up.
set -euo pipefail
cd "$(dirname "$0")/.."
WEBVIEW_DIR="src/anthias_webview"
BUILD_DIR="${WEBVIEW_DIR}/tests/build"
mkdir -p "${BUILD_DIR}"
pushd "${BUILD_DIR}" >/dev/null
qmake6 ../tests.pro
make -j"$(nproc)"
# QTEST_MAIN's generated binary exits non-zero on any test failure.
# ``QT_QPA_PLATFORM=offscreen`` skips connecting to a real display
# server / framebuffer — the tests don't render, they exercise
# the QMediaPlayer / QGraphicsVideoItem API surface plus the
# rotation transform.
QT_QPA_PLATFORM=offscreen ./AnthiasWebviewTests
popd >/dev/null