dxcam 0.3.0.dev1__tar.gz → 0.4.0.dev1__tar.gz
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.
- {dxcam-0.3.0.dev1/dxcam.egg-info → dxcam-0.4.0.dev1}/PKG-INFO +23 -18
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/README.md +22 -17
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam/__init__.py +10 -7
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam/_libs/d3d11.py +11 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam/_libs/dxgi.py +1 -0
- dxcam-0.4.0.dev1/dxcam/core/__init__.py +6 -0
- dxcam-0.4.0.dev1/dxcam/core/com_ptr.py +26 -0
- dxcam-0.4.0.dev1/dxcam/core/duplicator_protocol.py +18 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam/core/dxgi_duplicator.py +76 -48
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam/core/stagesurf.py +75 -9
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam/core/winrt_duplicator.py +34 -10
- dxcam-0.4.0.dev1/dxcam/dxcam.py +820 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam/processor/__init__.py +1 -2
- dxcam-0.4.0.dev1/dxcam/processor/_cython_kernels.c +34023 -0
- dxcam-0.4.0.dev1/dxcam/processor/_cython_kernels.pyx +900 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam/processor/_numpy_kernels.c +152 -152
- dxcam-0.4.0.dev1/dxcam/processor/base.py +90 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam/processor/cv2_processor.py +1 -2
- dxcam-0.4.0.dev1/dxcam/processor/cython_processor.py +158 -0
- dxcam-0.4.0.dev1/dxcam/runtime/__init__.py +18 -0
- {dxcam-0.3.0.dev1/dxcam/core → dxcam-0.4.0.dev1/dxcam/runtime}/backend.py +6 -5
- dxcam-0.4.0.dev1/dxcam/runtime/capture_worker.py +138 -0
- {dxcam-0.3.0.dev1/dxcam/core → dxcam-0.4.0.dev1/dxcam/runtime}/display_recovery.py +5 -4
- dxcam-0.4.0.dev1/dxcam/runtime/frame_buffer.py +159 -0
- {dxcam-0.3.0.dev1/dxcam/core → dxcam-0.4.0.dev1/dxcam/runtime}/output_recovery.py +33 -22
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam/types.py +1 -1
- dxcam-0.4.0.dev1/dxcam/util/frame.py +76 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam/util/io.py +18 -14
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1/dxcam.egg-info}/PKG-INFO +23 -18
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam.egg-info/SOURCES.txt +14 -8
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/pyproject.toml +3 -2
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/setup.py +6 -0
- dxcam-0.4.0.dev1/tests/test_processor_parity.py +187 -0
- dxcam-0.3.0.dev1/dxcam/core/__init__.py +0 -10
- dxcam-0.3.0.dev1/dxcam/core/capture_loop.py +0 -85
- dxcam-0.3.0.dev1/dxcam/core/capture_runtime.py +0 -123
- dxcam-0.3.0.dev1/dxcam/core/duplicator.py +0 -22
- dxcam-0.3.0.dev1/dxcam/dxcam.py +0 -839
- dxcam-0.3.0.dev1/dxcam/processor/base.py +0 -125
- dxcam-0.3.0.dev1/tests/test_bgra_conversion_parity.py +0 -88
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/LICENSE +0 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/MANIFEST.in +0 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam/_libs/__init__.py +0 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam/_libs/user32.py +0 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam/core/device.py +0 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam/core/dxgi_errors.py +0 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam/core/output.py +0 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam/processor/_numpy_kernels.pyx +0 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam/processor/numpy_processor.py +0 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam/util/__init__.py +0 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam/util/timer.py +0 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam.egg-info/dependency_links.txt +0 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam.egg-info/requires.txt +0 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/dxcam.egg-info/top_level.txt +0 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/setup.cfg +0 -0
- {dxcam-0.3.0.dev1 → dxcam-0.4.0.dev1}/tests/test_dxgi_transient_policy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dxcam
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0.dev1
|
|
4
4
|
Summary: A Python high-performance screenshot library for Windows using Desktop Duplication API
|
|
5
5
|
Author: ra1nty
|
|
6
6
|
License-Expression: MIT
|
|
@@ -104,14 +104,16 @@ camera = dxcam.create(
|
|
|
104
104
|
processor_backend="cv2" # default OpenCV processor
|
|
105
105
|
)
|
|
106
106
|
```
|
|
107
|
+
Note:
|
|
108
|
+
- From 0.4.0 the `dxcam.create(max_buffer_len=...)` is deprecated, screen capture now always uses a tripple buffer.
|
|
107
109
|
|
|
108
110
|
### Screenshot
|
|
109
111
|
```python
|
|
110
112
|
frame = camera.grab()
|
|
111
113
|
```
|
|
112
|
-
`grab()` returns a `numpy.ndarray`. `None` if no new frame is available since the last capture
|
|
114
|
+
`grab()` returns a `numpy.ndarray`. `None` if no new frame is available since the last capture; use `camera.grab(new_frame_only=False)` to make dxcam always return the latest frame.
|
|
113
115
|
|
|
114
|
-
Use `
|
|
116
|
+
Use `camera.grab_into(dst)` to reuse caller-managed memory.
|
|
115
117
|
|
|
116
118
|
To capture a region:
|
|
117
119
|
```python
|
|
@@ -134,14 +136,13 @@ camera.is_capturing # False
|
|
|
134
136
|
for _ in range(1000):
|
|
135
137
|
frame = camera.get_latest_frame() # blocks until a frame is available
|
|
136
138
|
```
|
|
137
|
-
>The screen capture mode spins up a thread that polls newly rendered frames and
|
|
139
|
+
>The screen capture mode spins up a thread that polls newly rendered frames and publishes them into a latest-only in-memory frame buffer. The blocking and `video_mode` behavior is designed for downstream video recording and machine learning workloads.
|
|
138
140
|
|
|
139
141
|
Useful variants:
|
|
140
142
|
- `camera.get_latest_frame(with_timestamp=True)` -> `(frame, frame_timestamp)` -> return frame timestamp
|
|
141
|
-
- `camera.
|
|
142
|
-
- `camera.grab(copy=False)` / `camera.grab_view()` -> zero-copy latest-frame snapshot
|
|
143
|
+
- `camera.get_latest_frame_into(dst)` -> write latest frame into caller-provided array
|
|
143
144
|
|
|
144
|
-
> When `start()` capture is running, calling `grab()` reads from the in-memory
|
|
145
|
+
> When `start()` capture is running, calling `grab()` reads from the in-memory frame buffer instead of directly polling the capture backend.
|
|
145
146
|
|
|
146
147
|
### Safely Releasing Resources
|
|
147
148
|
`release()` stops capture, frees buffers, and releases capture resources.
|
|
@@ -193,13 +194,13 @@ Supported modes: `"RGB"`, `"RGBA"`, `"BGR"`, `"BGRA"`, `"GRAY"`.
|
|
|
193
194
|
Notes:
|
|
194
195
|
- Data is returned as `numpy.ndarray`.
|
|
195
196
|
- `BGRA` does not require OpenCV and is the leanest dependency path.
|
|
196
|
-
- `RGB`, `BGR`, `RGBA`, `GRAY` require conversion (`cv2` or compiled `numpy` backend).
|
|
197
|
+
- `RGB`, `BGR`, `RGBA`, `GRAY` require conversion (`cv2`, `cython`, or compiled `numpy` backend).
|
|
197
198
|
|
|
198
199
|
### Frame Buffer
|
|
199
|
-
DXcam uses a fixed-
|
|
200
|
+
DXcam uses a fixed three-slot latest-only frame buffer in-memory. Readers always consume the newest published frame, and older staged frames may be overwritten once they are no longer leased.
|
|
200
201
|
|
|
201
202
|
```python
|
|
202
|
-
camera = dxcam.create(
|
|
203
|
+
camera = dxcam.create() # max_buffer_len is accepted but ignored
|
|
203
204
|
```
|
|
204
205
|
|
|
205
206
|
### Target FPS
|
|
@@ -224,7 +225,7 @@ For `backend="dxgi"`, this value comes from `DXGI_OUTDUPL_FRAME_INFO.LastPresent
|
|
|
224
225
|
For `backend="winrt"`, this value is derived from WinRT `SystemRelativeTime`.
|
|
225
226
|
|
|
226
227
|
### Video Mode
|
|
227
|
-
With `video_mode=True`, DXcam
|
|
228
|
+
With `video_mode=True`, DXcam continues publishing at target FPS, reusing the previous frame when no new frame is rendered.
|
|
228
229
|
|
|
229
230
|
```python
|
|
230
231
|
import cv2
|
|
@@ -261,22 +262,22 @@ Guideline:
|
|
|
261
262
|
- Try `winrt` if it performs better on your machine or fits your app constraints.
|
|
262
263
|
|
|
263
264
|
### Processor Backend
|
|
264
|
-
DXcam capture backends (`dxgi`/`winrt`)
|
|
265
|
-
The processor backend then handles post-processing:
|
|
265
|
+
DXcam capture backends (`dxgi`/`winrt`) acquire raw BGRA frame. The processor backend then handles post-processing:
|
|
266
266
|
- optional rotation/cropping preparation
|
|
267
267
|
- color conversion to your `output_color`
|
|
268
268
|
|
|
269
269
|
Recommended backend choice:
|
|
270
270
|
- OpenCV installed: use `cv2` (default)
|
|
271
|
-
- No OpenCV installed: use `numpy`
|
|
271
|
+
- No OpenCV installed: use `numpy`/`cython`
|
|
272
272
|
|
|
273
273
|
Use it like this:
|
|
274
274
|
```python
|
|
275
275
|
camera = dxcam.create(processor_backend="cv2")
|
|
276
|
+
camera = dxcam.create(processor_backend="cython")
|
|
276
277
|
camera = dxcam.create(processor_backend="numpy")
|
|
277
278
|
```
|
|
278
279
|
|
|
279
|
-
Official Windows wheels already include the compiled
|
|
280
|
+
Official Windows wheels already include the compiled Cython processor kernels.
|
|
280
281
|
|
|
281
282
|
Only for source installs:
|
|
282
283
|
```bash
|
|
@@ -288,6 +289,10 @@ If `processor_backend="numpy"` is selected but compiled kernels are unavailable,
|
|
|
288
289
|
DXcam logs a warning and falls back to `cv2` behavior. In that fallback path,
|
|
289
290
|
install OpenCV for non-`BGRA` output modes.
|
|
290
291
|
|
|
292
|
+
If `processor_backend="cython"` is selected but compiled kernels are unavailable,
|
|
293
|
+
DXcam raises a runtime error because that backend is explicitly the direct
|
|
294
|
+
Cython path.
|
|
295
|
+
|
|
291
296
|
## Benchmarks
|
|
292
297
|
When using a similar logic (only capture newly rendered frames) running on a 240fps output, ```DXCam, python-mss, D3DShot``` benchmarked as follow:
|
|
293
298
|
|
|
@@ -299,10 +304,10 @@ When using a similar logic (only capture newly rendered frames) running on a 240
|
|
|
299
304
|
The benchmark is across 5 runs, with a light-moderate usage on my PC (5900X + 3090; Chrome ~30tabs, VS Code opened, etc.), I used the [Blur Buster UFO test](https://www.testufo.com/framerates#count=5&background=stars&pps=960) to constantly render 240 fps on my monitor. DXcam captured almost every frame rendered. You will see some benchmarks online claiming 1000+fps capture while most of them is busy-spinning a for loop on a staled frame (no new frame rendered on screen in test scenario).
|
|
300
305
|
|
|
301
306
|
### For Targeting FPS:
|
|
302
|
-
| (Target)\\(mean,std) | DXcam | python-mss | D3DShot |
|
|
307
|
+
| (Target)\\(mean,std) | DXcam (4k) | python-mss | D3DShot (1080p) |
|
|
303
308
|
|------------- |-------- |------------|---------|
|
|
304
|
-
| 60fps |
|
|
305
|
-
| 30fps | 30.
|
|
309
|
+
| 60fps | 59.99, 0.04 :checkered_flag: | N/A | 47.11, 1.33 |
|
|
310
|
+
| 30fps | 30.00, 0.00 :checkered_flag: | N/A | 21.24, 0.17 |
|
|
306
311
|
|
|
307
312
|
|
|
308
313
|
## Work Referenced
|
|
@@ -59,14 +59,16 @@ camera = dxcam.create(
|
|
|
59
59
|
processor_backend="cv2" # default OpenCV processor
|
|
60
60
|
)
|
|
61
61
|
```
|
|
62
|
+
Note:
|
|
63
|
+
- From 0.4.0 the `dxcam.create(max_buffer_len=...)` is deprecated, screen capture now always uses a tripple buffer.
|
|
62
64
|
|
|
63
65
|
### Screenshot
|
|
64
66
|
```python
|
|
65
67
|
frame = camera.grab()
|
|
66
68
|
```
|
|
67
|
-
`grab()` returns a `numpy.ndarray`. `None` if no new frame is available since the last capture
|
|
69
|
+
`grab()` returns a `numpy.ndarray`. `None` if no new frame is available since the last capture; use `camera.grab(new_frame_only=False)` to make dxcam always return the latest frame.
|
|
68
70
|
|
|
69
|
-
Use `
|
|
71
|
+
Use `camera.grab_into(dst)` to reuse caller-managed memory.
|
|
70
72
|
|
|
71
73
|
To capture a region:
|
|
72
74
|
```python
|
|
@@ -89,14 +91,13 @@ camera.is_capturing # False
|
|
|
89
91
|
for _ in range(1000):
|
|
90
92
|
frame = camera.get_latest_frame() # blocks until a frame is available
|
|
91
93
|
```
|
|
92
|
-
>The screen capture mode spins up a thread that polls newly rendered frames and
|
|
94
|
+
>The screen capture mode spins up a thread that polls newly rendered frames and publishes them into a latest-only in-memory frame buffer. The blocking and `video_mode` behavior is designed for downstream video recording and machine learning workloads.
|
|
93
95
|
|
|
94
96
|
Useful variants:
|
|
95
97
|
- `camera.get_latest_frame(with_timestamp=True)` -> `(frame, frame_timestamp)` -> return frame timestamp
|
|
96
|
-
- `camera.
|
|
97
|
-
- `camera.grab(copy=False)` / `camera.grab_view()` -> zero-copy latest-frame snapshot
|
|
98
|
+
- `camera.get_latest_frame_into(dst)` -> write latest frame into caller-provided array
|
|
98
99
|
|
|
99
|
-
> When `start()` capture is running, calling `grab()` reads from the in-memory
|
|
100
|
+
> When `start()` capture is running, calling `grab()` reads from the in-memory frame buffer instead of directly polling the capture backend.
|
|
100
101
|
|
|
101
102
|
### Safely Releasing Resources
|
|
102
103
|
`release()` stops capture, frees buffers, and releases capture resources.
|
|
@@ -148,13 +149,13 @@ Supported modes: `"RGB"`, `"RGBA"`, `"BGR"`, `"BGRA"`, `"GRAY"`.
|
|
|
148
149
|
Notes:
|
|
149
150
|
- Data is returned as `numpy.ndarray`.
|
|
150
151
|
- `BGRA` does not require OpenCV and is the leanest dependency path.
|
|
151
|
-
- `RGB`, `BGR`, `RGBA`, `GRAY` require conversion (`cv2` or compiled `numpy` backend).
|
|
152
|
+
- `RGB`, `BGR`, `RGBA`, `GRAY` require conversion (`cv2`, `cython`, or compiled `numpy` backend).
|
|
152
153
|
|
|
153
154
|
### Frame Buffer
|
|
154
|
-
DXcam uses a fixed-
|
|
155
|
+
DXcam uses a fixed three-slot latest-only frame buffer in-memory. Readers always consume the newest published frame, and older staged frames may be overwritten once they are no longer leased.
|
|
155
156
|
|
|
156
157
|
```python
|
|
157
|
-
camera = dxcam.create(
|
|
158
|
+
camera = dxcam.create() # max_buffer_len is accepted but ignored
|
|
158
159
|
```
|
|
159
160
|
|
|
160
161
|
### Target FPS
|
|
@@ -179,7 +180,7 @@ For `backend="dxgi"`, this value comes from `DXGI_OUTDUPL_FRAME_INFO.LastPresent
|
|
|
179
180
|
For `backend="winrt"`, this value is derived from WinRT `SystemRelativeTime`.
|
|
180
181
|
|
|
181
182
|
### Video Mode
|
|
182
|
-
With `video_mode=True`, DXcam
|
|
183
|
+
With `video_mode=True`, DXcam continues publishing at target FPS, reusing the previous frame when no new frame is rendered.
|
|
183
184
|
|
|
184
185
|
```python
|
|
185
186
|
import cv2
|
|
@@ -216,22 +217,22 @@ Guideline:
|
|
|
216
217
|
- Try `winrt` if it performs better on your machine or fits your app constraints.
|
|
217
218
|
|
|
218
219
|
### Processor Backend
|
|
219
|
-
DXcam capture backends (`dxgi`/`winrt`)
|
|
220
|
-
The processor backend then handles post-processing:
|
|
220
|
+
DXcam capture backends (`dxgi`/`winrt`) acquire raw BGRA frame. The processor backend then handles post-processing:
|
|
221
221
|
- optional rotation/cropping preparation
|
|
222
222
|
- color conversion to your `output_color`
|
|
223
223
|
|
|
224
224
|
Recommended backend choice:
|
|
225
225
|
- OpenCV installed: use `cv2` (default)
|
|
226
|
-
- No OpenCV installed: use `numpy`
|
|
226
|
+
- No OpenCV installed: use `numpy`/`cython`
|
|
227
227
|
|
|
228
228
|
Use it like this:
|
|
229
229
|
```python
|
|
230
230
|
camera = dxcam.create(processor_backend="cv2")
|
|
231
|
+
camera = dxcam.create(processor_backend="cython")
|
|
231
232
|
camera = dxcam.create(processor_backend="numpy")
|
|
232
233
|
```
|
|
233
234
|
|
|
234
|
-
Official Windows wheels already include the compiled
|
|
235
|
+
Official Windows wheels already include the compiled Cython processor kernels.
|
|
235
236
|
|
|
236
237
|
Only for source installs:
|
|
237
238
|
```bash
|
|
@@ -243,6 +244,10 @@ If `processor_backend="numpy"` is selected but compiled kernels are unavailable,
|
|
|
243
244
|
DXcam logs a warning and falls back to `cv2` behavior. In that fallback path,
|
|
244
245
|
install OpenCV for non-`BGRA` output modes.
|
|
245
246
|
|
|
247
|
+
If `processor_backend="cython"` is selected but compiled kernels are unavailable,
|
|
248
|
+
DXcam raises a runtime error because that backend is explicitly the direct
|
|
249
|
+
Cython path.
|
|
250
|
+
|
|
246
251
|
## Benchmarks
|
|
247
252
|
When using a similar logic (only capture newly rendered frames) running on a 240fps output, ```DXCam, python-mss, D3DShot``` benchmarked as follow:
|
|
248
253
|
|
|
@@ -254,10 +259,10 @@ When using a similar logic (only capture newly rendered frames) running on a 240
|
|
|
254
259
|
The benchmark is across 5 runs, with a light-moderate usage on my PC (5900X + 3090; Chrome ~30tabs, VS Code opened, etc.), I used the [Blur Buster UFO test](https://www.testufo.com/framerates#count=5&background=stars&pps=960) to constantly render 240 fps on my monitor. DXcam captured almost every frame rendered. You will see some benchmarks online claiming 1000+fps capture while most of them is busy-spinning a for loop on a staled frame (no new frame rendered on screen in test scenario).
|
|
255
260
|
|
|
256
261
|
### For Targeting FPS:
|
|
257
|
-
| (Target)\\(mean,std) | DXcam | python-mss | D3DShot |
|
|
262
|
+
| (Target)\\(mean,std) | DXcam (4k) | python-mss | D3DShot (1080p) |
|
|
258
263
|
|------------- |-------- |------------|---------|
|
|
259
|
-
| 60fps |
|
|
260
|
-
| 30fps | 30.
|
|
264
|
+
| 60fps | 59.99, 0.04 :checkered_flag: | N/A | 47.11, 1.33 |
|
|
265
|
+
| 30fps | 30.00, 0.00 :checkered_flag: | N/A | 21.24, 0.17 |
|
|
261
266
|
|
|
262
267
|
|
|
263
268
|
## Work Referenced
|
|
@@ -24,7 +24,7 @@ import weakref
|
|
|
24
24
|
from types import FrameType
|
|
25
25
|
from typing import Any, Callable, cast
|
|
26
26
|
|
|
27
|
-
from dxcam.
|
|
27
|
+
from dxcam.runtime.backend import normalize_backend_name
|
|
28
28
|
from dxcam.dxcam import DXCamera, Output, Device
|
|
29
29
|
from dxcam.processor import normalize_processor_backend_name
|
|
30
30
|
from dxcam.types import CaptureBackend, ColorMode, ProcessorBackend, Region
|
|
@@ -140,8 +140,8 @@ class DXFactory(metaclass=Singleton):
|
|
|
140
140
|
if existing_camera is not None:
|
|
141
141
|
logger.warning(
|
|
142
142
|
"DXCamera instance already exists for device=%s output=%s backend=%s; "
|
|
143
|
-
"returning existing instance.
|
|
144
|
-
"
|
|
143
|
+
"returning the existing instance. Call release() before "
|
|
144
|
+
"recreating it with new parameters.",
|
|
145
145
|
device_idx,
|
|
146
146
|
output_idx,
|
|
147
147
|
backend,
|
|
@@ -239,11 +239,14 @@ def create(
|
|
|
239
239
|
the primary output.
|
|
240
240
|
region: Optional capture region as ``(left, top, right, bottom)``.
|
|
241
241
|
output_color: Output pixel format.
|
|
242
|
-
max_buffer_len:
|
|
242
|
+
max_buffer_len: Kept for API compatibility. Threaded capture uses a
|
|
243
|
+
fixed three-slot latest-only frame buffer.
|
|
243
244
|
backend: Capture backend, ``"dxgi"`` or ``"winrt"``.
|
|
244
|
-
processor_backend: Post-processing backend, ``"cv2"`` (default)
|
|
245
|
-
or ``"numpy"``. The ``"
|
|
246
|
-
|
|
245
|
+
processor_backend: Post-processing backend, ``"cv2"`` (default),
|
|
246
|
+
``"cython"``, or ``"numpy"``. The ``"cython"`` backend uses the
|
|
247
|
+
direct compiled Cython processor, while ``"numpy"`` uses compiled
|
|
248
|
+
conversion kernels when available and falls back to cv2 behavior
|
|
249
|
+
otherwise.
|
|
247
250
|
|
|
248
251
|
Returns:
|
|
249
252
|
A :class:`dxcam.dxcam.DXCamera` instance.
|
|
@@ -19,6 +19,9 @@ D3D11_USAGE_IMMUTABLE = 1
|
|
|
19
19
|
D3D11_USAGE_DYNAMIC = 2
|
|
20
20
|
D3D11_USAGE_STAGING = 3
|
|
21
21
|
|
|
22
|
+
D3D11_MAP_READ = 1
|
|
23
|
+
D3D11_MAP_FLAG_DO_NOT_WAIT = 0x100000
|
|
24
|
+
|
|
22
25
|
DXGI_FORMAT_B8G8R8A8_UNORM = 87
|
|
23
26
|
|
|
24
27
|
|
|
@@ -55,6 +58,14 @@ class D3D11_TEXTURE2D_DESC(ctypes.Structure):
|
|
|
55
58
|
]
|
|
56
59
|
|
|
57
60
|
|
|
61
|
+
class D3D11_MAPPED_SUBRESOURCE(ctypes.Structure):
|
|
62
|
+
_fields_ = [
|
|
63
|
+
("pData", ctypes.c_void_p),
|
|
64
|
+
("RowPitch", wintypes.UINT),
|
|
65
|
+
("DepthPitch", wintypes.UINT),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
58
69
|
class ID3D11DeviceChild(comtypes.IUnknown):
|
|
59
70
|
_iid_ = comtypes.GUID("{1841e5c8-16b0-489b-bcc8-44cfb0d5deae}")
|
|
60
71
|
_methods_ = [
|
|
@@ -10,6 +10,7 @@ DXGI_ERROR_NOT_FOUND = 0x887A0002
|
|
|
10
10
|
DXGI_ERROR_SESSION_DISCONNECTED = 0x887A0028
|
|
11
11
|
DXGI_ERROR_UNSUPPORTED = 0x887A0004
|
|
12
12
|
DXGI_ERROR_WAIT_TIMEOUT = 0x887A0027
|
|
13
|
+
DXGI_ERROR_WAS_STILL_DRAWING = 0x887A000A
|
|
13
14
|
|
|
14
15
|
DXGI_OUTDUPL_FLAG_NONE = 0x0
|
|
15
16
|
DXGI_OUTDUPL_FLAG_COMPOSITED_UI_CAPTURE_ONLY = 0x1
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def clear_com_pointer(pointer: Any) -> None:
|
|
8
|
+
"""Set a COM pointer value to NULL in-place without calling Release()."""
|
|
9
|
+
if pointer is None:
|
|
10
|
+
return
|
|
11
|
+
try:
|
|
12
|
+
raw = ctypes.cast(ctypes.byref(pointer), ctypes.POINTER(ctypes.c_void_p))
|
|
13
|
+
raw[0] = None
|
|
14
|
+
except Exception:
|
|
15
|
+
# Best-effort cleanup helper; callers may still overwrite the attribute.
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def release_com_pointer(pointer: Any) -> None:
|
|
20
|
+
"""Release a COM pointer once, then null it to avoid late double-Release."""
|
|
21
|
+
if pointer is None or not pointer:
|
|
22
|
+
return
|
|
23
|
+
try:
|
|
24
|
+
pointer.Release()
|
|
25
|
+
finally:
|
|
26
|
+
clear_com_pointer(pointer)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, ContextManager, Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@runtime_checkable
|
|
7
|
+
class FrameDuplicator(Protocol):
|
|
8
|
+
"""Minimal capture backend contract consumed by DXCamera/runtime."""
|
|
9
|
+
|
|
10
|
+
texture: Any
|
|
11
|
+
|
|
12
|
+
def acquire_frame(
|
|
13
|
+
self, wait_for_frame: bool = False
|
|
14
|
+
) -> ContextManager[tuple[bool, bool, int]]: ...
|
|
15
|
+
|
|
16
|
+
def ticks_to_seconds(self, ticks: int) -> float: ...
|
|
17
|
+
|
|
18
|
+
def release(self) -> None: ...
|
|
@@ -3,8 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
import ctypes
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
|
+
from contextlib import contextmanager
|
|
6
7
|
from dataclasses import InitVar, dataclass, field
|
|
7
|
-
from typing import Any, cast
|
|
8
|
+
from typing import Any, Iterator, cast
|
|
8
9
|
|
|
9
10
|
import comtypes
|
|
10
11
|
|
|
@@ -17,6 +18,7 @@ from dxcam._libs.dxgi import (
|
|
|
17
18
|
IDXGIOutput5,
|
|
18
19
|
IDXGIResource,
|
|
19
20
|
)
|
|
21
|
+
from dxcam.core.com_ptr import release_com_pointer
|
|
20
22
|
from dxcam.core.device import Device
|
|
21
23
|
from dxcam.core.dxgi_errors import (
|
|
22
24
|
DXGITransientContext,
|
|
@@ -40,7 +42,6 @@ class DXGIDuplicator:
|
|
|
40
42
|
"""Desktop Duplication API wrapper for acquiring frame textures."""
|
|
41
43
|
|
|
42
44
|
texture: Any = field(default_factory=lambda: ctypes.POINTER(ID3D11Texture2D)())
|
|
43
|
-
early_release_frame: bool = True
|
|
44
45
|
duplicator: Any = None
|
|
45
46
|
updated: bool = False
|
|
46
47
|
output: InitVar[Output | None] = None
|
|
@@ -51,6 +52,10 @@ class DXGIDuplicator:
|
|
|
51
52
|
performance_frequency: int = 0
|
|
52
53
|
_frame_held: bool = False
|
|
53
54
|
|
|
55
|
+
def _drop_texture_reference(self) -> None:
|
|
56
|
+
release_com_pointer(self.texture)
|
|
57
|
+
self.texture = ctypes.POINTER(ID3D11Texture2D)()
|
|
58
|
+
|
|
54
59
|
def __post_init__(self, output: Output | None, device: Device | None) -> None:
|
|
55
60
|
if output is None or device is None:
|
|
56
61
|
raise ValueError(
|
|
@@ -126,60 +131,77 @@ class DXGIDuplicator:
|
|
|
126
131
|
self.duplicator = ctypes.POINTER(IDXGIOutputDuplication)()
|
|
127
132
|
return False
|
|
128
133
|
|
|
129
|
-
def
|
|
134
|
+
def _update_frame(self, wait_for_frame: bool = False) -> bool:
|
|
130
135
|
del wait_for_frame # DXGI path does not use extra wait logic.
|
|
131
136
|
if self._frame_held:
|
|
132
|
-
if not self.
|
|
137
|
+
if not self._release_frame():
|
|
133
138
|
self.updated = False
|
|
134
139
|
return False
|
|
135
140
|
|
|
136
141
|
info = DXGI_OUTDUPL_FRAME_INFO()
|
|
137
142
|
res = ctypes.POINTER(IDXGIResource)()
|
|
138
143
|
try:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
except comtypes.COMError as ce:
|
|
145
|
-
hresult = com_error_hresult_u32(ce)
|
|
146
|
-
if hresult == DXGI_ERROR_WAIT_TIMEOUT_U32:
|
|
147
|
-
self.updated = False
|
|
148
|
-
self.accumulated_frames = 0
|
|
149
|
-
return True
|
|
150
|
-
if is_transient_hresult(
|
|
151
|
-
hresult,
|
|
152
|
-
DXGITransientContext.FRAME_INFO,
|
|
153
|
-
DXGITransientContext.SYSTEM_TRANSITION,
|
|
154
|
-
):
|
|
155
|
-
logger.warning(
|
|
156
|
-
"Desktop duplication access loss/system transition detected "
|
|
157
|
-
"(HRESULT=0x%08X). Triggering output-change recovery.",
|
|
158
|
-
hresult,
|
|
144
|
+
try:
|
|
145
|
+
self.duplicator.AcquireNextFrame(
|
|
146
|
+
0,
|
|
147
|
+
ctypes.byref(info),
|
|
148
|
+
ctypes.byref(res),
|
|
159
149
|
)
|
|
150
|
+
except comtypes.COMError as ce:
|
|
151
|
+
hresult = com_error_hresult_u32(ce)
|
|
152
|
+
if hresult == DXGI_ERROR_WAIT_TIMEOUT_U32:
|
|
153
|
+
self.updated = False
|
|
154
|
+
self.accumulated_frames = 0
|
|
155
|
+
return True
|
|
156
|
+
if is_transient_hresult(
|
|
157
|
+
hresult,
|
|
158
|
+
DXGITransientContext.FRAME_INFO,
|
|
159
|
+
DXGITransientContext.SYSTEM_TRANSITION,
|
|
160
|
+
):
|
|
161
|
+
logger.warning(
|
|
162
|
+
"Desktop duplication access loss/system transition detected "
|
|
163
|
+
"(HRESULT=0x%08X). Triggering output-change recovery.",
|
|
164
|
+
hresult,
|
|
165
|
+
)
|
|
166
|
+
self.updated = False
|
|
167
|
+
self._frame_held = False
|
|
168
|
+
return False
|
|
169
|
+
raise
|
|
170
|
+
self._frame_held = True
|
|
171
|
+
try:
|
|
172
|
+
resource = cast(Any, res)
|
|
173
|
+
self._drop_texture_reference()
|
|
174
|
+
self.texture = resource.QueryInterface(ID3D11Texture2D)
|
|
175
|
+
except comtypes.COMError:
|
|
176
|
+
self._release_frame()
|
|
177
|
+
self.texture = ctypes.POINTER(ID3D11Texture2D)()
|
|
160
178
|
self.updated = False
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
self.
|
|
171
|
-
self.updated = False
|
|
179
|
+
return True
|
|
180
|
+
present_ticks = int(info.LastPresentTime)
|
|
181
|
+
self.accumulated_frames = int(info.AccumulatedFrames)
|
|
182
|
+
if present_ticks > 0:
|
|
183
|
+
self.latest_frame_ticks = present_ticks
|
|
184
|
+
else:
|
|
185
|
+
mouse_ticks = int(info.LastMouseUpdateTime)
|
|
186
|
+
if mouse_ticks > 0:
|
|
187
|
+
self.latest_frame_ticks = mouse_ticks
|
|
188
|
+
self.updated = True
|
|
172
189
|
return True
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
190
|
+
finally:
|
|
191
|
+
release_com_pointer(res)
|
|
192
|
+
|
|
193
|
+
@contextmanager
|
|
194
|
+
def acquire_frame(
|
|
195
|
+
self, wait_for_frame: bool = False
|
|
196
|
+
) -> Iterator[tuple[bool, bool, int]]:
|
|
197
|
+
ok = self._update_frame(wait_for_frame=wait_for_frame)
|
|
198
|
+
updated = ok and self.updated
|
|
199
|
+
frame_ticks = self.latest_frame_ticks
|
|
200
|
+
try:
|
|
201
|
+
yield ok, updated, frame_ticks
|
|
202
|
+
finally:
|
|
203
|
+
if ok and updated:
|
|
204
|
+
self._finish_frame()
|
|
183
205
|
|
|
184
206
|
@property
|
|
185
207
|
def latest_frame_time(self) -> float:
|
|
@@ -190,7 +212,7 @@ class DXGIDuplicator:
|
|
|
190
212
|
return 0.0
|
|
191
213
|
return ticks / self.performance_frequency
|
|
192
214
|
|
|
193
|
-
def
|
|
215
|
+
def _release_frame(self) -> bool:
|
|
194
216
|
"""Per Microsoft Doc
|
|
195
217
|
https://learn.microsoft.com/en-us/windows/win32/api/dxgi1_2/nf-dxgi1_2-idxgioutputduplication-releaseframe#remarks
|
|
196
218
|
This should be called just before AquireNextFrame, but we found audio artifacts
|
|
@@ -203,6 +225,7 @@ class DXGIDuplicator:
|
|
|
203
225
|
"""
|
|
204
226
|
if self.duplicator is None or not self._frame_held:
|
|
205
227
|
return True
|
|
228
|
+
self._drop_texture_reference()
|
|
206
229
|
try:
|
|
207
230
|
self.duplicator.ReleaseFrame()
|
|
208
231
|
except comtypes.COMError as ce:
|
|
@@ -223,11 +246,16 @@ class DXGIDuplicator:
|
|
|
223
246
|
self._frame_held = False
|
|
224
247
|
return True
|
|
225
248
|
|
|
249
|
+
def _finish_frame(self) -> bool:
|
|
250
|
+
# DXGI path releases immediately after staging copy for better pacing.
|
|
251
|
+
return self._release_frame()
|
|
252
|
+
|
|
226
253
|
def release(self) -> None:
|
|
254
|
+
self._drop_texture_reference()
|
|
227
255
|
if self.duplicator is not None:
|
|
228
|
-
self.
|
|
256
|
+
self._release_frame()
|
|
229
257
|
try:
|
|
230
|
-
self.duplicator
|
|
258
|
+
release_com_pointer(self.duplicator)
|
|
231
259
|
except comtypes.COMError:
|
|
232
260
|
logger.debug(
|
|
233
261
|
"Ignoring COMError while releasing duplicator.", exc_info=True
|