Skip to content

Commit 6b7910d

Browse files
committed
refactor(viewer): swap libmpv embed for QtMultimedia + GStreamer
The libmpv-embedded path engaged HW decode correctly on all 4 Qt6 boards but didn't move Pi 4 frame drops below the subprocess-mpv baseline (562–2973 drops/60 s, same range as PR #2885). Real-device verbose mpv logging confirmed the decoders engaged; the bottleneck was V3D 6.0 fillrate through libmpv-render → QOpenGLWidget FBO → Qt-compositor → eglfs swap. Skipping the FBO indirection by porting to QOpenGLWindow crashed under eglfs's single-native-window-per- process limit (reverted at f057198). QtMultimedia + gstreamer is the next try: - ``VideoView`` rewritten around QMediaPlayer + QVideoWidget + QAudioOutput. QVideoWidget paints inside MainWindow's existing eglfs native window, so we don't trip the single-window restriction. Stats logger keeps the same /data/.anthias/mpv-stats.log schema (INIT / LOADFILE / PLAYING / SAMPLE / END_FILE) with a drop estimate computed from container_fps × elapsed − frames- delivered (QVideoSink::videoFrameChanged counter). - ``QT_MEDIA_BACKEND=gstreamer`` is set in the viewer Dockerfile so Qt picks the gstreamer backend over its ffmpeg one — the rpi ``v4l2slh264dec`` / ``v4l2slh265dec`` elements (in rpt3 ``gstreamer1.0-plugins-bad``, confirmed via ``dpkg-deb -c`` on the .deb) route directly to QVideoSink. - ``docker/_rpt1-ffmpeg-pin.j2`` extends the rpi-archive pin to ``gstreamer1.0-*`` + ``libgstreamer*`` so plugins-bad wins from rpt3 over stock Debian (priority bump 100 → 1001). - ``viewer_extra_apt_dependencies`` swaps libmpv2 for the gstreamer1.0-{alsa,libav,plugins-{base,good,bad,ugly}} + libqt6multimedia6 + libqt6multimediawidgets6 + qt6-multimedia-dev set. - ``MPVMediaPlayer`` Python options shrink to audio-device + video-rotate (Pi 4 only). Removed: hwdec, video-sync, vd-lavc-threads, the _PI_HWDEC_BY_CODEC table, _probe_video_codec, _pi_hwdec_for_uri. Codec dispatch is now gstreamer's job. - Python tests drop the ~12 per-codec / ffprobe-dispatch tests; C++ tests drop the mpv_get_property_string round-trip in favour of QMediaPlayer construction + option-passthrough assertions. Codec-gate symmetry test replaced with "gate codecs ⊆ {h264, hevc}" — broader, catches a relaxation of the upload gate at the same time. Pi 4 perf gain is unverified — Pi 4 testbed went offline mid-session (along with Pi 5 and Rock Pi 4). Real-device validation pending.
1 parent f057198 commit 6b7910d

10 files changed

Lines changed: 533 additions & 887 deletions

File tree

docker/Dockerfile.viewer.j2

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,14 @@ RUN --mount=type=cache,target=/var/cache/apt,id=qt6-builder-apt-{{ artifact_boar
1717
apt-get update && \
1818
apt-get install -y --no-install-recommends \
1919
build-essential \
20-
libmpv-dev \
21-
pkg-config \
2220
qt6-base-dev \
21+
qt6-multimedia-dev \
2322
qt6-webengine-dev
24-
# libmpv-dev + pkg-config: AnthiasWebview.pro pulls libmpv via
25-
# ``PKGCONFIG += mpv`` so the in-tree VideoView can call into
26-
# libmpv via mpv_render_context (issue #2904). Debian Trixie ships
27-
# libmpv2 in main; the dev package is builder-only, the runtime
28-
# .so is added to viewer_extra_apt_dependencies below.
23+
# qt6-multimedia-dev: AnthiasWebview's VideoView wraps QMediaPlayer +
24+
# QVideoWidget (issue #2904, see ``src/anthias_webview/AnthiasWebview.pro``).
25+
# Runtime backend is gstreamer (forced via ``QT_MEDIA_BACKEND`` env
26+
# below); plugin discovery happens at runtime against the
27+
# ``gstreamer1.0-*`` packages added to ``viewer_extra_apt_dependencies``.
2928
COPY src/anthias_webview/AnthiasWebview.pro /src/anthias_webview/AnthiasWebview.pro
3029
COPY src/anthias_webview/src /src/anthias_webview/src
3130
COPY src/anthias_webview/res /src/anthias_webview/res
@@ -120,6 +119,18 @@ ENV QT_QPA_PLATFORM=linuxfb
120119
ENV QT_LOGGING_RULES=*.debug=true
121120
ENV QT_QPA_DEBUG=1
122121

122+
{% if is_qt6 %}
123+
# Force QtMultimedia's gstreamer backend (issue #2904). Qt 6.8's
124+
# default backend selection on Debian Trixie probes ffmpeg first,
125+
# but ffmpeg-backed playback would re-introduce the same offscreen-
126+
# decode-then-CPU-readback path that capped Pi 4 at 2973 vo
127+
# drops/60 s during the libmpv-render trial. Gstreamer routes the
128+
# v4l2 stateless decoders (``v4l2slh264dec`` / ``v4l2slh265dec``
129+
# from rpt3 ``gstreamer1.0-plugins-bad`` on Pi/arm64; ``vaapi`` on
130+
# x86) straight into ``QVideoSink``.
131+
ENV QT_MEDIA_BACKEND=gstreamer
132+
{% endif %}
133+
123134
ENV GIT_HASH={{ git_hash }}
124135
ENV GIT_SHORT_HASH={{ git_short_hash }}
125136
ENV GIT_BRANCH={{ git_branch }}

docker/_rpt1-ffmpeg-pin.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
4848
echo "deb [signed-by=/etc/apt/keyrings/raspberrypi-archive.pgp] https://archive.raspberrypi.com/debian/ trixie main" \
4949
> /etc/apt/sources.list.d/raspberrypi.list && \
5050
printf '%s\n' \
51-
'Package: mpv libavcodec* libavdevice* libavfilter* libavformat* libavutil* libpostproc* libswresample* libswscale* ffmpeg' \
51+
'Package: mpv libavcodec* libavdevice* libavfilter* libavformat* libavutil* libpostproc* libswresample* libswscale* ffmpeg gstreamer1.0-* libgstreamer* libgstreamer-plugins-*' \
5252
'Pin: origin archive.raspberrypi.com' \
5353
'Pin-Priority: 1001' \
5454
'' \

src/anthias_viewer/media_player.py

Lines changed: 26 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import logging
22
import os
3-
import subprocess
43
from typing import Any, ClassVar
54

6-
from anthias_common.board import ARM64_DEVICE_TYPES, resolve_device_key
5+
from anthias_common.board import ARM64_DEVICE_TYPES
76
from anthias_common.device_helper import get_device_type
87
from anthias_common.utils import clamp_screen_rotation
98
from anthias_server.settings import settings
@@ -234,112 +233,6 @@ def is_playing(self) -> bool:
234233
raise NotImplementedError
235234

236235

237-
# Per-codec hwdec preference on Pi, per-board:
238-
#
239-
# Pi 4: H.264 → v4l2m2m-copy (V3D V4L2 M2M decoder via
240-
# bcm2835-codec, up to 1080p60)
241-
# HEVC → drm-copy (FFmpeg v4l2_request_hevc, up to
242-
# 4Kp60 via the dedicated HEVC block)
243-
#
244-
# Pi 5: H.264 → auto-copy (Hantro G1 silicon exists but mpv
245-
# has no v4l2-request H.264 hwdec
246-
# upstream; passing v4l2m2m-copy
247-
# here would just log "Could not
248-
# find a valid device" errors before
249-
# silently SW-falling-back. The
250-
# playback envelope (HEVC 4Kp60) means
251-
# every asset normalised post-rollout
252-
# lands as HEVC, so this branch only
253-
# fires for legacy variants the
254-
# re-render walker hasn't caught yet.)
255-
# HEVC → drm-copy (Hantro G2, up to 4Kp60. Requires
256-
# `dtoverlay=vc4-kms-v3d,cma-512` in
257-
# /boot/firmware/config.txt — the
258-
# stock 64 MB CMA region can't fit
259-
# a 4K HEVC dst buffer pool, and the
260-
# kernel cmdline `cma=` route silently
261-
# orphans the rpi-hevc-dec driver.)
262-
#
263-
# generic-arm64 (Armbian SBCs — Rock Pi 4, Orange Pi, etc.):
264-
# H.264 → v4l2m2m-copy (RK3399 Hantro via the v4l2m2m
265-
# HEVC → v4l2m2m-copy driver. mpv's --hwdec=help on the
266-
# latest-generic-arm64 image lists
267-
# both h264_v4l2m2m and hevc_v4l2m2m;
268-
# on boards without a working driver
269-
# mpv logs a warning and SW-falls-
270-
# back at runtime.)
271-
#
272-
# `auto-copy` is the universal safe fallback when ffprobe can't
273-
# read the codec (missing file, network URI we don't probe, etc.).
274-
#
275-
# An earlier revision did this with a Lua on_load hook, but
276-
# video-codec-name is empty at every event mpv exposes to scripts
277-
# before hwdec init (on_load, on_preloaded). ffprobing from Python
278-
# at launch time is both simpler and the only thing that actually
279-
# works.
280-
_PI_HWDEC_BY_CODEC: dict[str, dict[str, str]] = {
281-
'pi4-64': {'h264': 'v4l2m2m-copy', 'hevc': 'drm-copy'},
282-
'pi5': {'hevc': 'drm-copy'},
283-
# Rock Pi 4 (RK3399, Radxa). Both codecs go through
284-
# ``drm-copy``: the arm64 viewer image now pulls the Raspberry
285-
# Pi repo's ffmpeg (``+rpt1``, with ``--enable-v4l2-request``
286-
# — same package family as Pi 4 / Pi 5), so mpv exposes the
287-
# stateless v4l2_request decoders that the RK3399's ``rkvdec``
288-
# (HEVC) and ``rockchip,rk3399-vpu-dec`` Hantro VPU (H.264)
289-
# implement. The ``start_viewer.sh`` entrypoint creates the
290-
# ``/dev/video-dec*`` symlinks the v4l2_request decoder
291-
# discovery code expects (privileged docker mounts its own
292-
# /dev tmpfs without udev's symlinks).
293-
'rockpi4': {'h264': 'drm-copy', 'hevc': 'drm-copy'},
294-
}
295-
296-
297-
def _probe_video_codec(uri: str) -> str:
298-
"""Return the canonical lowercase video codec name for ``uri``.
299-
300-
Empty string on probe failure (missing file, unreadable codec,
301-
ffprobe absent, etc.) — callers should then pick a safe
302-
fallback like ``auto-copy``. Short timeout because this runs
303-
synchronously before every mpv launch.
304-
"""
305-
try:
306-
result = subprocess.run(
307-
[
308-
'ffprobe',
309-
'-v',
310-
'error',
311-
'-select_streams',
312-
'v:0',
313-
'-show_entries',
314-
'stream=codec_name',
315-
'-of',
316-
'default=nw=1:nk=1',
317-
uri,
318-
],
319-
capture_output=True,
320-
text=True,
321-
timeout=5,
322-
)
323-
return result.stdout.strip().lower()
324-
except (subprocess.SubprocessError, OSError):
325-
return ''
326-
327-
328-
def _pi_hwdec_for_uri(uri: str) -> str:
329-
"""mpv ``--hwdec=`` value for ``uri`` on Pi 4 / Pi 5 / Rock Pi 4.
330-
331-
Reads the board key via ``resolve_device_key``, which upgrades a
332-
catch-all ``arm64`` DEVICE_TYPE to the specific subtype the
333-
host_agent publishes (Rock Pi 4 → ``rockpi4``). ``auto-copy`` is
334-
the safe fallback when ffprobe can't read the codec; the upload
335-
gate (`anthias_server.processing._hw_decoded_codecs`) prevents
336-
any codec outside ``_PI_HWDEC_BY_CODEC`` reaching this branch in
337-
the first place.
338-
"""
339-
board_map = _PI_HWDEC_BY_CODEC.get(resolve_device_key(), {})
340-
return board_map.get(_probe_video_codec(uri), 'auto-copy')
341-
342-
343236
def _marshal_dbus_options(options: dict[str, str]) -> dict:
344237
"""Wrap each value as a ``GLib.Variant('s', ...)`` for pydbus.
345238
@@ -357,49 +250,40 @@ def _marshal_dbus_options(options: dict[str, str]) -> dict:
357250

358251

359252
def _build_mpv_options(uri: str) -> dict[str, str]:
360-
"""Build the per-file mpv option dict sent over D-Bus to libmpv.
361-
362-
Mirrors what the old ``subprocess.Popen([mpv, ...])`` argv
363-
composed, minus the bits that are now Qt's responsibility:
364-
365-
* ``--vo=...`` is gone — libmpv embedded via ``mpv_render_context``
366-
paints into the QOpenGLWidget's FBO; there is no separate VO.
367-
* ``--drm-mode=1920x1080@60`` on Pi 4 is gone — Qt's ``eglfs``
368-
KMS config (``docker/eglfs-kms-pi4.json``) pins the framebuffer
369-
to 1080p at QPA init.
370-
* ``--no-terminal`` / stdout redirection is gone — libmpv's
371-
logging is routed through ``mpv_request_log_messages`` to the
372-
AnthiasWebview process's stderr in C++ (see
373-
src/anthias_webview/src/videoview.cpp).
374-
375-
Kept: ``hwdec`` (per-board, per-codec via ffprobe),
376-
``audio-device`` (per-board ALSA), ``video-sync`` (display
377-
resample — same rationale as before), ``vd-lavc-threads`` (Pi 4 /
378-
Pi 5 decoder thread count), ``video-rotate`` (Pi 4 only — cage
379-
boards rotate via wlr-randr).
253+
"""Build the per-file option dict sent over D-Bus to QtMultimedia.
254+
255+
QtMultimedia + GStreamer auto-picks the best decoder element
256+
via ``decodebin3`` (``v4l2slh264dec`` / ``v4l2slh265dec`` on
257+
Pi 4 / Pi 5 / Rock Pi 4 via the rpt3 ``gstreamer1.0-plugins-bad``
258+
pin in docker/_rpt1-ffmpeg-pin.j2, ``vaapi`` on x86). The
259+
application no longer dispatches per-codec hwdec; the
260+
options dict shrinks to:
261+
262+
* ``audio-device`` — ALSA device name (the same string mpv
263+
consumed; gstreamer's ALSA sink and Qt's ``QAudioDevice``
264+
resolve it identically).
265+
* ``video-rotate`` — Pi 4 only. Cage / wayland boards already
266+
get the transform from wlr-randr at the compositor level;
267+
sending ``video-rotate`` on top would double-rotate.
268+
269+
The ``uri`` argument is kept on the signature for symmetry
270+
with the libmpv era (where it fed ffprobe). It's no longer
271+
read because gstreamer handles codec probing internally —
272+
but a future codec-specific tuning may re-introduce it.
380273
"""
274+
del uri # see docstring; kept for signature compatibility.
381275
device_type = os.environ.get('DEVICE_TYPE', '')
382276

383-
if resolve_device_key() in _PI_HWDEC_BY_CODEC:
384-
hwdec_value = _pi_hwdec_for_uri(uri)
385-
else:
386-
hwdec_value = 'auto-copy'
387-
388277
options: dict[str, str] = {
389-
'hwdec': hwdec_value,
390-
'video-sync': 'display-resample',
391278
'audio-device': f'alsa/{get_alsa_audio_device()}',
392279
}
393280

394-
if device_type in ('pi4-64', 'pi5'):
395-
options['vd-lavc-threads'] = '4'
396-
397281
# Rotation: cage/wlroots boards rotate via wlr-randr (issue
398282
# #2856, wired in src/anthias_viewer/__init__.py) and Qt's
399-
# wayland QPA inherits the transform — passing video-rotate to
400-
# libmpv on top of that would double-rotate. On Pi 4 (eglfs,
401-
# no compositor) Qt has no transform plumbing, so libmpv has
402-
# to apply the rotation itself.
283+
# wayland QPA inherits the transform — passing video-rotate
284+
# on top would double-rotate. On Pi 4 (eglfs, no compositor)
285+
# Qt has no transform plumbing, so the video pipeline has to
286+
# apply the rotation itself.
403287
rotation = _screen_rotation()
404288
if rotation and device_type == 'pi4-64':
405289
options['video-rotate'] = str(rotation)

src/anthias_webview/AnthiasWebview.pro

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
TEMPLATE = app
22

3-
QT += webenginecore webenginewidgets dbus openglwidgets
4-
CONFIG += c++17 link_pkgconfig
3+
QT += webenginecore webenginewidgets dbus multimedia multimediawidgets
4+
CONFIG += c++17
55

6-
# libmpv embedded inside AnthiasWebview (issue #2904). Replaces the
7-
# external mpv subprocess MPVMediaPlayer used to launch — one Qt
8-
# process now owns the framebuffer / Wayland surface for video too,
9-
# eliminating the two-process DRM-master contention that cost
10-
# 600-2800 vo drops per 60 s clip on Pi 4 under linuxfb. The Debian
11-
# Trixie ``libmpv-dev`` package supplies pkg-config metadata; the
12-
# runtime link is to ``libmpv.so`` (provided by ``libmpv2`` at
13-
# runtime, see docker/Dockerfile.viewer.j2).
14-
PKGCONFIG += mpv
6+
# QtMultimedia is the in-process video pipeline (issue #2904). An
7+
# earlier revision linked libmpv via ``mpv_render_context`` into a
8+
# ``QOpenGLWidget``; that engaged HW decode correctly but Pi 4 V3D
9+
# 6.0 couldn't sustain 60 fps through libmpv-render's GL upload +
10+
# FBO + Qt compositor pipeline. QtMultimedia with the gstreamer
11+
# backend (forced via ``QT_MEDIA_BACKEND=gstreamer`` in the runtime
12+
# env) routes the rpi v4l2 stateless decoders (``v4l2slh264dec`` /
13+
# ``v4l2slh265dec`` from rpt3 ``gstreamer1.0-plugins-bad``) directly
14+
# to ``QVideoSink`` — fewer copies, no FBO indirection, and
15+
# ``QVideoWidget`` paints inside MainWindow's single eglfs native
16+
# window so we don't trip eglfs's single-window-per-process limit.
1517

1618
SOURCES += src/main.cpp \
1719
src/mainwindow.cpp \

0 commit comments

Comments
 (0)