@maciejwojs/screen-capture 0.1.0

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.
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # @maciejwojs/screen-capture
2
+
3
+ Native Node.js addon for screen capture. Packages are published to NPM with provenance (OIDC) and include prebuilt binaries via GitHub Actions.
4
+
5
+ ## Current state
6
+
7
+ - Windows backend is implemented with `Windows.Graphics.Capture` + D3D11.
8
+ - Linux backend is implemented with `xdg-desktop-portal` (D-Bus) and PipeWire stream, supporting modern desktop environments (Wayland/X11).
9
+ - Runtime loading uses `node-gyp-build`, so local build and `prebuilds/` binaries are both supported.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ bun install
15
+ ```
16
+
17
+ ## Local build
18
+
19
+ ```bash
20
+ bun run build # to compile TypeScript
21
+ bun x node-gyp rebuild
22
+ ```
23
+
24
+ ## Prebuilt binaries (prebuildify)
25
+
26
+ Build prebuild for current platform:
27
+
28
+ ```bash
29
+ bun run prebuildify
30
+ ```
31
+
32
+ Build prebuilds for selected platforms:
33
+
34
+ ```bash
35
+ bun run prebuildify:all
36
+ ```
37
+
38
+ Output goes to `prebuilds/` and is loaded automatically by `dist/index.js`.
39
+ Scripts are configured to run `node-gyp` via `node-gyp` internally and build TypeScript beforehand.
40
+
41
+ ## How to add other systems / window managers
42
+
43
+ Recommended backend mapping:
44
+
45
+ - Windows: `Windows.Graphics.Capture` (already in place)
46
+ - Linux: PipeWire portal (`xdg-desktop-portal`) with DMA-BUF limits (already in place)
47
+ - macOS: ScreenCaptureKit (or CGDisplayStream as fallback)
48
+
49
+ Practical architecture:
50
+
51
+ - Keep one JS API (`ScreenCapture`) for all OSes.
52
+ - Keep one addon target in `binding.gyp`.
53
+ - Keep per-platform backend in separate `.cpp` files and select in `binding.gyp` conditions.
54
+ - Current split is:
55
+ - `src/addon.cpp` - shared N-API wrapper
56
+ - `src/win/platform_capture_win.cpp` - Windows backend
57
+ - `src/linux/platform_capture_linux.cpp` - Linux backend entry point
58
+ - `src/platform_capture_stub.cpp` - fallback for other systems
59
+ - `src/platform_capture.hpp` - common backend interface
60
+ - For unsupported combinations, return a clear runtime error (already done).
61
+
62
+ To add next platform, create next backend file (for example `src/macos/platform_capture_macos.cpp`) and add it in matching `binding.gyp` condition branch.
63
+
64
+ This gives you one npm package with many prebuilt binaries and no user-side compile step.
package/binding.gyp ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "targets": [
3
+ {
4
+ "target_name": "screen_capture_addon",
5
+ "sources": [
6
+ "src/addon.cpp"
7
+ ],
8
+ "include_dirs": [
9
+ "<!@(node -p \"require('node-addon-api').include\")"
10
+ ],
11
+ "dependencies": [
12
+ "<!(node -p \"require('node-addon-api').gyp\")"
13
+ ],
14
+ "defines": [
15
+ "NAPI_DISABLE_CPP_EXCEPTIONS"
16
+ ],
17
+ "cflags_cc": [
18
+ "-std=c++20",
19
+ "-fexceptions"
20
+ ],
21
+ "conditions": [
22
+ ["OS=='win'", {
23
+ "sources": [
24
+ "src/win/platform_capture_win.cpp"
25
+ ],
26
+ "msvs_settings": {
27
+ "VCCLCompilerTool": {
28
+ "AdditionalOptions": [
29
+ "/EHsc",
30
+ "/std:c++20"
31
+ ]
32
+ }
33
+ },
34
+ "libraries": [
35
+ "runtimeobject.lib",
36
+ "d3d11.lib",
37
+ "dxgi.lib"
38
+ ]
39
+ }],
40
+ ["OS=='linux'", {
41
+ "sources": [
42
+ "src/linux/platform_capture_linux.cpp"
43
+ ],
44
+ "cflags_cc": [
45
+ "<!@(pkg-config --cflags gio-2.0 gio-unix-2.0 glib-2.0 gobject-2.0 libpipewire-0.3)"
46
+ ],
47
+ "libraries": [
48
+ "<!@(pkg-config --libs gio-2.0 gio-unix-2.0 glib-2.0 gobject-2.0 libpipewire-0.3)"
49
+ ]
50
+ }],
51
+ ["OS!='win' and OS!='linux'", {
52
+ "sources": [
53
+ "src/platform_capture_stub.cpp"
54
+ ]
55
+ }]
56
+ ]
57
+ }
58
+ ]
59
+ }
@@ -0,0 +1,23 @@
1
+ export interface SharedHandleInfo {
2
+ handle: bigint;
3
+ width: number;
4
+ height: number;
5
+ stride: number;
6
+ offset: number;
7
+ planeSize: bigint;
8
+ pixelFormat: number;
9
+ modifier: bigint;
10
+ bufferType: number;
11
+ chunkSize: number;
12
+ }
13
+ export interface IScreenCapture {
14
+ start(): void;
15
+ stop(): void;
16
+ getSharedHandle(): SharedHandleInfo | null;
17
+ }
18
+ export interface INativeAddon {
19
+ ScreenCapture: new () => IScreenCapture;
20
+ }
21
+ declare const native: INativeAddon;
22
+ export declare const ScreenCapture: new () => IScreenCapture;
23
+ export default native;
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import nodeGypBuild from 'node-gyp-build';
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = path.dirname(__filename);
6
+ const rootDir = path.resolve(__dirname, '..');
7
+ const native = nodeGypBuild(rootDir);
8
+ export const ScreenCapture = native.ScreenCapture;
9
+ export default native;
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@maciejwojs/screen-capture",
3
+ "version": "0.1.0",
4
+ "description": "Native screen capture addon for Node.js",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/maciejwojs/screen-capture.git"
9
+ },
10
+ "keywords": [
11
+ "screen-capture",
12
+ "desktop-capture",
13
+ "node-addon-api",
14
+ "napi"
15
+ ],
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "os": [
19
+ "win32",
20
+ "linux",
21
+ "darwin"
22
+ ],
23
+ "files": [
24
+ "dist",
25
+ "binding.gyp",
26
+ "src",
27
+ "prebuilds"
28
+ ],
29
+ "module": "dist/index.js",
30
+ "type": "module",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js",
35
+ "default": "./dist/index.js"
36
+ }
37
+ },
38
+ "scripts": {
39
+ "build": "bun x tsc",
40
+ "rebuild": "node-gyp rebuild",
41
+ "prebuildify": "bun run build && prebuildify --napi --strip --node-gyp \"bun x --silent node-gyp\"",
42
+ "prebuildify:all": "bun run build && prebuildify --napi --strip --node-gyp \"bun x --silent node-gyp\" --platform win32 --platform linux --platform darwin",
43
+ "test:load": "bun run build && node -e \"import('./dist/index.js').then(() => console.log('addon loaded'))\"",
44
+ "install": "node-gyp-build",
45
+ "prepack": "bun run build"
46
+ },
47
+ "devDependencies": {
48
+ "@types/bun": "latest",
49
+ "@types/node": "^25.5.0",
50
+ "node-gyp": "^12.2.0",
51
+ "prebuildify": "^6.0.1"
52
+ },
53
+ "peerDependencies": {
54
+ "typescript": "^5"
55
+ },
56
+ "dependencies": {
57
+ "node-addon-api": "^8.7.0",
58
+ "node-gyp-build": "^4.8.4"
59
+ }
60
+ }
package/src/addon.cpp ADDED
@@ -0,0 +1,70 @@
1
+ #include <napi.h>
2
+
3
+ #include <exception>
4
+ #include <memory>
5
+
6
+ #include "platform_capture.hpp"
7
+
8
+ class ScreenCapture : public Napi::ObjectWrap<ScreenCapture> {
9
+ public:
10
+ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
11
+ Napi::Function func = DefineClass(env, "ScreenCapture", {
12
+ InstanceMethod("start", &ScreenCapture::Start),
13
+ InstanceMethod("stop", &ScreenCapture::Stop),
14
+ InstanceMethod("getSharedHandle", &ScreenCapture::GetSharedHandle)
15
+ });
16
+
17
+ auto* constructor = new Napi::FunctionReference();
18
+ *constructor = Napi::Persistent(func);
19
+ env.SetInstanceData(constructor);
20
+
21
+ exports.Set("ScreenCapture", func);
22
+ return exports;
23
+ }
24
+
25
+ explicit ScreenCapture(const Napi::CallbackInfo& info)
26
+ : Napi::ObjectWrap<ScreenCapture>(info),
27
+ m_backend(CreatePlatformCapture()) {
28
+ }
29
+
30
+ private:
31
+ std::unique_ptr<IPlatformCapture> m_backend;
32
+
33
+ Napi::Value Start(const Napi::CallbackInfo& info) {
34
+ try {
35
+ m_backend->Start(info.Env());
36
+ } catch (const std::exception& e) {
37
+ Napi::Error::New(info.Env(), e.what()).ThrowAsJavaScriptException();
38
+ }
39
+ return info.Env().Undefined();
40
+ }
41
+
42
+ Napi::Value Stop(const Napi::CallbackInfo& info) {
43
+ m_backend->Stop();
44
+ return info.Env().Undefined();
45
+ }
46
+
47
+ Napi::Value GetSharedHandle(const Napi::CallbackInfo& info) {
48
+ auto shared = m_backend->GetSharedHandle();
49
+ if (!shared.has_value()) return info.Env().Null();
50
+
51
+ Napi::Object obj = Napi::Object::New(info.Env());
52
+ obj.Set("handle", Napi::BigInt::New(info.Env(), shared->handle));
53
+ obj.Set("width", shared->width);
54
+ obj.Set("height", shared->height);
55
+ obj.Set("stride", shared->stride);
56
+ obj.Set("offset", shared->offset);
57
+ obj.Set("planeSize", Napi::BigInt::New(info.Env(), shared->planeSize));
58
+ obj.Set("pixelFormat", shared->pixelFormat);
59
+ obj.Set("modifier", Napi::BigInt::New(info.Env(), shared->modifier));
60
+ obj.Set("bufferType", shared->bufferType);
61
+ obj.Set("chunkSize", shared->chunkSize);
62
+ return obj;
63
+ }
64
+ };
65
+
66
+ Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
67
+ return ScreenCapture::Init(env, exports);
68
+ }
69
+
70
+ NODE_API_MODULE(screen_capture_addon, InitAll)
@@ -0,0 +1,748 @@
1
+ #ifdef __linux__
2
+
3
+ #include "../platform_capture.hpp"
4
+
5
+ #include <gio/gio.h>
6
+ #include <gio/gunixfdlist.h>
7
+ #include <pipewire/keys.h>
8
+ #include <pipewire/pipewire.h>
9
+ #include <pipewire/stream.h>
10
+ #include <spa/param/buffers.h>
11
+ #include <spa/param/video/format-utils.h>
12
+ #include <spa/pod/builder.h>
13
+ #include <spa/utils/result.h>
14
+
15
+ #include <atomic>
16
+ #include <cstdint>
17
+ #include <cstdlib>
18
+ #include <cstring>
19
+ #include <iostream>
20
+ #include <memory>
21
+ #include <mutex>
22
+ #include <random>
23
+ #include <sstream>
24
+ #include <stdexcept>
25
+ #include <string>
26
+ #include <thread>
27
+ #include <unistd.h>
28
+
29
+ #include <fstream>
30
+
31
+ namespace {
32
+
33
+ std::ofstream& GetLogFile() {
34
+ static std::ofstream logFile("/tmp/screen-capture-electron.log", std::ios::app);
35
+ return logFile;
36
+ }
37
+
38
+ #define LOG(msg) do { GetLogFile() << msg << std::endl; } while(0)
39
+
40
+ std::string gen_token() {
41
+ static std::mt19937 rng(std::random_device{}());
42
+ static std::uniform_int_distribution<int> dist(0, 15);
43
+
44
+ std::stringstream ss;
45
+ ss << std::hex;
46
+ for (int i = 0; i < 8; i++) {
47
+ ss << dist(rng);
48
+ }
49
+ return ss.str();
50
+ }
51
+
52
+ struct StreamState {
53
+ pw_main_loop* pw_loop = nullptr;
54
+ pw_context* context = nullptr;
55
+ pw_core* core = nullptr;
56
+ pw_stream* stream = nullptr;
57
+ spa_hook stream_listener{};
58
+ };
59
+
60
+ enum class PortalStage {
61
+ Idle,
62
+ CreatingSession,
63
+ SelectingSources,
64
+ StartingSession,
65
+ OpeningRemote,
66
+ };
67
+
68
+ } // namespace
69
+
70
+ class LinuxPlatformCapture final : public IPlatformCapture {
71
+ public:
72
+ LinuxPlatformCapture() = default;
73
+
74
+ ~LinuxPlatformCapture() override {
75
+ Stop();
76
+ }
77
+
78
+ void Start(Napi::Env) override {
79
+ bool expected = false;
80
+ if (!m_running.compare_exchange_strong(expected, true)) {
81
+ return;
82
+ }
83
+
84
+ m_worker = std::thread(&LinuxPlatformCapture::RunCaptureFlow, this);
85
+ }
86
+
87
+ void Stop() override {
88
+ m_stopRequested.store(true);
89
+
90
+ {
91
+ std::lock_guard<std::mutex> lock(m_stateMutex);
92
+ if (m_glibLoop) {
93
+ g_main_loop_quit(m_glibLoop);
94
+ }
95
+ if (m_pwLoop) {
96
+ pw_main_loop_quit(m_pwLoop);
97
+ }
98
+ }
99
+
100
+ if (m_worker.joinable() && std::this_thread::get_id() != m_worker.get_id()) {
101
+ m_worker.join();
102
+ }
103
+
104
+ CleanupSharedHandle();
105
+ m_running.store(false);
106
+ }
107
+
108
+ std::optional<SharedHandleInfo> GetSharedHandle() const override {
109
+ std::lock_guard<std::mutex> lock(m_stateMutex);
110
+ if (!m_sharedHandle.has_value() || m_frameConsumed || m_sharedFd < 0) {
111
+ return std::nullopt;
112
+ }
113
+
114
+ SharedHandleInfo info = *m_sharedHandle;
115
+ // Dup the FD for JS so it takes exclusive ownership of the new FD.
116
+ info.handle = static_cast<uint64_t>(dup(m_sharedFd));
117
+ m_frameConsumed = true;
118
+ return info;
119
+ }
120
+
121
+ private:
122
+ mutable std::mutex m_stateMutex;
123
+ std::thread m_worker;
124
+ std::atomic<bool> m_running{false};
125
+ std::atomic<bool> m_stopRequested{false};
126
+
127
+ GMainLoop* m_glibLoop = nullptr;
128
+ GDBusConnection* m_connection = nullptr;
129
+ pw_main_loop* m_pwLoop = nullptr;
130
+ StreamState m_streamState{};
131
+ PortalStage m_stage = PortalStage::Idle;
132
+
133
+ std::string m_sessionHandle;
134
+ std::optional<SharedHandleInfo> m_sharedHandle;
135
+ uint32_t m_streamNodeId = PW_ID_ANY;
136
+ uint32_t m_width = 0;
137
+ uint32_t m_height = 0;
138
+ uint32_t m_stride = 0;
139
+ uint32_t m_offset = 0;
140
+ uint64_t m_planeSize = 0;
141
+ uint32_t m_pixelFormat = 0;
142
+ uint64_t m_modifier = 0;
143
+ uint32_t m_bufferType = 0;
144
+ uint32_t m_chunkSize = 0;
145
+ bool m_loggedNonDmabuf = false;
146
+ mutable bool m_frameConsumed = false;
147
+
148
+ int m_pendingPipewireFd = -1;
149
+ int m_sharedFd = -1;
150
+
151
+ void RunCaptureFlow() {
152
+ int pipewireFd = -1;
153
+
154
+ try {
155
+ pw_init(nullptr, nullptr);
156
+
157
+ {
158
+ std::lock_guard<std::mutex> lock(m_stateMutex);
159
+ m_glibLoop = g_main_loop_new(nullptr, FALSE);
160
+ }
161
+
162
+ m_connection = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr);
163
+ if (!m_connection) {
164
+ throw std::runtime_error("Unable to connect to the session D-Bus");
165
+ }
166
+
167
+ g_dbus_connection_signal_subscribe(
168
+ m_connection,
169
+ "org.freedesktop.portal.Desktop",
170
+ "org.freedesktop.portal.Request",
171
+ "Response",
172
+ nullptr,
173
+ nullptr,
174
+ G_DBUS_SIGNAL_FLAGS_NONE,
175
+ &LinuxPlatformCapture::OnPortalResponse,
176
+ this,
177
+ nullptr);
178
+
179
+ GetLogFile() << "[1/4] CreateSession..." << std::endl;
180
+ GVariantBuilder builder;
181
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
182
+ g_variant_builder_add(&builder, "{sv}", "session_handle_token", g_variant_new_string(("s" + gen_token()).c_str()));
183
+ g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(("t" + gen_token()).c_str()));
184
+
185
+ m_stage = PortalStage::CreatingSession;
186
+ CallPortalMethod("CreateSession", g_variant_new("(a{sv})", &builder));
187
+
188
+ g_main_loop_run(m_glibLoop);
189
+
190
+ if (m_stopRequested.load()) {
191
+ throw std::runtime_error("Capture stopped before PipeWire remote was opened");
192
+ }
193
+
194
+ {
195
+ std::lock_guard<std::mutex> lock(m_stateMutex);
196
+ pipewireFd = m_pendingPipewireFd;
197
+ m_pendingPipewireFd = -1;
198
+ }
199
+
200
+ if (pipewireFd < 0) {
201
+ throw std::runtime_error("PipeWire file descriptor was not received from the portal");
202
+ }
203
+
204
+ StartPipewireStream(pipewireFd);
205
+ } catch (const std::exception& e) {
206
+ GetLogFile() << "[Linux capture] " << e.what() << std::endl;
207
+ if (pipewireFd >= 0) {
208
+ close(pipewireFd);
209
+ }
210
+ }
211
+
212
+ CleanupPortal();
213
+ CleanupPipewire();
214
+
215
+ m_stopRequested.store(false);
216
+ m_running.store(false);
217
+ }
218
+
219
+ void CallPortalMethod(const char* method, GVariant* params) {
220
+ GError* error = nullptr;
221
+ GVariant* result = g_dbus_connection_call_sync(
222
+ m_connection,
223
+ "org.freedesktop.portal.Desktop",
224
+ "/org/freedesktop/portal/desktop",
225
+ "org.freedesktop.portal.ScreenCast",
226
+ method,
227
+ params,
228
+ G_VARIANT_TYPE("(o)"),
229
+ G_DBUS_CALL_FLAGS_NONE,
230
+ -1,
231
+ nullptr,
232
+ &error);
233
+
234
+ if (!result) {
235
+ std::string message = error ? error->message : "Unknown portal error";
236
+ if (error) {
237
+ g_error_free(error);
238
+ }
239
+ throw std::runtime_error(message);
240
+ }
241
+
242
+ g_variant_unref(result);
243
+ }
244
+
245
+ void StartSession() {
246
+ GetLogFile() << "[3/4] Start..." << std::endl;
247
+ GVariantBuilder builder;
248
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
249
+ m_stage = PortalStage::StartingSession;
250
+ CallPortalMethod("Start", g_variant_new("(osa{sv})", m_sessionHandle.c_str(), "", &builder));
251
+ }
252
+
253
+ void SelectSources() {
254
+ GetLogFile() << "[2/4] SelectSources..." << std::endl;
255
+ GVariantBuilder builder;
256
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
257
+ g_variant_builder_add(&builder, "{sv}", "types", g_variant_new_uint32(1));
258
+ g_variant_builder_add(&builder, "{sv}", "multiple", g_variant_new_boolean(FALSE));
259
+ g_variant_builder_add(&builder, "{sv}", "cursor_mode", g_variant_new_uint32(2));
260
+
261
+ m_stage = PortalStage::SelectingSources;
262
+ CallPortalMethod("SelectSources", g_variant_new("(oa{sv})", m_sessionHandle.c_str(), &builder));
263
+ }
264
+
265
+ void OpenPipeWireRemote() {
266
+ GetLogFile() << "[4/4] OpenPipeWireRemote..." << std::endl;
267
+ GVariantBuilder builder;
268
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
269
+
270
+ GError* error = nullptr;
271
+ GUnixFDList* outFdList = nullptr;
272
+
273
+ GVariant* result = g_dbus_connection_call_with_unix_fd_list_sync(
274
+ m_connection,
275
+ "org.freedesktop.portal.Desktop",
276
+ "/org/freedesktop/portal/desktop",
277
+ "org.freedesktop.portal.ScreenCast",
278
+ "OpenPipeWireRemote",
279
+ g_variant_new("(oa{sv})", m_sessionHandle.c_str(), &builder),
280
+ G_VARIANT_TYPE("(h)"),
281
+ G_DBUS_CALL_FLAGS_NONE,
282
+ -1,
283
+ nullptr,
284
+ &outFdList,
285
+ nullptr,
286
+ &error);
287
+
288
+ if (!result) {
289
+ std::string message = error ? error->message : "Unknown portal error";
290
+ if (error) {
291
+ g_error_free(error);
292
+ }
293
+ throw std::runtime_error(message);
294
+ }
295
+
296
+ gint32 fdIndex = -1;
297
+ g_variant_get(result, "(h)", &fdIndex);
298
+ int fd = g_unix_fd_list_get(outFdList, fdIndex, &error);
299
+
300
+ g_variant_unref(result);
301
+ if (outFdList) {
302
+ g_object_unref(outFdList);
303
+ }
304
+
305
+ if (fd < 0) {
306
+ std::string message = error ? error->message : "Unable to extract PipeWire FD";
307
+ if (error) {
308
+ g_error_free(error);
309
+ }
310
+ throw std::runtime_error(message);
311
+ }
312
+
313
+ {
314
+ std::lock_guard<std::mutex> lock(m_stateMutex);
315
+ m_pendingPipewireFd = fd;
316
+ }
317
+
318
+ m_stage = PortalStage::OpeningRemote;
319
+ g_main_loop_quit(m_glibLoop);
320
+ }
321
+
322
+ void StartPipewireStream(int pipewireFd) {
323
+ m_streamState.pw_loop = pw_main_loop_new(nullptr);
324
+ if (!m_streamState.pw_loop) {
325
+ throw std::runtime_error("Unable to create the PipeWire main loop");
326
+ }
327
+
328
+ {
329
+ std::lock_guard<std::mutex> lock(m_stateMutex);
330
+ m_pwLoop = m_streamState.pw_loop;
331
+ }
332
+
333
+ pw_loop* loop = pw_main_loop_get_loop(m_streamState.pw_loop);
334
+ m_streamState.context = pw_context_new(loop, nullptr, 0);
335
+ if (!m_streamState.context) {
336
+ throw std::runtime_error("Unable to create the PipeWire context");
337
+ }
338
+
339
+ m_streamState.core = pw_context_connect_fd(m_streamState.context, pipewireFd, nullptr, 0);
340
+ if (!m_streamState.core) {
341
+ close(pipewireFd);
342
+ throw std::runtime_error("Unable to connect to the PipeWire core through the portal FD");
343
+ }
344
+
345
+ m_streamState.stream = pw_stream_new(
346
+ m_streamState.core,
347
+ "electron-capture",
348
+ pw_properties_new(
349
+ PW_KEY_MEDIA_TYPE, "Video",
350
+ PW_KEY_MEDIA_CATEGORY, "Capture",
351
+ PW_KEY_MEDIA_ROLE, "Screen",
352
+ nullptr));
353
+
354
+ if (!m_streamState.stream) {
355
+ throw std::runtime_error("Unable to create the PipeWire stream");
356
+ }
357
+
358
+ uint8_t buffer[2048];
359
+ spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
360
+ const spa_pod* params[2];
361
+
362
+ const spa_rectangle minSize = SPA_RECTANGLE(1, 1);
363
+ const spa_rectangle defaultSize = SPA_RECTANGLE(1920, 1080);
364
+ const spa_rectangle maxSize = SPA_RECTANGLE(8192, 8192);
365
+
366
+ const spa_fraction minFramerate = SPA_FRACTION(0, 1);
367
+ const spa_fraction defaultFramerate = SPA_FRACTION(60, 1);
368
+ const spa_fraction maxFramerate = SPA_FRACTION(144, 1);
369
+
370
+ params[0] = static_cast<const spa_pod*>(spa_pod_builder_add_object(
371
+ &builder,
372
+ SPA_TYPE_OBJECT_Format,
373
+ SPA_PARAM_EnumFormat,
374
+ SPA_FORMAT_mediaType,
375
+ SPA_POD_Id(SPA_MEDIA_TYPE_video),
376
+ SPA_FORMAT_mediaSubtype,
377
+ SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
378
+ SPA_FORMAT_VIDEO_format,
379
+ SPA_POD_CHOICE_ENUM_Id(7, SPA_VIDEO_FORMAT_BGRA, SPA_VIDEO_FORMAT_BGRA, SPA_VIDEO_FORMAT_RGBA, SPA_VIDEO_FORMAT_BGRx, SPA_VIDEO_FORMAT_RGBx, SPA_VIDEO_FORMAT_xBGR, SPA_VIDEO_FORMAT_xRGB),
380
+ SPA_FORMAT_VIDEO_size,
381
+ SPA_POD_CHOICE_RANGE_Rectangle(&defaultSize, &minSize, &maxSize),
382
+ SPA_FORMAT_VIDEO_framerate,
383
+ SPA_POD_CHOICE_RANGE_Fraction(&defaultFramerate, &minFramerate, &maxFramerate),
384
+ SPA_FORMAT_VIDEO_modifier,
385
+ SPA_POD_CHOICE_ENUM_Long(3, 0ULL, 0ULL, 0x00ffffffffffffffULL)));
386
+
387
+ params[1] = static_cast<const spa_pod*>(spa_pod_builder_add_object(
388
+ &builder,
389
+ SPA_TYPE_OBJECT_ParamBuffers,
390
+ SPA_PARAM_Buffers,
391
+ SPA_PARAM_BUFFERS_dataType,
392
+ SPA_POD_CHOICE_FLAGS_Int(1 << SPA_DATA_DmaBuf)));
393
+
394
+ pw_stream_add_listener(
395
+ m_streamState.stream,
396
+ &m_streamState.stream_listener,
397
+ &kStreamEvents,
398
+ this);
399
+
400
+ GetLogFile() << "[PipeWire] Connecting stream..." << std::endl;
401
+ uint32_t targetId = m_streamNodeId == PW_ID_ANY ? PW_ID_ANY : m_streamNodeId;
402
+ int result = pw_stream_connect(
403
+ m_streamState.stream,
404
+ PW_DIRECTION_INPUT,
405
+ targetId,
406
+ static_cast<pw_stream_flags>(PW_STREAM_FLAG_AUTOCONNECT),
407
+ params,
408
+ 2);
409
+
410
+ if (result < 0) {
411
+ throw std::runtime_error(std::string("pw_stream_connect failed: ") + spa_strerror(result));
412
+ }
413
+
414
+ pw_main_loop_run(m_streamState.pw_loop);
415
+ }
416
+
417
+ void CleanupPortal() {
418
+ if (m_connection) {
419
+ g_object_unref(m_connection);
420
+ m_connection = nullptr;
421
+ }
422
+
423
+ if (m_glibLoop) {
424
+ g_main_loop_unref(m_glibLoop);
425
+ m_glibLoop = nullptr;
426
+ }
427
+
428
+ m_sessionHandle.clear();
429
+ m_streamNodeId = PW_ID_ANY;
430
+ m_stage = PortalStage::Idle;
431
+ }
432
+
433
+ void CleanupPipewire() {
434
+ if (m_streamState.stream) {
435
+ pw_stream_destroy(m_streamState.stream);
436
+ m_streamState.stream = nullptr;
437
+ }
438
+
439
+ if (m_streamState.core) {
440
+ pw_core_disconnect(m_streamState.core);
441
+ m_streamState.core = nullptr;
442
+ }
443
+
444
+ if (m_streamState.context) {
445
+ pw_context_destroy(m_streamState.context);
446
+ m_streamState.context = nullptr;
447
+ }
448
+
449
+ if (m_streamState.pw_loop) {
450
+ pw_main_loop_destroy(m_streamState.pw_loop);
451
+ m_streamState.pw_loop = nullptr;
452
+ }
453
+
454
+ {
455
+ std::lock_guard<std::mutex> lock(m_stateMutex);
456
+ m_pwLoop = nullptr;
457
+ }
458
+
459
+ CleanupSharedHandle();
460
+ }
461
+
462
+ void CleanupSharedHandle() {
463
+ std::lock_guard<std::mutex> lock(m_stateMutex);
464
+ m_sharedHandle.reset();
465
+
466
+ if (m_sharedFd >= 0) {
467
+ close(m_sharedFd);
468
+ m_sharedFd = -1;
469
+ }
470
+
471
+ m_width = 0;
472
+ m_height = 0;
473
+ m_stride = 0;
474
+ m_offset = 0;
475
+ m_planeSize = 0;
476
+ m_pixelFormat = 0;
477
+ m_modifier = 0;
478
+ m_bufferType = 0;
479
+ m_chunkSize = 0;
480
+ m_loggedNonDmabuf = false;
481
+ m_frameConsumed = false;
482
+ }
483
+
484
+ void PublishSharedHandleLocked() {
485
+ if (m_sharedFd < 0 || m_width == 0 || m_height == 0) {
486
+ return;
487
+ }
488
+
489
+ m_sharedHandle = SharedHandleInfo{
490
+ static_cast<uint64_t>(m_sharedFd),
491
+ m_width,
492
+ m_height,
493
+ m_stride,
494
+ m_offset,
495
+ m_planeSize,
496
+ m_pixelFormat,
497
+ m_modifier,
498
+ m_bufferType,
499
+ m_chunkSize,
500
+ };
501
+ }
502
+
503
+ void UpdateSharedHandleFromFd(int fd) {
504
+ int duplicatedFd = dup(fd);
505
+ if (duplicatedFd < 0) {
506
+ return;
507
+ }
508
+
509
+ std::lock_guard<std::mutex> lock(m_stateMutex);
510
+ if (m_sharedFd >= 0) {
511
+ close(m_sharedFd);
512
+ }
513
+ m_sharedFd = duplicatedFd;
514
+ m_frameConsumed = false;
515
+ PublishSharedHandleLocked();
516
+ }
517
+
518
+ static void OnStreamStateChanged(void*, pw_stream_state oldState, pw_stream_state state, const char* error) {
519
+ GetLogFile() << "[PipeWire] State: " << pw_stream_state_as_string(state)
520
+ << " (old: " << pw_stream_state_as_string(oldState) << ")" << std::endl;
521
+ if (state == PW_STREAM_STATE_ERROR && error) {
522
+ GetLogFile() << "[PipeWire] Stream error: " << error << std::endl;
523
+ }
524
+ }
525
+
526
+ static void OnStreamParamChanged(void* data, uint32_t id, const spa_pod* param) {
527
+ auto* self = static_cast<LinuxPlatformCapture*>(data);
528
+ if (!param || id != SPA_PARAM_Format) {
529
+ return;
530
+ }
531
+
532
+ spa_video_info_raw info{};
533
+ if (spa_format_video_raw_parse(param, &info) < 0) {
534
+ return;
535
+ }
536
+
537
+ {
538
+ std::lock_guard<std::mutex> lock(self->m_stateMutex);
539
+ self->m_width = info.size.width;
540
+ self->m_height = info.size.height;
541
+ self->m_pixelFormat = static_cast<uint32_t>(info.format);
542
+ self->m_modifier = info.modifier;
543
+ self->PublishSharedHandleLocked();
544
+ }
545
+
546
+ uint8_t buffer[1024];
547
+ spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
548
+ const spa_pod* params[1];
549
+ params[0] = static_cast<const spa_pod*>(spa_pod_builder_add_object(
550
+ &builder,
551
+ SPA_TYPE_OBJECT_Format,
552
+ SPA_PARAM_Format,
553
+ SPA_FORMAT_mediaType,
554
+ SPA_POD_Id(SPA_MEDIA_TYPE_video),
555
+ SPA_FORMAT_mediaSubtype,
556
+ SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
557
+ SPA_FORMAT_VIDEO_format,
558
+ SPA_POD_Id(info.format),
559
+ SPA_FORMAT_VIDEO_size,
560
+ SPA_POD_Rectangle(&info.size),
561
+ SPA_FORMAT_VIDEO_framerate,
562
+ SPA_POD_Fraction(&info.framerate)));
563
+
564
+ pw_stream_update_params(self->m_streamState.stream, params, 1);
565
+ }
566
+
567
+ static void OnStreamProcess(void* userdata) {
568
+ auto* self = static_cast<LinuxPlatformCapture*>(userdata);
569
+ if (!self->m_streamState.stream) {
570
+ return;
571
+ }
572
+
573
+ pw_buffer* buffer = pw_stream_dequeue_buffer(self->m_streamState.stream);
574
+ if (!buffer) {
575
+ return;
576
+ }
577
+
578
+ spa_buffer* spaBuffer = buffer->buffer;
579
+ if (spaBuffer && spaBuffer->n_datas > 0) {
580
+ spa_data& data = spaBuffer->datas[0];
581
+ const uint32_t chunkSize = data.chunk ? data.chunk->size : 0;
582
+ if ((data.type == SPA_DATA_DmaBuf || data.type == SPA_DATA_MemFd) && data.fd >= 0) {
583
+ {
584
+ std::lock_guard<std::mutex> lock(self->m_stateMutex);
585
+ self->m_bufferType = static_cast<uint32_t>(data.type);
586
+ self->m_chunkSize = chunkSize;
587
+ if (data.chunk) {
588
+ self->m_stride = data.chunk->stride;
589
+ self->m_offset = data.chunk->offset;
590
+ self->m_planeSize = data.maxsize;
591
+ } else {
592
+ self->m_stride = 0;
593
+ self->m_offset = 0;
594
+ self->m_planeSize = data.maxsize;
595
+ }
596
+ }
597
+ self->UpdateSharedHandleFromFd(data.fd);
598
+ self->m_loggedNonDmabuf = false;
599
+ } else {
600
+ std::lock_guard<std::mutex> lock(self->m_stateMutex);
601
+ self->m_bufferType = static_cast<uint32_t>(data.type);
602
+ self->m_chunkSize = chunkSize;
603
+ }
604
+
605
+ if (data.type != SPA_DATA_DmaBuf && data.type != SPA_DATA_MemFd && !self->m_loggedNonDmabuf) {
606
+ self->m_loggedNonDmabuf = true;
607
+ GetLogFile() << "[PipeWire] Non-DMA buffer type received: " << data.type
608
+ << " (expecting SPA_DATA_DmaBuf=" << SPA_DATA_DmaBuf << ")" << std::endl;
609
+ }
610
+
611
+ if (data.type == SPA_DATA_DmaBuf) {
612
+ GetLogFile() << "[PipeWire] Frame DMA-BUF: chunk_size=" << chunkSize << " maxsize=" << data.maxsize << std::endl;
613
+ } else if (data.type == SPA_DATA_MemFd) {
614
+ GetLogFile() << "[PipeWire] Frame MemFd: chunk_size=" << chunkSize << " maxsize=" << data.maxsize << std::endl;
615
+ } else if (data.type == SPA_DATA_MemPtr) {
616
+ GetLogFile() << "[PipeWire] Frame MemPtr: chunk_size=" << chunkSize << " maxsize=" << data.maxsize << std::endl;
617
+ }
618
+ }
619
+
620
+ pw_stream_queue_buffer(self->m_streamState.stream, buffer);
621
+ }
622
+
623
+ static void OnPortalResponse(
624
+ GDBusConnection*,
625
+ const gchar*,
626
+ const gchar*,
627
+ const gchar*,
628
+ const gchar*,
629
+ GVariant* parameters,
630
+ gpointer userData) {
631
+ auto* self = static_cast<LinuxPlatformCapture*>(userData);
632
+
633
+ guint32 responseCode = 1;
634
+ GVariantIter* results = nullptr;
635
+ g_variant_get(parameters, "(ua{sv})", &responseCode, &results);
636
+ auto freeResults = [&results]() {
637
+ if (results) {
638
+ g_variant_iter_free(results);
639
+ results = nullptr;
640
+ }
641
+ };
642
+
643
+ if (responseCode != 0) {
644
+ GetLogFile() << "[Portal] Response error: " << responseCode << std::endl;
645
+ freeResults();
646
+ if (self->m_glibLoop) {
647
+ g_main_loop_quit(self->m_glibLoop);
648
+ }
649
+ return;
650
+ }
651
+
652
+ try {
653
+ if (self->m_stage == PortalStage::CreatingSession) {
654
+ const gchar* key = nullptr;
655
+ GVariant* value = nullptr;
656
+ bool foundSession = false;
657
+
658
+ while (results && g_variant_iter_next(results, "{sv}", &key, &value)) {
659
+ if (g_strcmp0(key, "session_handle") == 0) {
660
+ self->m_sessionHandle = g_variant_get_string(value, nullptr);
661
+ foundSession = true;
662
+ }
663
+
664
+ g_variant_unref(value);
665
+ }
666
+
667
+ freeResults();
668
+
669
+ if (!foundSession || self->m_sessionHandle.empty()) {
670
+ throw std::runtime_error("CreateSession response did not include a session handle");
671
+ }
672
+
673
+ self->SelectSources();
674
+ return;
675
+ }
676
+
677
+ if (self->m_stage == PortalStage::SelectingSources) {
678
+ freeResults();
679
+ self->StartSession();
680
+ return;
681
+ }
682
+
683
+ if (self->m_stage == PortalStage::StartingSession) {
684
+ const gchar* key = nullptr;
685
+ GVariant* value = nullptr;
686
+ bool foundNode = false;
687
+ uint32_t nodeId = PW_ID_ANY;
688
+
689
+ while (results && g_variant_iter_next(results, "{sv}", &key, &value)) {
690
+ if (g_strcmp0(key, "streams") == 0 && g_variant_is_of_type(value, G_VARIANT_TYPE("a(ua{sv})"))) {
691
+ GVariantIter streamIter;
692
+ g_variant_iter_init(&streamIter, value);
693
+ GVariant* streamTuple = g_variant_iter_next_value(&streamIter);
694
+ if (streamTuple) {
695
+ GVariant* props = nullptr;
696
+ g_variant_get(streamTuple, "(u@a{sv})", &nodeId, &props);
697
+ if (props) {
698
+ g_variant_unref(props);
699
+ }
700
+ g_variant_unref(streamTuple);
701
+ foundNode = true;
702
+ }
703
+ }
704
+
705
+ g_variant_unref(value);
706
+ }
707
+
708
+ freeResults();
709
+
710
+ if (!foundNode || nodeId == PW_ID_ANY) {
711
+ throw std::runtime_error("Start response did not include a valid PipeWire stream node id");
712
+ }
713
+
714
+ self->m_streamNodeId = nodeId;
715
+ self->OpenPipeWireRemote();
716
+ return;
717
+ }
718
+
719
+ freeResults();
720
+ if (self->m_glibLoop) {
721
+ g_main_loop_quit(self->m_glibLoop);
722
+ }
723
+ } catch (const std::exception& e) {
724
+ freeResults();
725
+ GetLogFile() << "[Portal] " << e.what() << std::endl;
726
+ if (self->m_glibLoop) {
727
+ g_main_loop_quit(self->m_glibLoop);
728
+ }
729
+ }
730
+ }
731
+
732
+ static const pw_stream_events kStreamEvents;
733
+ };
734
+
735
+ const pw_stream_events LinuxPlatformCapture::kStreamEvents = [] {
736
+ pw_stream_events events{};
737
+ events.version = PW_VERSION_STREAM_EVENTS;
738
+ events.state_changed = LinuxPlatformCapture::OnStreamStateChanged;
739
+ events.param_changed = LinuxPlatformCapture::OnStreamParamChanged;
740
+ events.process = LinuxPlatformCapture::OnStreamProcess;
741
+ return events;
742
+ }();
743
+
744
+ std::unique_ptr<IPlatformCapture> CreatePlatformCapture() {
745
+ return std::make_unique<LinuxPlatformCapture>();
746
+ }
747
+
748
+ #endif
@@ -0,0 +1,30 @@
1
+ #pragma once
2
+
3
+ #include <napi.h>
4
+ #include <cstdint>
5
+ #include <memory>
6
+ #include <optional>
7
+
8
+ struct SharedHandleInfo {
9
+ uint64_t handle = 0;
10
+ uint32_t width = 0;
11
+ uint32_t height = 0;
12
+ uint32_t stride = 0;
13
+ uint32_t offset = 0;
14
+ uint64_t planeSize = 0;
15
+ uint32_t pixelFormat = 0;
16
+ uint64_t modifier = 0;
17
+ uint32_t bufferType = 0;
18
+ uint32_t chunkSize = 0;
19
+ };
20
+
21
+ class IPlatformCapture {
22
+ public:
23
+ virtual ~IPlatformCapture() = default;
24
+
25
+ virtual void Start(Napi::Env env) = 0;
26
+ virtual void Stop() = 0;
27
+ virtual std::optional<SharedHandleInfo> GetSharedHandle() const = 0;
28
+ };
29
+
30
+ std::unique_ptr<IPlatformCapture> CreatePlatformCapture();
@@ -0,0 +1,24 @@
1
+ #ifndef _WIN32
2
+
3
+ #include "platform_capture.hpp"
4
+
5
+ #include <stdexcept>
6
+
7
+ class StubPlatformCapture final : public IPlatformCapture {
8
+ public:
9
+ void Start(Napi::Env) override {
10
+ throw std::runtime_error("Screen capture backend is not implemented for this platform yet");
11
+ }
12
+
13
+ void Stop() override {}
14
+
15
+ std::optional<SharedHandleInfo> GetSharedHandle() const override {
16
+ return std::nullopt;
17
+ }
18
+ };
19
+
20
+ std::unique_ptr<IPlatformCapture> CreatePlatformCapture() {
21
+ return std::make_unique<StubPlatformCapture>();
22
+ }
23
+
24
+ #endif
package/src/types.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ declare module 'node-gyp-build' {
2
+ function nodeGypBuild(path: string): any;
3
+ export default nodeGypBuild;
4
+ }
@@ -0,0 +1,277 @@
1
+ #ifdef _WIN32
2
+
3
+ #include "../platform_capture.hpp"
4
+
5
+ #include <atomic>
6
+ #include <stdexcept>
7
+ #include <thread>
8
+ #include <mutex>
9
+ #include <condition_variable>
10
+ #include <windows.h>
11
+ #include <d3d11.h>
12
+ #include <dxgi1_2.h>
13
+ #include <winrt/base.h>
14
+ #include <winrt/Windows.Foundation.h>
15
+ #include <winrt/Windows.Graphics.h>
16
+ #include <winrt/Windows.Graphics.Capture.h>
17
+ #include <winrt/Windows.Graphics.DirectX.Direct3D11.h>
18
+ #include <windows.graphics.capture.interop.h>
19
+ #include <windows.graphics.directx.direct3d11.interop.h>
20
+
21
+ using namespace winrt;
22
+ using namespace winrt::Windows::Graphics::Capture;
23
+ using namespace winrt::Windows::Graphics::DirectX::Direct3D11;
24
+
25
+ struct __declspec(uuid("A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1"))
26
+ IDirect3DDxgiInterfaceAccess : ::IUnknown {
27
+ virtual HRESULT __stdcall GetInterface(REFIID iid, void** p) = 0;
28
+ };
29
+
30
+ inline IDirect3DDevice CreateDirect3DDevice(IDXGIDevice* dxgiDevice) {
31
+ com_ptr<::IInspectable> inspectable;
32
+ check_hresult(CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice, inspectable.put()));
33
+ return inspectable.as<IDirect3DDevice>();
34
+ }
35
+
36
+ class WinPlatformCapture final : public IPlatformCapture {
37
+ public:
38
+ WinPlatformCapture() = default;
39
+
40
+ ~WinPlatformCapture() override {
41
+ StopInternal();
42
+ }
43
+
44
+ static void CleanupHook(void* arg) {
45
+ if (arg) {
46
+ static_cast<WinPlatformCapture*>(arg)->StopInternal();
47
+ }
48
+ }
49
+
50
+ void Start(Napi::Env env) override {
51
+ if (m_isRunning) return;
52
+
53
+ m_env = env;
54
+ napi_add_env_cleanup_hook(m_env, CleanupHook, this);
55
+
56
+ m_isRunning = true;
57
+ m_captureThread = std::thread([this]() {
58
+ try {
59
+ init_apartment(apartment_type::multi_threaded);
60
+ InitializeD3D();
61
+
62
+ HMONITOR monitor = MonitorFromWindow(nullptr, MONITOR_DEFAULTTOPRIMARY);
63
+
64
+ auto interop = get_activation_factory<GraphicsCaptureItem, IGraphicsCaptureItemInterop>();
65
+ check_hresult(
66
+ interop->CreateForMonitor(
67
+ monitor,
68
+ guid_of<GraphicsCaptureItem>(),
69
+ put_abi(m_item)
70
+ )
71
+ );
72
+
73
+ com_ptr<IDXGIDevice> dxgiDevice;
74
+ m_device->QueryInterface(__uuidof(IDXGIDevice), dxgiDevice.put_void());
75
+
76
+ m_winrtDevice = CreateDirect3DDevice(dxgiDevice.get());
77
+
78
+ m_width = m_item.Size().Width;
79
+ m_height = m_item.Size().Height;
80
+
81
+ CreateFramePoolAndSession();
82
+
83
+ // Wait until StopInternal() is called
84
+ std::unique_lock<std::mutex> lock(m_mutex);
85
+ m_cv.wait_for(lock, std::chrono::milliseconds(100), [this]() { return !m_isRunning.load(); });
86
+ while (m_isRunning.load()) {
87
+ m_cv.wait_for(lock, std::chrono::milliseconds(100), [this]() { return !m_isRunning.load(); });
88
+ }
89
+
90
+ CleanupCapture();
91
+
92
+ } catch (const hresult_error& e) {
93
+ OutputDebugStringW(e.message().c_str());
94
+ } catch (const std::exception& e) {
95
+ OutputDebugStringA(e.what());
96
+ } catch (...) {
97
+ OutputDebugStringA("Capture thread error");
98
+ }
99
+ });
100
+ }
101
+
102
+ void Stop() override {
103
+ StopInternal();
104
+ }
105
+
106
+ std::optional<SharedHandleInfo> GetSharedHandle() const override {
107
+ HANDLE handle = m_sharedHandle.load();
108
+ if (!handle) return std::nullopt;
109
+
110
+ SharedHandleInfo info;
111
+ info.handle = reinterpret_cast<uint64_t>(handle);
112
+ info.width = m_width;
113
+ info.height = m_height;
114
+ return info;
115
+ }
116
+
117
+ private:
118
+ napi_env m_env{ nullptr };
119
+ std::thread m_captureThread;
120
+ std::atomic<bool> m_isRunning{ false };
121
+ std::mutex m_mutex;
122
+ std::condition_variable m_cv;
123
+
124
+ com_ptr<ID3D11Device> m_device;
125
+ com_ptr<ID3D11DeviceContext> m_context;
126
+
127
+ IDirect3DDevice m_winrtDevice{ nullptr };
128
+
129
+ com_ptr<ID3D11Texture2D> m_sharedTex;
130
+ std::atomic<HANDLE> m_sharedHandle{ nullptr };
131
+
132
+ GraphicsCaptureItem m_item{ nullptr };
133
+ Direct3D11CaptureFramePool m_framePool{ nullptr };
134
+ GraphicsCaptureSession m_session{ nullptr };
135
+ winrt::event_token m_token{};
136
+
137
+ uint32_t m_width = 0;
138
+ uint32_t m_height = 0;
139
+
140
+ void InitializeD3D() {
141
+ D3D_FEATURE_LEVEL levels[] = { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0 };
142
+ check_hresult(D3D11CreateDevice(
143
+ nullptr,
144
+ D3D_DRIVER_TYPE_HARDWARE,
145
+ nullptr,
146
+ D3D11_CREATE_DEVICE_BGRA_SUPPORT,
147
+ levels,
148
+ ARRAYSIZE(levels),
149
+ D3D11_SDK_VERSION,
150
+ m_device.put(),
151
+ nullptr,
152
+ m_context.put()
153
+ ));
154
+ }
155
+
156
+ void StopInternal() {
157
+ if (m_env) {
158
+ napi_remove_env_cleanup_hook(m_env, CleanupHook, this);
159
+ m_env = nullptr;
160
+ }
161
+
162
+ if (!m_isRunning.exchange(false)) return;
163
+ m_cv.notify_all();
164
+
165
+ if (m_captureThread.joinable()) {
166
+ m_captureThread.join();
167
+ }
168
+ }
169
+
170
+ void CleanupCapture() {
171
+ if (m_framePool) {
172
+ if (m_token.value) {
173
+ m_framePool.FrameArrived(m_token);
174
+ m_token.value = 0;
175
+ }
176
+ m_framePool.Close();
177
+ m_framePool = nullptr;
178
+ }
179
+
180
+ if (m_session) {
181
+ m_session.Close();
182
+ m_session = nullptr;
183
+ }
184
+
185
+ m_item = nullptr;
186
+ m_winrtDevice = nullptr;
187
+ m_sharedTex = nullptr;
188
+ m_device = nullptr;
189
+ m_context = nullptr;
190
+
191
+ HANDLE handle = m_sharedHandle.exchange(nullptr);
192
+ if (handle) CloseHandle(handle);
193
+ }
194
+
195
+ void CreateFramePoolAndSession() {
196
+ if (m_framePool) {
197
+ if (m_token.value) {
198
+ m_framePool.FrameArrived(m_token);
199
+ m_token.value = 0;
200
+ }
201
+ m_framePool.Close();
202
+ m_framePool = nullptr;
203
+ }
204
+ if (m_session) {
205
+ m_session.Close();
206
+ m_session = nullptr;
207
+ }
208
+
209
+ m_framePool = Direct3D11CaptureFramePool::CreateFreeThreaded(
210
+ m_winrtDevice,
211
+ winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized,
212
+ 2,
213
+ m_item.Size()
214
+ );
215
+
216
+ m_session = m_framePool.CreateCaptureSession(m_item);
217
+ m_session.IsCursorCaptureEnabled(false);
218
+ m_session.IsBorderRequired(false);
219
+ m_session.IncludeSecondaryWindows(true);
220
+
221
+ m_token = m_framePool.FrameArrived({ this, &WinPlatformCapture::OnFrame });
222
+ m_session.StartCapture();
223
+ }
224
+
225
+ void OnFrame(Direct3D11CaptureFramePool const& sender,
226
+ winrt::Windows::Foundation::IInspectable const&) {
227
+ try {
228
+ auto frame = sender.TryGetNextFrame();
229
+ if (!frame) return;
230
+
231
+ auto size = frame.ContentSize();
232
+
233
+ if (size.Width == 0 || size.Height == 0) return;
234
+
235
+ if (size.Width != m_width || size.Height != m_height) {
236
+ m_width = size.Width;
237
+ m_height = size.Height;
238
+ CreateFramePoolAndSession();
239
+ }
240
+
241
+ auto surface = frame.Surface().as<IDirect3DSurface>();
242
+ auto access = surface.as<IDirect3DDxgiInterfaceAccess>();
243
+
244
+ com_ptr<ID3D11Texture2D> srcTex;
245
+ access->GetInterface(__uuidof(ID3D11Texture2D), srcTex.put_void());
246
+
247
+ HANDLE expected = nullptr;
248
+ if (m_sharedHandle.compare_exchange_strong(expected, nullptr)) {
249
+ com_ptr<IDXGIResource1> res;
250
+ srcTex->QueryInterface(__uuidof(IDXGIResource1), res.put_void());
251
+
252
+ HANDLE handle = nullptr;
253
+ res->CreateSharedHandle(
254
+ nullptr,
255
+ DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE,
256
+ nullptr,
257
+ &handle
258
+ );
259
+
260
+ if (handle) {
261
+ m_sharedHandle.store(handle);
262
+ m_sharedTex = srcTex;
263
+ }
264
+ }
265
+ } catch (const std::exception& e) {
266
+ OutputDebugStringA(e.what());
267
+ } catch (...) {
268
+ OutputDebugStringA("Unknown error in OnFrame");
269
+ }
270
+ }
271
+ };
272
+
273
+ std::unique_ptr<IPlatformCapture> CreatePlatformCapture() {
274
+ return std::make_unique<WinPlatformCapture>();
275
+ }
276
+
277
+ #endif