electron-native-screenshare 1.0.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/LICENSE +21 -0
- package/README.md +199 -0
- package/binding.gyp +67 -0
- package/lib/index.js +198 -0
- package/package.json +58 -0
- package/src/linux/addon.cpp +107 -0
- package/src/linux/pipewire_capture.cpp +344 -0
- package/src/linux/pipewire_capture.h +64 -0
- package/src/mac/addon.cpp +107 -0
- package/src/mac/coreaudio_capture.h +61 -0
- package/src/mac/coreaudio_capture.mm +267 -0
- package/src/win/addon.cpp +136 -0
- package/src/win/wasapi_capture.cpp +223 -0
- package/src/win/wasapi_capture.h +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CilginSinek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# electron-native-screenshare
|
|
2
|
+
|
|
3
|
+
Cross-platform native audio capture for Electron screen sharing with **process-level audio isolation**.
|
|
4
|
+
|
|
5
|
+
Uses low-level OS APIs instead of web APIs for **lower latency, lower CPU usage, and true per-process audio control**.
|
|
6
|
+
|
|
7
|
+
| Platform | API | Min Version | Include Mode | Exclude Mode |
|
|
8
|
+
|----------|-----|-------------|:------------:|:------------:|
|
|
9
|
+
| Windows | WASAPI Process Loopback | Windows 10 2004 | ✅ Per-process | ✅ Per-process |
|
|
10
|
+
| macOS | ScreenCaptureKit | macOS 13 Ventura | ✅ Per-app | ✅ Per-app |
|
|
11
|
+
| Linux | PipeWire | 0.3.26+ | ✅ Per-process | ⚠️ System audio* |
|
|
12
|
+
|
|
13
|
+
> \* Linux exclude mode captures all system audio from the default output. Per-process exclusion is an OS-level limitation of PipeWire.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install electron-native-screenshare
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Platform Prerequisites
|
|
22
|
+
|
|
23
|
+
**Windows**: Visual Studio Build Tools with "Desktop development with C++" workload.
|
|
24
|
+
|
|
25
|
+
**macOS**: Xcode Command Line Tools. macOS 13 (Ventura) or later required.
|
|
26
|
+
|
|
27
|
+
**Linux**:
|
|
28
|
+
```bash
|
|
29
|
+
sudo apt install libpipewire-0.3-dev libx11-dev
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
> ⚠️ If PipeWire is not installed on Linux, the module will load but audio functions will throw a descriptive error at runtime. It will **not** crash your application.
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
const {
|
|
38
|
+
startCapture,
|
|
39
|
+
stopCapture,
|
|
40
|
+
getPidFromWindowHandle,
|
|
41
|
+
isAvailable
|
|
42
|
+
} = require('electron-native-screenshare');
|
|
43
|
+
|
|
44
|
+
// Check if native module loaded successfully
|
|
45
|
+
if (!isAvailable()) {
|
|
46
|
+
console.warn('Native audio capture is not available on this platform');
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
### `startCapture(processId?, isIncludeMode?, onData?)`
|
|
53
|
+
|
|
54
|
+
Starts audio capture with process-level isolation.
|
|
55
|
+
|
|
56
|
+
**Parameters:**
|
|
57
|
+
| Name | Type | Default | Description |
|
|
58
|
+
|------|------|---------|-------------|
|
|
59
|
+
| `processId` | `number` | `process.pid` | Target process ID |
|
|
60
|
+
| `isIncludeMode` | `boolean` | `false` | `true` = capture only target, `false` = exclude target |
|
|
61
|
+
| `onData` | `function` | `() => {}` | Callback `(data: Buffer, meta: AudioMetadata) => void` |
|
|
62
|
+
|
|
63
|
+
**Returns:** `boolean` — `true` if started successfully.
|
|
64
|
+
|
|
65
|
+
**Throws:** `Error` if native module unavailable or initialization fails.
|
|
66
|
+
|
|
67
|
+
#### AudioMetadata
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
interface AudioMetadata {
|
|
71
|
+
sampleRate: number; // e.g., 48000
|
|
72
|
+
channels: number; // e.g., 2 (stereo)
|
|
73
|
+
bitsPerSample: number; // e.g., 32
|
|
74
|
+
isFloat: boolean; // true = IEEE float, false = integer PCM
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### `stopCapture()`
|
|
79
|
+
|
|
80
|
+
Stops the active capture session. Safe to call even if nothing is capturing.
|
|
81
|
+
|
|
82
|
+
**Returns:** `boolean`
|
|
83
|
+
|
|
84
|
+
### `getPidFromWindowHandle(windowHandle)`
|
|
85
|
+
|
|
86
|
+
Resolves a native window handle to its owning process ID.
|
|
87
|
+
|
|
88
|
+
| Platform | Handle Type | Notes |
|
|
89
|
+
|----------|-------------|-------|
|
|
90
|
+
| Windows | `HWND` | Auto-resolves UWP ApplicationFrameWindow → child process |
|
|
91
|
+
| macOS | `CGWindowID` | Uses CGWindowListCopyWindowInfo |
|
|
92
|
+
| Linux | X11 Window ID | Uses `_NET_WM_PID` atom |
|
|
93
|
+
|
|
94
|
+
**Parameters:**
|
|
95
|
+
| Name | Type | Description |
|
|
96
|
+
|------|------|-------------|
|
|
97
|
+
| `windowHandle` | `number` | Native window handle from Electron's `desktopCapturer` |
|
|
98
|
+
|
|
99
|
+
**Returns:** `number` — Process ID, or `0` if not found.
|
|
100
|
+
|
|
101
|
+
### `isAvailable()`
|
|
102
|
+
|
|
103
|
+
Returns `true` if the native module loaded successfully.
|
|
104
|
+
|
|
105
|
+
### `getPlatform()`
|
|
106
|
+
|
|
107
|
+
Returns the current platform: `'win32'`, `'darwin'`, or `'linux'`.
|
|
108
|
+
|
|
109
|
+
### `getLoadError()`
|
|
110
|
+
|
|
111
|
+
Returns the load error message if the native module failed, or `null` on success.
|
|
112
|
+
|
|
113
|
+
## Usage Examples
|
|
114
|
+
|
|
115
|
+
### Screen Sharing (Exclude Mode)
|
|
116
|
+
|
|
117
|
+
Capture all system audio **except** your Electron app:
|
|
118
|
+
|
|
119
|
+
```javascript
|
|
120
|
+
const { startCapture, stopCapture } = require('electron-native-screenshare');
|
|
121
|
+
|
|
122
|
+
// Your app's audio will NOT go into the stream
|
|
123
|
+
startCapture(process.pid, false, (audioData, meta) => {
|
|
124
|
+
// audioData: Buffer of raw PCM float32 samples
|
|
125
|
+
// meta: { sampleRate: 48000, channels: 2, bitsPerSample: 32, isFloat: true }
|
|
126
|
+
|
|
127
|
+
// Send to WebRTC, write to file, or process as needed
|
|
128
|
+
webrtcTrack.write(audioData);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Later...
|
|
132
|
+
stopCapture();
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Window Sharing (Include Mode)
|
|
136
|
+
|
|
137
|
+
Capture **only** a specific window's audio:
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
const {
|
|
141
|
+
startCapture,
|
|
142
|
+
stopCapture,
|
|
143
|
+
getPidFromWindowHandle
|
|
144
|
+
} = require('electron-native-screenshare');
|
|
145
|
+
|
|
146
|
+
// Get sources from Electron's desktopCapturer
|
|
147
|
+
const sources = await desktopCapturer.getSources({ types: ['window'] });
|
|
148
|
+
const target = sources.find(s => s.name === 'Spotify');
|
|
149
|
+
|
|
150
|
+
// Extract window handle and resolve PID
|
|
151
|
+
const hwnd = parseInt(target.id.split(':')[1]);
|
|
152
|
+
const pid = getPidFromWindowHandle(hwnd);
|
|
153
|
+
|
|
154
|
+
// Capture only Spotify's audio
|
|
155
|
+
startCapture(pid, true, (audioData, meta) => {
|
|
156
|
+
// Only Spotify's audio is in the buffer
|
|
157
|
+
webrtcTrack.write(audioData);
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Graceful Degradation
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
const capture = require('electron-native-screenshare');
|
|
165
|
+
|
|
166
|
+
if (!capture.isAvailable()) {
|
|
167
|
+
const error = capture.getLoadError();
|
|
168
|
+
console.warn(`Audio capture unavailable: ${error}`);
|
|
169
|
+
// Fall back to Electron's built-in audio capture
|
|
170
|
+
// or disable audio in your sharing feature
|
|
171
|
+
} else {
|
|
172
|
+
capture.startCapture(process.pid, false, onAudioData);
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## How It Works
|
|
177
|
+
|
|
178
|
+
### Windows — WASAPI Process Loopback
|
|
179
|
+
Uses the Windows Audio Session API (WASAPI) with `AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK` (Windows 10 2004+). This provides true kernel-level process audio isolation with zero mixing overhead.
|
|
180
|
+
|
|
181
|
+
### macOS — ScreenCaptureKit
|
|
182
|
+
Uses Apple's `SCStream` with `SCContentFilter` (macOS 13+). Filters by `SCRunningApplication` to include/exclude specific apps. Audio is delivered via `SCStreamOutput` delegate as `CMSampleBuffer`.
|
|
183
|
+
|
|
184
|
+
### Linux — PipeWire
|
|
185
|
+
Uses PipeWire's `pw_stream` API for audio capture. Include mode connects directly to the target process's audio node via `PW_KEY_TARGET_OBJECT`. Exclude mode captures from the default sink monitor.
|
|
186
|
+
|
|
187
|
+
## CI/CD
|
|
188
|
+
|
|
189
|
+
| Workflow | Trigger | Description |
|
|
190
|
+
|----------|---------|-------------|
|
|
191
|
+
| `test.yml` | Push to `main`, PRs | Builds and tests on Windows, macOS, Linux × Node 18/20/22 |
|
|
192
|
+
| `publish-npm.yml` | GitHub Release | Publishes to npm (requires passing tests) |
|
|
193
|
+
| `publish-github.yml` | GitHub Release | Publishes to GitHub Packages as `@CilginSinek/electron-native-screenshare` |
|
|
194
|
+
|
|
195
|
+
> Tests **must pass** before any publish. If the test pipeline fails, the package will not be published.
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
[MIT](LICENSE)
|
package/binding.gyp
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"targets": [
|
|
3
|
+
{
|
|
4
|
+
"target_name": "topluyo_capture",
|
|
5
|
+
"include_dirs": [
|
|
6
|
+
"<!@(node -p \"require('node-addon-api').include\")"
|
|
7
|
+
],
|
|
8
|
+
"dependencies": [
|
|
9
|
+
"<!(node -p \"require('node-addon-api').gyp\")"
|
|
10
|
+
],
|
|
11
|
+
"defines": [
|
|
12
|
+
"NAPI_DISABLE_CPP_EXCEPTIONS"
|
|
13
|
+
],
|
|
14
|
+
"conditions": [
|
|
15
|
+
["OS==\"win\"", {
|
|
16
|
+
"sources": [
|
|
17
|
+
"src/win/addon.cpp",
|
|
18
|
+
"src/win/wasapi_capture.cpp"
|
|
19
|
+
],
|
|
20
|
+
"defines": [
|
|
21
|
+
"_WIN32_WINNT=0x0A00",
|
|
22
|
+
"NTDDI_VERSION=0x0A00000A"
|
|
23
|
+
],
|
|
24
|
+
"libraries": [
|
|
25
|
+
"-lMmdevapi.lib",
|
|
26
|
+
"-lAvrt.lib"
|
|
27
|
+
]
|
|
28
|
+
}],
|
|
29
|
+
["OS==\"mac\"", {
|
|
30
|
+
"sources": [
|
|
31
|
+
"src/mac/addon.cpp",
|
|
32
|
+
"src/mac/coreaudio_capture.mm"
|
|
33
|
+
],
|
|
34
|
+
"xcode_settings": {
|
|
35
|
+
"CLANG_ENABLE_OBJC_ARC": "YES",
|
|
36
|
+
"OTHER_CPLUSPLUSFLAGS": ["-std=c++17", "-ObjC++"],
|
|
37
|
+
"MACOSX_DEPLOYMENT_TARGET": "13.0"
|
|
38
|
+
},
|
|
39
|
+
"link_settings": {
|
|
40
|
+
"libraries": [
|
|
41
|
+
"-framework CoreAudio",
|
|
42
|
+
"-framework CoreMedia",
|
|
43
|
+
"-framework CoreGraphics",
|
|
44
|
+
"-framework ScreenCaptureKit",
|
|
45
|
+
"-framework Foundation"
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
}],
|
|
49
|
+
["OS==\"linux\"", {
|
|
50
|
+
"sources": [
|
|
51
|
+
"src/linux/addon.cpp",
|
|
52
|
+
"src/linux/pipewire_capture.cpp"
|
|
53
|
+
],
|
|
54
|
+
"cflags_cc": [
|
|
55
|
+
"-std=c++17",
|
|
56
|
+
"<!@(pkg-config --cflags libpipewire-0.3 2>/dev/null || echo '')",
|
|
57
|
+
"<!@(pkg-config --cflags x11 2>/dev/null || echo '')"
|
|
58
|
+
],
|
|
59
|
+
"libraries": [
|
|
60
|
+
"<!@(pkg-config --libs libpipewire-0.3 2>/dev/null || echo '-lpipewire-0.3')",
|
|
61
|
+
"<!@(pkg-config --libs x11 2>/dev/null || echo '-lX11')"
|
|
62
|
+
]
|
|
63
|
+
}]
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* electron-native-screenshare
|
|
3
|
+
*
|
|
4
|
+
* Cross-platform native audio capture for Electron screen sharing
|
|
5
|
+
* with process-level audio isolation.
|
|
6
|
+
*
|
|
7
|
+
* Unified API — platform detection is automatic. The consumer never
|
|
8
|
+
* needs to import OS-specific modules.
|
|
9
|
+
*
|
|
10
|
+
* Supported platforms:
|
|
11
|
+
* - Windows 10 2004+ (WASAPI Process Loopback)
|
|
12
|
+
* - macOS 13 Ventura+ (ScreenCaptureKit)
|
|
13
|
+
* - Linux (PipeWire 0.3.26+)
|
|
14
|
+
*
|
|
15
|
+
* @module electron-native-screenshare
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
const platform = os.platform();
|
|
24
|
+
const SUPPORTED_PLATFORMS = ['win32', 'darwin', 'linux'];
|
|
25
|
+
|
|
26
|
+
/** @type {import('./types').NativeCapture | null} */
|
|
27
|
+
let capture = null;
|
|
28
|
+
|
|
29
|
+
/** @type {string | null} */
|
|
30
|
+
let loadError = null;
|
|
31
|
+
|
|
32
|
+
// --- Native module loading with graceful degradation ---
|
|
33
|
+
|
|
34
|
+
if (!SUPPORTED_PLATFORMS.includes(platform)) {
|
|
35
|
+
loadError = `[electron-native-screenshare] Unsupported platform: "${platform}". ` +
|
|
36
|
+
`Supported: ${SUPPORTED_PLATFORMS.join(', ')}. ` +
|
|
37
|
+
`Audio capture functions will throw when called.`;
|
|
38
|
+
console.warn(loadError);
|
|
39
|
+
} else {
|
|
40
|
+
try {
|
|
41
|
+
capture = require('../build/Release/topluyo_capture.node');
|
|
42
|
+
} catch (e) {
|
|
43
|
+
const platformHints = {
|
|
44
|
+
win32: 'Ensure Visual Studio Build Tools and Windows 10 SDK are installed.',
|
|
45
|
+
darwin: 'Ensure Xcode Command Line Tools are installed. Requires macOS 13+.',
|
|
46
|
+
linux: 'Ensure libpipewire-0.3-dev and libx11-dev are installed. ' +
|
|
47
|
+
'Run: sudo apt install libpipewire-0.3-dev libx11-dev'
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
loadError = `[electron-native-screenshare] Failed to load native module on ${platform}.\n` +
|
|
51
|
+
` Error: ${e.message}\n` +
|
|
52
|
+
` Hint: ${platformHints[platform] || 'Check native build dependencies.'}`;
|
|
53
|
+
console.warn(loadError);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Throws a descriptive error if the native module isn't loaded.
|
|
59
|
+
* @private
|
|
60
|
+
*/
|
|
61
|
+
function ensureLoaded() {
|
|
62
|
+
if (!capture) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
loadError ||
|
|
65
|
+
`[electron-native-screenshare] Native module is not available on ${platform}.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Starts audio capture with process-level isolation.
|
|
72
|
+
*
|
|
73
|
+
* Behavior depends on `isIncludeMode`:
|
|
74
|
+
* - `false` (default): Captures ALL system audio EXCEPT the specified process.
|
|
75
|
+
* Use for screen sharing — your app's audio won't leak into the stream.
|
|
76
|
+
* - `true`: Captures ONLY the specified process's audio.
|
|
77
|
+
* Use for window sharing — only that window's audio goes to the stream.
|
|
78
|
+
*
|
|
79
|
+
* @param {number} [processId] - Target process ID. Defaults to `process.pid` (current process).
|
|
80
|
+
* @param {boolean} [isIncludeMode=false] - `true` to include only the target, `false` to exclude it.
|
|
81
|
+
* @param {function(Buffer, AudioMetadata): void} [onData] - Callback receiving raw PCM audio chunks.
|
|
82
|
+
* - `data` {Buffer} — Raw PCM audio (float32, stereo, 48kHz by default)
|
|
83
|
+
* - `meta` {AudioMetadata} — `{ sampleRate, channels, bitsPerSample, isFloat }`
|
|
84
|
+
* @returns {boolean} `true` if capture started successfully.
|
|
85
|
+
* @throws {Error} If the native module failed to load or initialization fails.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* const { startCapture, stopCapture } = require('electron-native-screenshare');
|
|
89
|
+
*
|
|
90
|
+
* // Screen share: exclude your app's audio
|
|
91
|
+
* startCapture(process.pid, false, (data, meta) => {
|
|
92
|
+
* console.log(`Got ${data.length} bytes, ${meta.sampleRate}Hz ${meta.channels}ch`);
|
|
93
|
+
* });
|
|
94
|
+
*
|
|
95
|
+
* // Window share: include only a specific window's audio
|
|
96
|
+
* startCapture(targetPid, true, (data, meta) => { ... });
|
|
97
|
+
*/
|
|
98
|
+
function startCapture(processId, isIncludeMode, onData) {
|
|
99
|
+
ensureLoaded();
|
|
100
|
+
|
|
101
|
+
if (processId === undefined || processId === null) {
|
|
102
|
+
processId = process.pid;
|
|
103
|
+
}
|
|
104
|
+
if (typeof processId !== 'number' || !Number.isInteger(processId) || processId < 0) {
|
|
105
|
+
throw new Error('[electron-native-screenshare] processId must be a non-negative integer.');
|
|
106
|
+
}
|
|
107
|
+
if (typeof isIncludeMode !== 'boolean') {
|
|
108
|
+
isIncludeMode = false;
|
|
109
|
+
}
|
|
110
|
+
if (typeof onData !== 'function') {
|
|
111
|
+
onData = () => {};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return capture.startCapture(processId, isIncludeMode, (data, meta) => onData(data, meta));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Stops the active audio capture session.
|
|
119
|
+
*
|
|
120
|
+
* Safe to call even if no capture is running.
|
|
121
|
+
*
|
|
122
|
+
* @returns {boolean} `true` if stopped successfully.
|
|
123
|
+
* @throws {Error} If the native module failed to load.
|
|
124
|
+
*/
|
|
125
|
+
function stopCapture() {
|
|
126
|
+
ensureLoaded();
|
|
127
|
+
return capture.stopCapture();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Resolves a native window handle to its owning process ID.
|
|
132
|
+
*
|
|
133
|
+
* Platform-specific handle types:
|
|
134
|
+
* - Windows: HWND (from Electron's `desktopCapturer` source ID)
|
|
135
|
+
* - macOS: CGWindowID
|
|
136
|
+
* - Linux: X11 Window ID
|
|
137
|
+
*
|
|
138
|
+
* On Windows, this also handles UWP ApplicationFrameWindow → actual child process resolution.
|
|
139
|
+
*
|
|
140
|
+
* @param {number} windowHandle - The native window handle (HWND / CGWindowID / X11 Window).
|
|
141
|
+
* @returns {number} Process ID owning the window, or `0` if not found.
|
|
142
|
+
* @throws {Error} If the native module failed to load or argument is invalid.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* const { getPidFromWindowHandle } = require('electron-native-screenshare');
|
|
146
|
+
*
|
|
147
|
+
* // From Electron desktopCapturer source:
|
|
148
|
+
* // source.id = "window:12345:0" → extract 12345
|
|
149
|
+
* const hwnd = parseInt(source.id.split(':')[1]);
|
|
150
|
+
* const pid = getPidFromWindowHandle(hwnd);
|
|
151
|
+
*/
|
|
152
|
+
function getPidFromWindowHandle(windowHandle) {
|
|
153
|
+
ensureLoaded();
|
|
154
|
+
|
|
155
|
+
if (typeof windowHandle !== 'number') {
|
|
156
|
+
throw new Error('[electron-native-screenshare] windowHandle must be a number.');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return capture.getPidFromHwnd(windowHandle);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Returns whether the native module loaded successfully on this platform.
|
|
164
|
+
*
|
|
165
|
+
* Use this to gracefully check availability before calling capture functions,
|
|
166
|
+
* instead of wrapping everything in try/catch.
|
|
167
|
+
*
|
|
168
|
+
* @returns {boolean} `true` if capture functions are available.
|
|
169
|
+
*/
|
|
170
|
+
function isAvailable() {
|
|
171
|
+
return capture !== null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Returns the current platform name.
|
|
176
|
+
* @returns {string} 'win32', 'darwin', 'linux', or the raw os.platform() string.
|
|
177
|
+
*/
|
|
178
|
+
function getPlatform() {
|
|
179
|
+
return platform;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* If the native module failed to load, returns the error message.
|
|
184
|
+
* Returns `null` if the module loaded successfully.
|
|
185
|
+
* @returns {string | null}
|
|
186
|
+
*/
|
|
187
|
+
function getLoadError() {
|
|
188
|
+
return loadError;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
startCapture,
|
|
193
|
+
stopCapture,
|
|
194
|
+
getPidFromWindowHandle,
|
|
195
|
+
isAvailable,
|
|
196
|
+
getPlatform,
|
|
197
|
+
getLoadError,
|
|
198
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "electron-native-screenshare",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Cross-platform native audio capture for Electron screen sharing with process-level isolation. WASAPI (Windows), ScreenCaptureKit (macOS), PipeWire (Linux).",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"lib/",
|
|
8
|
+
"src/",
|
|
9
|
+
"binding.gyp",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"os": [
|
|
14
|
+
"win32",
|
|
15
|
+
"linux",
|
|
16
|
+
"darwin"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18.0.0"
|
|
20
|
+
},
|
|
21
|
+
"gypfile": true,
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "node-gyp rebuild",
|
|
24
|
+
"clean": "node-gyp clean",
|
|
25
|
+
"test": "jest --verbose --forceExit",
|
|
26
|
+
"prepublishOnly": "npm test"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"electron",
|
|
30
|
+
"screen-share",
|
|
31
|
+
"audio-capture",
|
|
32
|
+
"wasapi",
|
|
33
|
+
"coreaudio",
|
|
34
|
+
"screencapturekit",
|
|
35
|
+
"pipewire",
|
|
36
|
+
"native",
|
|
37
|
+
"process-isolation"
|
|
38
|
+
],
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/CilginSinek/electron-native-screenshare.git"
|
|
42
|
+
},
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/CilginSinek/electron-native-screenshare/issues"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/CilginSinek/electron-native-screenshare#readme",
|
|
47
|
+
"author": "CilginSinek",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"node-addon-api": "^7.0.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"jest": "^29.7.0"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linux N-API addon — mirrors the Windows addon.cpp interface exactly.
|
|
3
|
+
*
|
|
4
|
+
* Exports:
|
|
5
|
+
* startCapture(processId, isIncludeMode, callback) → boolean
|
|
6
|
+
* stopCapture() → boolean
|
|
7
|
+
* getPidFromHwnd(windowId) → number
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
#include <napi.h>
|
|
11
|
+
#include "pipewire_capture.h"
|
|
12
|
+
|
|
13
|
+
static PipewireCapture capture;
|
|
14
|
+
static Napi::ThreadSafeFunction tsfn;
|
|
15
|
+
|
|
16
|
+
Napi::Value StartCapture(const Napi::CallbackInfo& info) {
|
|
17
|
+
Napi::Env env = info.Env();
|
|
18
|
+
|
|
19
|
+
uint32_t processId = 0;
|
|
20
|
+
if (info.Length() > 0 && info[0].IsNumber()) {
|
|
21
|
+
processId = info[0].As<Napi::Number>().Uint32Value();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
bool isIncludeMode = false;
|
|
25
|
+
if (info.Length() > 1 && info[1].IsBoolean()) {
|
|
26
|
+
isIncludeMode = info[1].As<Napi::Boolean>().Value();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (info.Length() < 3 || !info[2].IsFunction()) {
|
|
30
|
+
Napi::TypeError::New(env, "Callback function expected as third argument").ThrowAsJavaScriptException();
|
|
31
|
+
return env.Null();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
std::string errorMsg;
|
|
35
|
+
int result = capture.Initialize(processId, isIncludeMode, errorMsg);
|
|
36
|
+
if (result != 0 || !errorMsg.empty()) {
|
|
37
|
+
char buf[512];
|
|
38
|
+
snprintf(buf, sizeof(buf), "PipeWire Init Failed: %s (code: %d)", errorMsg.c_str(), result);
|
|
39
|
+
Napi::TypeError::New(env, buf).ThrowAsJavaScriptException();
|
|
40
|
+
return env.Null();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
tsfn = Napi::ThreadSafeFunction::New(
|
|
44
|
+
env,
|
|
45
|
+
info[2].As<Napi::Function>(),
|
|
46
|
+
"PipeWireCaptureCallback",
|
|
47
|
+
0,
|
|
48
|
+
1
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
auto callback = [](const uint8_t* data, size_t length, PipewireCapture::AudioMetadata metadata) {
|
|
52
|
+
if (!tsfn) return;
|
|
53
|
+
|
|
54
|
+
struct Payload {
|
|
55
|
+
std::vector<uint8_t> buffer;
|
|
56
|
+
PipewireCapture::AudioMetadata meta;
|
|
57
|
+
};
|
|
58
|
+
auto* payload = new Payload{ std::vector<uint8_t>(data, data + length), metadata };
|
|
59
|
+
|
|
60
|
+
auto napiCallback = [](Napi::Env env, Napi::Function jsCallback, Payload* p) {
|
|
61
|
+
Napi::Object metaObj = Napi::Object::New(env);
|
|
62
|
+
metaObj.Set("sampleRate", p->meta.sampleRate);
|
|
63
|
+
metaObj.Set("channels", p->meta.channels);
|
|
64
|
+
metaObj.Set("bitsPerSample", p->meta.bitsPerSample);
|
|
65
|
+
metaObj.Set("isFloat", p->meta.isFloat);
|
|
66
|
+
|
|
67
|
+
Napi::Buffer<uint8_t> buffer = Napi::Buffer<uint8_t>::Copy(env, p->buffer.data(), p->buffer.size());
|
|
68
|
+
jsCallback.Call({ buffer, metaObj });
|
|
69
|
+
delete p;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
tsfn.NonBlockingCall(payload, napiCallback);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
capture.Start(callback);
|
|
76
|
+
return Napi::Boolean::New(env, true);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Napi::Value StopCapture(const Napi::CallbackInfo& info) {
|
|
80
|
+
Napi::Env env = info.Env();
|
|
81
|
+
capture.Stop();
|
|
82
|
+
if (tsfn) {
|
|
83
|
+
tsfn.Release();
|
|
84
|
+
tsfn = nullptr;
|
|
85
|
+
}
|
|
86
|
+
return Napi::Boolean::New(env, true);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
Napi::Value GetPidFromHwnd(const Napi::CallbackInfo& info) {
|
|
90
|
+
Napi::Env env = info.Env();
|
|
91
|
+
if (info.Length() < 1 || !info[0].IsNumber()) {
|
|
92
|
+
Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException();
|
|
93
|
+
return env.Null();
|
|
94
|
+
}
|
|
95
|
+
uint32_t windowId = info[0].As<Napi::Number>().Uint32Value();
|
|
96
|
+
uint32_t pid = getPidFromWindowId(windowId);
|
|
97
|
+
return Napi::Number::New(env, pid);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
101
|
+
exports.Set(Napi::String::New(env, "startCapture"), Napi::Function::New(env, StartCapture));
|
|
102
|
+
exports.Set(Napi::String::New(env, "stopCapture"), Napi::Function::New(env, StopCapture));
|
|
103
|
+
exports.Set(Napi::String::New(env, "getPidFromHwnd"), Napi::Function::New(env, GetPidFromHwnd));
|
|
104
|
+
return exports;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
NODE_API_MODULE(topluyo_capture, Init)
|