node-mac-recorder 2.10.40 → 2.12.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.10.40",
3
+ "version": "2.12.0",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -9,6 +9,7 @@
9
9
 
10
10
  // Import screen capture
11
11
  #import "screen_capture.h"
12
+ #import "screen_capture_kit.h"
12
13
 
13
14
  // Cursor tracker function declarations
14
15
  Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports);
@@ -159,6 +160,46 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
159
160
  }
160
161
 
161
162
  @try {
163
+ // Try ScreenCaptureKit first (macOS 12.3+)
164
+ if (@available(macOS 12.3, *)) {
165
+ if ([ScreenCaptureKitRecorder isScreenCaptureKitAvailable]) {
166
+ NSLog(@"🎯 Using ScreenCaptureKit - overlay windows will be automatically excluded");
167
+
168
+ // Create configuration for ScreenCaptureKit
169
+ NSMutableDictionary *sckConfig = [NSMutableDictionary dictionary];
170
+ sckConfig[@"displayId"] = @(displayID);
171
+ sckConfig[@"captureCursor"] = @(captureCursor);
172
+ sckConfig[@"includeSystemAudio"] = @(includeSystemAudio);
173
+ sckConfig[@"includeMicrophone"] = @(includeMicrophone);
174
+ sckConfig[@"audioDeviceId"] = audioDeviceId;
175
+ sckConfig[@"outputPath"] = [NSString stringWithUTF8String:outputPath.c_str()];
176
+
177
+ if (!CGRectIsNull(captureRect)) {
178
+ sckConfig[@"captureRect"] = @{
179
+ @"x": @(captureRect.origin.x),
180
+ @"y": @(captureRect.origin.y),
181
+ @"width": @(captureRect.size.width),
182
+ @"height": @(captureRect.size.height)
183
+ };
184
+ }
185
+
186
+ // Use ScreenCaptureKit with window exclusion
187
+ NSError *sckError = nil;
188
+ if ([ScreenCaptureKitRecorder startRecordingWithConfiguration:sckConfig
189
+ delegate:g_delegate
190
+ error:&sckError]) {
191
+ NSLog(@"✅ ScreenCaptureKit recording started with window exclusion");
192
+ g_isRecording = true;
193
+ return Napi::Boolean::New(env, true);
194
+ } else {
195
+ NSLog(@"⚠️ ScreenCaptureKit failed (%@), falling back to AVFoundation", sckError.localizedDescription);
196
+ }
197
+ }
198
+ }
199
+
200
+ // Fallback: Use AVFoundation (older macOS or ScreenCaptureKit failure)
201
+ NSLog(@"📼 Falling back to AVFoundation - overlay windows may appear in recording");
202
+
162
203
  // Create capture session
163
204
  g_captureSession = [[AVCaptureSession alloc] init];
164
205
  [g_captureSession beginConfiguration];
@@ -176,6 +217,9 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
176
217
  // Set cursor capture
177
218
  g_screenInput.capturesCursor = captureCursor;
178
219
 
220
+ // Configure screen input options
221
+ g_screenInput.capturesMouseClicks = NO;
222
+
179
223
  if ([g_captureSession canAddInput:g_screenInput]) {
180
224
  [g_captureSession addInput:g_screenInput];
181
225
  } else {
@@ -330,8 +374,17 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
330
374
  }
331
375
 
332
376
  @try {
333
- [g_movieFileOutput stopRecording];
334
- [g_captureSession stopRunning];
377
+ if (g_movieFileOutput) {
378
+ [g_movieFileOutput stopRecording];
379
+ }
380
+ if (g_captureSession) {
381
+ [g_captureSession stopRunning];
382
+ }
383
+
384
+ // Try to stop ScreenCaptureKit if it's being used
385
+ if (@available(macOS 12.3, *)) {
386
+ [ScreenCaptureKitRecorder stopRecording];
387
+ }
335
388
 
336
389
  g_isRecording = false;
337
390
  return Napi::Boolean::New(env, true);
@@ -0,0 +1,186 @@
1
+ #import "screen_capture_kit.h"
2
+
3
+ // Global state
4
+ static SCStream *g_stream = nil;
5
+ static id<SCStreamDelegate> g_streamDelegate = nil;
6
+ static BOOL g_isRecording = NO;
7
+
8
+ @interface ScreenCaptureKitRecorderDelegate : NSObject <SCStreamDelegate>
9
+ @property (nonatomic, copy) void (^completionHandler)(NSURL *outputURL, NSError *error);
10
+ @end
11
+
12
+ @implementation ScreenCaptureKitRecorderDelegate
13
+ - (void)stream:(SCStream *)stream didStopWithError:(NSError *)error {
14
+ NSLog(@"ScreenCaptureKit recording stopped with error: %@", error);
15
+ }
16
+ @end
17
+
18
+ @implementation ScreenCaptureKitRecorder
19
+
20
+ + (BOOL)isScreenCaptureKitAvailable {
21
+ if (@available(macOS 12.3, *)) {
22
+ return YES;
23
+ }
24
+ return NO;
25
+ }
26
+
27
+ + (BOOL)startRecordingWithConfiguration:(NSDictionary *)config
28
+ delegate:(id)delegate
29
+ error:(NSError **)error {
30
+
31
+ if (@available(macOS 12.3, *)) {
32
+ @try {
33
+ // Get current app PID to exclude overlay windows
34
+ NSRunningApplication *currentApp = [NSRunningApplication currentApplication];
35
+ pid_t currentPID = currentApp.processIdentifier;
36
+
37
+ // Get all shareable content synchronously for immediate response
38
+ dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
39
+ __block BOOL success = NO;
40
+ __block NSError *contentError = nil;
41
+
42
+ [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *error) {
43
+ if (error) {
44
+ NSLog(@"Failed to get shareable content: %@", error);
45
+ contentError = error;
46
+ dispatch_semaphore_signal(semaphore);
47
+ return;
48
+ }
49
+
50
+ // Find display to record
51
+ SCDisplay *targetDisplay = content.displays.firstObject; // Default to first display
52
+ if (config[@"displayId"]) {
53
+ CGDirectDisplayID displayID = [config[@"displayId"] unsignedIntValue];
54
+ for (SCDisplay *display in content.displays) {
55
+ if (display.displayID == displayID) {
56
+ targetDisplay = display;
57
+ break;
58
+ }
59
+ }
60
+ }
61
+
62
+ // Get current process and Electron app windows to exclude
63
+ NSMutableArray *excludedWindows = [NSMutableArray array];
64
+ NSMutableArray *excludedApps = [NSMutableArray array];
65
+
66
+ // Exclude current Node.js process windows (overlay selectors)
67
+ for (SCWindow *window in content.windows) {
68
+ if (window.owningApplication.processID == currentPID) {
69
+ [excludedWindows addObject:window];
70
+ NSLog(@"🚫 Excluding Node.js overlay window: %@ (PID: %d)", window.title, currentPID);
71
+ }
72
+ }
73
+
74
+ // Also try to exclude Electron app if running (common overlay use case)
75
+ for (SCWindow *window in content.windows) {
76
+ NSString *appName = window.owningApplication.applicationName;
77
+ if ([appName containsString:@"Electron"] ||
78
+ [appName isEqualToString:@"electron"] ||
79
+ [window.title containsString:@"Electron"]) {
80
+ [excludedWindows addObject:window];
81
+ NSLog(@"🚫 Excluding Electron window: %@ from %@", window.title, appName);
82
+ }
83
+ }
84
+
85
+ NSLog(@"📊 Total windows to exclude: %lu", (unsigned long)excludedWindows.count);
86
+
87
+ // Create content filter - exclude overlay windows from recording
88
+ SCContentFilter *filter = [[SCContentFilter alloc]
89
+ initWithDisplay:targetDisplay
90
+ excludingWindows:excludedWindows];
91
+ NSLog(@"🎯 Using window-level exclusion for overlay prevention");
92
+
93
+ // Create stream configuration
94
+ SCStreamConfiguration *streamConfig = [[SCStreamConfiguration alloc] init];
95
+
96
+ // Handle capture area if specified
97
+ if (config[@"captureRect"]) {
98
+ NSDictionary *rect = config[@"captureRect"];
99
+ streamConfig.width = [rect[@"width"] integerValue];
100
+ streamConfig.height = [rect[@"height"] integerValue];
101
+ // Note: ScreenCaptureKit crop rect would need additional handling
102
+ } else {
103
+ streamConfig.width = (NSInteger)targetDisplay.width;
104
+ streamConfig.height = (NSInteger)targetDisplay.height;
105
+ }
106
+
107
+ streamConfig.minimumFrameInterval = CMTimeMake(1, 60); // 60 FPS
108
+ streamConfig.queueDepth = 5;
109
+ streamConfig.showsCursor = [config[@"captureCursor"] boolValue];
110
+ streamConfig.capturesAudio = [config[@"includeSystemAudio"] boolValue];
111
+
112
+ // Create delegate
113
+ g_streamDelegate = [[ScreenCaptureKitRecorderDelegate alloc] init];
114
+
115
+ // Create and start stream
116
+ g_stream = [[SCStream alloc] initWithFilter:filter
117
+ configuration:streamConfig
118
+ delegate:g_streamDelegate];
119
+
120
+ [g_stream startCaptureWithCompletionHandler:^(NSError *streamError) {
121
+ if (streamError) {
122
+ NSLog(@"❌ Failed to start ScreenCaptureKit recording: %@", streamError);
123
+ contentError = streamError;
124
+ g_isRecording = NO;
125
+ } else {
126
+ NSLog(@"✅ ScreenCaptureKit recording started successfully (excluding %lu overlay windows)", (unsigned long)excludedWindows.count);
127
+ g_isRecording = YES;
128
+ success = YES;
129
+ }
130
+ dispatch_semaphore_signal(semaphore);
131
+ }];
132
+ }];
133
+
134
+ // Wait for completion (with timeout)
135
+ dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC);
136
+ if (dispatch_semaphore_wait(semaphore, timeout) == 0) {
137
+ if (contentError && error) {
138
+ *error = contentError;
139
+ }
140
+ return success;
141
+ } else {
142
+ NSLog(@"⏰ ScreenCaptureKit initialization timeout");
143
+ if (error) {
144
+ *error = [NSError errorWithDomain:@"ScreenCaptureKitError"
145
+ code:-2
146
+ userInfo:@{NSLocalizedDescriptionKey: @"Initialization timeout"}];
147
+ }
148
+ return NO;
149
+ }
150
+
151
+ } @catch (NSException *exception) {
152
+ NSLog(@"ScreenCaptureKit recording exception: %@", exception);
153
+ if (error) {
154
+ *error = [NSError errorWithDomain:@"ScreenCaptureKitError"
155
+ code:-1
156
+ userInfo:@{NSLocalizedDescriptionKey: exception.reason}];
157
+ }
158
+ return NO;
159
+ }
160
+ }
161
+
162
+ return NO;
163
+ }
164
+
165
+ + (void)stopRecording {
166
+ if (@available(macOS 12.3, *)) {
167
+ if (g_stream && g_isRecording) {
168
+ [g_stream stopCaptureWithCompletionHandler:^(NSError *error) {
169
+ if (error) {
170
+ NSLog(@"Error stopping ScreenCaptureKit recording: %@", error);
171
+ } else {
172
+ NSLog(@"ScreenCaptureKit recording stopped successfully");
173
+ }
174
+ g_isRecording = NO;
175
+ g_stream = nil;
176
+ g_streamDelegate = nil;
177
+ }];
178
+ }
179
+ }
180
+ }
181
+
182
+ + (BOOL)isRecording {
183
+ return g_isRecording;
184
+ }
185
+
186
+ @end
@@ -15,6 +15,23 @@ static NSButton *g_selectButton = nil;
15
15
  static NSTimer *g_trackingTimer = nil;
16
16
  static NSDictionary *g_selectedWindowInfo = nil;
17
17
  static NSMutableArray *g_allWindows = nil;
18
+
19
+ // Functions to hide/show main overlay window during recording
20
+ void hideAllOverlayWindows() {
21
+ if (g_overlayWindow && [g_overlayWindow isVisible]) {
22
+ [g_overlayWindow setAlphaValue:0.0];
23
+ [g_overlayWindow orderOut:nil];
24
+ NSLog(@"🫥 Hidden main overlay window for recording");
25
+ }
26
+ }
27
+
28
+ void showAllOverlayWindows() {
29
+ if (g_overlayWindow) {
30
+ [g_overlayWindow setAlphaValue:1.0];
31
+ [g_overlayWindow orderFront:nil];
32
+ NSLog(@"👁️ Restored main overlay window after recording");
33
+ }
34
+ }
18
35
  static NSDictionary *g_currentWindowUnderCursor = nil;
19
36
  static bool g_bringToFrontEnabled = false; // Default disabled for overlay-only highlighting
20
37
  static bool g_hasToggledWindow = false; // Track if any window is currently toggled