node-mac-recorder 2.0.2 → 2.0.3

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.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
Binary file
@@ -5,6 +5,9 @@
5
5
  static SCStream *g_scStream = nil;
6
6
  static SCRecordingOutput *g_scRecordingOutput = nil;
7
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;
8
11
 
9
12
  @interface ScreenCaptureKitRecorder () <SCRecordingOutputDelegate>
10
13
  @end
@@ -110,25 +113,41 @@ static NSURL *g_outputURL = nil;
110
113
  }
111
114
  }
112
115
 
116
+ // Keep strong references to excluded items for the lifetime of the stream
117
+ g_appsToExclude = [appsToExclude copy];
118
+ g_windowsToExclude = [windowsToExclude copy];
119
+
113
120
  SCContentFilter *filter = nil;
114
121
  if (appsToExclude.count > 0) {
115
122
  if (@available(macOS 13.0, *)) {
116
- filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingApplications:appsToExclude exceptingWindows:windowsToExclude];
123
+ filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingApplications:(g_appsToExclude ?: @[]) exceptingWindows:(g_windowsToExclude ?: @[])];
117
124
  }
118
125
  }
119
126
  if (!filter) {
120
127
  if (@available(macOS 13.0, *)) {
121
- filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:windowsToExclude];
128
+ filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:(g_windowsToExclude ?: @[])];
122
129
  }
123
130
  }
124
131
  if (!filter) { return NO; }
125
132
 
126
133
  SCStreamConfiguration *cfg = [[SCStreamConfiguration alloc] init];
127
134
  if (captureArea && captureArea[@"width"] && captureArea[@"height"]) {
128
- CGRect src = CGRectMake([captureArea[@"x"] doubleValue],
129
- [captureArea[@"y"] doubleValue],
130
- [captureArea[@"width"] doubleValue],
131
- [captureArea[@"height"] doubleValue]);
135
+ CGRect displayFrame = targetDisplay.frame;
136
+ double x = [captureArea[@"x"] doubleValue];
137
+ double yBottom = [captureArea[@"y"] doubleValue];
138
+ double w = [captureArea[@"width"] doubleValue];
139
+ double h = [captureArea[@"height"] doubleValue];
140
+
141
+ // Convert bottom-left origin (used by legacy path) to top-left for SC
142
+ double y = displayFrame.size.height - yBottom - h;
143
+
144
+ // Clamp to display bounds to avoid invalid sourceRect
145
+ if (w < 1) w = 1; if (h < 1) h = 1;
146
+ if (x < 0) x = 0; if (y < 0) y = 0;
147
+ if (x + w > displayFrame.size.width) w = MAX(1, displayFrame.size.width - x);
148
+ if (y + h > displayFrame.size.height) h = MAX(1, displayFrame.size.height - y);
149
+
150
+ CGRect src = CGRectMake(x, y, w, h);
132
151
  cfg.sourceRect = src;
133
152
  cfg.width = (int)src.size.width;
134
153
  cfg.height = (int)src.size.height;
@@ -139,38 +158,63 @@ static NSURL *g_outputURL = nil;
139
158
  }
140
159
  cfg.showsCursor = captureCursorNum.boolValue;
141
160
  if (includeMicNum || includeSystemAudioNum) {
142
- cfg.capturesAudio = YES;
161
+ if (@available(macOS 13.0, *)) {
162
+ cfg.capturesAudio = YES;
163
+ }
143
164
  }
144
165
 
145
- g_scStream = [[SCStream alloc] initWithFilter:filter configuration:cfg delegate:nil];
146
-
147
- if (@available(macOS 14.0, *)) {
148
- SCRecordingOutputConfiguration *recCfg = [[SCRecordingOutputConfiguration alloc] init];
149
- g_outputURL = [NSURL fileURLWithPath:outputPath];
150
- recCfg.outputURL = g_outputURL;
151
- recCfg.outputFileType = AVFileTypeQuickTimeMovie;
152
- g_scRecordingOutput = [[SCRecordingOutput alloc] initWithConfiguration:recCfg delegate:(id<SCRecordingOutputDelegate>)delegate ?: (id<SCRecordingOutputDelegate>)self];
153
- NSError *addErr = nil;
154
- BOOL added = [g_scStream addRecordingOutput:g_scRecordingOutput error:&addErr];
155
- if (!added) {
156
- if (error) { *error = addErr ?: [NSError errorWithDomain:@"ScreenCaptureKitRecorder" code:-3 userInfo:nil]; }
157
- g_scRecordingOutput = nil; g_scStream = nil; g_outputURL = nil;
158
- return NO;
159
- }
166
+ __block NSError *startErr = nil;
167
+ __block BOOL startedOK = NO;
168
+ dispatch_semaphore_t startSem = dispatch_semaphore_create(0);
169
+ dispatch_async(dispatch_get_main_queue(), ^{
170
+ if (@available(macOS 15.0, *)) {
171
+ g_scStream = [[SCStream alloc] initWithFilter:filter configuration:cfg delegate:nil];
172
+
173
+ SCRecordingOutputConfiguration *recCfg = [[SCRecordingOutputConfiguration alloc] init];
174
+ g_outputURL = [NSURL fileURLWithPath:outputPath];
175
+ recCfg.outputURL = g_outputURL;
176
+ recCfg.outputFileType = AVFileTypeQuickTimeMovie;
177
+
178
+ id<SCRecordingOutputDelegate> delegateObject = (id<SCRecordingOutputDelegate>)delegate;
179
+ if (!delegateObject) {
180
+ if (!g_scDelegate) {
181
+ g_scDelegate = [[ScreenCaptureKitRecorder alloc] init];
182
+ }
183
+ delegateObject = (id<SCRecordingOutputDelegate>)g_scDelegate;
184
+ }
185
+ g_scRecordingOutput = [[SCRecordingOutput alloc] initWithConfiguration:recCfg delegate:delegateObject];
186
+
187
+ NSError *addErr = nil;
188
+ BOOL added = [g_scStream addRecordingOutput:g_scRecordingOutput error:&addErr];
189
+ if (!added) {
190
+ startErr = addErr ?: [NSError errorWithDomain:@"ScreenCaptureKitRecorder" code:-3 userInfo:nil];
191
+ g_scRecordingOutput = nil; g_scStream = nil; g_outputURL = nil;
192
+ dispatch_semaphore_signal(startSem);
193
+ return;
194
+ }
160
195
 
161
- __block NSError *startErr = nil;
162
- dispatch_semaphore_t startSem = dispatch_semaphore_create(0);
163
- [g_scStream startCaptureWithCompletionHandler:^(NSError * _Nullable err) {
164
- startErr = err;
196
+ [g_scStream startCaptureWithCompletionHandler:^(NSError * _Nullable err) {
197
+ startErr = err;
198
+ startedOK = (err == nil);
199
+ dispatch_semaphore_signal(startSem);
200
+ }];
201
+ } else {
202
+ startErr = [NSError errorWithDomain:@"ScreenCaptureKitRecorder" code:-4 userInfo:@{NSLocalizedDescriptionKey: @"Requires macOS 15+"}];
165
203
  dispatch_semaphore_signal(startSem);
166
- }];
167
- dispatch_semaphore_wait(startSem, DISPATCH_TIME_FOREVER);
168
- if (startErr) {
169
- if (error) { *error = startErr; }
170
- [g_scStream removeRecordingOutput:g_scRecordingOutput error:nil];
171
- g_scRecordingOutput = nil; g_scStream = nil; g_outputURL = nil;
172
- return NO;
173
204
  }
205
+ });
206
+ dispatch_semaphore_wait(startSem, DISPATCH_TIME_FOREVER);
207
+ if (startErr) {
208
+ if (error) { *error = startErr; }
209
+ if (g_scRecordingOutput && g_scStream) {
210
+ if (@available(macOS 15.0, *)) {
211
+ [g_scStream removeRecordingOutput:g_scRecordingOutput error:nil];
212
+ }
213
+ }
214
+ g_scRecordingOutput = nil; g_scStream = nil; g_outputURL = nil;
215
+ return NO;
216
+ }
217
+ if (startedOK) {
174
218
  return YES;
175
219
  }
176
220