From f1e7448aad8852d34aa060b4cc9dd5fa342e1893 Mon Sep 17 00:00:00 2001 From: Matteo Benedetto Date: Tue, 24 Mar 2026 11:28:50 +0100 Subject: [PATCH] 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. --- .../player/gstreamer_backend.py | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/r36s_dlna_browser/player/gstreamer_backend.py b/src/r36s_dlna_browser/player/gstreamer_backend.py index 3923fd7..110ca5d 100644 --- a/src/r36s_dlna_browser/player/gstreamer_backend.py +++ b/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 # GstBin before the appsink so playbin accepts it as a single video-sink. # - # videoscale(method=nearest-neighbour, add-borders=True) scales the - # decoded source to fit within the video area while preserving the - # original aspect ratio. Black NV12 borders fill any leftover space - # (letterbox / pillarbox), avoiding any stretch distortion. + # videoscale(method=nearest-neighbour) scales the decoded source to the + # video-area width while letting GStreamer pick the output height from + # the source's native aspect ratio. Constraining only the width (not + # 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 # cache lines are fetched; Python memmove drops from ~32 ms to ~1 ms. # - # capsfilter — enforces the output NV12 dimensions. - # Use the actual video area inside the HUD (full window minus margins) - # 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). + # capsfilter — enforces format=NV12 and output width; height is left as + # a range so GStreamer can choose the correct value from the source AR. 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_h = max(2, vp_h - vp_top - vp_bottom) + video_w = max(4, vp_w - vp_left - vp_right) scale_w = (video_w // 2) * 2 - scale_h = (video_h // 2) * 2 - if scale_w < 2 or scale_h < 2: - scale_w, scale_h = 640, 480 - log.info("NV12 appsink: videoscale(nearest) → %dx%d before appsink", scale_w, scale_h) + if scale_w < 4: + scale_w = 640 + log.info("NV12 appsink: videoscale(nearest) → width=%d (AR-preserving) before appsink", scale_w) scale = self._gst.ElementFactory.make("videoscale", "vscale") capsfilter = self._gst.ElementFactory.make("capsfilter", "vcaps") @@ -463,15 +461,13 @@ class GStreamerBackend(PlayerBackend): # nearest-neighbour: accesses only the source pixels needed for each # output sample (strided reads), skipping ~56% of source rows entirely. - # add-borders=True: letterbox/pillarbox to preserve the source aspect - # ratio instead of stretching to fill the target dimensions. + # Height is unconstrained (range 2–2160) so GStreamer picks whatever + # height preserves the source aspect ratio for the chosen width. scale.set_property("method", 0) - scale.set_property("add-borders", True) capsfilter.set_property( "caps", self._gst.Caps.from_string( - f"video/x-raw,format=NV12,width={scale_w},height={scale_h}," - f"pixel-aspect-ratio=1/1" + f"video/x-raw,format=NV12,width={scale_w},height=(int)[2,2160]" ), )