Browse Source

fix: preserve AR by constraining only width in NV12 videoscale capsfilter

Constraining both width AND height caused GStreamer to stretch the video
to fill the target box, distorting the aspect ratio when the box was not
the same AR as the source (e.g. 720x600 vs a 16:9 source).

Fix: only constrain width in the capsfilter (height=(int)[2,2160]).
GStreamer then picks the height from the source's native DAR, naturally
preserving aspect ratio without relying on add-borders.
_fit_frame_to_viewport centres the resulting frame in the SDL viewport.
main
Matteo Benedetto 1 week ago
parent
commit
f1e7448aad
  1. 36
      src/r36s_dlna_browser/player/gstreamer_backend.py

36
src/r36s_dlna_browser/player/gstreamer_backend.py

@ -431,26 +431,24 @@ class GStreamerBackend(PlayerBackend):
# Hardware decode (NV12): insert a videoscale → capsfilter chain inside a # Hardware decode (NV12): insert a videoscale → capsfilter chain inside a
# GstBin before the appsink so playbin accepts it as a single video-sink. # GstBin before the appsink so playbin accepts it as a single video-sink.
# #
# videoscale(method=nearest-neighbour, add-borders=True) scales the # videoscale(method=nearest-neighbour) scales the decoded source to the
# decoded source to fit within the video area while preserving the # video-area width while letting GStreamer pick the output height from
# original aspect ratio. Black NV12 borders fill any leftover space # the source's native aspect ratio. Constraining only the width (not
# (letterbox / pillarbox), avoiding any stretch distortion. # both width and height) means GStreamer will never stretch the video:
# it always preserves the source DAR. The resulting frame is then
# centred in the SDL viewport by _fit_frame_to_viewport() which adds
# the necessary letterbox/pillarbox margins through SDL_RenderCopy.
# Nearest-neighbour skips ~56% of source rows so only ~44% of source # Nearest-neighbour skips ~56% of source rows so only ~44% of source
# cache lines are fetched; Python memmove drops from ~32 ms to ~1 ms. # cache lines are fetched; Python memmove drops from ~32 ms to ~1 ms.
# #
# capsfilter — enforces the output NV12 dimensions. # capsfilter — enforces format=NV12 and output width; height is left as
# Use the actual video area inside the HUD (full window minus margins) # a range so GStreamer can choose the correct value from the source AR.
# so the scale target matches the drawable region exactly.
# Dimensions are rounded down to even numbers (NV12 chroma subsampling
# requires both width and height to be divisible by 2).
vp_w, vp_h, vp_top, vp_bottom, vp_left, vp_right = self._viewport vp_w, vp_h, vp_top, vp_bottom, vp_left, vp_right = self._viewport
video_w = max(2, vp_w - vp_left - vp_right) video_w = max(4, vp_w - vp_left - vp_right)
video_h = max(2, vp_h - vp_top - vp_bottom)
scale_w = (video_w // 2) * 2 scale_w = (video_w // 2) * 2
scale_h = (video_h // 2) * 2 if scale_w < 4:
if scale_w < 2 or scale_h < 2: scale_w = 640
scale_w, scale_h = 640, 480 log.info("NV12 appsink: videoscale(nearest) → width=%d (AR-preserving) before appsink", scale_w)
log.info("NV12 appsink: videoscale(nearest) → %dx%d before appsink", scale_w, scale_h)
scale = self._gst.ElementFactory.make("videoscale", "vscale") scale = self._gst.ElementFactory.make("videoscale", "vscale")
capsfilter = self._gst.ElementFactory.make("capsfilter", "vcaps") capsfilter = self._gst.ElementFactory.make("capsfilter", "vcaps")
@ -463,15 +461,13 @@ class GStreamerBackend(PlayerBackend):
# nearest-neighbour: accesses only the source pixels needed for each # nearest-neighbour: accesses only the source pixels needed for each
# output sample (strided reads), skipping ~56% of source rows entirely. # output sample (strided reads), skipping ~56% of source rows entirely.
# add-borders=True: letterbox/pillarbox to preserve the source aspect # Height is unconstrained (range 2–2160) so GStreamer picks whatever
# ratio instead of stretching to fill the target dimensions. # height preserves the source aspect ratio for the chosen width.
scale.set_property("method", 0) scale.set_property("method", 0)
scale.set_property("add-borders", True)
capsfilter.set_property( capsfilter.set_property(
"caps", "caps",
self._gst.Caps.from_string( self._gst.Caps.from_string(
f"video/x-raw,format=NV12,width={scale_w},height={scale_h}," f"video/x-raw,format=NV12,width={scale_w},height=(int)[2,2160]"
f"pixel-aspect-ratio=1/1"
), ),
) )

Loading…
Cancel
Save