node-mac-recorder 2.2.1 → 2.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # node-mac-recorder
2
2
 
3
- A powerful native macOS screen recording Node.js package with advanced window selection, multi-display support, and granular audio controls. Built with AVFoundation and ScreenCaptureKit for optimal performance.
3
+ A powerful native macOS screen recording Node.js package with advanced window selection, multi-display support, and granular audio controls. Built with AVFoundation for optimal performance.
4
4
 
5
5
  ## Features
6
6
 
@@ -12,7 +12,6 @@ A powerful native macOS screen recording Node.js package with advanced window se
12
12
  - 🖱️ **Multi-Display Support** - Automatic display detection and selection
13
13
  - 🎨 **Cursor Control** - Toggle cursor visibility in recordings
14
14
  - 🖱️ **Cursor Tracking** - Track mouse position, cursor types, and click events
15
- - 🚫 **Exclude Apps/Windows (macOS 15+)** - Use ScreenCaptureKit to exclude specific apps (bundle id/PID) and windows
16
15
 
17
16
  🎵 **Granular Audio Controls**
18
17
 
@@ -50,11 +49,6 @@ npm install node-mac-recorder
50
49
  - **Screen Recording Permission** (automatically requested)
51
50
  - **CPU Architecture**: Intel (x64) and Apple Silicon (ARM64) supported
52
51
 
53
- ScreenCaptureKit path and exclusion support:
54
-
55
- - Requires macOS 15.0+ for on-disk recording via `SCRecordingOutput`
56
- - On older systems (<=14), the library falls back to AVFoundation automatically (exclusions not available)
57
-
58
52
  ### Build Requirements
59
53
 
60
54
  ```bash
@@ -116,22 +110,9 @@ await recorder.startRecording("./recording.mov", {
116
110
  quality: "high", // 'low', 'medium', 'high'
117
111
  frameRate: 30, // FPS (15, 30, 60)
118
112
  captureCursor: false, // Show cursor (default: false)
119
-
120
- // ScreenCaptureKit (macOS 15+) - optional, backward compatible
121
- useScreenCaptureKit: false, // If true and available, prefers ScreenCaptureKit
122
- excludedAppBundleIds: ["com.apple.Safari"], // Exclude by bundle id
123
- excludedPIDs: [process.pid], // Exclude by PID
124
- excludedWindowIds: [12345, 67890], // Exclude specific window IDs
125
- // When running under Electron, autoExcludeSelf defaults to true
126
- autoExcludeSelf: true,
127
113
  });
128
114
  ```
129
115
 
130
- Notes
131
-
132
- - If any of `excludedAppBundleIds`, `excludedPIDs`, `excludedWindowIds` are provided, the library automatically switches to ScreenCaptureKit on supported macOS versions.
133
- - When running inside Electron, the current app PID is excluded by default (`autoExcludeSelf: true`).
134
-
135
116
  #### `stopRecording()`
136
117
 
137
118
  Stops the current recording.
@@ -324,29 +305,6 @@ await new Promise((resolve) => setTimeout(resolve, 10000)); // 10 seconds
324
305
  await recorder.stopRecording();
325
306
  ```
326
307
 
327
- ### Exclude Electron app and other windows (ScreenCaptureKit)
328
-
329
- ```javascript
330
- const recorder = new MacRecorder();
331
-
332
- // By default, when running inside Electron, your app is auto-excluded.
333
- // You can also exclude other apps/windows explicitly:
334
- await recorder.startRecording("./excluded.mov", {
335
- captureCursor: true,
336
- // Prefer SC explicitly (optional — auto-enabled when exclusions are set)
337
- useScreenCaptureKit: true,
338
- excludedAppBundleIds: ["com.apple.Safari"],
339
- excludedWindowIds: [
340
- /* CGWindowID list */
341
- ],
342
- // autoExcludeSelf is true by default on Electron; set false to include your app
343
- // autoExcludeSelf: false,
344
- });
345
-
346
- // ... later
347
- await recorder.stopRecording();
348
- ```
349
-
350
308
  ### Multi-Display Recording
351
309
 
352
310
  ```javascript
@@ -404,17 +362,16 @@ audioDevices.forEach((device, i) => {
404
362
  });
405
363
 
406
364
  // Find system audio device (like BlackHole, Soundflower, etc.)
407
- const systemAudioDevice = audioDevices.find(
408
- (device) =>
409
- device.name.toLowerCase().includes("blackhole") ||
410
- device.name.toLowerCase().includes("soundflower") ||
411
- device.name.toLowerCase().includes("loopback") ||
412
- device.name.toLowerCase().includes("aggregate")
365
+ const systemAudioDevice = audioDevices.find(device =>
366
+ device.name.toLowerCase().includes('blackhole') ||
367
+ device.name.toLowerCase().includes('soundflower') ||
368
+ device.name.toLowerCase().includes('loopback') ||
369
+ device.name.toLowerCase().includes('aggregate')
413
370
  );
414
371
 
415
372
  if (systemAudioDevice) {
416
373
  console.log(`Using system audio device: ${systemAudioDevice.name}`);
417
-
374
+
418
375
  // Record with specific system audio device
419
376
  await recorder.startRecording("./system-audio-specific.mov", {
420
377
  includeMicrophone: false,
@@ -423,10 +380,8 @@ if (systemAudioDevice) {
423
380
  captureArea: { x: 0, y: 0, width: 1, height: 1 }, // Minimal video
424
381
  });
425
382
  } else {
426
- console.log(
427
- "No system audio device found. Installing BlackHole or Soundflower recommended."
428
- );
429
-
383
+ console.log("No system audio device found. Installing BlackHole or Soundflower recommended.");
384
+
430
385
  // Record with default system audio capture (may not work without virtual audio device)
431
386
  await recorder.startRecording("./system-audio-default.mov", {
432
387
  includeMicrophone: false,
@@ -436,7 +391,7 @@ if (systemAudioDevice) {
436
391
  }
437
392
 
438
393
  // Record for 10 seconds
439
- await new Promise((resolve) => setTimeout(resolve, 10000));
394
+ await new Promise(resolve => setTimeout(resolve, 10000));
440
395
  await recorder.stopRecording();
441
396
  ```
442
397
 
@@ -445,7 +400,7 @@ await recorder.stopRecording();
445
400
  For reliable system audio capture, install a virtual audio device:
446
401
 
447
402
  1. **BlackHole** (Free): https://github.com/ExistentialAudio/BlackHole
448
- 2. **Soundflower** (Free): https://github.com/mattingalls/Soundflower
403
+ 2. **Soundflower** (Free): https://github.com/mattingalls/Soundflower
449
404
  3. **Loopback** (Paid): https://rogueamoeba.com/loopback/
450
405
 
451
406
  These create aggregate audio devices that the package can detect and use for system audio capture.
@@ -765,11 +720,6 @@ npm cache clean --force
765
720
  xcode-select --install
766
721
  ```
767
722
 
768
- ### ScreenCaptureKit availability
769
-
770
- - Exclusions require macOS 15+ (uses `SCRecordingOutput`).
771
- - On macOS 12.3–14, the module will fall back to AVFoundation (no exclusions). This is automatic and backward-compatible.
772
-
773
723
  ### Recording Issues
774
724
 
775
725
  1. **Empty/Black Video**: Check screen recording permissions
@@ -824,24 +774,12 @@ MIT License - see [LICENSE](LICENSE) file for details.
824
774
 
825
775
  ### Latest Updates
826
776
 
827
- - ✅ ScreenCaptureKit path with exclusions (apps by bundle id/PID and window IDs) on macOS 15+
828
- - ✅ Electron apps auto-excluded by default (can be disabled with `autoExcludeSelf: false`)
829
- - ✅ Prebuilt binaries for darwin-arm64 and darwin-x64; automatic loading via `node-gyp-build`
830
- - ✅ Cursor Tracking: Track mouse position, cursor types, and click events with JSON export
831
- - ✅ Window Recording: Automatic coordinate conversion for multi-display setups
832
- - ✅ Audio Controls: Separate microphone and system audio controls
833
- - ✅ Display Selection: Multi-monitor support with automatic detection
834
- - ✅ Smart Filtering: Improved window detection and filtering
835
- - ✅ Performance: Optimized native implementation
836
-
837
- ## Prebuilt binaries
838
-
839
- This package ships prebuilt native binaries for macOS:
840
-
841
- - darwin-arm64 (Apple Silicon)
842
- - darwin-x64 (Intel)
843
-
844
- At runtime, the correct binary is loaded automatically via `node-gyp-build`. If a prebuilt is not available for your environment, the module falls back to a local build.
777
+ - ✅ **Cursor Tracking**: Track mouse position, cursor types, and click events with JSON export
778
+ - ✅ **Window Recording**: Automatic coordinate conversion for multi-display setups
779
+ - ✅ **Audio Controls**: Separate microphone and system audio controls
780
+ - ✅ **Display Selection**: Multi-monitor support with automatic detection
781
+ - ✅ **Smart Filtering**: Improved window detection and filtering
782
+ - ✅ **Performance**: Optimized native implementation
845
783
 
846
784
  ---
847
785
 
package/binding.gyp CHANGED
@@ -5,7 +5,6 @@
5
5
  "sources": [
6
6
  "src/mac_recorder.mm",
7
7
  "src/screen_capture.mm",
8
- "src/screen_capture_kit.mm",
9
8
  "src/audio_capture.mm",
10
9
  "src/cursor_tracker.mm",
11
10
  "src/window_selector.mm"
@@ -22,11 +21,8 @@
22
21
  "GCC_ENABLE_CPP_EXCEPTIONS": "YES",
23
22
  "CLANG_CXX_LIBRARY": "libc++",
24
23
  "MACOSX_DEPLOYMENT_TARGET": "12.3",
25
- <<<<<<< HEAD
26
- =======
27
24
  "ARCHS": ["arm64"],
28
25
  "VALID_ARCHS": ["arm64"],
29
- >>>>>>> screencapture
30
26
  "OTHER_CFLAGS": [
31
27
  "-ObjC++",
32
28
  "-fmodules"
@@ -35,22 +31,15 @@
35
31
  },
36
32
  "link_settings": {
37
33
  "libraries": [
38
- <<<<<<< HEAD
39
- =======
40
34
  "-framework ScreenCaptureKit",
41
35
  "-framework AVFoundation",
42
36
  "-framework CoreMedia",
43
37
  "-framework CoreVideo",
44
- >>>>>>> screencapture
45
38
  "-framework Foundation",
46
39
  "-framework AppKit",
47
40
  "-framework ApplicationServices",
48
41
  "-framework Carbon",
49
- "-framework Accessibility",
50
- "-framework CoreAudio",
51
- "-framework AVFoundation",
52
- "-framework CoreMedia",
53
- "-framework CoreVideo"
42
+ "-framework Accessibility"
54
43
  ]
55
44
  },
56
45
  "defines": [
package/index.js CHANGED
@@ -2,24 +2,19 @@ const { EventEmitter } = require("events");
2
2
  const path = require("path");
3
3
  const fs = require("fs");
4
4
 
5
- // Native modülü yükle (prebuilt > local build fallback)
5
+ // Native modülü yükle
6
6
  let nativeBinding;
7
7
  try {
8
- // Prebuilt
9
- nativeBinding = require("node-gyp-build")(__dirname);
10
- } catch (e) {
8
+ nativeBinding = require("./build/Release/mac_recorder.node");
9
+ } catch (error) {
11
10
  try {
12
- nativeBinding = require("./build/Release/mac_recorder.node");
13
- } catch (error) {
14
- try {
15
- nativeBinding = require("./build/Debug/mac_recorder.node");
16
- } catch (debugError) {
17
- throw new Error(
18
- 'Native module not found. Please run "npm run build" to compile the native module.\n' +
19
- "Original error: " +
20
- (error?.message || e?.message)
21
- );
22
- }
11
+ nativeBinding = require("./build/Debug/mac_recorder.node");
12
+ } catch (debugError) {
13
+ throw new Error(
14
+ 'Native module not found. Please run "npm run build" to compile the native module.\n' +
15
+ "Original error: " +
16
+ error.message
17
+ );
23
18
  }
24
19
  }
25
20
 
@@ -50,12 +45,6 @@ class MacRecorder extends EventEmitter {
50
45
  showClicks: false,
51
46
  displayId: null, // Hangi ekranı kaydedeceği (null = ana ekran)
52
47
  windowId: null, // Hangi pencereyi kaydedeceği (null = tam ekran)
53
- // SC (gizli, opsiyonel) - mevcut kullanıcıları bozmaz
54
- useScreenCaptureKit: false,
55
- excludedAppBundleIds: [],
56
- excludedPIDs: [],
57
- excludedWindowIds: [],
58
- autoExcludeSelf: !!(process.versions && process.versions.electron),
59
48
  };
60
49
 
61
50
  // Display cache için async initialization
@@ -129,22 +118,11 @@ class MacRecorder extends EventEmitter {
129
118
  audioDeviceId: options.audioDeviceId || null, // null = default device
130
119
  systemAudioDeviceId: options.systemAudioDeviceId || null, // null = auto-detect system audio device
131
120
  captureArea: options.captureArea || null,
132
- <<<<<<< HEAD
133
- useScreenCaptureKit: options.useScreenCaptureKit || false,
134
- excludedAppBundleIds: options.excludedAppBundleIds || [],
135
- excludedPIDs: options.excludedPIDs || [],
136
- excludedWindowIds: options.excludedWindowIds || [],
137
- autoExcludeSelf:
138
- typeof options.autoExcludeSelf === "boolean"
139
- ? options.autoExcludeSelf
140
- : !!(process.versions && process.versions.electron),
141
- =======
142
121
  // Exclusion options
143
122
  excludeCurrentApp: options.excludeCurrentApp || false,
144
123
  excludeWindowIds: Array.isArray(options.excludeWindowIds)
145
124
  ? options.excludeWindowIds
146
125
  : [],
147
- >>>>>>> screencapture
148
126
  };
149
127
  }
150
128
 
@@ -305,17 +283,9 @@ class MacRecorder extends EventEmitter {
305
283
  windowId: this.options.windowId || null, // null = tam ekran
306
284
  audioDeviceId: this.options.audioDeviceId || null, // null = default device
307
285
  systemAudioDeviceId: this.options.systemAudioDeviceId || null, // null = auto-detect system audio device
308
- <<<<<<< HEAD
309
- useScreenCaptureKit: this.options.useScreenCaptureKit || false,
310
- excludedAppBundleIds: this.options.excludedAppBundleIds || [],
311
- excludedPIDs: this.options.excludedPIDs || [],
312
- excludedWindowIds: this.options.excludedWindowIds || [],
313
- autoExcludeSelf: this.options.autoExcludeSelf === true,
314
- =======
315
286
  // Exclusion options passthrough
316
287
  excludeCurrentApp: this.options.excludeCurrentApp || false,
317
288
  excludeWindowIds: this.options.excludeWindowIds || [],
318
- >>>>>>> screencapture
319
289
  };
320
290
 
321
291
  // Manuel captureArea varsa onu kullan
@@ -328,34 +298,10 @@ class MacRecorder extends EventEmitter {
328
298
  };
329
299
  }
330
300
 
331
- // SC yolu: kullanıcıdan SC talebi varsa veya exclude listeleri doluysa ve SC mevcutsa otomatik kullan
332
- let success;
333
- try {
334
- const wantsSC = !!(
335
- this.options.useScreenCaptureKit ||
336
- this.options.excludedAppBundleIds?.length ||
337
- this.options.excludedPIDs?.length ||
338
- this.options.excludedWindowIds?.length
339
- );
340
- const scAvailable =
341
- typeof nativeBinding.isScreenCaptureKitAvailable === "function" &&
342
- nativeBinding.isScreenCaptureKitAvailable();
343
- if (wantsSC && scAvailable) {
344
- const scOptions = {
345
- ...recordingOptions,
346
- useScreenCaptureKit: true,
347
- };
348
- success = nativeBinding.startRecording(outputPath, scOptions);
349
- } else {
350
- success = nativeBinding.startRecording(
351
- outputPath,
352
- recordingOptions
353
- );
354
- }
355
- } catch (e) {
356
- // Fallback AVFoundation
357
- success = nativeBinding.startRecording(outputPath, recordingOptions);
358
- }
301
+ const success = nativeBinding.startRecording(
302
+ outputPath,
303
+ recordingOptions
304
+ );
359
305
 
360
306
  if (success) {
361
307
  this.isRecording = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.2.1",
3
+ "version": "2.4.0",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -38,19 +38,14 @@
38
38
  "build": "node-gyp build",
39
39
  "rebuild": "node-gyp rebuild",
40
40
  "clean": "node-gyp clean",
41
- "prebuild:darwin:arm64": "prebuildify --napi --strip --platform darwin --arch arm64",
42
- "prebuild:darwin:x64": "prebuildify --napi --strip --platform darwin --arch x64",
43
- "prebuild": "npm run prebuild:darwin:arm64 && npm run prebuild:darwin:x64",
44
41
  "test:window-selector": "node window-selector-test.js",
45
42
  "example:window-selector": "node examples/window-selector-example.js"
46
43
  },
47
44
  "dependencies": {
48
- "node-addon-api": "^7.0.0",
49
- "node-gyp-build": "^4.6.0"
45
+ "node-addon-api": "^7.0.0"
50
46
  },
51
47
  "devDependencies": {
52
- "node-gyp": "^10.0.0",
53
- "prebuildify": "^5.0.0"
48
+ "node-gyp": "^10.0.0"
54
49
  },
55
50
  "gypfile": true
56
51
  }
@@ -1,10 +1,7 @@
1
1
  #import <napi.h>
2
2
  #import <ScreenCaptureKit/ScreenCaptureKit.h>
3
- <<<<<<< HEAD
4
- =======
5
3
  #import <AVFoundation/AVFoundation.h>
6
4
  #import <CoreMedia/CoreMedia.h>
7
- >>>>>>> screencapture
8
5
  #import <AppKit/AppKit.h>
9
6
  #import <Foundation/Foundation.h>
10
7
  #import <CoreGraphics/CoreGraphics.h>
@@ -21,13 +18,9 @@ Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports);
21
18
  // Window selector function declarations
22
19
  Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports);
23
20
 
24
- <<<<<<< HEAD
25
- @interface MacRecorderDelegate : NSObject
26
- =======
27
21
  // ScreenCaptureKit Recording Delegate
28
22
  API_AVAILABLE(macos(12.3))
29
23
  @interface SCKRecorderDelegate : NSObject <SCStreamDelegate, SCStreamOutput>
30
- >>>>>>> screencapture
31
24
  @property (nonatomic, copy) void (^completionHandler)(NSURL *outputURL, NSError *error);
32
25
  @property (nonatomic, copy) void (^startedHandler)(void);
33
26
  @property (nonatomic, strong) AVAssetWriter *assetWriter;
@@ -41,20 +34,6 @@ API_AVAILABLE(macos(12.3))
41
34
  @property (nonatomic, assign) BOOL startFailed;
42
35
  @end
43
36
 
44
- <<<<<<< HEAD
45
- @implementation MacRecorderDelegate
46
- - (void)recordingDidStart {
47
- NSLog(@"[mac_recorder] ScreenCaptureKit recording started");
48
- }
49
- - (void)recordingDidFinish:(NSURL *)outputURL error:(NSError *)error {
50
- if (error) {
51
- NSLog(@"[mac_recorder] ScreenCaptureKit recording finished with error: %@", error.localizedDescription);
52
- } else {
53
- NSLog(@"[mac_recorder] ScreenCaptureKit recording finished OK → %@", outputURL.path);
54
- }
55
- if (self.completionHandler) {
56
- self.completionHandler(outputURL, error);
57
- =======
58
37
  @implementation SCKRecorderDelegate
59
38
 
60
39
  // Standard SCStreamDelegate method - should be called automatically
@@ -67,7 +46,6 @@ API_AVAILABLE(macos(12.3))
67
46
  NSLog(@"🛑 Stream stopped with error: %@", error ? error.localizedDescription : @"none");
68
47
  if (self.completionHandler) {
69
48
  self.completionHandler(self.outputURL, error);
70
- >>>>>>> screencapture
71
49
  }
72
50
  }
73
51
 
@@ -142,15 +120,6 @@ API_AVAILABLE(macos(12.3))
142
120
 
143
121
  @end
144
122
 
145
- <<<<<<< HEAD
146
- // Global state for recording
147
- static MacRecorderDelegate *g_delegate = nil;
148
- static bool g_isRecording = false;
149
-
150
- // Helper function to cleanup recording resources
151
- void cleanupRecording() {
152
- g_delegate = nil;
153
- =======
154
123
  // Global state for ScreenCaptureKit recording
155
124
  static SCStream *g_scStream = nil;
156
125
  static SCKRecorderDelegate *g_scDelegate = nil;
@@ -201,7 +170,6 @@ void cleanupSCKRecording() {
201
170
  }
202
171
  g_scDelegate = nil;
203
172
  }
204
- >>>>>>> screencapture
205
173
  g_isRecording = false;
206
174
  }
207
175
 
@@ -246,31 +214,13 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
246
214
  NSLog(@"✅ Screen recording permission verified");
247
215
 
248
216
  std::string outputPath = info[0].As<Napi::String>().Utf8Value();
249
- NSLog(@"[mac_recorder] StartRecording: output=%@", [NSString stringWithUTF8String:outputPath.c_str()]);
250
217
 
251
- <<<<<<< HEAD
252
- // Options parsing (shared)
253
- CGRect captureRect = CGRectNull;
254
- bool captureCursor = false; // Default olarak cursor gizli
255
- bool includeMicrophone = false; // Default olarak mikrofon kapalı
256
- bool includeSystemAudio = true; // Default olarak sistem sesi açık
257
- CGDirectDisplayID displayID = CGMainDisplayID(); // Default ana ekran
258
- NSString *audioDeviceId = nil; // Default audio device ID
259
- NSString *systemAudioDeviceId = nil; // System audio device ID
260
- bool forceUseSC = false;
261
- // Exclude options for ScreenCaptureKit (optional, backward compatible)
262
- NSMutableArray<NSString*> *excludedAppBundleIds = [NSMutableArray array];
263
- NSMutableArray<NSNumber*> *excludedPIDs = [NSMutableArray array];
264
- NSMutableArray<NSNumber*> *excludedWindowIds = [NSMutableArray array];
265
- bool autoExcludeSelf = false;
266
- =======
267
218
  // Default options
268
219
  bool captureCursor = false;
269
220
  bool includeSystemAudio = true;
270
221
  CGDirectDisplayID displayID = 0; // Will be set to first available display
271
222
  uint32_t windowID = 0;
272
223
  CGRect captureRect = CGRectNull;
273
- >>>>>>> screencapture
274
224
 
275
225
  // Parse options
276
226
  if (info.Length() > 1 && info[1].IsObject()) {
@@ -285,53 +235,6 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
285
235
  includeSystemAudio = options.Get("includeSystemAudio").As<Napi::Boolean>();
286
236
  }
287
237
 
288
- <<<<<<< HEAD
289
- // System audio device ID
290
- if (options.Has("systemAudioDeviceId") && !options.Get("systemAudioDeviceId").IsNull()) {
291
- std::string sysDeviceId = options.Get("systemAudioDeviceId").As<Napi::String>().Utf8Value();
292
- systemAudioDeviceId = [NSString stringWithUTF8String:sysDeviceId.c_str()];
293
- }
294
-
295
- // ScreenCaptureKit toggle (optional)
296
- if (options.Has("useScreenCaptureKit")) {
297
- forceUseSC = options.Get("useScreenCaptureKit").As<Napi::Boolean>();
298
- }
299
-
300
- // Exclusion lists (optional)
301
- if (options.Has("excludedAppBundleIds") && options.Get("excludedAppBundleIds").IsArray()) {
302
- Napi::Array arr = options.Get("excludedAppBundleIds").As<Napi::Array>();
303
- for (uint32_t i = 0; i < arr.Length(); i++) {
304
- if (!arr.Get(i).IsUndefined() && !arr.Get(i).IsNull()) {
305
- std::string s = arr.Get(i).As<Napi::String>().Utf8Value();
306
- [excludedAppBundleIds addObject:[NSString stringWithUTF8String:s.c_str()]];
307
- }
308
- }
309
- }
310
- if (options.Has("excludedPIDs") && options.Get("excludedPIDs").IsArray()) {
311
- Napi::Array arr = options.Get("excludedPIDs").As<Napi::Array>();
312
- for (uint32_t i = 0; i < arr.Length(); i++) {
313
- if (!arr.Get(i).IsUndefined() && !arr.Get(i).IsNull()) {
314
- double v = arr.Get(i).As<Napi::Number>().DoubleValue();
315
- [excludedPIDs addObject:@( (pid_t)v )];
316
- }
317
- }
318
- }
319
- if (options.Has("excludedWindowIds") && options.Get("excludedWindowIds").IsArray()) {
320
- Napi::Array arr = options.Get("excludedWindowIds").As<Napi::Array>();
321
- for (uint32_t i = 0; i < arr.Length(); i++) {
322
- if (!arr.Get(i).IsUndefined() && !arr.Get(i).IsNull()) {
323
- double v = arr.Get(i).As<Napi::Number>().DoubleValue();
324
- [excludedWindowIds addObject:@( (uint32_t)v )];
325
- }
326
- }
327
- }
328
- if (options.Has("autoExcludeSelf")) {
329
- autoExcludeSelf = options.Get("autoExcludeSelf").As<Napi::Boolean>();
330
- }
331
-
332
- // Display ID
333
- =======
334
- >>>>>>> screencapture
335
238
  if (options.Has("displayId") && !options.Get("displayId").IsNull()) {
336
239
  uint32_t tempDisplayID = options.Get("displayId").As<Napi::Number>().Uint32Value();
337
240
  if (tempDisplayID != 0) {
@@ -356,64 +259,6 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
356
259
  }
357
260
  }
358
261
 
359
- <<<<<<< HEAD
360
- @try {
361
- // Always prefer ScreenCaptureKit if available
362
- NSLog(@"[mac_recorder] Checking ScreenCaptureKit availability");
363
- if (@available(macOS 12.3, *)) {
364
- if ([ScreenCaptureKitRecorder isScreenCaptureKitAvailable]) {
365
- NSMutableDictionary *scConfig = [@{} mutableCopy];
366
- scConfig[@"displayId"] = @(displayID);
367
- if (!CGRectIsNull(captureRect)) {
368
- scConfig[@"captureArea"] = @{ @"x": @(captureRect.origin.x),
369
- @"y": @(captureRect.origin.y),
370
- @"width": @(captureRect.size.width),
371
- @"height": @(captureRect.size.height) };
372
- }
373
- scConfig[@"captureCursor"] = @(captureCursor);
374
- scConfig[@"includeMicrophone"] = @(includeMicrophone);
375
- scConfig[@"includeSystemAudio"] = @(includeSystemAudio);
376
- if (excludedAppBundleIds.count) scConfig[@"excludedAppBundleIds"] = excludedAppBundleIds;
377
- if (excludedPIDs.count) scConfig[@"excludedPIDs"] = excludedPIDs;
378
- if (excludedWindowIds.count) scConfig[@"excludedWindowIds"] = excludedWindowIds;
379
- // Auto exclude current app by PID if requested
380
- if (autoExcludeSelf) {
381
- pid_t pid = getpid();
382
- NSMutableArray *arr = [NSMutableArray arrayWithArray:scConfig[@"excludedPIDs"] ?: @[]];
383
- [arr addObject:@(pid)];
384
- scConfig[@"excludedPIDs"] = arr;
385
- }
386
-
387
- // Output path for SC
388
- std::string outputPathStr = info[0].As<Napi::String>().Utf8Value();
389
- scConfig[@"outputPath"] = [NSString stringWithUTF8String:outputPathStr.c_str()];
390
-
391
- NSError *scErr = nil;
392
- NSLog(@"[mac_recorder] Using ScreenCaptureKit path (displayId=%u)", displayID);
393
-
394
- // Create and set up delegate
395
- g_delegate = [[MacRecorderDelegate alloc] init];
396
-
397
- BOOL ok = [ScreenCaptureKitRecorder startRecordingWithConfiguration:scConfig delegate:g_delegate error:&scErr];
398
- if (ok) {
399
- g_isRecording = true;
400
- NSLog(@"[mac_recorder] ScreenCaptureKit startRecording → OK");
401
- return Napi::Boolean::New(env, true);
402
- }
403
- NSLog(@"[mac_recorder] ScreenCaptureKit startRecording → FAIL: %@", scErr.localizedDescription);
404
- cleanupRecording();
405
- return Napi::Boolean::New(env, false);
406
- }
407
- } else {
408
- NSLog(@"[mac_recorder] ScreenCaptureKit not available");
409
- cleanupRecording();
410
- return Napi::Boolean::New(env, false);
411
- }
412
-
413
- } @catch (NSException *exception) {
414
- cleanupRecording();
415
- return Napi::Boolean::New(env, false);
416
- =======
417
262
  // Create output URL
418
263
  NSURL *outputURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:outputPath.c_str()]];
419
264
  NSLog(@"📁 Output URL: %@", outputURL.absoluteString);
@@ -543,13 +388,8 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
543
388
 
544
389
  contentFilter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:excluded];
545
390
  NSLog(@"✅ Content filter created for display recording");
546
- >>>>>>> screencapture
547
391
  }
548
392
 
549
- <<<<<<< HEAD
550
- if (!g_isRecording) {
551
- return Napi::Boolean::New(env, false);
552
- =======
553
393
  // Get actual display dimensions for proper video configuration
554
394
  CGRect displayBounds = CGDisplayBounds(displayID);
555
395
  NSSize videoSize = NSMakeSize(displayBounds.size.width, displayBounds.size.height);
@@ -571,27 +411,9 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
571
411
  NSLog(@"🔊 Audio configuration: capture=%@, excludeProcess=%@", includeSystemAudio ? @"YES" : @"NO", @"YES");
572
412
  } else {
573
413
  NSLog(@"⚠️ macOS 13.0+ features not available");
574
- >>>>>>> screencapture
575
414
  }
576
415
  config.showsCursor = captureCursor;
577
416
 
578
- <<<<<<< HEAD
579
- @try {
580
- NSLog(@"[mac_recorder] StopRecording called");
581
-
582
- // Stop ScreenCaptureKit recording
583
- NSLog(@"[mac_recorder] Stopping ScreenCaptureKit stream");
584
- if (@available(macOS 12.3, *)) {
585
- [ScreenCaptureKitRecorder stopRecording];
586
- }
587
- g_isRecording = false;
588
- cleanupRecording();
589
- NSLog(@"[mac_recorder] ScreenCaptureKit stopped");
590
- return Napi::Boolean::New(env, true);
591
-
592
- } @catch (NSException *exception) {
593
- cleanupRecording();
594
- =======
595
417
  if (!CGRectIsNull(captureRect)) {
596
418
  config.sourceRect = captureRect;
597
419
  // Update video size if capture rect is specified
@@ -611,7 +433,6 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
611
433
 
612
434
  if (writerError) {
613
435
  NSLog(@"❌ Failed to create asset writer: %@", writerError.localizedDescription);
614
- >>>>>>> screencapture
615
436
  return Napi::Boolean::New(env, false);
616
437
  }
617
438
 
@@ -635,117 +456,6 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
635
456
  NSLog(@"❌ Cannot add video input to asset writer");
636
457
  }
637
458
 
638
- <<<<<<< HEAD
639
- @try {
640
- NSMutableArray *devices = [NSMutableArray array];
641
-
642
- // Use CoreAudio to get audio devices since we're removing AVFoundation
643
- AudioObjectPropertyAddress propertyAddress = {
644
- kAudioHardwarePropertyDevices,
645
- kAudioObjectPropertyScopeGlobal,
646
- kAudioObjectPropertyElementMain
647
- };
648
-
649
- UInt32 dataSize = 0;
650
- OSStatus status = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize);
651
- if (status != noErr) {
652
- return Napi::Array::New(env, 0);
653
- }
654
-
655
- UInt32 deviceCount = dataSize / sizeof(AudioDeviceID);
656
- AudioDeviceID *audioDevices = (AudioDeviceID *)malloc(dataSize);
657
-
658
- status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize, audioDevices);
659
- if (status != noErr) {
660
- free(audioDevices);
661
- return Napi::Array::New(env, 0);
662
- }
663
-
664
- for (UInt32 i = 0; i < deviceCount; i++) {
665
- AudioDeviceID deviceID = audioDevices[i];
666
-
667
- // Check if device has input streams
668
- AudioObjectPropertyAddress streamsAddress = {
669
- kAudioDevicePropertyStreams,
670
- kAudioDevicePropertyScopeInput,
671
- kAudioObjectPropertyElementMain
672
- };
673
-
674
- UInt32 streamsSize = 0;
675
- status = AudioObjectGetPropertyDataSize(deviceID, &streamsAddress, 0, NULL, &streamsSize);
676
- if (status != noErr || streamsSize == 0) {
677
- continue; // Skip output-only devices
678
- }
679
-
680
- // Get device name
681
- AudioObjectPropertyAddress nameAddress = {
682
- kAudioDevicePropertyDeviceNameCFString,
683
- kAudioObjectPropertyScopeGlobal,
684
- kAudioObjectPropertyElementMain
685
- };
686
-
687
- CFStringRef deviceNameRef = NULL;
688
- UInt32 nameSize = sizeof(CFStringRef);
689
- status = AudioObjectGetPropertyData(deviceID, &nameAddress, 0, NULL, &nameSize, &deviceNameRef);
690
-
691
- NSString *deviceName = @"Unknown Device";
692
- if (status == noErr && deviceNameRef) {
693
- deviceName = (__bridge NSString *)deviceNameRef;
694
- }
695
-
696
- // Get device UID
697
- AudioObjectPropertyAddress uidAddress = {
698
- kAudioDevicePropertyDeviceUID,
699
- kAudioObjectPropertyScopeGlobal,
700
- kAudioObjectPropertyElementMain
701
- };
702
-
703
- CFStringRef deviceUIDRef = NULL;
704
- UInt32 uidSize = sizeof(CFStringRef);
705
- status = AudioObjectGetPropertyData(deviceID, &uidAddress, 0, NULL, &uidSize, &deviceUIDRef);
706
-
707
- NSString *deviceUID = [NSString stringWithFormat:@"%u", deviceID];
708
- if (status == noErr && deviceUIDRef) {
709
- deviceUID = (__bridge NSString *)deviceUIDRef;
710
- }
711
-
712
- // Check if this is the default input device
713
- AudioObjectPropertyAddress defaultAddress = {
714
- kAudioHardwarePropertyDefaultInputDevice,
715
- kAudioObjectPropertyScopeGlobal,
716
- kAudioObjectPropertyElementMain
717
- };
718
-
719
- AudioDeviceID defaultDeviceID = kAudioDeviceUnknown;
720
- UInt32 defaultSize = sizeof(AudioDeviceID);
721
- AudioObjectGetPropertyData(kAudioObjectSystemObject, &defaultAddress, 0, NULL, &defaultSize, &defaultDeviceID);
722
-
723
- BOOL isDefault = (deviceID == defaultDeviceID);
724
-
725
- [devices addObject:@{
726
- @"id": deviceUID,
727
- @"name": deviceName,
728
- @"manufacturer": @"Unknown",
729
- @"isDefault": @(isDefault)
730
- }];
731
-
732
- if (deviceNameRef) CFRelease(deviceNameRef);
733
- if (deviceUIDRef) CFRelease(deviceUIDRef);
734
- }
735
-
736
- free(audioDevices);
737
-
738
- // Convert to NAPI array
739
- Napi::Array result = Napi::Array::New(env, devices.count);
740
- for (NSUInteger i = 0; i < devices.count; i++) {
741
- NSDictionary *device = devices[i];
742
- Napi::Object deviceObj = Napi::Object::New(env);
743
- deviceObj.Set("id", Napi::String::New(env, [device[@"id"] UTF8String]));
744
- deviceObj.Set("name", Napi::String::New(env, [device[@"name"] UTF8String]));
745
- deviceObj.Set("manufacturer", Napi::String::New(env, [device[@"manufacturer"] UTF8String]));
746
- deviceObj.Set("isDefault", Napi::Boolean::New(env, [device[@"isDefault"] boolValue]));
747
- result[i] = deviceObj;
748
- =======
749
459
  // Audio input settings (if needed)
750
460
  if (includeSystemAudio) {
751
461
  NSDictionary *audioSettings = @{
@@ -785,7 +495,6 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
785
495
  NSLog(@"✅ Audio output attached to SCStream");
786
496
  } else {
787
497
  NSLog(@"⚠️ Failed to attach audio output to SCStream (audio may be disabled): %@", outputError.localizedDescription);
788
- >>>>>>> screencapture
789
498
  }
790
499
  }
791
500
 
@@ -950,19 +659,6 @@ Napi::Value GetWindows(const Napi::CallbackInfo& info) {
950
659
  Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
951
660
  Napi::Env env = info.Env();
952
661
 
953
- <<<<<<< HEAD
954
- @try {
955
- // Check screen recording permission using ScreenCaptureKit
956
- bool hasScreenPermission = true;
957
-
958
- if (@available(macOS 12.3, *)) {
959
- // Try to get shareable content to test ScreenCaptureKit permissions
960
- @try {
961
- SCShareableContent *content = [SCShareableContent currentShareableContent];
962
- hasScreenPermission = (content != nil && content.displays.count > 0);
963
- } @catch (NSException *exception) {
964
- hasScreenPermission = false;
965
- =======
966
662
  // Check screen recording permission
967
663
  bool hasPermission = CGPreflightScreenCaptureAccess();
968
664
 
@@ -1032,41 +728,8 @@ Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
1032
728
 
1033
729
  devices.Set(index++, deviceObj);
1034
730
  CFRelease(deviceName);
1035
- >>>>>>> screencapture
1036
- }
1037
- } else {
1038
- // Fallback for older macOS versions
1039
- if (@available(macOS 10.15, *)) {
1040
- // Try to create a display stream to test permissions
1041
- CGDisplayStreamRef stream = CGDisplayStreamCreate(
1042
- CGMainDisplayID(),
1043
- 1, 1,
1044
- kCVPixelFormatType_32BGRA,
1045
- nil,
1046
- ^(CGDisplayStreamFrameStatus status, uint64_t displayTime, IOSurfaceRef frameSurface, CGDisplayStreamUpdateRef updateRef) {
1047
- // Empty handler
1048
- }
1049
- );
1050
-
1051
- if (stream) {
1052
- CFRelease(stream);
1053
- hasScreenPermission = true;
1054
- } else {
1055
- hasScreenPermission = false;
1056
- }
1057
731
  }
1058
732
  }
1059
- <<<<<<< HEAD
1060
-
1061
- // For audio permission, we'll use a simpler check since we're using CoreAudio
1062
- bool hasAudioPermission = true;
1063
-
1064
- return Napi::Boolean::New(env, hasScreenPermission && hasAudioPermission);
1065
-
1066
- } @catch (NSException *exception) {
1067
- return Napi::Boolean::New(env, false);
1068
- =======
1069
- >>>>>>> screencapture
1070
733
  }
1071
734
 
1072
735
  free(audioDevices);
@@ -1075,29 +738,6 @@ Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
1075
738
 
1076
739
  // Initialize the addon
1077
740
  Napi::Object Init(Napi::Env env, Napi::Object exports) {
1078
- <<<<<<< HEAD
1079
- exports.Set(Napi::String::New(env, "startRecording"), Napi::Function::New(env, StartRecording));
1080
- exports.Set(Napi::String::New(env, "stopRecording"), Napi::Function::New(env, StopRecording));
1081
-
1082
- exports.Set(Napi::String::New(env, "getAudioDevices"), Napi::Function::New(env, GetAudioDevices));
1083
- exports.Set(Napi::String::New(env, "getDisplays"), Napi::Function::New(env, GetDisplays));
1084
- exports.Set(Napi::String::New(env, "getWindows"), Napi::Function::New(env, GetWindows));
1085
- exports.Set(Napi::String::New(env, "getRecordingStatus"), Napi::Function::New(env, GetRecordingStatus));
1086
- exports.Set(Napi::String::New(env, "checkPermissions"), Napi::Function::New(env, CheckPermissions));
1087
- // ScreenCaptureKit availability (optional for clients)
1088
- exports.Set(Napi::String::New(env, "isScreenCaptureKitAvailable"), Napi::Function::New(env, [](const Napi::CallbackInfo& info){
1089
- Napi::Env env = info.Env();
1090
- if (@available(macOS 12.3, *)) {
1091
- bool available = [ScreenCaptureKitRecorder isScreenCaptureKitAvailable];
1092
- return Napi::Boolean::New(env, available);
1093
- }
1094
- return Napi::Boolean::New(env, false);
1095
- }));
1096
-
1097
- // Thumbnail functions
1098
- exports.Set(Napi::String::New(env, "getWindowThumbnail"), Napi::Function::New(env, GetWindowThumbnail));
1099
- exports.Set(Napi::String::New(env, "getDisplayThumbnail"), Napi::Function::New(env, GetDisplayThumbnail));
1100
- =======
1101
741
  exports.Set("startRecording", Napi::Function::New(env, StartRecording));
1102
742
  exports.Set("stopRecording", Napi::Function::New(env, StopRecording));
1103
743
  exports.Set("isRecording", Napi::Function::New(env, IsRecording));
@@ -1105,7 +745,6 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
1105
745
  exports.Set("getWindows", Napi::Function::New(env, GetWindows));
1106
746
  exports.Set("checkPermissions", Napi::Function::New(env, CheckPermissions));
1107
747
  exports.Set("getAudioDevices", Napi::Function::New(env, GetAudioDevices));
1108
- >>>>>>> screencapture
1109
748
 
1110
749
  // Initialize cursor tracker
1111
750
  InitCursorTracker(env, exports);
@@ -1,9 +1,6 @@
1
- <<<<<<< HEAD
2
- =======
3
1
  #import "screen_capture.h"
4
2
  #import <ScreenCaptureKit/ScreenCaptureKit.h>
5
3
  #import <AVFoundation/AVFoundation.h>
6
- >>>>>>> screencapture
7
4
  #import <CoreGraphics/CoreGraphics.h>
8
5
  #import <AppKit/AppKit.h>
9
6
 
@@ -77,11 +74,7 @@
77
74
  NSURL *fileURL = [NSURL fileURLWithPath:filePath];
78
75
  CGImageDestinationRef destination = CGImageDestinationCreateWithURL(
79
76
  (__bridge CFURLRef)fileURL,
80
- <<<<<<< HEAD
81
- CFSTR("public.png"),
82
- =======
83
77
  (__bridge CFStringRef)@"public.png",
84
- >>>>>>> screencapture
85
78
  1,
86
79
  NULL
87
80
  );
@@ -110,14 +103,6 @@
110
103
 
111
104
  + (CGImageRef)createScreenshotFromDisplay:(CGDirectDisplayID)displayID
112
105
  rect:(CGRect)rect {
113
- <<<<<<< HEAD
114
- if (CGRectIsNull(rect)) {
115
- // Capture entire display
116
- return CGDisplayCreateImage(displayID);
117
- } else {
118
- // Capture specific rect
119
- return CGDisplayCreateImageForRect(displayID, rect);
120
- =======
121
106
  if (CGRectIsNull(rect) || CGRectIsEmpty(rect)) {
122
107
  rect = CGDisplayBounds(displayID);
123
108
  }
@@ -156,7 +141,6 @@ Napi::Value GetAvailableDisplays(const Napi::CallbackInfo& info) {
156
141
  displayObj.Set("isPrimary", Napi::Boolean::New(env, [[displayInfo objectForKey:@"isPrimary"] boolValue]));
157
142
 
158
143
  displaysArray.Set(static_cast<uint32_t>(i), displayObj);
159
- >>>>>>> screencapture
160
144
  }
161
145
 
162
146
  return displaysArray;
package/test-sck.js CHANGED
@@ -1,55 +1,3 @@
1
- <<<<<<< HEAD
2
- const nativeBinding = require('./build/Release/mac_recorder.node');
3
-
4
- console.log('=== ScreenCaptureKit Migration Test ===');
5
- console.log('ScreenCaptureKit available:', nativeBinding.isScreenCaptureKitAvailable());
6
- console.log('Displays:', nativeBinding.getDisplays().length);
7
- console.log('Audio devices:', nativeBinding.getAudioDevices().length);
8
- console.log('Permissions OK:', nativeBinding.checkPermissions());
9
-
10
- const displays = nativeBinding.getDisplays();
11
- console.log('\nDisplay info:', displays[0]);
12
-
13
- const audioDevices = nativeBinding.getAudioDevices();
14
- console.log('\nFirst audio device:', audioDevices[0]);
15
-
16
- // Test starting and stopping recording
17
- console.log('\n=== Recording Test ===');
18
- const outputPath = '/tmp/test-recording-sck.mov';
19
-
20
- try {
21
- console.log('Starting recording...');
22
- const success = nativeBinding.startRecording(outputPath, {
23
- displayId: displays[0].id,
24
- captureCursor: true,
25
- includeMicrophone: false,
26
- includeSystemAudio: false
27
- });
28
-
29
- console.log('Recording started:', success);
30
-
31
- if (success) {
32
- setTimeout(() => {
33
- console.log('Stopping recording...');
34
- const stopped = nativeBinding.stopRecording();
35
- console.log('Recording stopped:', stopped);
36
-
37
- // Check if file was created
38
- const fs = require('fs');
39
- setTimeout(() => {
40
- if (fs.existsSync(outputPath)) {
41
- const stats = fs.statSync(outputPath);
42
- console.log(`Recording file created: ${outputPath} (${stats.size} bytes)`);
43
- } else {
44
- console.log('Recording file not found');
45
- }
46
- }, 1000);
47
- }, 3000); // Record for 3 seconds
48
- }
49
- } catch (error) {
50
- console.error('Recording test failed:', error.message);
51
- }
52
- =======
53
1
  const MacRecorder = require('./index');
54
2
 
55
3
  function testScreenCaptureKit() {
@@ -105,5 +53,4 @@ function testScreenCaptureKit() {
105
53
  }
106
54
  }
107
55
 
108
- testScreenCaptureKit();
109
- >>>>>>> screencapture
56
+ testScreenCaptureKit();
Binary file
@@ -1,72 +0,0 @@
1
- /*
2
- Simple test runner: starts a 2s recording with ScreenCaptureKit and exclusions.
3
- */
4
- const fs = require("fs");
5
- const path = require("path");
6
- const MacRecorder = require("..");
7
-
8
- async function sleep(ms) {
9
- return new Promise((resolve) => setTimeout(resolve, ms));
10
- }
11
-
12
- async function main() {
13
- const recorder = new MacRecorder();
14
- const outDir = path.resolve(process.cwd(), "test-output");
15
- const outPath = path.join(outDir, `sc-exclude-${Date.now()}.mov`);
16
- await fs.promises.mkdir(outDir, { recursive: true });
17
-
18
- console.log("[TEST] Starting 2s recording with SC exclusions...");
19
- // Try to ensure overlays are not active in this process
20
-
21
- const perms = await recorder.checkPermissions();
22
- if (!perms?.screenRecording) {
23
- console.error(
24
- "[TEST] Screen Recording permission is not granted. Enable it in System Settings → Privacy & Security → Screen Recording for Terminal/Node, then re-run."
25
- );
26
- process.exit(1);
27
- }
28
-
29
- try {
30
- await recorder.startRecording(outPath, {
31
- useScreenCaptureKit: true,
32
- captureCursor: false,
33
- excludedAppBundleIds: ["com.apple.Safari"],
34
- });
35
- } catch (e) {
36
- console.error("[TEST] Failed to start recording:", e.message);
37
- process.exit(1);
38
- }
39
-
40
- await sleep(2000);
41
-
42
- try {
43
- const result = await recorder.stopRecording();
44
- console.log("[TEST] Stopped. Result:", result);
45
- } catch (e) {
46
- console.error("[TEST] Failed to stop recording:", e.message);
47
- process.exit(1);
48
- }
49
-
50
- // SCRecordingOutput write may be async; wait up to 10s for the file
51
- const deadline = Date.now() + 10000;
52
- let stats = null;
53
- while (Date.now() < deadline) {
54
- if (fs.existsSync(outPath)) {
55
- stats = fs.statSync(outPath);
56
- if (stats.size > 0) break;
57
- }
58
- await sleep(200);
59
- }
60
-
61
- if (stats && fs.existsSync(outPath)) {
62
- console.log(`[TEST] Output saved: ${outPath} (${stats.size} bytes)`);
63
- } else {
64
- console.error("[TEST] Output file not found or empty:", outPath);
65
- process.exit(1);
66
- }
67
- }
68
-
69
- main().catch((e) => {
70
- console.error("[TEST] Unhandled error:", e);
71
- process.exit(1);
72
- });
@@ -1,222 +0,0 @@
1
- #import "screen_capture_kit.h"
2
- #import <ScreenCaptureKit/ScreenCaptureKit.h>
3
- #import <AVFoundation/AVFoundation.h>
4
-
5
- static SCStream *g_scStream = nil;
6
- static SCRecordingOutput *g_scRecordingOutput = nil;
7
- static NSURL *g_outputURL = nil;
8
- static ScreenCaptureKitRecorder *g_scDelegate = nil;
9
- static NSArray<SCRunningApplication *> *g_appsToExclude = nil;
10
- static NSArray<SCWindow *> *g_windowsToExclude = nil;
11
-
12
- @interface ScreenCaptureKitRecorder () <SCRecordingOutputDelegate>
13
- @end
14
-
15
- @implementation ScreenCaptureKitRecorder
16
-
17
- + (BOOL)isScreenCaptureKitAvailable {
18
- if (@available(macOS 14.0, *)) {
19
- Class streamClass = NSClassFromString(@"SCStream");
20
- Class recordingOutputClass = NSClassFromString(@"SCRecordingOutput");
21
- return (streamClass != nil && recordingOutputClass != nil);
22
- }
23
- return NO;
24
- }
25
-
26
- + (BOOL)isRecording {
27
- return g_scStream != nil;
28
- }
29
-
30
- + (BOOL)startRecordingWithConfiguration:(NSDictionary *)config
31
- delegate:(id)delegate
32
- error:(NSError **)error {
33
- if (![self isScreenCaptureKitAvailable]) {
34
- if (error) {
35
- *error = [NSError errorWithDomain:@"ScreenCaptureKitRecorder"
36
- code:-1
37
- userInfo:@{NSLocalizedDescriptionKey: @"ScreenCaptureKit not available"}];
38
- }
39
- return NO;
40
- }
41
-
42
- if (g_scStream) {
43
- return NO;
44
- }
45
-
46
- @try {
47
- SCShareableContent *content = nil;
48
- if (@available(macOS 13.0, *)) {
49
- NSLog(@"[SCK] Fetching shareable content...");
50
- content = [SCShareableContent currentShareableContent];
51
- }
52
- if (!content) {
53
- if (error) { *error = [NSError errorWithDomain:@"ScreenCaptureKitRecorder" code:-2 userInfo:@{NSLocalizedDescriptionKey:@"Failed to get shareable content"}]; }
54
- NSLog(@"[SCK] Failed to get shareable content");
55
- return NO;
56
- }
57
-
58
- NSNumber *displayIdNumber = config[@"displayId"]; // CGDirectDisplayID
59
- NSDictionary *captureArea = config[@"captureArea"]; // {x,y,width,height}
60
- NSNumber *captureCursorNum = config[@"captureCursor"];
61
- NSNumber *includeMicNum = config[@"includeMicrophone"];
62
- NSNumber *includeSystemAudioNum = config[@"includeSystemAudio"];
63
- NSArray<NSString *> *excludedBundleIds = config[@"excludedAppBundleIds"];
64
- NSArray<NSNumber *> *excludedPIDs = config[@"excludedPIDs"];
65
- NSArray<NSNumber *> *excludedWindowIds = config[@"excludedWindowIds"];
66
- NSString *outputPath = config[@"outputPath"];
67
-
68
- SCDisplay *targetDisplay = nil;
69
- if (displayIdNumber) {
70
- uint32_t wanted = (uint32_t)displayIdNumber.unsignedIntValue;
71
- for (SCDisplay *d in content.displays) {
72
- if (d.displayID == wanted) { targetDisplay = d; break; }
73
- }
74
- }
75
- if (!targetDisplay) {
76
- targetDisplay = content.displays.firstObject;
77
- if (!targetDisplay) { NSLog(@"[SCK] No displays found"); return NO; }
78
- }
79
- NSLog(@"[SCK] Using displayID=%u", targetDisplay.displayID);
80
-
81
- NSMutableArray<SCRunningApplication*> *appsToExclude = [NSMutableArray array];
82
- if (excludedBundleIds.count > 0) {
83
- for (SCRunningApplication *app in content.applications) {
84
- if ([excludedBundleIds containsObject:app.bundleIdentifier]) {
85
- [appsToExclude addObject:app];
86
- }
87
- }
88
- }
89
- if (excludedPIDs.count > 0) {
90
- for (SCRunningApplication *app in content.applications) {
91
- if ([excludedPIDs containsObject:@(app.processID)]) {
92
- [appsToExclude addObject:app];
93
- }
94
- }
95
- }
96
-
97
- NSMutableArray<SCWindow*> *windowsToExclude = [NSMutableArray array];
98
- if (excludedWindowIds.count > 0) {
99
- for (SCWindow *w in content.windows) {
100
- if ([excludedWindowIds containsObject:@(w.windowID)]) {
101
- [windowsToExclude addObject:w];
102
- }
103
- }
104
- }
105
-
106
- // Keep strong references to excluded items for the lifetime of the stream
107
- g_appsToExclude = [appsToExclude copy];
108
- g_windowsToExclude = [windowsToExclude copy];
109
-
110
- SCContentFilter *filter = nil;
111
- if (appsToExclude.count > 0) {
112
- if (@available(macOS 13.0, *)) {
113
- filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingApplications:(g_appsToExclude ?: @[]) exceptingWindows:(g_windowsToExclude ?: @[])];
114
- }
115
- }
116
- if (!filter) {
117
- if (@available(macOS 13.0, *)) {
118
- filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:(g_windowsToExclude ?: @[])];
119
- }
120
- }
121
- if (!filter) { NSLog(@"[SCK] Failed to create filter"); return NO; }
122
-
123
- SCStreamConfiguration *cfg = [[SCStreamConfiguration alloc] init];
124
- if (captureArea && captureArea[@"width"] && captureArea[@"height"]) {
125
- CGRect displayFrame = targetDisplay.frame;
126
- double x = [captureArea[@"x"] doubleValue];
127
- double yBottom = [captureArea[@"y"] doubleValue];
128
- double w = [captureArea[@"width"] doubleValue];
129
- double h = [captureArea[@"height"] doubleValue];
130
-
131
- // Convert bottom-left origin (used by legacy path) to top-left for SC
132
- double y = displayFrame.size.height - yBottom - h;
133
-
134
- // Clamp to display bounds to avoid invalid sourceRect
135
- if (w < 1) w = 1; if (h < 1) h = 1;
136
- if (x < 0) x = 0; if (y < 0) y = 0;
137
- if (x + w > displayFrame.size.width) w = MAX(1, displayFrame.size.width - x);
138
- if (y + h > displayFrame.size.height) h = MAX(1, displayFrame.size.height - y);
139
-
140
- CGRect src = CGRectMake(x, y, w, h);
141
- cfg.sourceRect = src;
142
- cfg.width = (int)src.size.width;
143
- cfg.height = (int)src.size.height;
144
- } else {
145
- CGRect displayFrame = targetDisplay.frame;
146
- cfg.width = (int)displayFrame.size.width;
147
- cfg.height = (int)displayFrame.size.height;
148
- }
149
- cfg.showsCursor = captureCursorNum.boolValue;
150
- if (includeMicNum || includeSystemAudioNum) {
151
- if (@available(macOS 13.0, *)) {
152
- cfg.capturesAudio = YES;
153
- }
154
- }
155
-
156
- if (@available(macOS 15.0, *)) {
157
- g_scStream = [[SCStream alloc] initWithFilter:filter configuration:cfg delegate:nil];
158
- NSLog(@"[SCK] Stream created. w=%d h=%d cursor=%@ audio=%@", cfg.width, cfg.height, cfg.showsCursor?@"YES":@"NO", (@available(macOS 13.0, *) ? (cfg.capturesAudio?@"YES":@"NO") : @"N/A"));
159
-
160
- SCRecordingOutputConfiguration *recCfg = [[SCRecordingOutputConfiguration alloc] init];
161
- g_outputURL = [NSURL fileURLWithPath:outputPath];
162
- recCfg.outputURL = g_outputURL;
163
- recCfg.outputFileType = AVFileTypeQuickTimeMovie;
164
-
165
- id<SCRecordingOutputDelegate> delegateObject = (id<SCRecordingOutputDelegate>)delegate;
166
- if (!delegateObject) {
167
- if (!g_scDelegate) {
168
- g_scDelegate = [[ScreenCaptureKitRecorder alloc] init];
169
- }
170
- delegateObject = (id<SCRecordingOutputDelegate>)g_scDelegate;
171
- }
172
- g_scRecordingOutput = [[SCRecordingOutput alloc] initWithConfiguration:recCfg delegate:delegateObject];
173
-
174
- NSError *addErr = nil;
175
- BOOL added = [g_scStream addRecordingOutput:g_scRecordingOutput error:&addErr];
176
- if (!added) {
177
- NSLog(@"[SCK] addRecordingOutput failed: %@", addErr.localizedDescription);
178
- if (error) { *error = addErr ?: [NSError errorWithDomain:@"ScreenCaptureKitRecorder" code:-3 userInfo:nil]; }
179
- g_scRecordingOutput = nil; g_scStream = nil; g_outputURL = nil;
180
- return NO;
181
- }
182
-
183
- [g_scStream startCaptureWithCompletionHandler:^(NSError * _Nullable err) {
184
- if (err) {
185
- NSLog(@"[SCK] startCapture error: %@", err.localizedDescription);
186
- [g_scStream removeRecordingOutput:g_scRecordingOutput error:nil];
187
- g_scRecordingOutput = nil; g_scStream = nil; g_outputURL = nil;
188
- } else {
189
- NSLog(@"[SCK] startCapture OK");
190
- }
191
- }];
192
- // Return immediately; capture will start asynchronously
193
- return YES;
194
- }
195
-
196
- return NO;
197
- } @catch (__unused NSException *ex) {
198
- return NO;
199
- }
200
- }
201
-
202
- + (void)stopRecording {
203
- if (!g_scStream) { return; }
204
- @try {
205
- dispatch_semaphore_t sem = dispatch_semaphore_create(0);
206
- [g_scStream stopCaptureWithCompletionHandler:^(NSError * _Nullable error) {
207
- dispatch_semaphore_signal(sem);
208
- }];
209
- dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
210
- } @catch (__unused NSException *ex) {
211
- }
212
- if (g_scRecordingOutput) {
213
- [g_scStream removeRecordingOutput:g_scRecordingOutput error:nil];
214
- }
215
- g_scRecordingOutput = nil;
216
- g_scStream = nil;
217
- g_outputURL = nil;
218
- }
219
-
220
- @end
221
-
222
-