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
capturekit/__init__.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""capturekit — monitor, exclusive-window and audio capture for Windows and Linux X11.
|
|
2
|
+
|
|
3
|
+
Quick start::
|
|
4
|
+
|
|
5
|
+
import capturekit as ck
|
|
6
|
+
|
|
7
|
+
print(ck.get_capabilities().summary())
|
|
8
|
+
win = next(w for w in ck.list_windows() if "Notepad" in w.title)
|
|
9
|
+
with ck.ScreenCapture(ck.WindowSource(win)) as cap:
|
|
10
|
+
frame = cap.grab() # BGRA numpy array in frame.data
|
|
11
|
+
|
|
12
|
+
rec = ck.Recorder(ck.WindowSource(win), [ck.ProcessAudioSource.for_window(win)],
|
|
13
|
+
"notepad.mkv", ck.RecordingConfig(fps=30))
|
|
14
|
+
rec.start(); ...; rec.stop()
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from . import _platform
|
|
20
|
+
from ._pipeline.clock import now_ns
|
|
21
|
+
from ._version import __version__
|
|
22
|
+
from .capabilities import Capabilities, Capability, Support
|
|
23
|
+
from .capture import ScreenCapture, screenshot
|
|
24
|
+
from .config import AudioMixMode, Container, EncoderConfig, RecordingConfig, TargetLostPolicy
|
|
25
|
+
from .errors import (
|
|
26
|
+
AccessDeniedError,
|
|
27
|
+
AudioDeviceDisconnectedError,
|
|
28
|
+
AudioDeviceError,
|
|
29
|
+
CapabilityUnavailableError,
|
|
30
|
+
CaptureError,
|
|
31
|
+
CaptureTimeoutError,
|
|
32
|
+
EncoderError,
|
|
33
|
+
EncoderNotAvailableError,
|
|
34
|
+
InvalidRegionError,
|
|
35
|
+
NativeError,
|
|
36
|
+
RecorderStateError,
|
|
37
|
+
TargetLostError,
|
|
38
|
+
TargetNotFoundError,
|
|
39
|
+
UnsupportedDisplayServerError,
|
|
40
|
+
UnsupportedPlatformError,
|
|
41
|
+
WindowClosedError,
|
|
42
|
+
)
|
|
43
|
+
from .recording import (
|
|
44
|
+
AudioTrackStats,
|
|
45
|
+
Recorder,
|
|
46
|
+
RecorderState,
|
|
47
|
+
RecordingResult,
|
|
48
|
+
RecordingStats,
|
|
49
|
+
VideoStats,
|
|
50
|
+
)
|
|
51
|
+
from .sources import (
|
|
52
|
+
MicrophoneSource,
|
|
53
|
+
MonitorBackend,
|
|
54
|
+
MonitorSource,
|
|
55
|
+
ProcessAudioSource,
|
|
56
|
+
SystemAudioSource,
|
|
57
|
+
WindowSource,
|
|
58
|
+
)
|
|
59
|
+
from .types import (
|
|
60
|
+
AudioChunk,
|
|
61
|
+
AudioDevice,
|
|
62
|
+
AudioDeviceKind,
|
|
63
|
+
AudioSession,
|
|
64
|
+
Frame,
|
|
65
|
+
Monitor,
|
|
66
|
+
PixelFormat,
|
|
67
|
+
Rect,
|
|
68
|
+
Window,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def list_monitors(display: str | None = None) -> list[Monitor]:
|
|
73
|
+
"""Enumerate monitors (``display`` selects an X server on Linux, e.g. ``":99"``)."""
|
|
74
|
+
result: list[Monitor] = _platform.backend().list_monitors(display)
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def list_windows(display: str | None = None, *, include_untitled: bool = False) -> list[Window]:
|
|
79
|
+
"""Enumerate capturable top-level windows."""
|
|
80
|
+
result: list[Window] = _platform.backend().list_windows(display, include_untitled)
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_window(handle: int, display: str | None = None) -> Window:
|
|
85
|
+
"""Describe the window with native ``handle`` (raises TargetNotFoundError)."""
|
|
86
|
+
result: Window = _platform.backend().get_window(handle, display)
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def list_audio_devices() -> list[AudioDevice]:
|
|
91
|
+
"""Enumerate audio input and output devices."""
|
|
92
|
+
result: list[AudioDevice] = _platform.backend().list_audio_devices()
|
|
93
|
+
return result
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def list_audio_sessions() -> list[AudioSession]:
|
|
97
|
+
"""Enumerate application audio sessions/streams with owning process IDs."""
|
|
98
|
+
result: list[AudioSession] = _platform.backend().list_audio_sessions()
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_capabilities(display: str | None = None) -> Capabilities:
|
|
103
|
+
"""Describe what this machine/display server supports."""
|
|
104
|
+
result: Capabilities = _platform.backend().capabilities(display)
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
__all__ = [
|
|
109
|
+
"AccessDeniedError",
|
|
110
|
+
"AudioChunk",
|
|
111
|
+
"AudioDevice",
|
|
112
|
+
"AudioDeviceDisconnectedError",
|
|
113
|
+
"AudioDeviceError",
|
|
114
|
+
"AudioDeviceKind",
|
|
115
|
+
"AudioMixMode",
|
|
116
|
+
"AudioSession",
|
|
117
|
+
"AudioTrackStats",
|
|
118
|
+
"Capabilities",
|
|
119
|
+
"Capability",
|
|
120
|
+
"CapabilityUnavailableError",
|
|
121
|
+
"CaptureError",
|
|
122
|
+
"CaptureTimeoutError",
|
|
123
|
+
"Container",
|
|
124
|
+
"EncoderConfig",
|
|
125
|
+
"EncoderError",
|
|
126
|
+
"EncoderNotAvailableError",
|
|
127
|
+
"Frame",
|
|
128
|
+
"InvalidRegionError",
|
|
129
|
+
"MicrophoneSource",
|
|
130
|
+
"Monitor",
|
|
131
|
+
"MonitorBackend",
|
|
132
|
+
"MonitorSource",
|
|
133
|
+
"NativeError",
|
|
134
|
+
"PixelFormat",
|
|
135
|
+
"ProcessAudioSource",
|
|
136
|
+
"Recorder",
|
|
137
|
+
"RecorderState",
|
|
138
|
+
"RecorderStateError",
|
|
139
|
+
"RecordingConfig",
|
|
140
|
+
"RecordingResult",
|
|
141
|
+
"RecordingStats",
|
|
142
|
+
"Rect",
|
|
143
|
+
"ScreenCapture",
|
|
144
|
+
"Support",
|
|
145
|
+
"SystemAudioSource",
|
|
146
|
+
"TargetLostError",
|
|
147
|
+
"TargetLostPolicy",
|
|
148
|
+
"TargetNotFoundError",
|
|
149
|
+
"UnsupportedDisplayServerError",
|
|
150
|
+
"UnsupportedPlatformError",
|
|
151
|
+
"VideoStats",
|
|
152
|
+
"Window",
|
|
153
|
+
"WindowClosedError",
|
|
154
|
+
"WindowSource",
|
|
155
|
+
"__version__",
|
|
156
|
+
"get_capabilities",
|
|
157
|
+
"get_window",
|
|
158
|
+
"list_audio_devices",
|
|
159
|
+
"list_audio_sessions",
|
|
160
|
+
"list_monitors",
|
|
161
|
+
"list_windows",
|
|
162
|
+
"now_ns",
|
|
163
|
+
"screenshot",
|
|
164
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Internal backend protocols (not public API)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import numpy.typing as npt
|
|
11
|
+
|
|
12
|
+
from ..types import Rect
|
|
13
|
+
|
|
14
|
+
Buffer = npt.NDArray[np.uint8]
|
|
15
|
+
Samples = npt.NDArray[np.float32]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(slots=True)
|
|
19
|
+
class AcquiredFrame:
|
|
20
|
+
data: Buffer
|
|
21
|
+
timestamp_ns: int
|
|
22
|
+
content_size: tuple[int, int]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(slots=True)
|
|
26
|
+
class VideoBackendInfo:
|
|
27
|
+
name: str
|
|
28
|
+
#: Output size when ``acquire(out=None)`` is used right after start.
|
|
29
|
+
initial_size: tuple[int, int]
|
|
30
|
+
details: dict[str, object] = field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class VideoBackend(Protocol):
|
|
34
|
+
"""Pull-model video source. All methods are called from one thread."""
|
|
35
|
+
|
|
36
|
+
info: VideoBackendInfo
|
|
37
|
+
|
|
38
|
+
def start(self) -> None: ...
|
|
39
|
+
|
|
40
|
+
def acquire(self, out: Buffer | None, timeout_s: float) -> AcquiredFrame | None:
|
|
41
|
+
"""Return the newest frame since the last call, or ``None``.
|
|
42
|
+
|
|
43
|
+
With ``out`` the frame is written at its top-left corner; the rest of
|
|
44
|
+
``out`` is cleared. Raises ``TargetLostError`` when the target is gone.
|
|
45
|
+
"""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
def close(self) -> None: ...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(slots=True)
|
|
52
|
+
class AudioPacket:
|
|
53
|
+
data: Samples # (frames, channels) float32
|
|
54
|
+
timestamp_ns: int
|
|
55
|
+
discontinuity: bool = False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
AudioCallback = Callable[[AudioPacket], None]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AudioBackend(Protocol):
|
|
62
|
+
"""Push-model audio source running its own capture thread."""
|
|
63
|
+
|
|
64
|
+
name: str
|
|
65
|
+
sample_rate: int
|
|
66
|
+
channels: int
|
|
67
|
+
|
|
68
|
+
def start(
|
|
69
|
+
self, on_packet: AudioCallback, on_error: Callable[[BaseException], None]
|
|
70
|
+
) -> None: ...
|
|
71
|
+
|
|
72
|
+
def stop(self) -> None: ...
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def blit_clear(out: Buffer, copied_w: int, copied_h: int) -> None:
|
|
76
|
+
"""Clear the parts of ``out`` not covered by a top-left copy."""
|
|
77
|
+
h, w = out.shape[0], out.shape[1]
|
|
78
|
+
if copied_h < h:
|
|
79
|
+
out[copied_h:, :] = 0
|
|
80
|
+
if copied_w < w:
|
|
81
|
+
out[: min(copied_h, h), copied_w:] = 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def crop_box(base: Rect, region: Rect | None) -> Rect:
|
|
85
|
+
"""Region (relative to ``base``) translated into absolute coordinates and
|
|
86
|
+
clipped to ``base``. Without a region the whole base is used."""
|
|
87
|
+
if region is None:
|
|
88
|
+
return base
|
|
89
|
+
return region.translate(base.x, base.y).intersect(base)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Linux X11/Xvfb and PulseAudio/PipeWire backends."""
|