node-mac-recorder 2.10.39 → 2.11.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.39",
3
+ "version": "2.11.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 (will exclude overlay windows automatically)
187
+ NSError *sckError = nil;
188
+ if ([ScreenCaptureKitRecorder startRecordingWithConfiguration:sckConfig
189
+ delegate:g_delegate
190
+ error:&sckError]) {
191
+ NSLog(@"✅ ScreenCaptureKit recording started with automatic overlay 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 {
@@ -0,0 +1,169 @@
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 app windows to exclude
63
+ NSMutableArray *excludedWindows = [NSMutableArray array];
64
+ for (SCWindow *window in content.windows) {
65
+ if (window.owningApplication.processID == currentPID) {
66
+ [excludedWindows addObject:window];
67
+ NSLog(@"🚫 Excluding overlay window: %@ (PID: %d)", window.title, currentPID);
68
+ }
69
+ }
70
+
71
+ // Create content filter - exclude current app windows
72
+ SCContentFilter *filter = [[SCContentFilter alloc]
73
+ initWithDisplay:targetDisplay
74
+ excludingWindows:excludedWindows];
75
+
76
+ // Create stream configuration
77
+ SCStreamConfiguration *streamConfig = [[SCStreamConfiguration alloc] init];
78
+
79
+ // Handle capture area if specified
80
+ if (config[@"captureRect"]) {
81
+ NSDictionary *rect = config[@"captureRect"];
82
+ streamConfig.width = [rect[@"width"] integerValue];
83
+ streamConfig.height = [rect[@"height"] integerValue];
84
+ // Note: ScreenCaptureKit crop rect would need additional handling
85
+ } else {
86
+ streamConfig.width = (NSInteger)targetDisplay.width;
87
+ streamConfig.height = (NSInteger)targetDisplay.height;
88
+ }
89
+
90
+ streamConfig.minimumFrameInterval = CMTimeMake(1, 60); // 60 FPS
91
+ streamConfig.queueDepth = 5;
92
+ streamConfig.showsCursor = [config[@"captureCursor"] boolValue];
93
+ streamConfig.capturesAudio = [config[@"includeSystemAudio"] boolValue];
94
+
95
+ // Create delegate
96
+ g_streamDelegate = [[ScreenCaptureKitRecorderDelegate alloc] init];
97
+
98
+ // Create and start stream
99
+ g_stream = [[SCStream alloc] initWithFilter:filter
100
+ configuration:streamConfig
101
+ delegate:g_streamDelegate];
102
+
103
+ [g_stream startCaptureWithCompletionHandler:^(NSError *streamError) {
104
+ if (streamError) {
105
+ NSLog(@"❌ Failed to start ScreenCaptureKit recording: %@", streamError);
106
+ contentError = streamError;
107
+ g_isRecording = NO;
108
+ } else {
109
+ NSLog(@"✅ ScreenCaptureKit recording started successfully (excluding %lu overlay windows)", (unsigned long)excludedWindows.count);
110
+ g_isRecording = YES;
111
+ success = YES;
112
+ }
113
+ dispatch_semaphore_signal(semaphore);
114
+ }];
115
+ }];
116
+
117
+ // Wait for completion (with timeout)
118
+ dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC);
119
+ if (dispatch_semaphore_wait(semaphore, timeout) == 0) {
120
+ if (contentError && error) {
121
+ *error = contentError;
122
+ }
123
+ return success;
124
+ } else {
125
+ NSLog(@"⏰ ScreenCaptureKit initialization timeout");
126
+ if (error) {
127
+ *error = [NSError errorWithDomain:@"ScreenCaptureKitError"
128
+ code:-2
129
+ userInfo:@{NSLocalizedDescriptionKey: @"Initialization timeout"}];
130
+ }
131
+ return NO;
132
+ }
133
+
134
+ } @catch (NSException *exception) {
135
+ NSLog(@"ScreenCaptureKit recording exception: %@", exception);
136
+ if (error) {
137
+ *error = [NSError errorWithDomain:@"ScreenCaptureKitError"
138
+ code:-1
139
+ userInfo:@{NSLocalizedDescriptionKey: exception.reason}];
140
+ }
141
+ return NO;
142
+ }
143
+ }
144
+
145
+ return NO;
146
+ }
147
+
148
+ + (void)stopRecording {
149
+ if (@available(macOS 12.3, *)) {
150
+ if (g_stream && g_isRecording) {
151
+ [g_stream stopCaptureWithCompletionHandler:^(NSError *error) {
152
+ if (error) {
153
+ NSLog(@"Error stopping ScreenCaptureKit recording: %@", error);
154
+ } else {
155
+ NSLog(@"ScreenCaptureKit recording stopped successfully");
156
+ }
157
+ g_isRecording = NO;
158
+ g_stream = nil;
159
+ g_streamDelegate = nil;
160
+ }];
161
+ }
162
+ }
163
+ }
164
+
165
+ + (BOOL)isRecording {
166
+ return g_isRecording;
167
+ }
168
+
169
+ @end
@@ -1428,7 +1428,7 @@ bool startScreenSelection() {
1428
1428
 
1429
1429
  // Create select button with more padding and hover effects
1430
1430
  NSButton *selectButton = [[HoverButton alloc] initWithFrame:NSMakeRect(0, 0, 200, 60)];
1431
- [selectButton setTitle:@"Start Record"];
1431
+ [selectButton setTitle:@"Start Record"];
1432
1432
  [selectButton setButtonType:NSButtonTypeMomentaryPushIn];
1433
1433
  [selectButton setBordered:NO];
1434
1434
  [selectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightRegular]];