fix(viewer): aspect-fit, gapless looping, and 30fps cap for pi1/2/3 video#3004
Merged
Conversation
…ideo 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>
There was a problem hiding this comment.
Pull request overview
This PR improves Raspberry Pi 1/2/3 (Qt5 linuxfb) video playback by moving the GStreamer looping/playback logic from an external bash + gst-launch respawn loop into an in-process Python helper, enabling aspect-correct “fit” scaling, gapless looping, and a 30fps output cap to reduce judder.
Changes:
- Replace
gst-launchrespawn looping with a dedicatedgst_fbdev_player.pyhelper process that manages aplaybinpipeline and looping behavior in-process. - Add unit tests for the helper’s pure functions (aspect-fit math, caps building, argv parsing, framebuffer clear).
- Update existing media player tests and documentation to reflect the new execution model and playback behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_media_player.py | Updates command-building and spawn tests to validate invoking the helper with CLI flags instead of gst-launch. |
| tests/test_gst_fbdev_player.py | Adds unit tests for helper utilities (fit-dim math, caps strings, sink description, fb clearing). |
| src/anthias_viewer/media_player.py | Switches Pi1/2/3 video playback to spawn the helper by path with explicit CLI parameters. |
| src/anthias_viewer/gst_fbdev_player.py | New helper implementing gapless looping, aspect-fit caps pinning, 30fps cap, and framebuffer clearing. |
| docs/board-enablement.md | Documents the new helper-based playback path and rationale (aspect-fit, looping, 30fps cap). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- 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>
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>
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>
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>
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>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Issues Fixed
Part of #2987 (the Pi 3 symptoms: stretched portrait videos, clips freezing and getting cut off at the start/end, jerky 50/60 fps playback).
Description
Moves Qt5-linuxfb (pi1/2/3) video playback from the
bash+gst-launchrelaunch loop into a small in-process GStreamer helper (anthias_viewer/gst_fbdev_player.py):pixel-aspect-ratio=1/1) on a capsfilter, so the bcm2835 ISP scales aspect-correct andfbdevsinkcenters the frame. The old fb-sized forced caps madev4l2convertfill the screen and park the distortion in a PAR thatfbdevsinkignores.about-to-finishre-queues the same URI for a gapless loop instead of rebuilding the pipeline each iteration (measured 0.4–1.7 s per loop on a Pi 4, more on a Pi 3 — all eaten out of the fixed slot duration while the last frame sat frozen). Flush-seek-on-EOS and a fresh rebuild remain as fallbacks.videorate drop-only=true max-rate=30drops 50/60 fps sources to an even half-cadence before the ISP + framebuffer blit (the chain sustains ~40 fps at 1080p on a Pi 3).FBIOGET_VSCREENINFOioctl (xres/yres— the fieldsfbdevsinkcenters against) instead of sysfsvirtual_size, which can be larger than the scanned-out mode.-m) so the package__init__never imports in the child.No image changes needed:
python3-gi+gir1.2-gstreamer-1.0already ship in the pi1/2/3 viewer image.Validation
Component level (shipped armhf
latest-pi3image on the Pi 4 testbed, binaries run natively): caps negotiation incl. 90° rotation, gapless looping across clip boundaries, SIGTERM → exit 0. Reproduced the original stretch (1080x1920 → 3840x2160 par 81/256) with the shipped pipeline first.Integrated stack (full viewer container —
asset_loop → view_video → helper spawn— from this branch on the Pi 4 testbed, armhf, faked device-tree model; webview stubbed with a D-Bus shim due to host-specific 32-bit QtWebEngine instability that does not occur on real Pi 3 silicon):1080x1920 → 608x1080negotiated AND verified at the pixel level — framebuffer rows read[656px black][608px content][656px black]. ✅state: RUNNING; an AC3 track (no decoder shipped) is skipped gracefully, video continues. ✅Outstanding on real Pi 3 hardware (Pi 3B+ reachable, currently headless — no
/dev/fb0without a display): the fbdev paint path, the literalvc4hdmicard open, and VideoCore IV decode rates. Will be run as soon as a display/dummy is attached; the audio pre-flight + VPU decode probes run there next regardless.Checklist
🤖 Generated with Claude Code