2020from eventsys import events
2121from eventsys .keys import default_quit_chord
2222
23- __all__ = ["FFmpegFrameRecorder" , "PGDisplay" , "get_events" , "poll_event" ]
24-
25-
26- class FFmpegFrameRecorder :
27- """Pipe fixed-size RGB24 frames to ffmpeg for MP4 output."""
28-
29- __slots__ = ("_closed" , "_frame_bytes" , "_frames" , "_proc" , "fps" , "height" , "path" , "width" )
30-
31- def __init__ (self , path , width , height , fps = 12 ):
32- import subprocess
33-
34- self .path = path
35- self .width = width
36- self .height = height
37- self .fps = fps
38- self ._frames = 0
39- self ._closed = False
40- self ._frame_bytes = width * height * 3
41- self ._proc = subprocess .Popen (
42- [
43- "ffmpeg" ,
44- "-y" ,
45- "-f" ,
46- "rawvideo" ,
47- "-pix_fmt" ,
48- "rgb24" ,
49- "-s" ,
50- f"{ width } x{ height } " ,
51- "-r" ,
52- str (fps ),
53- "-i" ,
54- "pipe:0" ,
55- "-an" ,
56- "-c:v" ,
57- "libx264" ,
58- "-pix_fmt" ,
59- "yuv420p" ,
60- path ,
61- ],
62- stdin = subprocess .PIPE ,
63- stdout = subprocess .DEVNULL ,
64- stderr = subprocess .PIPE ,
65- )
66-
67- def write (self , rgb_bytes ):
68- if self ._closed :
69- return
70- if len (rgb_bytes ) != self ._frame_bytes :
71- raise ValueError (
72- f"frame size { len (rgb_bytes )} != expected { self ._frame_bytes } "
73- f"for { self .width } x{ self .height } RGB24"
74- )
75- self ._proc .stdin .write (rgb_bytes )
76- self ._frames += 1
77-
78- def close (self ):
79- if self ._closed :
80- return self ._frames
81- self ._closed = True
82- try :
83- self ._proc .stdin .close ()
84- except Exception :
85- pass
86- err = self ._proc .stderr .read ().decode ("utf-8" , errors = "replace" )
87- try :
88- self ._proc .stderr .close ()
89- except Exception :
90- pass
91- rc = self ._proc .wait ()
92- if rc != 0 :
93- tail = "\n " .join (err .strip ().splitlines ()[- 8 :])
94- raise RuntimeError (f"ffmpeg exited { rc } for { self .path } :\n { tail } " )
95- return self ._frames
23+ __all__ = ["PGDisplay" , "get_events" , "poll_event" ]
9624
9725
9826def _pg_key_name (key ):
@@ -170,7 +98,7 @@ def _handle_window_close(window):
17098 return None
17199
172100
173- def _convert (e ):
101+ def _convert (e ): # noqa: PLR0911 — one return per pygame event type
174102 """Convert a pygame event to an eventsys namedtuple."""
175103 t = e .type
176104 if t == pg .QUIT :
@@ -215,6 +143,9 @@ def _convert(e):
215143 win ,
216144 )
217145 if t in (pg .KEYDOWN , pg .KEYUP ):
146+ # Match browser / SDL backends: drop OS auto-repeat KEYDOWNs.
147+ if t == pg .KEYDOWN and getattr (e , "repeat" , False ):
148+ return None
218149 return events .Key (t , _pg_key_name (e .key ), e .key , e .mod , getattr (e , "scancode" , 0 ), win )
219150 if t == pg .JOYAXISMOTION :
220151 return events .JoyAxisMotion (t , e .instance_id , e .axis , e .value / 32767.0 )
@@ -522,11 +453,19 @@ def _video_active(self) -> bool:
522453 except pg .error :
523454 return False
524455
525- def _buffer_rgb (self ) -> bytes :
526- """Export the logical framebuffer as packed RGB24 bytes."""
527- if hasattr (pg .image , "tostring" ):
528- return pg .image .tostring (self ._buffer , "RGB" )
529- return pg .image .tobytes (self ._buffer , "RGB" )
456+ def capture_rgb (self ):
457+ """Return the visible window as ``(RGB24 bytes, width, height)``."""
458+ if not self ._video_active ():
459+ raise RuntimeError ("pygame display is not active" )
460+ self .show ()
461+ width , height = self ._window .get_size ()
462+ return self ._window_rgb (), width , height
463+
464+ def _window_rgb (self ):
465+ """Export the composed visible window as packed RGB24 bytes."""
466+ if hasattr (pg .image , "tobytes" ):
467+ return pg .image .tobytes (self ._window , "RGB" )
468+ return pg .image .tostring (self ._window , "RGB" )
530469
531470 @property
532471 def frame_recording (self ) -> bool :
@@ -535,9 +474,11 @@ def frame_recording(self) -> bool:
535474
536475 def open_frame_recorder (self , path , * , fps = 12 , width = None , height = None ):
537476 """Attach an ffmpeg-backed recorder that receives one RGB24 frame per ``show()``."""
477+ from frame_recorder import FFmpegFrameRecorder
478+
538479 self .close_frame_recorder ()
539- w = self .width if width is None else width
540- h = self .height if height is None else height
480+ w = int ( self .width * self . _scale ) if width is None else width
481+ h = int ( self .height * self . _scale ) if height is None else height
541482 self ._frame_recorder = FFmpegFrameRecorder (path , w , h , fps )
542483 return self ._frame_recorder
543484
@@ -546,11 +487,13 @@ def close_frame_recorder(self):
546487 recorder = self ._frame_recorder
547488 self ._frame_recorder = None
548489 if recorder is not None :
549- recorder .close ()
490+ return recorder .close ()
491+ return 0
550492
551493 def _record_frame (self , rgb_bytes ) -> None :
552- if self ._frame_recorder is not None :
553- self ._frame_recorder .write (rgb_bytes )
494+ recorder = self ._frame_recorder
495+ if recorder is not None :
496+ recorder .write (rgb_bytes )
554497
555498 def render (self , renderRect = None ) -> None :
556499 """
@@ -604,7 +547,7 @@ def show(self, _timer=None) -> None:
604547 self .render ()
605548 self ._render_dirty = False
606549 if self ._frame_recorder is not None :
607- self ._record_frame (self ._buffer_rgb ())
550+ self ._record_frame (self ._window_rgb ())
608551 try :
609552 self ._pg_window .flip ()
610553 except pg .error :
0 commit comments