node-mac-recorder 2.1.3 → 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/.claude/settings.local.json +6 -1
- package/README.md +17 -79
- package/backup/binding.gyp +44 -0
- package/backup/src/audio_capture.mm +116 -0
- package/backup/src/cursor_tracker.mm +518 -0
- package/backup/src/mac_recorder.mm +829 -0
- package/backup/src/screen_capture.h +19 -0
- package/backup/src/screen_capture.mm +162 -0
- package/backup/src/screen_capture_kit.h +15 -0
- package/backup/src/window_selector.mm +1457 -0
- package/binding.gyp +15 -10
- package/index.js +22 -62
- package/package.json +3 -8
- package/src/audio_capture.mm +96 -40
- package/src/cursor_tracker.mm +3 -4
- package/src/mac_recorder.mm +604 -715
- package/src/screen_capture.h +5 -0
- package/src/screen_capture.mm +141 -60
- package/src/window_selector.mm +12 -22
- package/test-api-compatibility.js +92 -0
- package/test-audio.js +94 -0
- package/test-comprehensive.js +164 -0
- package/test-recording.js +142 -0
- package/test-sck-simple.js +37 -0
- package/test-sck.js +50 -44
- package/test-sync.js +52 -0
- package/test-windows.js +57 -0
- package/prebuilds/darwin-arm64/node.napi.node +0 -0
- package/prebuilds/darwin-x64/node.napi.node +0 -0
- package/scripts/test-exclude.js +0 -72
- package/src/screen_capture_kit.mm +0 -222
|
@@ -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
|
-
|