node-mac-recorder 2.10.40 → 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 +1 -1
- package/src/mac_recorder.mm +44 -0
- package/src/screen_capture_kit.mm +169 -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 (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
|