@ -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) → %d x %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] "
) ,
)