capturekit 0.1.0__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.
- capturekit/__init__.py +164 -0
- capturekit/_backends/__init__.py +0 -0
- capturekit/_backends/base.py +89 -0
- capturekit/_backends/linux/__init__.py +1 -0
- capturekit/_backends/linux/_xlib.py +655 -0
- capturekit/_backends/linux/api.py +160 -0
- capturekit/_backends/linux/pulse.py +464 -0
- capturekit/_backends/linux/x11.py +720 -0
- capturekit/_backends/windows/__init__.py +0 -0
- capturekit/_backends/windows/_com.py +350 -0
- capturekit/_backends/windows/_d3d11.py +307 -0
- capturekit/_backends/windows/_win32.py +420 -0
- capturekit/_backends/windows/_winrt.py +162 -0
- capturekit/_backends/windows/api.py +214 -0
- capturekit/_backends/windows/cursor.py +66 -0
- capturekit/_backends/windows/dxgi_duplication.py +310 -0
- capturekit/_backends/windows/wasapi.py +570 -0
- capturekit/_backends/windows/wgc.py +398 -0
- capturekit/_pipeline/__init__.py +1 -0
- capturekit/_pipeline/aligner.py +177 -0
- capturekit/_pipeline/clock.py +47 -0
- capturekit/_pipeline/frame_queue.py +120 -0
- capturekit/_pipeline/mixer.py +80 -0
- capturekit/_pipeline/timeline.py +133 -0
- capturekit/_platform.py +24 -0
- capturekit/_version.py +1 -0
- capturekit/_worker.py +100 -0
- capturekit/capabilities.py +79 -0
- capturekit/capture.py +141 -0
- capturekit/config.py +85 -0
- capturekit/encoding/__init__.py +5 -0
- capturekit/encoding/ffmpeg.py +356 -0
- capturekit/encoding/pipes.py +212 -0
- capturekit/errors.py +109 -0
- capturekit/py.typed +0 -0
- capturekit/recording.py +940 -0
- capturekit/sources.py +124 -0
- capturekit/types.py +211 -0
- capturekit-0.1.0.dist-info/METADATA +112 -0
- capturekit-0.1.0.dist-info/RECORD +41 -0
- capturekit-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"""Win32 helpers: DPI context, monitor and window enumeration, process info."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import ctypes
|
|
7
|
+
import os
|
|
8
|
+
from collections.abc import Iterator
|
|
9
|
+
from ctypes import POINTER, Structure, byref, c_int, c_long, c_uint, c_void_p, sizeof
|
|
10
|
+
from ctypes import wintypes as wt
|
|
11
|
+
|
|
12
|
+
from ...types import Monitor, Rect, Window
|
|
13
|
+
|
|
14
|
+
user32 = ctypes.WinDLL("user32", use_last_error=True)
|
|
15
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
16
|
+
dwmapi = ctypes.WinDLL("dwmapi")
|
|
17
|
+
try:
|
|
18
|
+
shcore: ctypes.WinDLL | None = ctypes.WinDLL("shcore")
|
|
19
|
+
except OSError: # pragma: no cover - Windows 7
|
|
20
|
+
shcore = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MONITORINFOEXW(Structure):
|
|
24
|
+
_fields_ = [
|
|
25
|
+
("cbSize", wt.DWORD),
|
|
26
|
+
("rcMonitor", wt.RECT),
|
|
27
|
+
("rcWork", wt.RECT),
|
|
28
|
+
("dwFlags", wt.DWORD),
|
|
29
|
+
("szDevice", wt.WCHAR * 32),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DISPLAY_DEVICEW(Structure):
|
|
34
|
+
_fields_ = [
|
|
35
|
+
("cb", wt.DWORD),
|
|
36
|
+
("DeviceName", wt.WCHAR * 32),
|
|
37
|
+
("DeviceString", wt.WCHAR * 128),
|
|
38
|
+
("StateFlags", wt.DWORD),
|
|
39
|
+
("DeviceID", wt.WCHAR * 128),
|
|
40
|
+
("DeviceKey", wt.WCHAR * 128),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DEVMODEW(Structure):
|
|
45
|
+
_fields_ = [
|
|
46
|
+
("dmDeviceName", wt.WCHAR * 32),
|
|
47
|
+
("dmSpecVersion", wt.WORD),
|
|
48
|
+
("dmDriverVersion", wt.WORD),
|
|
49
|
+
("dmSize", wt.WORD),
|
|
50
|
+
("dmDriverExtra", wt.WORD),
|
|
51
|
+
("dmFields", wt.DWORD),
|
|
52
|
+
("dmPositionX", wt.LONG),
|
|
53
|
+
("dmPositionY", wt.LONG),
|
|
54
|
+
("dmDisplayOrientation", wt.DWORD),
|
|
55
|
+
("dmDisplayFixedOutput", wt.DWORD),
|
|
56
|
+
("dmColor", ctypes.c_short),
|
|
57
|
+
("dmDuplex", ctypes.c_short),
|
|
58
|
+
("dmYResolution", ctypes.c_short),
|
|
59
|
+
("dmTTOption", ctypes.c_short),
|
|
60
|
+
("dmCollate", ctypes.c_short),
|
|
61
|
+
("dmFormName", wt.WCHAR * 32),
|
|
62
|
+
("dmLogPixels", wt.WORD),
|
|
63
|
+
("dmBitsPerPel", wt.DWORD),
|
|
64
|
+
("dmPelsWidth", wt.DWORD),
|
|
65
|
+
("dmPelsHeight", wt.DWORD),
|
|
66
|
+
("dmDisplayFlags", wt.DWORD),
|
|
67
|
+
("dmDisplayFrequency", wt.DWORD),
|
|
68
|
+
("dmICMMethod", wt.DWORD),
|
|
69
|
+
("dmICMIntent", wt.DWORD),
|
|
70
|
+
("dmMediaType", wt.DWORD),
|
|
71
|
+
("dmDitherType", wt.DWORD),
|
|
72
|
+
("dmReserved1", wt.DWORD),
|
|
73
|
+
("dmReserved2", wt.DWORD),
|
|
74
|
+
("dmPanningWidth", wt.DWORD),
|
|
75
|
+
("dmPanningHeight", wt.DWORD),
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
MONITORENUMPROC = ctypes.WINFUNCTYPE(wt.BOOL, wt.HMONITOR, wt.HDC, POINTER(wt.RECT), wt.LPARAM)
|
|
80
|
+
WNDENUMPROC = ctypes.WINFUNCTYPE(wt.BOOL, wt.HWND, wt.LPARAM)
|
|
81
|
+
|
|
82
|
+
user32.EnumDisplayMonitors.argtypes = (wt.HDC, c_void_p, MONITORENUMPROC, wt.LPARAM)
|
|
83
|
+
user32.EnumDisplayMonitors.restype = wt.BOOL
|
|
84
|
+
user32.GetMonitorInfoW.argtypes = (wt.HMONITOR, c_void_p)
|
|
85
|
+
user32.GetMonitorInfoW.restype = wt.BOOL
|
|
86
|
+
user32.EnumDisplaySettingsW.argtypes = (wt.LPCWSTR, wt.DWORD, c_void_p)
|
|
87
|
+
user32.EnumDisplaySettingsW.restype = wt.BOOL
|
|
88
|
+
user32.EnumDisplayDevicesW.argtypes = (wt.LPCWSTR, wt.DWORD, c_void_p, wt.DWORD)
|
|
89
|
+
user32.EnumDisplayDevicesW.restype = wt.BOOL
|
|
90
|
+
user32.EnumWindows.argtypes = (WNDENUMPROC, wt.LPARAM)
|
|
91
|
+
user32.EnumWindows.restype = wt.BOOL
|
|
92
|
+
user32.IsWindow.argtypes = (wt.HWND,)
|
|
93
|
+
user32.IsWindow.restype = wt.BOOL
|
|
94
|
+
user32.IsWindowVisible.argtypes = (wt.HWND,)
|
|
95
|
+
user32.IsWindowVisible.restype = wt.BOOL
|
|
96
|
+
user32.IsIconic.argtypes = (wt.HWND,)
|
|
97
|
+
user32.IsIconic.restype = wt.BOOL
|
|
98
|
+
user32.GetWindowTextLengthW.argtypes = (wt.HWND,)
|
|
99
|
+
user32.GetWindowTextLengthW.restype = c_int
|
|
100
|
+
user32.GetWindowTextW.argtypes = (wt.HWND, wt.LPWSTR, c_int)
|
|
101
|
+
user32.GetWindowTextW.restype = c_int
|
|
102
|
+
user32.GetClassNameW.argtypes = (wt.HWND, wt.LPWSTR, c_int)
|
|
103
|
+
user32.GetClassNameW.restype = c_int
|
|
104
|
+
user32.GetWindowThreadProcessId.argtypes = (wt.HWND, POINTER(wt.DWORD))
|
|
105
|
+
user32.GetWindowThreadProcessId.restype = wt.DWORD
|
|
106
|
+
user32.GetWindowLongPtrW.argtypes = (wt.HWND, c_int)
|
|
107
|
+
user32.GetWindowLongPtrW.restype = ctypes.c_ssize_t
|
|
108
|
+
user32.GetAncestor.argtypes = (wt.HWND, c_uint)
|
|
109
|
+
user32.GetAncestor.restype = wt.HWND
|
|
110
|
+
user32.GetWindowRect.argtypes = (wt.HWND, POINTER(wt.RECT))
|
|
111
|
+
user32.GetWindowRect.restype = wt.BOOL
|
|
112
|
+
user32.GetClientRect.argtypes = (wt.HWND, POINTER(wt.RECT))
|
|
113
|
+
user32.GetClientRect.restype = wt.BOOL
|
|
114
|
+
user32.ClientToScreen.argtypes = (wt.HWND, POINTER(wt.POINT))
|
|
115
|
+
user32.ClientToScreen.restype = wt.BOOL
|
|
116
|
+
user32.GetWindowDisplayAffinity.argtypes = (wt.HWND, POINTER(wt.DWORD))
|
|
117
|
+
user32.GetWindowDisplayAffinity.restype = wt.BOOL
|
|
118
|
+
user32.MonitorFromWindow.argtypes = (wt.HWND, wt.DWORD)
|
|
119
|
+
user32.MonitorFromWindow.restype = wt.HMONITOR
|
|
120
|
+
user32.SetThreadDpiAwarenessContext.argtypes = (c_void_p,)
|
|
121
|
+
user32.SetThreadDpiAwarenessContext.restype = c_void_p
|
|
122
|
+
dwmapi.DwmGetWindowAttribute.argtypes = (wt.HWND, wt.DWORD, c_void_p, wt.DWORD)
|
|
123
|
+
dwmapi.DwmGetWindowAttribute.restype = c_long
|
|
124
|
+
kernel32.OpenProcess.argtypes = (wt.DWORD, wt.BOOL, wt.DWORD)
|
|
125
|
+
kernel32.OpenProcess.restype = wt.HANDLE
|
|
126
|
+
kernel32.CloseHandle.argtypes = (wt.HANDLE,)
|
|
127
|
+
kernel32.CloseHandle.restype = wt.BOOL
|
|
128
|
+
kernel32.QueryFullProcessImageNameW.argtypes = (wt.HANDLE, wt.DWORD, wt.LPWSTR, POINTER(wt.DWORD))
|
|
129
|
+
kernel32.QueryFullProcessImageNameW.restype = wt.BOOL
|
|
130
|
+
if shcore is not None:
|
|
131
|
+
shcore.GetDpiForMonitor.argtypes = (wt.HMONITOR, c_int, POINTER(c_uint), POINTER(c_uint))
|
|
132
|
+
shcore.GetDpiForMonitor.restype = c_long
|
|
133
|
+
|
|
134
|
+
MONITORINFOF_PRIMARY = 1
|
|
135
|
+
ENUM_CURRENT_SETTINGS = 0xFFFFFFFF
|
|
136
|
+
GWL_STYLE = -16
|
|
137
|
+
GWL_EXSTYLE = -20
|
|
138
|
+
WS_CHILD = 0x40000000
|
|
139
|
+
WS_EX_TOOLWINDOW = 0x00000080
|
|
140
|
+
WS_EX_NOREDIRECTIONBITMAP = 0x00200000
|
|
141
|
+
GA_ROOT = 2
|
|
142
|
+
DWMWA_EXTENDED_FRAME_BOUNDS = 9
|
|
143
|
+
DWMWA_CLOAKED = 14
|
|
144
|
+
WDA_NONE = 0
|
|
145
|
+
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
|
146
|
+
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = c_void_p(-4)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@contextlib.contextmanager
|
|
150
|
+
def per_monitor_dpi_aware() -> Iterator[None]:
|
|
151
|
+
"""Use physical-pixel coordinates on this thread only.
|
|
152
|
+
|
|
153
|
+
The process-wide DPI awareness belongs to the host application, so we
|
|
154
|
+
switch the *thread* context temporarily instead.
|
|
155
|
+
"""
|
|
156
|
+
old = user32.SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)
|
|
157
|
+
try:
|
|
158
|
+
yield
|
|
159
|
+
finally:
|
|
160
|
+
if old:
|
|
161
|
+
user32.SetThreadDpiAwarenessContext(c_void_p(old))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _rect(r: wt.RECT) -> Rect:
|
|
165
|
+
return Rect.from_ltrb(r.left, r.top, r.right, r.bottom)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# --- monitors ----------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def list_monitors() -> list[Monitor]:
|
|
172
|
+
handles: list[int] = []
|
|
173
|
+
|
|
174
|
+
def cb(hmon: int, _hdc: int, _rc: object, _lp: int) -> bool:
|
|
175
|
+
handles.append(hmon)
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
proc = MONITORENUMPROC(cb)
|
|
179
|
+
monitors: list[Monitor] = []
|
|
180
|
+
with per_monitor_dpi_aware():
|
|
181
|
+
user32.EnumDisplayMonitors(None, None, proc, 0)
|
|
182
|
+
for hmon in handles:
|
|
183
|
+
info = MONITORINFOEXW()
|
|
184
|
+
info.cbSize = sizeof(info)
|
|
185
|
+
if not user32.GetMonitorInfoW(hmon, byref(info)):
|
|
186
|
+
continue
|
|
187
|
+
device = info.szDevice
|
|
188
|
+
scale = 1.0
|
|
189
|
+
if shcore is not None:
|
|
190
|
+
dx, dy = c_uint(), c_uint()
|
|
191
|
+
if shcore.GetDpiForMonitor(hmon, 0, byref(dx), byref(dy)) == 0:
|
|
192
|
+
scale = dx.value / 96.0
|
|
193
|
+
mode = DEVMODEW()
|
|
194
|
+
mode.dmSize = sizeof(mode)
|
|
195
|
+
refresh = None
|
|
196
|
+
if user32.EnumDisplaySettingsW(device, ENUM_CURRENT_SETTINGS, byref(mode)):
|
|
197
|
+
refresh = float(mode.dmDisplayFrequency) or None
|
|
198
|
+
dd = DISPLAY_DEVICEW()
|
|
199
|
+
dd.cb = sizeof(dd)
|
|
200
|
+
name = device
|
|
201
|
+
if user32.EnumDisplayDevicesW(device, 0, byref(dd), 0) and dd.DeviceString:
|
|
202
|
+
name = dd.DeviceString
|
|
203
|
+
monitors.append(
|
|
204
|
+
Monitor(
|
|
205
|
+
id=device,
|
|
206
|
+
index=0,
|
|
207
|
+
name=name,
|
|
208
|
+
rect=_rect(info.rcMonitor),
|
|
209
|
+
is_primary=bool(info.dwFlags & MONITORINFOF_PRIMARY),
|
|
210
|
+
scale=scale,
|
|
211
|
+
refresh_rate=refresh,
|
|
212
|
+
native_handle=int(hmon),
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
# Stable order: primary first, then left-to-right, top-to-bottom.
|
|
216
|
+
monitors.sort(key=lambda m: (not m.is_primary, m.rect.x, m.rect.y))
|
|
217
|
+
return [
|
|
218
|
+
Monitor(
|
|
219
|
+
m.id, i, m.name, m.rect, m.is_primary, m.scale, m.refresh_rate, None, m.native_handle
|
|
220
|
+
)
|
|
221
|
+
for i, m in enumerate(monitors)
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# --- windows -------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def is_window(hwnd: int) -> bool:
|
|
229
|
+
return bool(user32.IsWindow(hwnd))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def window_title(hwnd: int) -> str:
|
|
233
|
+
n = user32.GetWindowTextLengthW(hwnd)
|
|
234
|
+
if n <= 0:
|
|
235
|
+
return ""
|
|
236
|
+
buf = ctypes.create_unicode_buffer(n + 1)
|
|
237
|
+
user32.GetWindowTextW(hwnd, buf, n + 1)
|
|
238
|
+
return buf.value
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def window_class(hwnd: int) -> str:
|
|
242
|
+
buf = ctypes.create_unicode_buffer(256)
|
|
243
|
+
user32.GetClassNameW(hwnd, buf, 256)
|
|
244
|
+
return buf.value
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def window_pid(hwnd: int) -> int | None:
|
|
248
|
+
pid = wt.DWORD()
|
|
249
|
+
user32.GetWindowThreadProcessId(hwnd, byref(pid))
|
|
250
|
+
return pid.value or None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def process_image_path(pid: int) -> str | None:
|
|
254
|
+
h = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
|
|
255
|
+
if not h:
|
|
256
|
+
return None
|
|
257
|
+
try:
|
|
258
|
+
size = wt.DWORD(1024)
|
|
259
|
+
buf = ctypes.create_unicode_buffer(size.value)
|
|
260
|
+
if kernel32.QueryFullProcessImageNameW(h, 0, buf, byref(size)):
|
|
261
|
+
return buf.value
|
|
262
|
+
return None
|
|
263
|
+
finally:
|
|
264
|
+
kernel32.CloseHandle(h)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def process_name(pid: int | None) -> str | None:
|
|
268
|
+
if not pid:
|
|
269
|
+
return None
|
|
270
|
+
path = process_image_path(pid)
|
|
271
|
+
return os.path.basename(path) if path else None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def is_cloaked(hwnd: int) -> bool:
|
|
275
|
+
cloaked = wt.DWORD()
|
|
276
|
+
hr = dwmapi.DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, byref(cloaked), sizeof(cloaked))
|
|
277
|
+
return hr == 0 and cloaked.value != 0
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def window_bounds(hwnd: int) -> Rect:
|
|
281
|
+
"""Visible window bounds (DWM extended frame bounds, physical pixels).
|
|
282
|
+
|
|
283
|
+
This matches the content that Windows Graphics Capture returns for a
|
|
284
|
+
window (no invisible resize borders / drop shadow).
|
|
285
|
+
"""
|
|
286
|
+
r = wt.RECT()
|
|
287
|
+
with per_monitor_dpi_aware():
|
|
288
|
+
hr = dwmapi.DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, byref(r), sizeof(r))
|
|
289
|
+
if hr != 0:
|
|
290
|
+
user32.GetWindowRect(hwnd, byref(r))
|
|
291
|
+
return _rect(r)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def client_rect_in_window(hwnd: int) -> Rect:
|
|
295
|
+
"""Client area relative to the visible window bounds (physical pixels)."""
|
|
296
|
+
with per_monitor_dpi_aware():
|
|
297
|
+
bounds = window_bounds(hwnd)
|
|
298
|
+
rc = wt.RECT()
|
|
299
|
+
user32.GetClientRect(hwnd, byref(rc))
|
|
300
|
+
pt = wt.POINT(0, 0)
|
|
301
|
+
user32.ClientToScreen(hwnd, byref(pt))
|
|
302
|
+
return Rect(pt.x - bounds.x, pt.y - bounds.y, rc.right - rc.left, rc.bottom - rc.top)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def display_affinity(hwnd: int) -> int:
|
|
306
|
+
aff = wt.DWORD()
|
|
307
|
+
if user32.GetWindowDisplayAffinity(hwnd, byref(aff)):
|
|
308
|
+
return int(aff.value)
|
|
309
|
+
return WDA_NONE
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def describe_window(hwnd: int) -> Window:
|
|
313
|
+
pid = window_pid(hwnd)
|
|
314
|
+
return Window(
|
|
315
|
+
handle=int(hwnd),
|
|
316
|
+
title=window_title(hwnd),
|
|
317
|
+
class_name=window_class(hwnd),
|
|
318
|
+
pid=pid,
|
|
319
|
+
process_name=process_name(pid),
|
|
320
|
+
rect=window_bounds(hwnd),
|
|
321
|
+
is_visible=bool(user32.IsWindowVisible(hwnd)),
|
|
322
|
+
is_minimized=bool(user32.IsIconic(hwnd)),
|
|
323
|
+
display=None,
|
|
324
|
+
capture_excluded=display_affinity(hwnd) != WDA_NONE,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _is_capturable(hwnd: int, include_untitled: bool) -> bool:
|
|
329
|
+
if not user32.IsWindowVisible(hwnd):
|
|
330
|
+
return False
|
|
331
|
+
style = user32.GetWindowLongPtrW(hwnd, GWL_STYLE)
|
|
332
|
+
exstyle = user32.GetWindowLongPtrW(hwnd, GWL_EXSTYLE)
|
|
333
|
+
if style & WS_CHILD or exstyle & WS_EX_TOOLWINDOW:
|
|
334
|
+
return False
|
|
335
|
+
if user32.GetAncestor(hwnd, GA_ROOT) != hwnd:
|
|
336
|
+
return False
|
|
337
|
+
if is_cloaked(hwnd): # hidden UWP frames, windows on other virtual desktops
|
|
338
|
+
return False
|
|
339
|
+
if not include_untitled and user32.GetWindowTextLengthW(hwnd) <= 0:
|
|
340
|
+
return False
|
|
341
|
+
return True
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def list_windows(include_untitled: bool = False) -> list[Window]:
|
|
345
|
+
hwnds: list[int] = []
|
|
346
|
+
|
|
347
|
+
def cb(hwnd: int, _lp: int) -> bool:
|
|
348
|
+
hwnds.append(hwnd)
|
|
349
|
+
return True
|
|
350
|
+
|
|
351
|
+
proc = WNDENUMPROC(cb)
|
|
352
|
+
user32.EnumWindows(proc, 0)
|
|
353
|
+
out: list[Window] = []
|
|
354
|
+
for hwnd in hwnds:
|
|
355
|
+
if not hwnd or not _is_capturable(hwnd, include_untitled):
|
|
356
|
+
continue
|
|
357
|
+
w = describe_window(hwnd)
|
|
358
|
+
if w.rect.is_empty() and not w.is_minimized:
|
|
359
|
+
continue
|
|
360
|
+
out.append(w)
|
|
361
|
+
return out
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# --- process tree --------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class PROCESSENTRY32W(Structure):
|
|
368
|
+
_fields_ = [
|
|
369
|
+
("dwSize", wt.DWORD),
|
|
370
|
+
("cntUsage", wt.DWORD),
|
|
371
|
+
("th32ProcessID", wt.DWORD),
|
|
372
|
+
("th32DefaultHeapID", ctypes.c_size_t),
|
|
373
|
+
("th32ModuleID", wt.DWORD),
|
|
374
|
+
("cntThreads", wt.DWORD),
|
|
375
|
+
("th32ParentProcessID", wt.DWORD),
|
|
376
|
+
("pcPriClassBase", c_long),
|
|
377
|
+
("dwFlags", wt.DWORD),
|
|
378
|
+
("szExeFile", wt.WCHAR * 260),
|
|
379
|
+
]
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
kernel32.CreateToolhelp32Snapshot.argtypes = (wt.DWORD, wt.DWORD)
|
|
383
|
+
kernel32.CreateToolhelp32Snapshot.restype = wt.HANDLE
|
|
384
|
+
kernel32.Process32FirstW.argtypes = (wt.HANDLE, POINTER(PROCESSENTRY32W))
|
|
385
|
+
kernel32.Process32FirstW.restype = wt.BOOL
|
|
386
|
+
kernel32.Process32NextW.argtypes = (wt.HANDLE, POINTER(PROCESSENTRY32W))
|
|
387
|
+
kernel32.Process32NextW.restype = wt.BOOL
|
|
388
|
+
TH32CS_SNAPPROCESS = 0x2
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def process_parents() -> dict[int, int]:
|
|
392
|
+
"""Map pid -> parent pid for all processes (snapshot)."""
|
|
393
|
+
snap = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
|
|
394
|
+
if not snap or snap == wt.HANDLE(-1).value:
|
|
395
|
+
return {}
|
|
396
|
+
try:
|
|
397
|
+
entry = PROCESSENTRY32W()
|
|
398
|
+
entry.dwSize = sizeof(entry)
|
|
399
|
+
result: dict[int, int] = {}
|
|
400
|
+
ok = kernel32.Process32FirstW(snap, byref(entry))
|
|
401
|
+
while ok:
|
|
402
|
+
result[int(entry.th32ProcessID)] = int(entry.th32ParentProcessID)
|
|
403
|
+
ok = kernel32.Process32NextW(snap, byref(entry))
|
|
404
|
+
return result
|
|
405
|
+
finally:
|
|
406
|
+
kernel32.CloseHandle(snap)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def process_tree(pid: int) -> set[int]:
|
|
410
|
+
"""``pid`` and all its descendants (the set WASAPI process loopback covers)."""
|
|
411
|
+
parents = process_parents()
|
|
412
|
+
tree = {pid}
|
|
413
|
+
changed = True
|
|
414
|
+
while changed:
|
|
415
|
+
changed = False
|
|
416
|
+
for child, parent in parents.items():
|
|
417
|
+
if parent in tree and child not in tree and child != parent:
|
|
418
|
+
tree.add(child)
|
|
419
|
+
changed = True
|
|
420
|
+
return tree
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""WinRT ABI helpers: HSTRING, activation factories, pinterface IIDs, delegates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
import uuid
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from ctypes import POINTER, byref, c_int64, c_long, c_uint, c_void_p, c_wchar_p
|
|
9
|
+
|
|
10
|
+
from ._com import GUID, ComObject, ComPtr, check_hr, ensure_mta_usage
|
|
11
|
+
|
|
12
|
+
_combase = ctypes.WinDLL("combase")
|
|
13
|
+
_combase.RoInitialize.argtypes = (c_uint,)
|
|
14
|
+
_combase.RoInitialize.restype = c_long
|
|
15
|
+
_combase.RoUninitialize.argtypes = ()
|
|
16
|
+
_combase.RoUninitialize.restype = None
|
|
17
|
+
_combase.WindowsCreateString.argtypes = (c_wchar_p, c_uint, POINTER(c_void_p))
|
|
18
|
+
_combase.WindowsCreateString.restype = c_long
|
|
19
|
+
_combase.WindowsDeleteString.argtypes = (c_void_p,)
|
|
20
|
+
_combase.WindowsDeleteString.restype = c_long
|
|
21
|
+
_combase.WindowsGetStringRawBuffer.argtypes = (c_void_p, POINTER(c_uint))
|
|
22
|
+
_combase.WindowsGetStringRawBuffer.restype = c_wchar_p
|
|
23
|
+
_combase.RoGetActivationFactory.argtypes = (c_void_p, c_void_p, POINTER(c_void_p))
|
|
24
|
+
_combase.RoGetActivationFactory.restype = c_long
|
|
25
|
+
|
|
26
|
+
RO_INIT_MULTITHREADED = 1
|
|
27
|
+
RPC_E_CHANGED_MODE = -2147417850
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RoApartment:
|
|
31
|
+
"""Initialise the Windows Runtime (MTA) on the current thread."""
|
|
32
|
+
|
|
33
|
+
def __enter__(self) -> RoApartment:
|
|
34
|
+
ensure_mta_usage()
|
|
35
|
+
hr = _combase.RoInitialize(RO_INIT_MULTITHREADED)
|
|
36
|
+
self._uninit = hr >= 0
|
|
37
|
+
if hr < 0 and hr != RPC_E_CHANGED_MODE:
|
|
38
|
+
check_hr(hr, "RoInitialize")
|
|
39
|
+
return self
|
|
40
|
+
|
|
41
|
+
def __exit__(self, *exc: object) -> None:
|
|
42
|
+
if self._uninit:
|
|
43
|
+
_combase.RoUninitialize()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class HString:
|
|
47
|
+
def __init__(self, text: str) -> None:
|
|
48
|
+
self.handle = c_void_p()
|
|
49
|
+
check_hr(
|
|
50
|
+
_combase.WindowsCreateString(text, len(text), byref(self.handle)),
|
|
51
|
+
"WindowsCreateString",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def close(self) -> None:
|
|
55
|
+
if self.handle:
|
|
56
|
+
_combase.WindowsDeleteString(self.handle)
|
|
57
|
+
self.handle = c_void_p()
|
|
58
|
+
|
|
59
|
+
def __enter__(self) -> HString:
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def __exit__(self, *exc: object) -> None:
|
|
63
|
+
self.close()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def hstring_to_str(handle: int | None) -> str:
|
|
67
|
+
"""Read and delete an HSTRING returned by a WinRT method."""
|
|
68
|
+
if not handle:
|
|
69
|
+
return ""
|
|
70
|
+
length = c_uint()
|
|
71
|
+
text = _combase.WindowsGetStringRawBuffer(handle, byref(length)) or ""
|
|
72
|
+
_combase.WindowsDeleteString(handle)
|
|
73
|
+
return text[: length.value]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def activation_factory(class_name: str, iid: GUID) -> ComPtr:
|
|
77
|
+
with HString(class_name) as hs:
|
|
78
|
+
out = c_void_p()
|
|
79
|
+
check_hr(
|
|
80
|
+
_combase.RoGetActivationFactory(hs.handle, byref(iid), byref(out)),
|
|
81
|
+
f"RoGetActivationFactory({class_name})",
|
|
82
|
+
)
|
|
83
|
+
return ComPtr(out.value, f"{class_name} factory")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
_PINTERFACE_NS = uuid.UUID("11f47ad5-7b73-42c0-abae-878b1e16adee")
|
|
87
|
+
_TYPED_EVENT_HANDLER = "{9de1c534-6ae1-11e0-84e1-18a905bcc53f}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def pinterface_iid(signature: str) -> GUID:
|
|
91
|
+
"""IID of a parameterised WinRT interface (UUIDv5 over the signature)."""
|
|
92
|
+
return GUID.from_buffer_copy(uuid.uuid5(_PINTERFACE_NS, signature).bytes_le)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def typed_event_handler_iid(sender_class: str, sender_default_iid: str) -> GUID:
|
|
96
|
+
"""IID of ``TypedEventHandler<sender_class, IInspectable>``."""
|
|
97
|
+
sig = (
|
|
98
|
+
f"pinterface({_TYPED_EVENT_HANDLER};"
|
|
99
|
+
f"rc({sender_class};{{{sender_default_iid.lower()}}});cinterface(IInspectable))"
|
|
100
|
+
)
|
|
101
|
+
return pinterface_iid(sig)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class EventDelegate(ComObject):
|
|
105
|
+
"""``TypedEventHandler<TSender, IInspectable>`` implemented in Python.
|
|
106
|
+
|
|
107
|
+
``callback(sender_ptr, args_ptr)`` runs on a WinRT thread-pool thread; keep
|
|
108
|
+
it short (signal an event).
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
_methods_ = (("Invoke", c_long, (c_void_p, c_void_p)),)
|
|
112
|
+
|
|
113
|
+
def __init__(self, iid: GUID, callback: Callable[[int | None, int | None], None]) -> None:
|
|
114
|
+
self._callback = callback
|
|
115
|
+
super().__init__((iid,))
|
|
116
|
+
|
|
117
|
+
def Invoke(self, sender: int | None, args: int | None) -> None:
|
|
118
|
+
self._callback(sender, args)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class EventRegistration:
|
|
122
|
+
"""Holds a delegate registered with ``add_X``; ``remove()`` unregisters."""
|
|
123
|
+
|
|
124
|
+
def __init__(self, source: ComPtr, remove_index: int, token: int, delegate: ComObject):
|
|
125
|
+
self._source = source
|
|
126
|
+
self._remove_index = remove_index
|
|
127
|
+
self._token = token
|
|
128
|
+
self._delegate: ComObject | None = delegate
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def add(
|
|
132
|
+
source: ComPtr, add_index: int, remove_index: int, delegate: ComObject, what: str
|
|
133
|
+
) -> EventRegistration:
|
|
134
|
+
token = c_int64()
|
|
135
|
+
source.hcall(
|
|
136
|
+
add_index, (c_void_p, c_void_p), c_void_p(delegate.ptr), byref(token), what=what
|
|
137
|
+
)
|
|
138
|
+
return EventRegistration(source, remove_index, token.value, delegate)
|
|
139
|
+
|
|
140
|
+
def remove(self) -> None:
|
|
141
|
+
if self._delegate is not None:
|
|
142
|
+
try:
|
|
143
|
+
self._source.call(self._remove_index, c_long, (c_int64,), self._token)
|
|
144
|
+
finally:
|
|
145
|
+
self._delegate.release()
|
|
146
|
+
self._delegate = None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def close_winrt(obj: ComPtr | None) -> None:
|
|
150
|
+
"""``IClosable::Close`` then release (ignores objects without IClosable)."""
|
|
151
|
+
if obj is None:
|
|
152
|
+
return
|
|
153
|
+
closable = obj.try_query(IID_IClosable, "IClosable")
|
|
154
|
+
if closable is not None:
|
|
155
|
+
try:
|
|
156
|
+
closable.call(6, c_long, ())
|
|
157
|
+
finally:
|
|
158
|
+
closable.release()
|
|
159
|
+
obj.release()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
IID_IClosable = GUID.from_str("30D5A829-7FA4-4026-83BB-D75BAE4EA99E")
|