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.
Files changed (41) hide show
  1. capturekit/__init__.py +164 -0
  2. capturekit/_backends/__init__.py +0 -0
  3. capturekit/_backends/base.py +89 -0
  4. capturekit/_backends/linux/__init__.py +1 -0
  5. capturekit/_backends/linux/_xlib.py +655 -0
  6. capturekit/_backends/linux/api.py +160 -0
  7. capturekit/_backends/linux/pulse.py +464 -0
  8. capturekit/_backends/linux/x11.py +720 -0
  9. capturekit/_backends/windows/__init__.py +0 -0
  10. capturekit/_backends/windows/_com.py +350 -0
  11. capturekit/_backends/windows/_d3d11.py +307 -0
  12. capturekit/_backends/windows/_win32.py +420 -0
  13. capturekit/_backends/windows/_winrt.py +162 -0
  14. capturekit/_backends/windows/api.py +214 -0
  15. capturekit/_backends/windows/cursor.py +66 -0
  16. capturekit/_backends/windows/dxgi_duplication.py +310 -0
  17. capturekit/_backends/windows/wasapi.py +570 -0
  18. capturekit/_backends/windows/wgc.py +398 -0
  19. capturekit/_pipeline/__init__.py +1 -0
  20. capturekit/_pipeline/aligner.py +177 -0
  21. capturekit/_pipeline/clock.py +47 -0
  22. capturekit/_pipeline/frame_queue.py +120 -0
  23. capturekit/_pipeline/mixer.py +80 -0
  24. capturekit/_pipeline/timeline.py +133 -0
  25. capturekit/_platform.py +24 -0
  26. capturekit/_version.py +1 -0
  27. capturekit/_worker.py +100 -0
  28. capturekit/capabilities.py +79 -0
  29. capturekit/capture.py +141 -0
  30. capturekit/config.py +85 -0
  31. capturekit/encoding/__init__.py +5 -0
  32. capturekit/encoding/ffmpeg.py +356 -0
  33. capturekit/encoding/pipes.py +212 -0
  34. capturekit/errors.py +109 -0
  35. capturekit/py.typed +0 -0
  36. capturekit/recording.py +940 -0
  37. capturekit/sources.py +124 -0
  38. capturekit/types.py +211 -0
  39. capturekit-0.1.0.dist-info/METADATA +112 -0
  40. capturekit-0.1.0.dist-info/RECORD +41 -0
  41. 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."""