@ -145,14 +145,12 @@ class _Frame:
width : int
height : int
pitch : int
pixels : bytes
pixel_format : str = " BGRA " # "BGRA" or "NV12"
# For NV12: pixels holds the FULL raw buffer (Y + interleaved UV); y_size is
# the byte offset where the UV plane starts. uv_pitch is the UV plane stride.
# Storing the whole buffer in one object avoids two separate bytes slices (each
# a copy) and lets render() do a single from_buffer_copy instead of two.
# For NV12: y_size is the byte offset where the UV plane starts.
y_size : int = 0
uv_pitch : int = 0
# Total raw buffer size (Y+UV for NV12, full frame for BGRA).
buf_size : int = 0
class GStreamerBackend ( PlayerBackend ) :
@ -197,6 +195,10 @@ class GStreamerBackend(PlayerBackend):
self . _resolution = " "
self . _hw_decoders : list | None = None # None = not yet probed
self . _frame_count = 0 # total frames decoded
# Pre-allocated ctypes buffer for zero-copy NV12 frame transfer.
# Sized on first frame; reused every frame to avoid per-frame allocation.
self . _raw_arr : ctypes . Array | None = None
self . _raw_arr_size : int = 0
def attach_window ( self , window : object ) - > None :
self . _window = window
@ -248,24 +250,15 @@ class GStreamerBackend(PlayerBackend):
if self . _frame_dirty :
if frame . pixel_format == " NV12 " and frame . y_size > 0 :
# NV12 upload via SDL_UpdateNVTexture.
# ONE from_buffer_copy of the full Y+UV buffer, then use
# ctypes.byref(arr, offset) to address Y at 0 and UV at y_size.
# This avoids the two extra bytes slices that were previously
# created in _on_new_sample, cutting per-frame copies from 5 to 2.
# _raw_arr already holds the frame data from _on_new_sample's
# memmove — pass pointers directly, no per-frame allocation.
try :
raw_buf = frame . pixels
arr = ( ctypes . c_ubyte * len ( raw_buf ) ) . from_buffer_copy ( raw_buf )
arr = self . _raw_arr
y_ptr = ctypes . cast ( arr , ctypes . POINTER ( ctypes . c_ubyte ) )
uv_ptr = ctypes . cast (
ctypes . byref ( arr , frame . y_size ) ,
ctypes . POINTER ( ctypes . c_ubyte ) ,
)
log . debug (
" SDL_UpdateNVTexture: %d x %d buf= %d y_size= %d pitch= %d uv_pitch= %d " ,
frame . width , frame . height ,
len ( raw_buf ) , frame . y_size ,
frame . pitch , frame . uv_pitch ,
)
result = sdl2 . SDL_UpdateNVTexture (
self . _texture , None ,
y_ptr , frame . pitch ,
@ -279,7 +272,7 @@ class GStreamerBackend(PlayerBackend):
)
return False
else :
pixel_buffer = ctypes . create_string_buffer ( frame . pixels )
pixel_buffer = ctypes . cast ( self . _raw_arr , ctypes . POINTER ( ctypes . c_ubyte ) )
result = sdl2 . SDL_UpdateTexture ( self . _texture , None , pixel_buffer , frame . pitch )
if result != 0 :
log . error (
@ -465,49 +458,62 @@ class GStreamerBackend(PlayerBackend):
pass
if fmt_str == " NV12 " :
# NV12: Y plane (stride[0]) followed immediately by interleaved UV plane (stride[1]).
# Store the WHOLE raw buffer in pixels without slicing — slicing bytes
# creates two extra copies (2 MB + 1 MB) that we can avoid. render()
# uses a single from_buffer_copy of the full buffer and ctypes.byref to
# address the UV plane at the y_size byte offset.
pitch = int ( info . stride [ 0 ] )
# NV12: Y plane (stride[0]) followed by interleaved UV plane (stride[1]).
pitch = int ( info . stride [ 0 ] )
uv_pitch = int ( info . stride [ 1 ] )
y_size = pitch * height
raw = buffer . extract_dup ( 0 , buffer . get_size ( ) )
if self . _frame_count == 0 :
log . info (
" First NV12 frame: %d x %d y_pitch= %d uv_pitch= %d "
" y_size= %d buf_total= %d " ,
width , height , pitch , uv_pitch , y_size , len ( raw ) ,
)
frame = _Frame (
width = width , height = height ,
pitch = pitch , pixels = raw ,
pixel_format = " NV12 " ,
y_size = y_size , uv_pitch = uv_pitch ,
)
y_size = pitch * height
buf_size = buffer . get_size ( )
else :
pitch = int ( info . stride [ 0 ] ) if info . stride else width * 4
pixels = buffer . extract_dup ( 0 , buffer . get_size ( ) )
if self . _frame_count == 0 :
log . info (
" First %s frame: %d x %d pitch= %d buf_total= %d " ,
fmt_str , width , height , pitch , buffer . get_size ( ) ,
)
frame = _Frame ( width = width , height = height , pitch = pitch , pixels = pixels )
uv_pitch = 0
y_size = 0
buf_size = buffer . get_size ( )
with self . _frame_lock :
self . _frame_count + = 1
if self . _frame_count < = 3 or self . _frame_count % 300 == 0 :
log . debug ( " Frame # %d fmt= %s %d x %d " , self . _frame_count , fmt_str , width , height )
prev_res = self . _resolution
self . _latest_frame = frame
self . _frame_dirty = True
if resolution != prev_res :
self . _resolution = resolution
if prev_res :
log . info ( " Resolution changed: %s -> %s " , prev_res , resolution )
self . _event_callback ( " resolution " , resolution )
# Map the GStreamer buffer (zero-copy access to decoded frame memory).
ok , map_info = buffer . map ( self . _gst . MapFlags . READ )
if not ok :
return self . _flow_ok ( )
try :
src_size = map_info . size
# memmove into _raw_arr inside the frame lock so render() never
# reads a partially-written buffer from the main thread.
with self . _frame_lock :
if self . _raw_arr is None or self . _raw_arr_size < src_size :
self . _raw_arr = ( ctypes . c_ubyte * src_size ) ( )
self . _raw_arr_size = src_size
ctypes . memmove ( self . _raw_arr , map_info . data , src_size )
self . _frame_count + = 1
if self . _frame_count == 1 :
log . info (
" First %s frame: %d x %d pitch= %d uv_pitch= %d "
" y_size= %d buf_total= %d (alloc ctypes buf %d ) " ,
fmt_str , width , height , pitch , uv_pitch ,
y_size , buf_size , src_size ,
)
elif self . _frame_count < = 3 or self . _frame_count % 300 == 0 :
log . debug ( " Frame # %d fmt= %s %d x %d " , self . _frame_count , fmt_str , width , height )
if fmt_str == " NV12 " :
frame = _Frame (
width = width , height = height , pitch = pitch ,
pixel_format = " NV12 " ,
y_size = y_size , uv_pitch = uv_pitch , buf_size = buf_size ,
)
else :
frame = _Frame ( width = width , height = height , pitch = pitch ,
buf_size = buf_size )
prev_res = self . _resolution
self . _latest_frame = frame
self . _frame_dirty = True
if resolution != prev_res :
self . _resolution = resolution
if prev_res :
log . info ( " Resolution changed: %s -> %s " , prev_res , resolution )
self . _event_callback ( " resolution " , resolution )
finally :
buffer . unmap ( map_info )
return self . _flow_ok ( )
@ -647,6 +653,8 @@ class GStreamerBackend(PlayerBackend):
self . _frame_dirty = False
self . _resolution = " "
self . _frame_count = 0
self . _raw_arr = None
self . _raw_arr_size = 0
self . _destroy_texture ( )
def _set_playing ( self , value : bool , notify : bool ) - > None :