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]" ), )