Files
Viktor Petersson 9863d8c9d3 fix(viewer): aspect-fit, gapless looping, and 30fps cap for pi1/2/3 video (#3004)
* fix(viewer): aspect-fit, gapless looping, and 30fps cap for pi1/2/3 video

Fixes the issue #2987 regressions on the Qt5 linuxfb boards by moving
playback from a bash gst-launch relaunch loop into a small in-process
GStreamer helper (anthias_viewer/gst_fbdev_player.py):

- Portrait/4:3 videos no longer stretch to the framebuffer: a CAPS-event
  pad probe reads the decoder's native dims + PAR and pins aspect-fit
  caps (pixel-aspect-ratio=1/1) on the capsfilter, so the bcm2835 ISP
  scales aspect-correct and fbdevsink centers the frame. The previous
  fb-sized forced caps parked the distortion in a PAR that fbdevsink
  ignores (reproduced on-device: 1080x1920 -> 3840x2160 par 81/256).
- Clips no longer freeze/cut at loop boundaries: playbin about-to-finish
  re-queues the same URI for a gapless loop instead of rebuilding the
  whole pipeline per iteration (0.4-1.7 s per loop measured on a Pi 4,
  several seconds on a Pi 3, all eaten out of the fixed slot duration).
  Flush-seek on EOS and NULL->PLAYING restart remain as fallbacks.
- 50/60 fps sources drop to an even 30 fps cadence up front (videorate
  drop-only) instead of juddering on irregular late-frame drops; the
  decode->ISP->memcpy chain sustains ~40 fps at 1080p on a Pi 3.
- The framebuffer is zeroed at startup so letterbox borders are black
  rather than remnants of the previous asset.
- The helper runs by path (not -m) so the package __init__ (Django
  settings, redis, D-Bus) never imports in the child; validated e2e on
  the armhf image: negotiation, rotation, looping, SIGTERM exit 0.

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

* fix(viewer): address review feedback on the fbdev player helper

- Correct the module docstring: the helper is executed by file path,
  not -m (the package __init__ must not import in the child)
- Fail fast with a clear log line when the GStreamer python bindings
  are missing instead of crashing with a traceback
- Clear the framebuffer in scanline-sized chunks so a 4K console
  doesn't peak a ~33 MB allocation on a 512 MB board

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

* fix(viewer): degrade to silent video when the audio branch fails

Integrated testbed run surfaced a wholesale-failure mode the relaunch
loop also had: a broken audio branch killed the video with it. Two
real-world triggers: the ALSA card is absent (HDMI audio disabled in
config.txt -> no vc4hdmi), and an undecodable audio codec (AC3 -
a52dec lives in plugins-ugly, not shipped). Retry once with
GST_PLAY_FLAG_AUDIO cleared on both the synchronous start failure
(alsasink can't reach READY) and the first async pipeline error; a
genuine video error recurs on the retry and still exits non-zero.

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

* fix(viewer): read the visible fb resolution via FBIOGET_VSCREENINFO

Integrated testbed run surfaced a divergence the sysfs read hides:
sysfs virtual_size reports xres_virtual/yres_virtual, which can be
larger than the scanned-out mode (panning / double-buffer configs —
observed live: visible 1920x1080, virtual 3840x2160). fbdevsink
centers/crops against varinfo.xres/yres, so scaling to the virtual
size paints mostly off-screen. Query the same ioctl fbdevsink uses;
keep the sysfs read (and the 1080p default) as fallbacks for hosts
without fb access.

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

* fix(viewer): swap the audio sink for fakesink on the video-only retry

Clearing GST_PLAY_FLAG_AUDIO is not sufficient: an element set on the
audio-sink property remains a playsink child and is still state-synced
with the pipeline, so a failing alsasink failed the retry too
(observed live on the testbed). Replace it with a fakesink when
degrading to silent video.

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

* fix(viewer): pre-flight the ALSA device and rebuild on audio failure

The integrated testbed run showed two things the previous in-place
retry missed:
- a playbin whose sink activation failed does not reliably restart
  after NULL: the video-only retry failed instantly on the reused
  element with no further GStreamer error;
- alsasink opens the PCM on NULL->READY, so a missing card is
  detectable synchronously before it can poison playbin's whole
  sink activation.

So: pre-flight the device with a standalone alsasink and only wire
the audio branch when it opens; on any pipeline error with audio
enabled (e.g. an undecodable AC3 track mid-preroll), tear down and
rebuild a fresh video-only playbin instead of restarting the errored
one. Genuine video errors recur on the rebuilt pipeline and still
exit non-zero.

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

* fix(viewer): satisfy mypy on the gi-typed returns

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:00:33 +02:00
..

Documentation

This page has moved to https://anthias.screenly.io/docs/.

The Anthias documentation now lives at https://anthias.screenly.io/docs/.