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 +1 -1
- package/src/mac_recorder.mm +55 -2
- package/src/screen_capture_kit.mm +186 -0
- package/src/window_selector.mm +17 -0
package/package.json
CHANGED
package/src/mac_recorder.mm
CHANGED
|
@@ -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
|
-
|
|
334
|
-
|
|
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
|
package/src/window_selector.mm
CHANGED
|
@@ -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
|