node-mac-recorder 2.4.7 → 2.4.8

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.
@@ -22,7 +22,10 @@
22
22
  "WebFetch(domain:github.com)",
23
23
  "WebFetch(domain:nonstrict.eu)",
24
24
  "Bash(cp:*)",
25
- "Bash(git checkout:*)"
25
+ "Bash(git checkout:*)",
26
+ "Bash(ELECTRON_VERSION=25.0.0 node -e \"\nconsole.log(''ELECTRON_VERSION env:'', process.env.ELECTRON_VERSION);\nconsole.log(''getenv result would be:'', process.env.ELECTRON_VERSION || ''null'');\n\")",
27
+ "Bash(ELECTRON_VERSION=25.0.0 node test-env-detection.js)",
28
+ "Bash(ELECTRON_VERSION=25.0.0 node test-native-call.js)"
26
29
  ],
27
30
  "deny": []
28
31
  }
@@ -0,0 +1,447 @@
1
+ # Window Selector Usage Guide
2
+
3
+ The `WindowSelector` module provides native macOS window and screen selection with overlay interfaces. This guide shows how to use it in both Node.js and Electron applications.
4
+
5
+ ## Basic Setup
6
+
7
+ ```javascript
8
+ const MacRecorder = require('node-mac-recorder');
9
+ const WindowSelector = MacRecorder.WindowSelector;
10
+
11
+ const selector = new WindowSelector();
12
+ ```
13
+
14
+ ## Core Features
15
+
16
+ ### 1. Window Selection with Overlay
17
+
18
+ Select any window on screen with visual highlights:
19
+
20
+ ```javascript
21
+ // Method 1: Promise-based selection
22
+ async function selectWindow() {
23
+ try {
24
+ const selectedWindow = await selector.selectWindow();
25
+ console.log('Selected window:', selectedWindow);
26
+ // Returns: { id, title, appName, x, y, width, height }
27
+ } catch (error) {
28
+ console.log('Selection cancelled or failed:', error.message);
29
+ }
30
+ }
31
+
32
+ // Method 2: Event-based selection with more control
33
+ async function selectWindowWithEvents() {
34
+ // Listen for events
35
+ selector.on('windowEntered', (window) => {
36
+ console.log('Mouse over window:', window.title);
37
+ });
38
+
39
+ selector.on('windowLeft', (window) => {
40
+ console.log('Mouse left window:', window.title);
41
+ });
42
+
43
+ selector.on('windowSelected', (window) => {
44
+ console.log('Window selected:', window);
45
+ // Handle selection
46
+ });
47
+
48
+ // Start selection
49
+ await selector.startSelection();
50
+
51
+ // Selection runs until user clicks or you call stopSelection()
52
+ // setTimeout(() => selector.stopSelection(), 10000); // Auto-stop after 10s
53
+ }
54
+ ```
55
+
56
+ ### 2. Screen Selection with Overlay
57
+
58
+ Select entire screens (useful for multi-monitor setups):
59
+
60
+ ```javascript
61
+ async function selectScreen() {
62
+ try {
63
+ const selectedScreen = await selector.selectScreen();
64
+ console.log('Selected screen:', selectedScreen);
65
+ // Returns: { id, width, height, x, y, name }
66
+ } catch (error) {
67
+ console.log('Screen selection cancelled:', error.message);
68
+ }
69
+ }
70
+
71
+ // Manual control
72
+ async function manualScreenSelection() {
73
+ await selector.startScreenSelection();
74
+
75
+ // Check for selection periodically
76
+ const checkSelection = setInterval(() => {
77
+ const selected = selector.getSelectedScreen();
78
+ if (selected) {
79
+ console.log('Screen selected:', selected);
80
+ clearInterval(checkSelection);
81
+ selector.stopScreenSelection();
82
+ }
83
+ }, 100);
84
+
85
+ // Auto-cancel after 30 seconds
86
+ setTimeout(() => {
87
+ clearInterval(checkSelection);
88
+ selector.stopScreenSelection();
89
+ }, 30000);
90
+ }
91
+ ```
92
+
93
+ ### 3. Recording Preview Overlays
94
+
95
+ Show preview of what will be recorded:
96
+
97
+ ```javascript
98
+ async function showRecordingPreview() {
99
+ // Get a window first
100
+ const recorder = new MacRecorder();
101
+ const windows = await recorder.getWindows();
102
+ const targetWindow = windows[0];
103
+
104
+ // Show preview overlay (darkens screen, highlights window)
105
+ await selector.showRecordingPreview(targetWindow);
106
+
107
+ // Show for 3 seconds
108
+ setTimeout(async () => {
109
+ await selector.hideRecordingPreview();
110
+ }, 3000);
111
+ }
112
+
113
+ // Screen recording preview
114
+ async function showScreenRecordingPreview() {
115
+ const recorder = new MacRecorder();
116
+ const displays = await recorder.getDisplays();
117
+ const targetScreen = displays[0];
118
+
119
+ await selector.showScreenRecordingPreview(targetScreen);
120
+
121
+ // Hide after delay
122
+ setTimeout(async () => {
123
+ await selector.hideScreenRecordingPreview();
124
+ }, 3000);
125
+ }
126
+ ```
127
+
128
+ ### 4. Status and Cleanup
129
+
130
+ ```javascript
131
+ // Check current status
132
+ const status = selector.getStatus();
133
+ console.log(status);
134
+ // Returns: { isSelecting, hasSelectedWindow, selectedWindow, nativeStatus }
135
+
136
+ // Cleanup when done
137
+ await selector.cleanup();
138
+ ```
139
+
140
+ ## Electron Integration
141
+
142
+ **IMPORTANT**: In Electron environments, the window selector automatically switches to "safe mode" to prevent NSWindow overlay crashes. Instead of creating native overlays, it provides window/screen lists that you can display in your Electron UI.
143
+
144
+ ### Main Process Usage
145
+
146
+ ```javascript
147
+ // In main.cjs or main.js
148
+ const { ipcMain } = require('electron');
149
+ const MacRecorder = require('node-mac-recorder');
150
+ const WindowSelector = MacRecorder.WindowSelector;
151
+
152
+ let windowSelector = null;
153
+
154
+ ipcMain.handle('window-selector-init', () => {
155
+ windowSelector = new WindowSelector();
156
+ // In Electron, this will automatically use safe mode
157
+ return true;
158
+ });
159
+
160
+ // Get available windows (safe for Electron)
161
+ ipcMain.handle('get-available-windows', async () => {
162
+ try {
163
+ const windows = await windowSelector.getAvailableWindows();
164
+ return { success: true, windows: windows };
165
+ } catch (error) {
166
+ return { success: false, error: error.message };
167
+ }
168
+ });
169
+
170
+ // Select window by ID (no overlay needed)
171
+ ipcMain.handle('select-window-by-id', async (event, windowInfo) => {
172
+ try {
173
+ const selectedWindow = windowSelector.selectWindowById(windowInfo);
174
+ return { success: true, window: selectedWindow };
175
+ } catch (error) {
176
+ return { success: false, error: error.message };
177
+ }
178
+ });
179
+
180
+ // Get available screens (safe for Electron)
181
+ ipcMain.handle('get-available-screens', async () => {
182
+ try {
183
+ const recorder = new MacRecorder();
184
+ const screens = await recorder.getDisplays();
185
+ return { success: true, screens: screens };
186
+ } catch (error) {
187
+ return { success: false, error: error.message };
188
+ }
189
+ });
190
+
191
+ ipcMain.handle('window-selector-cleanup', async () => {
192
+ if (windowSelector) {
193
+ await windowSelector.cleanup();
194
+ windowSelector = null;
195
+ }
196
+ return true;
197
+ });
198
+ ```
199
+
200
+ ### Renderer Process Usage
201
+
202
+ ```javascript
203
+ // In renderer.js or React component
204
+ const { ipcRenderer } = require('electron');
205
+
206
+ class ScreenRecorder {
207
+ async selectWindow() {
208
+ // Initialize selector
209
+ await ipcRenderer.invoke('window-selector-init');
210
+
211
+ // Select window
212
+ const result = await ipcRenderer.invoke('window-selector-select');
213
+
214
+ if (result.success) {
215
+ console.log('Selected window:', result.window);
216
+ return result.window;
217
+ } else {
218
+ throw new Error(result.error);
219
+ }
220
+ }
221
+
222
+ async selectScreen() {
223
+ await ipcRenderer.invoke('window-selector-init');
224
+
225
+ const result = await ipcRenderer.invoke('screen-selector-select');
226
+
227
+ if (result.success) {
228
+ console.log('Selected screen:', result.screen);
229
+ return result.screen;
230
+ } else {
231
+ throw new Error(result.error);
232
+ }
233
+ }
234
+
235
+ async cleanup() {
236
+ await ipcRenderer.invoke('window-selector-cleanup');
237
+ }
238
+ }
239
+
240
+ // Usage in React component
241
+ function RecordingComponent() {
242
+ const [selectedWindow, setSelectedWindow] = useState(null);
243
+ const recorder = new ScreenRecorder();
244
+
245
+ const handleSelectWindow = async () => {
246
+ try {
247
+ const window = await recorder.selectWindow();
248
+ setSelectedWindow(window);
249
+ } catch (error) {
250
+ console.error('Window selection failed:', error);
251
+ }
252
+ };
253
+
254
+ return (
255
+ <div>
256
+ <button onClick={handleSelectWindow}>
257
+ Select Window to Record
258
+ </button>
259
+ {selectedWindow && (
260
+ <div>
261
+ Selected: {selectedWindow.title} ({selectedWindow.appName})
262
+ </div>
263
+ )}
264
+ </div>
265
+ );
266
+ }
267
+ ```
268
+
269
+ ## Complete Electron Example
270
+
271
+ ```javascript
272
+ // main.cjs
273
+ const { app, BrowserWindow, ipcMain } = require('electron');
274
+ const MacRecorder = require('node-mac-recorder');
275
+ const WindowSelector = MacRecorder.WindowSelector;
276
+
277
+ let mainWindow;
278
+ let windowSelector;
279
+ let recorder;
280
+
281
+ function createWindow() {
282
+ mainWindow = new BrowserWindow({
283
+ width: 800,
284
+ height: 600,
285
+ webPreferences: {
286
+ nodeIntegration: true,
287
+ contextIsolation: false
288
+ }
289
+ });
290
+
291
+ mainWindow.loadFile('index.html');
292
+ }
293
+
294
+ // Initialize services
295
+ ipcMain.handle('init-services', async () => {
296
+ recorder = new MacRecorder();
297
+ windowSelector = new WindowSelector();
298
+ return true;
299
+ });
300
+
301
+ // Window selection
302
+ ipcMain.handle('select-window', async () => {
303
+ try {
304
+ const window = await windowSelector.selectWindow();
305
+ return { success: true, data: window };
306
+ } catch (error) {
307
+ return { success: false, error: error.message };
308
+ }
309
+ });
310
+
311
+ // Screen selection
312
+ ipcMain.handle('select-screen', async () => {
313
+ try {
314
+ const screen = await windowSelector.selectScreen();
315
+ return { success: true, data: screen };
316
+ } catch (error) {
317
+ return { success: false, error: error.message };
318
+ }
319
+ });
320
+
321
+ // Start recording
322
+ ipcMain.handle('start-recording', async (event, windowInfo, outputPath) => {
323
+ try {
324
+ const options = {
325
+ windowId: windowInfo.id,
326
+ captureCursor: true,
327
+ includeSystemAudio: false
328
+ };
329
+
330
+ await recorder.startRecording(outputPath, options);
331
+ return { success: true };
332
+ } catch (error) {
333
+ return { success: false, error: error.message };
334
+ }
335
+ });
336
+
337
+ // Stop recording
338
+ ipcMain.handle('stop-recording', async () => {
339
+ try {
340
+ await recorder.stopRecording();
341
+ return { success: true };
342
+ } catch (error) {
343
+ return { success: false, error: error.message };
344
+ }
345
+ });
346
+
347
+ app.whenReady().then(createWindow);
348
+ ```
349
+
350
+ ```html
351
+ <!-- index.html -->
352
+ <!DOCTYPE html>
353
+ <html>
354
+ <head>
355
+ <title>Screen Recorder</title>
356
+ </head>
357
+ <body>
358
+ <h1>Screen Recorder</h1>
359
+
360
+ <button id="selectWindow">Select Window</button>
361
+ <button id="selectScreen">Select Screen</button>
362
+ <button id="startRecord">Start Recording</button>
363
+ <button id="stopRecord">Stop Recording</button>
364
+
365
+ <div id="status"></div>
366
+
367
+ <script>
368
+ const { ipcRenderer } = require('electron');
369
+
370
+ let selectedWindow = null;
371
+ let selectedScreen = null;
372
+
373
+ // Initialize
374
+ ipcRenderer.invoke('init-services');
375
+
376
+ document.getElementById('selectWindow').addEventListener('click', async () => {
377
+ const result = await ipcRenderer.invoke('select-window');
378
+ if (result.success) {
379
+ selectedWindow = result.data;
380
+ document.getElementById('status').innerHTML =
381
+ `Selected Window: ${selectedWindow.title}`;
382
+ } else {
383
+ alert('Window selection failed: ' + result.error);
384
+ }
385
+ });
386
+
387
+ document.getElementById('selectScreen').addEventListener('click', async () => {
388
+ const result = await ipcRenderer.invoke('select-screen');
389
+ if (result.success) {
390
+ selectedScreen = result.data;
391
+ document.getElementById('status').innerHTML =
392
+ `Selected Screen: ${selectedScreen.width}x${selectedScreen.height}`;
393
+ } else {
394
+ alert('Screen selection failed: ' + result.error);
395
+ }
396
+ });
397
+
398
+ document.getElementById('startRecord').addEventListener('click', async () => {
399
+ if (!selectedWindow) {
400
+ alert('Please select a window first');
401
+ return;
402
+ }
403
+
404
+ const outputPath = `/tmp/recording-${Date.now()}.mov`;
405
+ const result = await ipcRenderer.invoke('start-recording', selectedWindow, outputPath);
406
+
407
+ if (result.success) {
408
+ document.getElementById('status').innerHTML = 'Recording started...';
409
+ } else {
410
+ alert('Recording failed: ' + result.error);
411
+ }
412
+ });
413
+
414
+ document.getElementById('stopRecord').addEventListener('click', async () => {
415
+ const result = await ipcRenderer.invoke('stop-recording');
416
+ if (result.success) {
417
+ document.getElementById('status').innerHTML = 'Recording stopped';
418
+ }
419
+ });
420
+ </script>
421
+ </body>
422
+ </html>
423
+ ```
424
+
425
+ ## Key Points for AI Integration
426
+
427
+ 1. **Asynchronous Operations**: All selection methods return Promises
428
+ 2. **Event-Driven**: Use events for real-time feedback during selection
429
+ 3. **Error Handling**: Always wrap in try-catch blocks
430
+ 4. **Cleanup Required**: Call `cleanup()` when done to prevent memory leaks
431
+ 5. **Permissions Required**: Needs macOS screen recording permissions
432
+ 6. **Multi-Monitor Support**: Screen selection works with multiple displays
433
+ 7. **Window Filtering**: Automatically filters out invalid/hidden windows
434
+
435
+ ## Permissions
436
+
437
+ The module requires macOS screen recording permissions. Users will be prompted automatically, or check programmatically:
438
+
439
+ ```javascript
440
+ const permissions = await selector.checkPermissions();
441
+ if (!permissions.screenRecording) {
442
+ console.log('Screen recording permission required');
443
+ // Guide user to System Preferences > Privacy & Security > Screen Recording
444
+ }
445
+ ```
446
+
447
+ This covers all major use cases for integrating window/screen selection into AI-powered applications.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.4.7",
3
+ "version": "2.4.8",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -974,6 +974,42 @@ bool hideScreenRecordingPreview() {
974
974
  Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
975
975
  Napi::Env env = info.Env();
976
976
 
977
+ // Electron safety check - prevent NSWindow crashes
978
+ const char* electronVersion = getenv("ELECTRON_VERSION");
979
+ const char* electronRunAs = getenv("ELECTRON_RUN_AS_NODE");
980
+
981
+ NSLog(@"🔍 Debug: electronVersion='%s', electronRunAs='%s'",
982
+ electronVersion ? electronVersion : "null",
983
+ electronRunAs ? electronRunAs : "null");
984
+
985
+ if (electronVersion || electronRunAs) {
986
+ NSLog(@"🔍 Detected Electron environment - using safe mode");
987
+
988
+ // In Electron, return window list without creating native NSWindow overlays
989
+ // The Electron app can handle UI selection itself
990
+ @try {
991
+ NSArray *windows = getAllSelectableWindows();
992
+
993
+ if (!windows || [windows count] == 0) {
994
+ NSLog(@"❌ No selectable windows found");
995
+ return Napi::Boolean::New(env, false);
996
+ }
997
+
998
+ // Store windows for later retrieval via getWindowSelectionStatus
999
+ g_allWindows = [windows mutableCopy];
1000
+ g_isWindowSelecting = true;
1001
+
1002
+ // Return true to indicate windows are available
1003
+ // Electron app should call getWindowSelectionStatus to get the list
1004
+ NSLog(@"✅ Electron-safe mode: %lu windows available for selection", (unsigned long)[windows count]);
1005
+ return Napi::Boolean::New(env, true);
1006
+
1007
+ } @catch (NSException *exception) {
1008
+ NSLog(@"❌ Exception in Electron-safe window selection: %@", [exception reason]);
1009
+ return Napi::Boolean::New(env, false);
1010
+ }
1011
+ }
1012
+
977
1013
  if (g_isWindowSelecting) {
978
1014
  NSLog(@"⚠️ Window selection already in progress");
979
1015
  return Napi::Boolean::New(env, false);
@@ -1338,6 +1374,51 @@ Napi::Value HideRecordingPreview(const Napi::CallbackInfo& info) {
1338
1374
  Napi::Value StartScreenSelection(const Napi::CallbackInfo& info) {
1339
1375
  Napi::Env env = info.Env();
1340
1376
 
1377
+ // Electron safety check - prevent NSWindow crashes
1378
+ const char* electronVersion = getenv("ELECTRON_VERSION");
1379
+ const char* electronRunAs = getenv("ELECTRON_RUN_AS_NODE");
1380
+
1381
+ NSLog(@"🔍 Screen Debug: electronVersion='%s', electronRunAs='%s'",
1382
+ electronVersion ? electronVersion : "null",
1383
+ electronRunAs ? electronRunAs : "null");
1384
+
1385
+ if (electronVersion || electronRunAs) {
1386
+ NSLog(@"🔍 Detected Electron environment - using safe screen selection");
1387
+
1388
+ // In Electron, return screen list without creating native NSWindow overlays
1389
+ @try {
1390
+ NSArray *screens = [NSScreen screens];
1391
+
1392
+ if (!screens || [screens count] == 0) {
1393
+ NSLog(@"❌ No screens available");
1394
+ return Napi::Boolean::New(env, false);
1395
+ }
1396
+
1397
+ // Store screens and select first one automatically for Electron
1398
+ g_allScreens = screens;
1399
+ g_isScreenSelecting = true;
1400
+
1401
+ NSScreen *mainScreen = [screens firstObject];
1402
+ g_selectedScreenInfo = @{
1403
+ @"id": @((int)[screens indexOfObject:mainScreen]),
1404
+ @"width": @((int)mainScreen.frame.size.width),
1405
+ @"height": @((int)mainScreen.frame.size.height),
1406
+ @"x": @((int)mainScreen.frame.origin.x),
1407
+ @"y": @((int)mainScreen.frame.origin.y)
1408
+ };
1409
+
1410
+ // Mark as complete so getSelectedScreenInfo returns the selection
1411
+ g_isScreenSelecting = false;
1412
+
1413
+ NSLog(@"✅ Electron-safe screen selection: %lu screens available", (unsigned long)[screens count]);
1414
+ return Napi::Boolean::New(env, true);
1415
+
1416
+ } @catch (NSException *exception) {
1417
+ NSLog(@"❌ Exception in Electron-safe screen selection: %@", [exception reason]);
1418
+ return Napi::Boolean::New(env, false);
1419
+ }
1420
+ }
1421
+
1341
1422
  @try {
1342
1423
  bool success = startScreenSelection();
1343
1424
  return Napi::Boolean::New(env, success);
@@ -33,13 +33,13 @@ class WindowSelector extends EventEmitter {
33
33
  this.selectedWindow = null;
34
34
  this.lastStatus = null;
35
35
 
36
- // Electron environment detection (for logging only)
36
+ // Electron environment detection
37
37
  this.isElectron = !!(process.versions && process.versions.electron) ||
38
38
  !!(process.env.ELECTRON_VERSION) ||
39
39
  !!(process.env.ELECTRON_RUN_AS_NODE);
40
40
 
41
41
  if (this.isElectron) {
42
- console.log("🔍 WindowSelector: Detected Electron environment");
42
+ console.log("🔍 WindowSelector: Detected Electron environment - using safe mode");
43
43
  }
44
44
  }
45
45
 
@@ -174,6 +174,59 @@ class WindowSelector extends EventEmitter {
174
174
  return this.selectedWindow;
175
175
  }
176
176
 
177
+ /**
178
+ * Electron'da kullanmak için mevcut pencereleri döndürür
179
+ * @returns {Array} Available windows for selection
180
+ */
181
+ async getAvailableWindows() {
182
+ if (this.isElectron) {
183
+ try {
184
+ // Start selection to populate window list (safe mode)
185
+ const success = nativeBinding.startWindowSelection();
186
+ if (success) {
187
+ // Get the populated window list
188
+ const status = nativeBinding.getWindowSelectionStatus();
189
+
190
+ // Stop selection immediately
191
+ nativeBinding.stopWindowSelection();
192
+
193
+ // Return windows from native status
194
+ // In Electron safe mode, these will be available without overlay
195
+ const MacRecorder = require("./index.js");
196
+ const recorder = new MacRecorder();
197
+ return await recorder.getWindows();
198
+ }
199
+ } catch (error) {
200
+ console.error("Error getting available windows in Electron:", error.message);
201
+ }
202
+ }
203
+
204
+ // Fallback to regular MacRecorder method
205
+ try {
206
+ const MacRecorder = require("./index.js");
207
+ const recorder = new MacRecorder();
208
+ return await recorder.getWindows();
209
+ } catch (error) {
210
+ console.error("Error getting windows:", error.message);
211
+ return [];
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Electron'da pencere seçmek için - overlay olmadan
217
+ * @param {Object} windowInfo - Selected window from getAvailableWindows()
218
+ */
219
+ selectWindowById(windowInfo) {
220
+ if (!windowInfo || !windowInfo.id) {
221
+ throw new Error("Valid window info required");
222
+ }
223
+
224
+ this.selectedWindow = windowInfo;
225
+ this.emit('windowSelected', windowInfo);
226
+
227
+ return windowInfo;
228
+ }
229
+
177
230
  /**
178
231
  * Seçim durumunu döndürür
179
232
  */