e2D 1.4.20__py3-none-any.whl → 1.4.24__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
e2D/winrec.py CHANGED
@@ -1,27 +1,211 @@
1
- from e2D.envs import *
1
+ from typing import Optional, TYPE_CHECKING
2
+ from e2D import V2
3
+ from e2D.envs import RootEnv, FONT_MONOSPACE_16
4
+ from e2D.colors import BLACK_COLOR_PYG
2
5
  import pygame as pg
3
6
  import numpy as np
4
7
  import cv2
8
+ import threading
9
+ import queue
10
+ import time
5
11
 
6
12
  class WinRec:
7
- def __init__(self, rootEnv:RootEnv, fps:int=30, path:str='output.mp4') -> None:
13
+ def __init__(self, rootEnv:RootEnv, fps:int=30, draw_on_screen:bool=True, path:str='output.mp4', font:pg.font.Font=FONT_MONOSPACE_16) -> None:
8
14
  self.rootEnv = rootEnv
9
15
  self.path = path
10
16
  self.fps = fps
11
- self.video_writer = cv2.VideoWriter(self.path, cv2.VideoWriter_fourcc(*'mp4v'), self.fps, self.rootEnv.screen_size()) #type: ignore
17
+ self.draw_on_screen = draw_on_screen
18
+ self.font = font
19
+ self.is_recording = True # Recording state (pause/resume)
20
+ self.screenshot_counter = 0 # Counter for screenshot filenames
21
+ self.recording_frames = 0 # Frames actually recorded (excludes paused frames)
22
+ self.pause_start_time = None # Track when recording was paused
23
+ self.total_pause_duration = 0.0 # Cumulative pause time
24
+ size = self.rootEnv.screen_size
25
+ self.video_writer = cv2.VideoWriter(self.path, cv2.VideoWriter_fourcc(*'mp4v'), self.fps, size()) #type: ignore
26
+
27
+ # Pre-allocate buffers for zero-copy operations
28
+ self.frame_buffer = np.empty(shape=(int(size.y), int(size.x), 3), dtype=np.uint8)
29
+
30
+ # Setup async video writing
31
+ self.frame_queue = queue.Queue(maxsize=120) # Buffer up to 4 seconds at 30fps
32
+ self.running = True
33
+
34
+ # Statistics tracking
35
+ self.frames_written = 0
36
+ self.frames_dropped = 0
37
+ self.write_start_time = time.time()
38
+ self.last_stat_update = time.time()
39
+ self.current_write_fps = 0.0
40
+
41
+ self.write_thread = threading.Thread(target=self._write_worker, daemon=False)
42
+ self.write_thread.start()
43
+
44
+ def _write_worker(self) -> None:
45
+ """Background thread that writes frames to video file."""
46
+ while self.running or not self.frame_queue.empty():
47
+ try:
48
+ frame = self.frame_queue.get(timeout=0.1)
49
+ self.video_writer.write(frame)
50
+ self.frames_written += 1
51
+ self.frame_queue.task_done()
52
+
53
+ # Update write FPS every second
54
+ current_time = time.time()
55
+ if current_time - self.last_stat_update >= 1.0:
56
+ elapsed = current_time - self.write_start_time
57
+ self.current_write_fps = self.frames_written / elapsed if elapsed > 0 else 0
58
+ self.last_stat_update = current_time
59
+ except queue.Empty:
60
+ continue
61
+
62
+ def handle_input(self) -> None:
63
+ """Handle recording control keyboard inputs (F9-F12)."""
64
+ # F9: Toggle pause/resume recording
65
+ if self.rootEnv.keyboard.get_key(pg.K_F9, "just_pressed"):
66
+ self.toggle_recording()
67
+ status = "REC" if self.is_recording else "PAUSED"
68
+ print(f"[Recording] {status}")
69
+
70
+ # F10: Restart recording (reset all and resume)
71
+ if self.rootEnv.keyboard.get_key(pg.K_F10, "just_pressed"):
72
+ self.restart()
73
+ print("[Recording] Restarted (buffer cleared, timers reset)")
74
+
75
+ # F12: Take screenshot
76
+ if self.rootEnv.keyboard.get_key(pg.K_F12, "just_pressed"):
77
+ screenshot_path = self.take_screenshot()
78
+ print(f"[Screenshot] Saved: {screenshot_path}")
12
79
 
13
80
  def update(self) -> None:
14
- frame = cv2.cvtColor(np.swapaxes(pg.surfarray.array3d(self.rootEnv.screen), 0, 1), cv2.COLOR_RGB2BGR)
15
- self.video_writer.write(frame)
81
+ # Handle keyboard input first
82
+ self.handle_input()
83
+
84
+ # Skip frame capture if recording is paused
85
+ if not self.is_recording:
86
+ return
87
+
88
+ # Increment recording frame counter
89
+ self.recording_frames += 1
90
+
91
+ # Use pixels3d for zero-copy view, then transpose (creates view, not copy)
92
+ pixels = pg.surfarray.pixels3d(self.rootEnv.screen)
93
+ transposed = np.transpose(pixels, (1, 0, 2))
94
+
95
+ # Convert color in-place to pre-allocated buffer
96
+ cv2.cvtColor(transposed, cv2.COLOR_RGB2BGR, dst=self.frame_buffer)
97
+
98
+ # Queue frame copy for async writing (non-blocking)
99
+ try:
100
+ self.frame_queue.put_nowait(self.frame_buffer.copy())
101
+ except queue.Full:
102
+ self.frames_dropped += 1 # Track dropped frames
16
103
 
17
104
  def get_rec_seconds(self) -> float:
18
- return self.rootEnv.current_frame/self.fps
105
+ """Get recorded time in seconds (excludes paused time)."""
106
+ return self.recording_frames / self.fps
107
+
108
+ def draw(self) -> None:
109
+ # Calculate statistics
110
+ buffer_size = self.frame_queue.qsize()
111
+ buffer_percent = (buffer_size / self.frame_queue.maxsize) * 100
112
+ buffer_seconds = buffer_size / self.fps if self.fps > 0 else 0
113
+
114
+ # Estimate optimal buffer size based on write performance
115
+ if self.current_write_fps > 0 and self.fps > 0:
116
+ write_lag = self.fps / self.current_write_fps
117
+ estimated_buffer = int(self.fps * 2 * write_lag) # 2 seconds of lag compensation
118
+ else:
119
+ estimated_buffer = self.frame_queue.maxsize
120
+
121
+ # Recording state indicator
122
+ rec_status = "REC" if self.is_recording else "PAUSED"
123
+
124
+ # Format with fixed width for stable display (monospace-friendly)
125
+ row1 = (f"[{rec_status}] RecFrames:{self.recording_frames:>6} | "
126
+ f"RecTime:{self.get_rec_seconds():>6.2f}s | "
127
+ f"AppTime:{self.rootEnv.runtime_seconds:>6.2f}s")
128
+ row2 = (f"Buffer:{buffer_size:>3}/{self.frame_queue.maxsize:<3} ({buffer_percent:>5.1f}%, {buffer_seconds:>4.1f}s) | "
129
+ f"WriteFPS:{self.current_write_fps:>5.1f}")
130
+ row3 = (f"Written:{self.frames_written:>6} | Dropped:{self.frames_dropped:>4} | OptBuf:{estimated_buffer:>3}")
131
+ row4 = "[F9]Pause/Resume [F10]Restart [F12]Screenshot"
19
132
 
20
- def draw(self, draw_on_screen=False) -> None:
21
- text = f"[cfps:{self.rootEnv.current_frame} || realtime:{round(self.get_rec_seconds(),2)} || apptime:{round(self.rootEnv.runtime_seconds,2)}]"
22
- pg.display.set_caption(text)
23
- if draw_on_screen: self.rootEnv.print(text, self.rootEnv.screen_size, pivot_position='bottom_right')
133
+ if self.draw_on_screen:
134
+ size = self.rootEnv.print(row4, self.rootEnv.screen_size - V2(16, 16), pivot_position='bottom_right', font=self.font, margin=V2(10, 10), bg_color=BLACK_COLOR_PYG, border_radius=10)
135
+ self.rootEnv.print(row3, self.rootEnv.screen_size - V2(16, 16 + size.y), pivot_position='bottom_right', font=self.font, margin=V2(10, 10), bg_color=BLACK_COLOR_PYG, border_radius=10)
136
+ self.rootEnv.print(row2, self.rootEnv.screen_size - V2(16, 16 + size.y * 2), pivot_position='bottom_right', font=self.font, margin=V2(10, 10), bg_color=BLACK_COLOR_PYG, border_radius=10)
137
+ self.rootEnv.print(row1, self.rootEnv.screen_size - V2(16, 16 + size.y * 3), pivot_position='bottom_right', font=self.font, margin=V2(10, 10), bg_color=BLACK_COLOR_PYG, border_radius=10)
138
+
139
+ def pause(self) -> None:
140
+ """Pause recording (stop capturing frames)."""
141
+ if self.is_recording:
142
+ self.is_recording = False
143
+ self.pause_start_time = time.time()
144
+
145
+ def resume(self) -> None:
146
+ """Resume recording (continue capturing frames)."""
147
+ if not self.is_recording and self.pause_start_time is not None:
148
+ self.total_pause_duration += time.time() - self.pause_start_time
149
+ self.pause_start_time = None
150
+ self.is_recording = True
151
+
152
+ def toggle_recording(self) -> None:
153
+ """Toggle between pause and resume."""
154
+ self.is_recording = not self.is_recording
155
+
156
+ def restart(self) -> None:
157
+ """Restart recording: clear buffer, reset all counters and timers, resume recording."""
158
+ self.clear_buffer()
159
+ self.recording_frames = 0
160
+ self.total_pause_duration = 0.0
161
+ self.pause_start_time = None
162
+ self.is_recording = True
163
+
164
+ def clear_buffer(self) -> None:
165
+ """Clear the frame queue and reset write statistics."""
166
+ # Clear the queue
167
+ while not self.frame_queue.empty():
168
+ try:
169
+ self.frame_queue.get_nowait()
170
+ self.frame_queue.task_done()
171
+ except queue.Empty:
172
+ break
173
+
174
+ # Reset write counters (but keep recording frames)
175
+ self.frames_written = 0
176
+ self.frames_dropped = 0
177
+ self.write_start_time = time.time()
178
+ self.last_stat_update = time.time()
179
+ self.current_write_fps = 0.0
180
+
181
+ def take_screenshot(self, filename:Optional[str]=None) -> str:
182
+ """Save current screen as PNG screenshot.
183
+
184
+ Args:
185
+ filename: Optional custom filename. If None, auto-generates with counter.
186
+
187
+ Returns:
188
+ str: Path to saved screenshot
189
+ """
190
+ if filename is None:
191
+ # Auto-generate filename with counter
192
+ base_path = self.path.rsplit('.', 1)[0] # Remove .mp4 extension
193
+ filename = f"{base_path}_screenshot_{self.screenshot_counter:04d}.png"
194
+ self.screenshot_counter += 1
195
+
196
+ # Capture current screen
197
+ pixels = pg.surfarray.pixels3d(self.rootEnv.screen)
198
+ transposed = np.transpose(pixels, (1, 0, 2))
199
+ screenshot_buffer = np.empty(shape=(int(self.rootEnv.screen_size.y), int(self.rootEnv.screen_size.x), 3), dtype=np.uint8)
200
+ cv2.cvtColor(transposed, cv2.COLOR_RGB2BGR, dst=screenshot_buffer)
201
+
202
+ # Save as PNG
203
+ cv2.imwrite(filename, screenshot_buffer)
204
+ return filename
24
205
 
25
206
  def quit(self) -> None:
26
- self.video_writer.release()
27
-
207
+ # Stop accepting new frames and wait for queue to flush
208
+ self.running = False
209
+ self.frame_queue.join() # Wait for all queued frames to be written
210
+ self.write_thread.join(timeout=5.0) # Wait for thread to finish
211
+ self.video_writer.release()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: e2D
3
- Version: 1.4.20
3
+ Version: 1.4.24
4
4
  Summary: Python library for 2D games. Streamlines dev with keyboard/mouse input, vector calculations, color manipulation, and collision detection. Simplify game creation and unleash creativity!
5
5
  Home-page: https://github.com/marick-py/e2D
6
6
  Author: Riccardo Mariani
@@ -0,0 +1,13 @@
1
+ e2D/__init__.py,sha256=ZTja5nMPyg3_fulzuSWhjA-JexSG9QHHr3sc0w7Qcpk,23857
2
+ e2D/__init__.pyi,sha256=1WNOYOyNyroL2LLrzE7CIJ7uubh_zhVbMvJINpy5PIY,53979
3
+ e2D/colors.py,sha256=4ZFcp6ZfdQ2eG3INw_Hx-KI2PkVustZ5TurnooHGSvw,19063
4
+ e2D/def_colors.py,sha256=3sJq2L6qFZ3svn2qEWIx0SinNXjb9huNaFigDeJipm8,43805
5
+ e2D/envs.py,sha256=5ah8SW9HNC3NqqZZfCBZs1BWtWHLA43dWwQruawzm9U,9240
6
+ e2D/plots.py,sha256=_d72ZJo-GIhcJ44XCphFH288cf_ZSwWcbLh_olgGjBc,35880
7
+ e2D/utils.py,sha256=Yamt6_JS6MVKrNtfzjP7BRWjCeXHuzFcFlL7H2Au7_M,29687
8
+ e2D/winrec.py,sha256=cZCqnCVwQ-n9HTcQj6vB4nKfu-YJ-GogtTTHiWDH3dc,9744
9
+ e2d-1.4.24.dist-info/licenses/LICENSE,sha256=hbjljn38VVW9en51B0qzRK-v2FBDijqRWbZIVTk7ipU,1094
10
+ e2d-1.4.24.dist-info/METADATA,sha256=UcXuDKt3kNK8fdd-o2ldBVfxkx8sMD1fz74lrtVtpxA,9634
11
+ e2d-1.4.24.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
12
+ e2d-1.4.24.dist-info/top_level.txt,sha256=3vKZ-CGzNlTCpzVMmM0Ht76krCofKw7hZ0wBf-dnKdM,4
13
+ e2d-1.4.24.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.3.1)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,13 +0,0 @@
1
- e2D/__init__.py,sha256=7UmgGI4WotqPLGXyGlNzddUdd5eK1VWqsb2BMAOA78g,22522
2
- e2D/__init__.pyi,sha256=ckPL2_iz5439-lD-wRLF7t54b_ndF4LT_A24_oyCCTU,45403
3
- e2D/colors.py,sha256=SwO52zowBs9l_VK1TjyT2GK1Npn5UAsHdCxz3lvaSKQ,18011
4
- e2D/def_colors.py,sha256=3sJq2L6qFZ3svn2qEWIx0SinNXjb9huNaFigDeJipm8,43805
5
- e2D/envs.py,sha256=1QnOEI2lCO3B6_EkIBBqgaXfPYLFE_u0_H6AwmZ1R9U,7171
6
- e2D/plots.py,sha256=_d72ZJo-GIhcJ44XCphFH288cf_ZSwWcbLh_olgGjBc,35880
7
- e2D/utils.py,sha256=42anOSUxvQ9SOof26VkJPsm6vN9AvckeW0U0RILzi8w,27843
8
- e2D/winrec.py,sha256=EFFfWYbk27NhS-rWD-BLChXvLjFW1uYZ5LkRGMj_Xo0,1116
9
- e2d-1.4.20.dist-info/licenses/LICENSE,sha256=hbjljn38VVW9en51B0qzRK-v2FBDijqRWbZIVTk7ipU,1094
10
- e2d-1.4.20.dist-info/METADATA,sha256=YOwcgB_fj4q1bHxAbh1uK-qcFEWAK8qoA4lZ4-dOFZI,9634
11
- e2d-1.4.20.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
12
- e2d-1.4.20.dist-info/top_level.txt,sha256=3vKZ-CGzNlTCpzVMmM0Ht76krCofKw7hZ0wBf-dnKdM,4
13
- e2d-1.4.20.dist-info/RECORD,,