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,655 @@
|
|
|
1
|
+
"""Xlib + extensions through ctypes.
|
|
2
|
+
|
|
3
|
+
Reference: docs/research/linux-x11-and-audio.md. Key safety rules:
|
|
4
|
+
|
|
5
|
+
* Xlib's default error handler calls ``exit()``: a process-global handler is
|
|
6
|
+
installed that records errors per Display; requests that may fail go
|
|
7
|
+
through :meth:`XDisplay.checked` (issue, ``XSync``, inspect).
|
|
8
|
+
* The default IO-error path also exits; ``XSetIOErrorExitHandler`` (libX11
|
|
9
|
+
>= 1.7) is used so a dying X server becomes a Python exception.
|
|
10
|
+
* One :class:`XDisplay` per thread; never shared.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import ctypes
|
|
16
|
+
import ctypes.util
|
|
17
|
+
import threading
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from ctypes import (
|
|
20
|
+
CFUNCTYPE,
|
|
21
|
+
POINTER,
|
|
22
|
+
Structure,
|
|
23
|
+
byref,
|
|
24
|
+
c_char_p,
|
|
25
|
+
c_int,
|
|
26
|
+
c_long,
|
|
27
|
+
c_short,
|
|
28
|
+
c_ubyte,
|
|
29
|
+
c_uint,
|
|
30
|
+
c_ulong,
|
|
31
|
+
c_ushort,
|
|
32
|
+
c_void_p,
|
|
33
|
+
)
|
|
34
|
+
from typing import Any, TypeVar
|
|
35
|
+
|
|
36
|
+
from ...errors import CapabilityUnavailableError, NativeError, TargetLostError
|
|
37
|
+
|
|
38
|
+
T = TypeVar("T")
|
|
39
|
+
|
|
40
|
+
XID = c_ulong
|
|
41
|
+
Window = c_ulong
|
|
42
|
+
Atom = c_ulong
|
|
43
|
+
Drawable = c_ulong
|
|
44
|
+
Pixmap = c_ulong
|
|
45
|
+
Bool = c_int
|
|
46
|
+
Status = c_int
|
|
47
|
+
|
|
48
|
+
ZPixmap = 2
|
|
49
|
+
AllPlanes = 0xFFFFFFFFFFFFFFFF
|
|
50
|
+
LSBFirst = 0
|
|
51
|
+
IsUnmapped, IsUnviewable, IsViewable = 0, 1, 2
|
|
52
|
+
InputOutput = 1
|
|
53
|
+
StructureNotifyMask = 1 << 17
|
|
54
|
+
PropertyChangeMask = 1 << 22
|
|
55
|
+
DestroyNotify, UnmapNotify, MapNotify, ConfigureNotify = 17, 18, 19, 22
|
|
56
|
+
CompositeRedirectAutomatic = 0
|
|
57
|
+
XDamageReportNonEmpty = 3
|
|
58
|
+
XA_WINDOW = 33
|
|
59
|
+
XA_CARDINAL = 6
|
|
60
|
+
AnyPropertyType = 0
|
|
61
|
+
BadWindow, BadMatch, BadDrawable, BadAccess = 3, 8, 9, 10
|
|
62
|
+
XRES_CLIENT_ID_PID_MASK = 1 << 1
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _load(name: str, soname: str, required: bool = True) -> ctypes.CDLL | None:
|
|
66
|
+
for cand in (soname, ctypes.util.find_library(name)):
|
|
67
|
+
if cand:
|
|
68
|
+
try:
|
|
69
|
+
return ctypes.CDLL(cand)
|
|
70
|
+
except OSError:
|
|
71
|
+
continue
|
|
72
|
+
if required:
|
|
73
|
+
raise CapabilityUnavailableError(
|
|
74
|
+
f"{soname} not found (install the X11 client library package, e.g. libx11-6)"
|
|
75
|
+
)
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# --- structures ------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class XImage(Structure):
|
|
83
|
+
_fields_ = [
|
|
84
|
+
("width", c_int),
|
|
85
|
+
("height", c_int),
|
|
86
|
+
("xoffset", c_int),
|
|
87
|
+
("format", c_int),
|
|
88
|
+
("data", c_void_p),
|
|
89
|
+
("byte_order", c_int),
|
|
90
|
+
("bitmap_unit", c_int),
|
|
91
|
+
("bitmap_bit_order", c_int),
|
|
92
|
+
("bitmap_pad", c_int),
|
|
93
|
+
("depth", c_int),
|
|
94
|
+
("bytes_per_line", c_int),
|
|
95
|
+
("bits_per_pixel", c_int),
|
|
96
|
+
("red_mask", c_ulong),
|
|
97
|
+
("green_mask", c_ulong),
|
|
98
|
+
("blue_mask", c_ulong),
|
|
99
|
+
("obdata", c_void_p),
|
|
100
|
+
("funcs", c_void_p * 6),
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class Visual(Structure):
|
|
105
|
+
_fields_ = [
|
|
106
|
+
("ext_data", c_void_p),
|
|
107
|
+
("visualid", c_ulong),
|
|
108
|
+
("class_", c_int),
|
|
109
|
+
("red_mask", c_ulong),
|
|
110
|
+
("green_mask", c_ulong),
|
|
111
|
+
("blue_mask", c_ulong),
|
|
112
|
+
("bits_per_rgb", c_int),
|
|
113
|
+
("map_entries", c_int),
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class XShmSegmentInfo(Structure):
|
|
118
|
+
_fields_ = [("shmseg", c_ulong), ("shmid", c_int), ("shmaddr", c_void_p), ("readOnly", Bool)]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class XWindowAttributes(Structure):
|
|
122
|
+
_fields_ = [
|
|
123
|
+
("x", c_int),
|
|
124
|
+
("y", c_int),
|
|
125
|
+
("width", c_int),
|
|
126
|
+
("height", c_int),
|
|
127
|
+
("border_width", c_int),
|
|
128
|
+
("depth", c_int),
|
|
129
|
+
("visual", c_void_p),
|
|
130
|
+
("root", Window),
|
|
131
|
+
("class_", c_int),
|
|
132
|
+
("bit_gravity", c_int),
|
|
133
|
+
("win_gravity", c_int),
|
|
134
|
+
("backing_store", c_int),
|
|
135
|
+
("backing_planes", c_ulong),
|
|
136
|
+
("backing_pixel", c_ulong),
|
|
137
|
+
("save_under", Bool),
|
|
138
|
+
("colormap", c_ulong),
|
|
139
|
+
("map_installed", Bool),
|
|
140
|
+
("map_state", c_int),
|
|
141
|
+
("all_event_masks", c_long),
|
|
142
|
+
("your_event_mask", c_long),
|
|
143
|
+
("do_not_propagate_mask", c_long),
|
|
144
|
+
("override_redirect", Bool),
|
|
145
|
+
("screen", c_void_p),
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class XErrorEvent(Structure):
|
|
150
|
+
_fields_ = [
|
|
151
|
+
("type", c_int),
|
|
152
|
+
("display", c_void_p),
|
|
153
|
+
("resourceid", XID),
|
|
154
|
+
("serial", c_ulong),
|
|
155
|
+
("error_code", c_ubyte),
|
|
156
|
+
("request_code", c_ubyte),
|
|
157
|
+
("minor_code", c_ubyte),
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class XAnyEvent(Structure):
|
|
162
|
+
_fields_ = [
|
|
163
|
+
("type", c_int),
|
|
164
|
+
("serial", c_ulong),
|
|
165
|
+
("send_event", Bool),
|
|
166
|
+
("display", c_void_p),
|
|
167
|
+
("window", Window),
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class XConfigureEvent(Structure):
|
|
172
|
+
_fields_ = [
|
|
173
|
+
("type", c_int),
|
|
174
|
+
("serial", c_ulong),
|
|
175
|
+
("send_event", Bool),
|
|
176
|
+
("display", c_void_p),
|
|
177
|
+
("event", Window),
|
|
178
|
+
("window", Window),
|
|
179
|
+
("x", c_int),
|
|
180
|
+
("y", c_int),
|
|
181
|
+
("width", c_int),
|
|
182
|
+
("height", c_int),
|
|
183
|
+
("border_width", c_int),
|
|
184
|
+
("above", Window),
|
|
185
|
+
("override_redirect", Bool),
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class XStructureEvent(Structure): # Destroy/Unmap/Map share event+window at the start
|
|
190
|
+
_fields_ = [
|
|
191
|
+
("type", c_int),
|
|
192
|
+
("serial", c_ulong),
|
|
193
|
+
("send_event", Bool),
|
|
194
|
+
("display", c_void_p),
|
|
195
|
+
("event", Window),
|
|
196
|
+
("window", Window),
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
XEvent = c_long * 24
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class XRRMonitorInfo(Structure):
|
|
204
|
+
_fields_ = [
|
|
205
|
+
("name", Atom),
|
|
206
|
+
("primary", Bool),
|
|
207
|
+
("automatic", Bool),
|
|
208
|
+
("noutput", c_int),
|
|
209
|
+
("x", c_int),
|
|
210
|
+
("y", c_int),
|
|
211
|
+
("width", c_int),
|
|
212
|
+
("height", c_int),
|
|
213
|
+
("mwidth", c_int),
|
|
214
|
+
("mheight", c_int),
|
|
215
|
+
("outputs", POINTER(c_ulong)),
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class XRRScreenResources(Structure):
|
|
220
|
+
_fields_ = [
|
|
221
|
+
("timestamp", c_ulong),
|
|
222
|
+
("configTimestamp", c_ulong),
|
|
223
|
+
("ncrtc", c_int),
|
|
224
|
+
("crtcs", POINTER(c_ulong)),
|
|
225
|
+
("noutput", c_int),
|
|
226
|
+
("outputs", POINTER(c_ulong)),
|
|
227
|
+
("nmode", c_int),
|
|
228
|
+
("modes", c_void_p),
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class XRRCrtcInfo(Structure):
|
|
233
|
+
_fields_ = [
|
|
234
|
+
("timestamp", c_ulong),
|
|
235
|
+
("x", c_int),
|
|
236
|
+
("y", c_int),
|
|
237
|
+
("width", c_uint),
|
|
238
|
+
("height", c_uint),
|
|
239
|
+
("mode", c_ulong),
|
|
240
|
+
("rotation", c_ushort),
|
|
241
|
+
("noutput", c_int),
|
|
242
|
+
("outputs", POINTER(c_ulong)),
|
|
243
|
+
("rotations", c_ushort),
|
|
244
|
+
("npossible", c_int),
|
|
245
|
+
("possible", POINTER(c_ulong)),
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class XineramaScreenInfo(Structure):
|
|
250
|
+
_fields_ = [
|
|
251
|
+
("screen_number", c_int),
|
|
252
|
+
("x_org", c_short),
|
|
253
|
+
("y_org", c_short),
|
|
254
|
+
("width", c_short),
|
|
255
|
+
("height", c_short),
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class XResClientIdSpec(Structure):
|
|
260
|
+
_fields_ = [("client", XID), ("mask", c_uint)]
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class XResClientIdValue(Structure):
|
|
264
|
+
_fields_ = [("spec", XResClientIdSpec), ("length", c_long), ("value", c_void_p)]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class XClassHint(Structure):
|
|
268
|
+
_fields_ = [("res_name", c_void_p), ("res_class", c_void_p)]
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# --- library setup ------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _sig(lib: ctypes.CDLL | None, name: str, restype: Any, *argtypes: Any) -> None:
|
|
275
|
+
if lib is None or not hasattr(lib, name):
|
|
276
|
+
return
|
|
277
|
+
fn = getattr(lib, name)
|
|
278
|
+
fn.restype = restype
|
|
279
|
+
fn.argtypes = argtypes
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
_x11_lib = _load("X11", "libX11.so.6")
|
|
283
|
+
assert _x11_lib is not None # required=True raises otherwise
|
|
284
|
+
x11: ctypes.CDLL = _x11_lib
|
|
285
|
+
xext = _load("Xext", "libXext.so.6", required=False)
|
|
286
|
+
xcomposite = _load("Xcomposite", "libXcomposite.so.1", required=False)
|
|
287
|
+
xdamage = _load("Xdamage", "libXdamage.so.1", required=False)
|
|
288
|
+
xfixes = _load("Xfixes", "libXfixes.so.3", required=False)
|
|
289
|
+
xrandr = _load("Xrandr", "libXrandr.so.2", required=False)
|
|
290
|
+
xres = _load("XRes", "libXRes.so.1", required=False)
|
|
291
|
+
xinerama = _load("Xinerama", "libXinerama.so.1", required=False)
|
|
292
|
+
libc = ctypes.CDLL(None, use_errno=True)
|
|
293
|
+
|
|
294
|
+
_sig(x11, "XInitThreads", Status)
|
|
295
|
+
_sig(x11, "XOpenDisplay", c_void_p, c_char_p)
|
|
296
|
+
_sig(x11, "XCloseDisplay", c_int, c_void_p)
|
|
297
|
+
_sig(x11, "XDefaultScreen", c_int, c_void_p)
|
|
298
|
+
_sig(x11, "XScreenCount", c_int, c_void_p)
|
|
299
|
+
_sig(x11, "XRootWindow", Window, c_void_p, c_int)
|
|
300
|
+
_sig(x11, "XDefaultVisual", c_void_p, c_void_p, c_int)
|
|
301
|
+
_sig(x11, "XDefaultDepth", c_int, c_void_p, c_int)
|
|
302
|
+
_sig(x11, "XDisplayWidth", c_int, c_void_p, c_int)
|
|
303
|
+
_sig(x11, "XDisplayHeight", c_int, c_void_p, c_int)
|
|
304
|
+
_sig(x11, "XSync", c_int, c_void_p, Bool)
|
|
305
|
+
_sig(x11, "XFlush", c_int, c_void_p)
|
|
306
|
+
_sig(x11, "XFree", c_int, c_void_p)
|
|
307
|
+
_sig(x11, "XNextRequest", c_ulong, c_void_p)
|
|
308
|
+
_sig(x11, "XSetErrorHandler", c_void_p, c_void_p)
|
|
309
|
+
_sig(x11, "XSetIOErrorExitHandler", None, c_void_p, c_void_p, c_void_p)
|
|
310
|
+
_sig(
|
|
311
|
+
x11, "XQueryExtension", Bool, c_void_p, c_char_p, POINTER(c_int), POINTER(c_int), POINTER(c_int)
|
|
312
|
+
)
|
|
313
|
+
_sig(x11, "XInternAtom", Atom, c_void_p, c_char_p, Bool)
|
|
314
|
+
_sig(x11, "XGetAtomName", c_void_p, c_void_p, Atom)
|
|
315
|
+
_sig(
|
|
316
|
+
x11,
|
|
317
|
+
"XGetWindowProperty",
|
|
318
|
+
c_int,
|
|
319
|
+
c_void_p,
|
|
320
|
+
Window,
|
|
321
|
+
Atom,
|
|
322
|
+
c_long,
|
|
323
|
+
c_long,
|
|
324
|
+
Bool,
|
|
325
|
+
Atom,
|
|
326
|
+
POINTER(Atom),
|
|
327
|
+
POINTER(c_int),
|
|
328
|
+
POINTER(c_ulong),
|
|
329
|
+
POINTER(c_ulong),
|
|
330
|
+
POINTER(c_void_p),
|
|
331
|
+
)
|
|
332
|
+
_sig(x11, "XGetWindowAttributes", Status, c_void_p, Window, POINTER(XWindowAttributes))
|
|
333
|
+
_sig(
|
|
334
|
+
x11,
|
|
335
|
+
"XTranslateCoordinates",
|
|
336
|
+
Bool,
|
|
337
|
+
c_void_p,
|
|
338
|
+
Window,
|
|
339
|
+
Window,
|
|
340
|
+
c_int,
|
|
341
|
+
c_int,
|
|
342
|
+
POINTER(c_int),
|
|
343
|
+
POINTER(c_int),
|
|
344
|
+
POINTER(Window),
|
|
345
|
+
)
|
|
346
|
+
_sig(
|
|
347
|
+
x11,
|
|
348
|
+
"XQueryTree",
|
|
349
|
+
Status,
|
|
350
|
+
c_void_p,
|
|
351
|
+
Window,
|
|
352
|
+
POINTER(Window),
|
|
353
|
+
POINTER(Window),
|
|
354
|
+
POINTER(POINTER(Window)),
|
|
355
|
+
POINTER(c_uint),
|
|
356
|
+
)
|
|
357
|
+
_sig(x11, "XGetClassHint", Status, c_void_p, Window, POINTER(XClassHint))
|
|
358
|
+
_sig(x11, "XFetchName", Status, c_void_p, Window, POINTER(c_void_p))
|
|
359
|
+
_sig(
|
|
360
|
+
x11,
|
|
361
|
+
"XGetImage",
|
|
362
|
+
POINTER(XImage),
|
|
363
|
+
c_void_p,
|
|
364
|
+
Drawable,
|
|
365
|
+
c_int,
|
|
366
|
+
c_int,
|
|
367
|
+
c_uint,
|
|
368
|
+
c_uint,
|
|
369
|
+
c_ulong,
|
|
370
|
+
c_int,
|
|
371
|
+
)
|
|
372
|
+
_sig(x11, "XDestroyImage", c_int, POINTER(XImage))
|
|
373
|
+
_sig(x11, "XSelectInput", c_int, c_void_p, Window, c_long)
|
|
374
|
+
_sig(x11, "XPending", c_int, c_void_p)
|
|
375
|
+
_sig(x11, "XNextEvent", c_int, c_void_p, POINTER(XEvent))
|
|
376
|
+
_sig(x11, "XFreePixmap", c_int, c_void_p, Pixmap)
|
|
377
|
+
_sig(x11, "XServerVendor", c_char_p, c_void_p)
|
|
378
|
+
_sig(x11, "XVendorRelease", c_int, c_void_p)
|
|
379
|
+
|
|
380
|
+
_sig(xext, "XShmQueryExtension", Bool, c_void_p)
|
|
381
|
+
_sig(
|
|
382
|
+
xext,
|
|
383
|
+
"XShmCreateImage",
|
|
384
|
+
POINTER(XImage),
|
|
385
|
+
c_void_p,
|
|
386
|
+
c_void_p,
|
|
387
|
+
c_uint,
|
|
388
|
+
c_int,
|
|
389
|
+
c_void_p,
|
|
390
|
+
POINTER(XShmSegmentInfo),
|
|
391
|
+
c_uint,
|
|
392
|
+
c_uint,
|
|
393
|
+
)
|
|
394
|
+
_sig(xext, "XShmAttach", Bool, c_void_p, POINTER(XShmSegmentInfo))
|
|
395
|
+
_sig(xext, "XShmDetach", Bool, c_void_p, POINTER(XShmSegmentInfo))
|
|
396
|
+
_sig(xext, "XShmGetImage", Bool, c_void_p, Drawable, POINTER(XImage), c_int, c_int, c_ulong)
|
|
397
|
+
|
|
398
|
+
_sig(xcomposite, "XCompositeQueryExtension", Bool, c_void_p, POINTER(c_int), POINTER(c_int))
|
|
399
|
+
_sig(xcomposite, "XCompositeQueryVersion", Status, c_void_p, POINTER(c_int), POINTER(c_int))
|
|
400
|
+
_sig(xcomposite, "XCompositeRedirectWindow", None, c_void_p, Window, c_int)
|
|
401
|
+
_sig(xcomposite, "XCompositeUnredirectWindow", None, c_void_p, Window, c_int)
|
|
402
|
+
_sig(xcomposite, "XCompositeNameWindowPixmap", Pixmap, c_void_p, Window)
|
|
403
|
+
|
|
404
|
+
_sig(xdamage, "XDamageQueryExtension", Bool, c_void_p, POINTER(c_int), POINTER(c_int))
|
|
405
|
+
_sig(xdamage, "XDamageCreate", XID, c_void_p, Drawable, c_int)
|
|
406
|
+
_sig(xdamage, "XDamageDestroy", None, c_void_p, XID)
|
|
407
|
+
_sig(xdamage, "XDamageSubtract", None, c_void_p, XID, XID, XID)
|
|
408
|
+
|
|
409
|
+
_sig(xrandr, "XRRQueryExtension", Bool, c_void_p, POINTER(c_int), POINTER(c_int))
|
|
410
|
+
_sig(xrandr, "XRRQueryVersion", Status, c_void_p, POINTER(c_int), POINTER(c_int))
|
|
411
|
+
_sig(xrandr, "XRRGetMonitors", POINTER(XRRMonitorInfo), c_void_p, Window, Bool, POINTER(c_int))
|
|
412
|
+
_sig(xrandr, "XRRFreeMonitors", None, POINTER(XRRMonitorInfo))
|
|
413
|
+
_sig(xrandr, "XRRGetScreenResourcesCurrent", POINTER(XRRScreenResources), c_void_p, Window)
|
|
414
|
+
_sig(xrandr, "XRRFreeScreenResources", None, POINTER(XRRScreenResources))
|
|
415
|
+
_sig(xrandr, "XRRGetCrtcInfo", POINTER(XRRCrtcInfo), c_void_p, POINTER(XRRScreenResources), c_ulong)
|
|
416
|
+
_sig(xrandr, "XRRFreeCrtcInfo", None, POINTER(XRRCrtcInfo))
|
|
417
|
+
_sig(xrandr, "XRRGetOutputPrimary", c_ulong, c_void_p, Window)
|
|
418
|
+
|
|
419
|
+
_sig(xinerama, "XineramaIsActive", Bool, c_void_p)
|
|
420
|
+
_sig(xinerama, "XineramaQueryScreens", POINTER(XineramaScreenInfo), c_void_p, POINTER(c_int))
|
|
421
|
+
|
|
422
|
+
_sig(xres, "XResQueryExtension", Bool, c_void_p, POINTER(c_int), POINTER(c_int))
|
|
423
|
+
_sig(
|
|
424
|
+
xres,
|
|
425
|
+
"XResQueryClientIds",
|
|
426
|
+
Status,
|
|
427
|
+
c_void_p,
|
|
428
|
+
c_long,
|
|
429
|
+
POINTER(XResClientIdSpec),
|
|
430
|
+
POINTER(c_long),
|
|
431
|
+
POINTER(POINTER(XResClientIdValue)),
|
|
432
|
+
)
|
|
433
|
+
_sig(xres, "XResGetClientPid", c_int, POINTER(XResClientIdValue))
|
|
434
|
+
_sig(xres, "XResClientIdsDestroy", None, c_long, POINTER(XResClientIdValue))
|
|
435
|
+
|
|
436
|
+
_sig(libc, "shmget", c_int, c_int, ctypes.c_size_t, c_int)
|
|
437
|
+
_sig(libc, "shmat", c_void_p, c_int, c_void_p, c_int)
|
|
438
|
+
_sig(libc, "shmdt", c_int, c_void_p)
|
|
439
|
+
_sig(libc, "shmctl", c_int, c_int, c_int, c_void_p)
|
|
440
|
+
|
|
441
|
+
IPC_PRIVATE, IPC_CREAT, IPC_RMID = 0, 0o1000, 0
|
|
442
|
+
|
|
443
|
+
# libX11 >= 1.8 calls XInitThreads itself; calling it again is harmless and
|
|
444
|
+
# needed for older versions (must precede any other Xlib call).
|
|
445
|
+
x11.XInitThreads()
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# --- error handling -------------------------------------------------------------------
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class XError(NativeError):
|
|
452
|
+
def __init__(self, what: str, error_code: int, request_code: int = 0) -> None:
|
|
453
|
+
self.error_code = error_code
|
|
454
|
+
self.request_code = request_code
|
|
455
|
+
super().__init__(f"{what}: X error {error_code} (request {request_code})", error_code)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class XConnectionLostError(TargetLostError):
|
|
459
|
+
"""The X server connection died (server exited, display closed)."""
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
_errors_lock = threading.Lock()
|
|
463
|
+
_errors: dict[int, list[tuple[int, int, int]]] = {}
|
|
464
|
+
_dead: set[int] = set()
|
|
465
|
+
_ours: set[int] = set() # Display pointers opened by capturekit
|
|
466
|
+
|
|
467
|
+
_ERROR_HANDLER = CFUNCTYPE(c_int, c_void_p, POINTER(XErrorEvent))
|
|
468
|
+
_IO_ERROR_HANDLER = CFUNCTYPE(c_int, c_void_p)
|
|
469
|
+
_IO_EXIT_HANDLER = CFUNCTYPE(None, c_void_p, c_void_p)
|
|
470
|
+
|
|
471
|
+
_sig(x11, "XSetIOErrorHandler", c_void_p, c_void_p)
|
|
472
|
+
|
|
473
|
+
# Handlers are process-global. Errors on displays capturekit does not own are
|
|
474
|
+
# forwarded to whatever handler was installed before (e.g. Tk/Qt), so other
|
|
475
|
+
# X clients in the same process keep their behaviour.
|
|
476
|
+
_prev_error: Any = None
|
|
477
|
+
_prev_io_error: Any = None
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _on_error(dpy: int | None, ev: Any) -> int:
|
|
481
|
+
key = int(dpy or 0)
|
|
482
|
+
with _errors_lock:
|
|
483
|
+
mine = key in _ours
|
|
484
|
+
if mine:
|
|
485
|
+
e = ev.contents
|
|
486
|
+
_errors.setdefault(key, []).append(
|
|
487
|
+
(int(e.error_code), int(e.request_code), int(e.resourceid))
|
|
488
|
+
)
|
|
489
|
+
if not mine and _prev_error is not None:
|
|
490
|
+
return int(_prev_error(dpy, ev))
|
|
491
|
+
return 0
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _on_io_error(dpy: int | None) -> int:
|
|
495
|
+
# Xlib's default IO error handler calls exit(1) *before* the per-display
|
|
496
|
+
# exit handler runs (libX11 XlibInt.c, verified 2026-10-11), so ours must
|
|
497
|
+
# return for capturekit's displays.
|
|
498
|
+
key = int(dpy or 0)
|
|
499
|
+
with _errors_lock:
|
|
500
|
+
mine = key in _ours
|
|
501
|
+
if mine:
|
|
502
|
+
_dead.add(key)
|
|
503
|
+
if not mine and _prev_io_error is not None:
|
|
504
|
+
return int(_prev_io_error(dpy))
|
|
505
|
+
return 0
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _on_io_exit(dpy: int | None, _data: int | None) -> None:
|
|
509
|
+
# Must not call Xlib here. Mark the display dead; Python code checks it.
|
|
510
|
+
with _errors_lock:
|
|
511
|
+
_dead.add(int(dpy or 0))
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
_error_handler_ref = _ERROR_HANDLER(_on_error)
|
|
515
|
+
_io_error_ref = _IO_ERROR_HANDLER(_on_io_error)
|
|
516
|
+
_io_exit_ref = _IO_EXIT_HANDLER(_on_io_exit)
|
|
517
|
+
_old = x11.XSetErrorHandler(ctypes.cast(_error_handler_ref, c_void_p))
|
|
518
|
+
_prev_error = _ERROR_HANDLER(_old) if _old else None
|
|
519
|
+
#: libX11 >= 1.7: a dying X server can be survived. Older versions exit the
|
|
520
|
+
#: process once the IO error handler returns, so we leave their default alone.
|
|
521
|
+
HAS_IO_EXIT_HANDLER = hasattr(x11, "XSetIOErrorExitHandler")
|
|
522
|
+
if HAS_IO_EXIT_HANDLER:
|
|
523
|
+
_old_io = x11.XSetIOErrorHandler(ctypes.cast(_io_error_ref, c_void_p))
|
|
524
|
+
_prev_io_error = _IO_ERROR_HANDLER(_old_io) if _old_io else None
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
class XDisplay:
|
|
528
|
+
"""A private X connection (one per thread)."""
|
|
529
|
+
|
|
530
|
+
def __init__(self, name: str) -> None:
|
|
531
|
+
self.name = name
|
|
532
|
+
ptr = x11.XOpenDisplay(name.encode())
|
|
533
|
+
if not ptr:
|
|
534
|
+
raise CapabilityUnavailableError(f"cannot open X display {name!r}")
|
|
535
|
+
self.ptr: int = int(ptr)
|
|
536
|
+
with _errors_lock:
|
|
537
|
+
_ours.add(self.ptr)
|
|
538
|
+
if HAS_IO_EXIT_HANDLER:
|
|
539
|
+
x11.XSetIOErrorExitHandler(self.ptr, ctypes.cast(_io_exit_ref, c_void_p), None)
|
|
540
|
+
self.screen = x11.XDefaultScreen(self.ptr)
|
|
541
|
+
self.root = int(x11.XRootWindow(self.ptr, self.screen))
|
|
542
|
+
self._atoms: dict[str, int] = {}
|
|
543
|
+
self._ext: dict[str, bool] = {}
|
|
544
|
+
self.thread = threading.get_ident()
|
|
545
|
+
|
|
546
|
+
# -- lifecycle -------------------------------------------------------------
|
|
547
|
+
def close(self) -> None:
|
|
548
|
+
if self.ptr:
|
|
549
|
+
ptr, self.ptr = self.ptr, 0
|
|
550
|
+
with _errors_lock:
|
|
551
|
+
_errors.pop(ptr, None)
|
|
552
|
+
# Even a dead connection must be closed to free Xlib's structures;
|
|
553
|
+
# XCloseDisplay on a broken connection does not touch the socket.
|
|
554
|
+
x11.XCloseDisplay(ptr)
|
|
555
|
+
with _errors_lock:
|
|
556
|
+
_dead.discard(ptr)
|
|
557
|
+
_ours.discard(ptr)
|
|
558
|
+
|
|
559
|
+
def __enter__(self) -> XDisplay:
|
|
560
|
+
return self
|
|
561
|
+
|
|
562
|
+
def __exit__(self, *exc: object) -> None:
|
|
563
|
+
self.close()
|
|
564
|
+
|
|
565
|
+
@property
|
|
566
|
+
def alive(self) -> bool:
|
|
567
|
+
with _errors_lock:
|
|
568
|
+
return bool(self.ptr) and self.ptr not in _dead
|
|
569
|
+
|
|
570
|
+
def ensure_alive(self) -> None:
|
|
571
|
+
if not self.alive:
|
|
572
|
+
raise XConnectionLostError(f"connection to X display {self.name!r} was lost")
|
|
573
|
+
|
|
574
|
+
# -- error-checked calls -----------------------------------------------------
|
|
575
|
+
def _take_errors(self) -> list[tuple[int, int, int]]:
|
|
576
|
+
with _errors_lock:
|
|
577
|
+
return _errors.pop(self.ptr, [])
|
|
578
|
+
|
|
579
|
+
def sync(self) -> None:
|
|
580
|
+
self.ensure_alive()
|
|
581
|
+
x11.XSync(self.ptr, False)
|
|
582
|
+
self.ensure_alive()
|
|
583
|
+
|
|
584
|
+
def checked(self, what: str, fn: Callable[..., T], *args: Any) -> T:
|
|
585
|
+
"""Call ``fn``, round-trip with XSync and raise :class:`XError` on errors."""
|
|
586
|
+
self.ensure_alive()
|
|
587
|
+
self._take_errors()
|
|
588
|
+
res = fn(*args)
|
|
589
|
+
x11.XSync(self.ptr, False)
|
|
590
|
+
self.ensure_alive()
|
|
591
|
+
errs = self._take_errors()
|
|
592
|
+
if errs:
|
|
593
|
+
code, req, _rid = errs[0]
|
|
594
|
+
raise XError(what, code, req)
|
|
595
|
+
return res
|
|
596
|
+
|
|
597
|
+
# -- helpers ----------------------------------------------------------------------
|
|
598
|
+
def atom(self, name: str) -> int:
|
|
599
|
+
a = self._atoms.get(name)
|
|
600
|
+
if a is None:
|
|
601
|
+
a = int(x11.XInternAtom(self.ptr, name.encode(), False))
|
|
602
|
+
self._atoms[name] = a
|
|
603
|
+
return a
|
|
604
|
+
|
|
605
|
+
def has_extension(self, name: str) -> bool:
|
|
606
|
+
if name not in self._ext:
|
|
607
|
+
op, ev, er = c_int(), c_int(), c_int()
|
|
608
|
+
self._ext[name] = bool(
|
|
609
|
+
x11.XQueryExtension(self.ptr, name.encode(), byref(op), byref(ev), byref(er))
|
|
610
|
+
)
|
|
611
|
+
return self._ext[name]
|
|
612
|
+
|
|
613
|
+
def extension_event_base(self, name: str) -> int | None:
|
|
614
|
+
op, ev, er = c_int(), c_int(), c_int()
|
|
615
|
+
if x11.XQueryExtension(self.ptr, name.encode(), byref(op), byref(ev), byref(er)):
|
|
616
|
+
return int(ev.value)
|
|
617
|
+
return None
|
|
618
|
+
|
|
619
|
+
def get_property(
|
|
620
|
+
self,
|
|
621
|
+
window: int,
|
|
622
|
+
name: str,
|
|
623
|
+
req_type: int = AnyPropertyType,
|
|
624
|
+
) -> tuple[int, int, bytes, int] | None:
|
|
625
|
+
"""(type, format, raw bytes, item count) or None. 32-bit items are C longs."""
|
|
626
|
+
actual_type, actual_format = Atom(), c_int()
|
|
627
|
+
nitems, after = c_ulong(), c_ulong()
|
|
628
|
+
data = c_void_p()
|
|
629
|
+
status = self.checked(
|
|
630
|
+
f"XGetWindowProperty({name})", x11.XGetWindowProperty, self.ptr, window,
|
|
631
|
+
self.atom(name), 0, 0x7FFFFFFF, False, req_type, byref(actual_type),
|
|
632
|
+
byref(actual_format), byref(nitems), byref(after), byref(data),
|
|
633
|
+
) # fmt: skip
|
|
634
|
+
if status != 0 or not data.value:
|
|
635
|
+
return None
|
|
636
|
+
try:
|
|
637
|
+
fmt = actual_format.value
|
|
638
|
+
item_size = {8: 1, 16: ctypes.sizeof(c_short), 32: ctypes.sizeof(c_long)}.get(fmt, 1)
|
|
639
|
+
raw = ctypes.string_at(data.value, nitems.value * item_size)
|
|
640
|
+
return int(actual_type.value), fmt, raw, int(nitems.value)
|
|
641
|
+
finally:
|
|
642
|
+
x11.XFree(data)
|
|
643
|
+
|
|
644
|
+
def get_longs(self, window: int, name: str, req_type: int = AnyPropertyType) -> list[int]:
|
|
645
|
+
prop = self.get_property(window, name, req_type)
|
|
646
|
+
if prop is None or prop[1] != 32:
|
|
647
|
+
return []
|
|
648
|
+
arr = (c_ulong * prop[3]).from_buffer_copy(prop[2])
|
|
649
|
+
return [int(v) for v in arr]
|
|
650
|
+
|
|
651
|
+
def get_string(self, window: int, name: str) -> str | None:
|
|
652
|
+
prop = self.get_property(window, name)
|
|
653
|
+
if prop is None or prop[1] != 8:
|
|
654
|
+
return None
|
|
655
|
+
return prop[2].decode("utf-8", "replace").rstrip("\x00")
|