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/__init__.py +71 -29
- e2D/__init__.pyi +244 -5
- e2D/colors.py +40 -20
- e2D/envs.py +56 -24
- e2D/utils.py +110 -74
- e2D/winrec.py +196 -12
- {e2d-1.4.20.dist-info → e2d-1.4.24.dist-info}/METADATA +1 -1
- e2d-1.4.24.dist-info/RECORD +13 -0
- {e2d-1.4.20.dist-info → e2d-1.4.24.dist-info}/WHEEL +1 -1
- e2d-1.4.20.dist-info/RECORD +0 -13
- {e2d-1.4.20.dist-info → e2d-1.4.24.dist-info}/licenses/LICENSE +0 -0
- {e2d-1.4.20.dist-info → e2d-1.4.24.dist-info}/top_level.txt +0 -0
e2D/winrec.py
CHANGED
|
@@ -1,27 +1,211 @@
|
|
|
1
|
-
from
|
|
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.
|
|
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
|
-
|
|
15
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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.
|
|
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,,
|
e2d-1.4.20.dist-info/RECORD
DELETED
|
@@ -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,,
|
|
File without changes
|
|
File without changes
|