node-mac-recorder 1.17.1 → 2.0.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/binding.gyp CHANGED
@@ -5,6 +5,7 @@
5
5
  "sources": [
6
6
  "src/mac_recorder.mm",
7
7
  "src/screen_capture.mm",
8
+ "src/screen_capture_kit.mm",
8
9
  "src/audio_capture.mm",
9
10
  "src/cursor_tracker.mm",
10
11
  "src/window_selector.mm"
package/index.js CHANGED
@@ -2,19 +2,24 @@ const { EventEmitter } = require("events");
2
2
  const path = require("path");
3
3
  const fs = require("fs");
4
4
 
5
- // Native modülü yükle
5
+ // Native modülü yükle (prebuilt > local build fallback)
6
6
  let nativeBinding;
7
7
  try {
8
- nativeBinding = require("./build/Release/mac_recorder.node");
9
- } catch (error) {
8
+ // Prebuilt
9
+ nativeBinding = require("node-gyp-build")(__dirname);
10
+ } catch (e) {
10
11
  try {
11
- nativeBinding = require("./build/Debug/mac_recorder.node");
12
- } catch (debugError) {
13
- throw new Error(
14
- 'Native module not found. Please run "npm run build" to compile the native module.\n' +
15
- "Original error: " +
16
- error.message
17
- );
12
+ nativeBinding = require("./build/Release/mac_recorder.node");
13
+ } catch (error) {
14
+ try {
15
+ nativeBinding = require("./build/Debug/mac_recorder.node");
16
+ } catch (debugError) {
17
+ throw new Error(
18
+ 'Native module not found. Please run "npm run build" to compile the native module.\n' +
19
+ "Original error: " +
20
+ (error?.message || e?.message)
21
+ );
22
+ }
18
23
  }
19
24
  }
20
25
 
@@ -45,6 +50,12 @@ class MacRecorder extends EventEmitter {
45
50
  showClicks: false,
46
51
  displayId: null, // Hangi ekranı kaydedeceği (null = ana ekran)
47
52
  windowId: null, // Hangi pencereyi kaydedeceği (null = tam ekran)
53
+ // SC (gizli, opsiyonel) - mevcut kullanıcıları bozmaz
54
+ useScreenCaptureKit: false,
55
+ excludedAppBundleIds: [],
56
+ excludedPIDs: [],
57
+ excludedWindowIds: [],
58
+ autoExcludeSelf: !!(process.versions && process.versions.electron),
48
59
  };
49
60
 
50
61
  // Display cache için async initialization
@@ -118,6 +129,14 @@ class MacRecorder extends EventEmitter {
118
129
  audioDeviceId: options.audioDeviceId || null, // null = default device
119
130
  systemAudioDeviceId: options.systemAudioDeviceId || null, // null = auto-detect system audio device
120
131
  captureArea: options.captureArea || null,
132
+ useScreenCaptureKit: options.useScreenCaptureKit || false,
133
+ excludedAppBundleIds: options.excludedAppBundleIds || [],
134
+ excludedPIDs: options.excludedPIDs || [],
135
+ excludedWindowIds: options.excludedWindowIds || [],
136
+ autoExcludeSelf:
137
+ typeof options.autoExcludeSelf === "boolean"
138
+ ? options.autoExcludeSelf
139
+ : !!(process.versions && process.versions.electron),
121
140
  };
122
141
  }
123
142
 
@@ -167,11 +186,12 @@ class MacRecorder extends EventEmitter {
167
186
  targetDisplayId = display.id; // Use actual display ID, not array index
168
187
  // Koordinatları display'e göre normalize et
169
188
  adjustedX = targetWindow.x - display.x;
170
-
189
+
171
190
  // Y coordinate conversion: CGWindow (top-left) to AVFoundation (bottom-left)
172
191
  // Overlay'deki dönüşümle aynı mantık: screenHeight - windowY - windowHeight
173
192
  const displayHeight = parseInt(display.resolution.split("x")[1]);
174
- const convertedY = displayHeight - targetWindow.y - targetWindow.height;
193
+ const convertedY =
194
+ displayHeight - targetWindow.y - targetWindow.height;
175
195
  adjustedY = Math.max(0, convertedY - display.y);
176
196
  break;
177
197
  }
@@ -206,7 +226,9 @@ class MacRecorder extends EventEmitter {
206
226
  this.options.displayId = targetDisplayId;
207
227
 
208
228
  // Recording için display bilgisini sakla (cursor capture için)
209
- const targetDisplay = displays.find(d => d.id === targetDisplayId);
229
+ const targetDisplay = displays.find(
230
+ (d) => d.id === targetDisplayId
231
+ );
210
232
  this.recordingDisplayInfo = {
211
233
  displayId: targetDisplayId,
212
234
  x: targetDisplay.x,
@@ -239,7 +261,9 @@ class MacRecorder extends EventEmitter {
239
261
  if (this.options.displayId !== null && !this.recordingDisplayInfo) {
240
262
  try {
241
263
  const displays = await this.getDisplays();
242
- const targetDisplay = displays.find(d => d.id === this.options.displayId);
264
+ const targetDisplay = displays.find(
265
+ (d) => d.id === this.options.displayId
266
+ );
243
267
  if (targetDisplay) {
244
268
  this.recordingDisplayInfo = {
245
269
  displayId: this.options.displayId,
@@ -273,6 +297,11 @@ class MacRecorder extends EventEmitter {
273
297
  windowId: this.options.windowId || null, // null = tam ekran
274
298
  audioDeviceId: this.options.audioDeviceId || null, // null = default device
275
299
  systemAudioDeviceId: this.options.systemAudioDeviceId || null, // null = auto-detect system audio device
300
+ useScreenCaptureKit: this.options.useScreenCaptureKit || false,
301
+ excludedAppBundleIds: this.options.excludedAppBundleIds || [],
302
+ excludedPIDs: this.options.excludedPIDs || [],
303
+ excludedWindowIds: this.options.excludedWindowIds || [],
304
+ autoExcludeSelf: this.options.autoExcludeSelf === true,
276
305
  };
277
306
 
278
307
  // Manuel captureArea varsa onu kullan
@@ -285,10 +314,34 @@ class MacRecorder extends EventEmitter {
285
314
  };
286
315
  }
287
316
 
288
- const success = nativeBinding.startRecording(
289
- outputPath,
290
- recordingOptions
291
- );
317
+ // SC yolu: kullanıcıdan SC talebi varsa veya exclude listeleri doluysa ve SC mevcutsa otomatik kullan
318
+ let success;
319
+ try {
320
+ const wantsSC = !!(
321
+ this.options.useScreenCaptureKit ||
322
+ this.options.excludedAppBundleIds?.length ||
323
+ this.options.excludedPIDs?.length ||
324
+ this.options.excludedWindowIds?.length
325
+ );
326
+ const scAvailable =
327
+ typeof nativeBinding.isScreenCaptureKitAvailable === "function" &&
328
+ nativeBinding.isScreenCaptureKitAvailable();
329
+ if (wantsSC && scAvailable) {
330
+ const scOptions = {
331
+ ...recordingOptions,
332
+ useScreenCaptureKit: true,
333
+ };
334
+ success = nativeBinding.startRecording(outputPath, scOptions);
335
+ } else {
336
+ success = nativeBinding.startRecording(
337
+ outputPath,
338
+ recordingOptions
339
+ );
340
+ }
341
+ } catch (e) {
342
+ // Fallback AVFoundation
343
+ success = nativeBinding.startRecording(outputPath, recordingOptions);
344
+ }
292
345
 
293
346
  if (success) {
294
347
  this.isRecording = true;
@@ -310,13 +363,13 @@ class MacRecorder extends EventEmitter {
310
363
  if (nativeStatus && !recordingStartedEmitted) {
311
364
  recordingStartedEmitted = true;
312
365
  clearInterval(checkRecordingStatus);
313
-
366
+
314
367
  // Kayıt gerçekten başladığı anda event emit et
315
368
  this.emit("recordingStarted", {
316
369
  outputPath: this.outputPath,
317
370
  timestamp: Date.now(), // Gerçek başlangıç zamanı
318
371
  options: this.options,
319
- nativeConfirmed: true
372
+ nativeConfirmed: true,
320
373
  });
321
374
  }
322
375
  } catch (error) {
@@ -328,12 +381,12 @@ class MacRecorder extends EventEmitter {
328
381
  outputPath: this.outputPath,
329
382
  timestamp: this.recordingStartTime,
330
383
  options: this.options,
331
- nativeConfirmed: false
384
+ nativeConfirmed: false,
332
385
  });
333
386
  }
334
387
  }
335
388
  }, 50); // Her 50ms kontrol et
336
-
389
+
337
390
  // Timeout fallback - 5 saniye sonra hala başlamamışsa emit et
338
391
  setTimeout(() => {
339
392
  if (!recordingStartedEmitted) {
@@ -343,11 +396,11 @@ class MacRecorder extends EventEmitter {
343
396
  outputPath: this.outputPath,
344
397
  timestamp: this.recordingStartTime,
345
398
  options: this.options,
346
- nativeConfirmed: false
399
+ nativeConfirmed: false,
347
400
  });
348
401
  }
349
402
  }, 5000);
350
-
403
+
351
404
  this.emit("started", this.outputPath);
352
405
  resolve(this.outputPath);
353
406
  } else {
@@ -588,7 +641,7 @@ class MacRecorder extends EventEmitter {
588
641
  width: options.windowInfo.width,
589
642
  height: options.windowInfo.height,
590
643
  windowRelative: true,
591
- windowInfo: options.windowInfo
644
+ windowInfo: options.windowInfo,
592
645
  };
593
646
  } else if (this.recordingDisplayInfo) {
594
647
  // Recording başlatılmışsa o display'i kullan
@@ -644,7 +697,7 @@ class MacRecorder extends EventEmitter {
644
697
  if (this.cursorDisplayInfo.windowRelative) {
645
698
  // Window-relative koordinatlar
646
699
  coordinateSystem = "window-relative";
647
-
700
+
648
701
  // Window bounds kontrolü - cursor window dışındaysa kaydetme
649
702
  if (
650
703
  x < 0 ||
@@ -657,7 +710,7 @@ class MacRecorder extends EventEmitter {
657
710
  } else {
658
711
  // Display-relative koordinatlar
659
712
  coordinateSystem = "display-relative";
660
-
713
+
661
714
  // Display bounds kontrolü
662
715
  if (
663
716
  x < 0 ||
@@ -682,9 +735,9 @@ class MacRecorder extends EventEmitter {
682
735
  windowInfo: {
683
736
  width: this.cursorDisplayInfo.width,
684
737
  height: this.cursorDisplayInfo.height,
685
- originalWindow: this.cursorDisplayInfo.windowInfo
686
- }
687
- })
738
+ originalWindow: this.cursorDisplayInfo.windowInfo,
739
+ },
740
+ }),
688
741
  };
689
742
 
690
743
  // Sadece eventType değiştiğinde veya pozisyon değiştiğinde kaydet
@@ -931,6 +984,6 @@ class MacRecorder extends EventEmitter {
931
984
  }
932
985
 
933
986
  // WindowSelector modülünü de export edelim
934
- MacRecorder.WindowSelector = require('./window-selector');
987
+ MacRecorder.WindowSelector = require("./window-selector");
935
988
 
936
989
  module.exports = MacRecorder;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "1.17.1",
3
+ "version": "2.0.0",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -38,14 +38,19 @@
38
38
  "build": "node-gyp build",
39
39
  "rebuild": "node-gyp rebuild",
40
40
  "clean": "node-gyp clean",
41
+ "prebuild:darwin:arm64": "prebuildify --napi --strip --platform darwin --arch arm64",
42
+ "prebuild:darwin:x64": "prebuildify --napi --strip --platform darwin --arch x64",
43
+ "prebuild": "npm run prebuild:darwin:arm64 && npm run prebuild:darwin:x64",
41
44
  "test:window-selector": "node window-selector-test.js",
42
45
  "example:window-selector": "node examples/window-selector-example.js"
43
46
  },
44
47
  "dependencies": {
45
- "node-addon-api": "^7.0.0"
48
+ "node-addon-api": "^7.0.0",
49
+ "node-gyp-build": "^4.6.0"
46
50
  },
47
51
  "devDependencies": {
48
- "node-gyp": "^10.0.0"
52
+ "node-gyp": "^10.0.0",
53
+ "prebuildify": "^5.0.0"
49
54
  },
50
55
  "gypfile": true
51
56
  }
@@ -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);
@@ -67,7 +68,7 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
67
68
 
68
69
  std::string outputPath = info[0].As<Napi::String>().Utf8Value();
69
70
 
70
- // Options parsing
71
+ // Options parsing (shared)
71
72
  CGRect captureRect = CGRectNull;
72
73
  bool captureCursor = false; // Default olarak cursor gizli
73
74
  bool includeMicrophone = false; // Default olarak mikrofon kapalı
@@ -75,6 +76,12 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
75
76
  CGDirectDisplayID displayID = CGMainDisplayID(); // Default ana ekran
76
77
  NSString *audioDeviceId = nil; // Default audio device ID
77
78
  NSString *systemAudioDeviceId = nil; // System audio device ID
79
+ bool forceUseSC = false;
80
+ // Exclude options for ScreenCaptureKit (optional, backward compatible)
81
+ NSMutableArray<NSString*> *excludedAppBundleIds = [NSMutableArray array];
82
+ NSMutableArray<NSNumber*> *excludedPIDs = [NSMutableArray array];
83
+ NSMutableArray<NSNumber*> *excludedWindowIds = [NSMutableArray array];
84
+ bool autoExcludeSelf = false;
78
85
 
79
86
  if (info.Length() > 1 && info[1].IsObject()) {
80
87
  Napi::Object options = info[1].As<Napi::Object>();
@@ -118,6 +125,43 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
118
125
  std::string sysDeviceId = options.Get("systemAudioDeviceId").As<Napi::String>().Utf8Value();
119
126
  systemAudioDeviceId = [NSString stringWithUTF8String:sysDeviceId.c_str()];
120
127
  }
128
+
129
+ // ScreenCaptureKit toggle (optional)
130
+ if (options.Has("useScreenCaptureKit")) {
131
+ forceUseSC = options.Get("useScreenCaptureKit").As<Napi::Boolean>();
132
+ }
133
+
134
+ // Exclusion lists (optional)
135
+ if (options.Has("excludedAppBundleIds") && options.Get("excludedAppBundleIds").IsArray()) {
136
+ Napi::Array arr = options.Get("excludedAppBundleIds").As<Napi::Array>();
137
+ for (uint32_t i = 0; i < arr.Length(); i++) {
138
+ if (!arr.Get(i).IsUndefined() && !arr.Get(i).IsNull()) {
139
+ std::string s = arr.Get(i).As<Napi::String>().Utf8Value();
140
+ [excludedAppBundleIds addObject:[NSString stringWithUTF8String:s.c_str()]];
141
+ }
142
+ }
143
+ }
144
+ if (options.Has("excludedPIDs") && options.Get("excludedPIDs").IsArray()) {
145
+ Napi::Array arr = options.Get("excludedPIDs").As<Napi::Array>();
146
+ for (uint32_t i = 0; i < arr.Length(); i++) {
147
+ if (!arr.Get(i).IsUndefined() && !arr.Get(i).IsNull()) {
148
+ double v = arr.Get(i).As<Napi::Number>().DoubleValue();
149
+ [excludedPIDs addObject:@( (pid_t)v )];
150
+ }
151
+ }
152
+ }
153
+ if (options.Has("excludedWindowIds") && options.Get("excludedWindowIds").IsArray()) {
154
+ Napi::Array arr = options.Get("excludedWindowIds").As<Napi::Array>();
155
+ for (uint32_t i = 0; i < arr.Length(); i++) {
156
+ if (!arr.Get(i).IsUndefined() && !arr.Get(i).IsNull()) {
157
+ double v = arr.Get(i).As<Napi::Number>().DoubleValue();
158
+ [excludedWindowIds addObject:@( (uint32_t)v )];
159
+ }
160
+ }
161
+ }
162
+ if (options.Has("autoExcludeSelf")) {
163
+ autoExcludeSelf = options.Get("autoExcludeSelf").As<Napi::Boolean>();
164
+ }
121
165
 
122
166
  // Display ID
123
167
  if (options.Has("displayId") && !options.Get("displayId").IsNull()) {
@@ -159,6 +203,43 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
159
203
  }
160
204
 
161
205
  @try {
206
+ // Prefer ScreenCaptureKit if requested or if exclusion lists provided and available
207
+ bool wantsSC = forceUseSC || excludedAppBundleIds.count > 0 || excludedPIDs.count > 0 || excludedWindowIds.count > 0 || autoExcludeSelf;
208
+ if (wantsSC && [ScreenCaptureKitRecorder isScreenCaptureKitAvailable]) {
209
+ NSMutableDictionary *scConfig = [@{} mutableCopy];
210
+ scConfig[@"displayId"] = @(displayID);
211
+ if (!CGRectIsNull(captureRect)) {
212
+ scConfig[@"captureArea"] = @{ @"x": @(captureRect.origin.x),
213
+ @"y": @(captureRect.origin.y),
214
+ @"width": @(captureRect.size.width),
215
+ @"height": @(captureRect.size.height) };
216
+ }
217
+ scConfig[@"captureCursor"] = @(captureCursor);
218
+ scConfig[@"includeMicrophone"] = @(includeMicrophone);
219
+ scConfig[@"includeSystemAudio"] = @(includeSystemAudio);
220
+ if (excludedAppBundleIds.count) scConfig[@"excludedAppBundleIds"] = excludedAppBundleIds;
221
+ if (excludedPIDs.count) scConfig[@"excludedPIDs"] = excludedPIDs;
222
+ if (excludedWindowIds.count) scConfig[@"excludedWindowIds"] = excludedWindowIds;
223
+ // Auto exclude current app by PID if requested
224
+ if (autoExcludeSelf) {
225
+ pid_t pid = getpid();
226
+ NSMutableArray *arr = [NSMutableArray arrayWithArray:scConfig[@"excludedPIDs"] ?: @[]];
227
+ [arr addObject:@(pid)];
228
+ scConfig[@"excludedPIDs"] = arr;
229
+ }
230
+
231
+ // Output path for SC
232
+ std::string outputPathStr = info[0].As<Napi::String>().Utf8Value();
233
+ scConfig[@"outputPath"] = [NSString stringWithUTF8String:outputPathStr.c_str()];
234
+
235
+ NSError *scErr = nil;
236
+ BOOL ok = [ScreenCaptureKitRecorder startRecordingWithConfiguration:scConfig delegate:nil error:&scErr];
237
+ if (ok) {
238
+ g_isRecording = true;
239
+ return Napi::Boolean::New(env, true);
240
+ }
241
+ // If SC failed, fall through to AVFoundation as fallback
242
+ }
162
243
  // Create capture session
163
244
  g_captureSession = [[AVCaptureSession alloc] init];
164
245
  [g_captureSession beginConfiguration];
@@ -325,14 +406,19 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
325
406
  Napi::Value StopRecording(const Napi::CallbackInfo& info) {
326
407
  Napi::Env env = info.Env();
327
408
 
328
- if (!g_isRecording || !g_movieFileOutput) {
409
+ if (!g_isRecording) {
329
410
  return Napi::Boolean::New(env, false);
330
411
  }
331
412
 
332
413
  @try {
333
- [g_movieFileOutput stopRecording];
334
- [g_captureSession stopRunning];
335
-
414
+ if (g_movieFileOutput) {
415
+ [g_movieFileOutput stopRecording];
416
+ [g_captureSession stopRunning];
417
+ g_isRecording = false;
418
+ return Napi::Boolean::New(env, true);
419
+ }
420
+ // Try ScreenCaptureKit stop
421
+ [ScreenCaptureKitRecorder stopRecording];
336
422
  g_isRecording = false;
337
423
  return Napi::Boolean::New(env, true);
338
424
 
@@ -812,6 +898,12 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
812
898
  exports.Set(Napi::String::New(env, "getWindows"), Napi::Function::New(env, GetWindows));
813
899
  exports.Set(Napi::String::New(env, "getRecordingStatus"), Napi::Function::New(env, GetRecordingStatus));
814
900
  exports.Set(Napi::String::New(env, "checkPermissions"), Napi::Function::New(env, CheckPermissions));
901
+ // ScreenCaptureKit availability (optional for clients)
902
+ exports.Set(Napi::String::New(env, "isScreenCaptureKitAvailable"), Napi::Function::New(env, [](const Napi::CallbackInfo& info){
903
+ Napi::Env env = info.Env();
904
+ bool available = [ScreenCaptureKitRecorder isScreenCaptureKitAvailable];
905
+ return Napi::Boolean::New(env, available);
906
+ }));
815
907
 
816
908
  // Thumbnail functions
817
909
  exports.Set(Napi::String::New(env, "getWindowThumbnail"), Napi::Function::New(env, GetWindowThumbnail));
@@ -0,0 +1,202 @@
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
+
9
+ @interface ScreenCaptureKitRecorder () <SCRecordingOutputDelegate>
10
+ @end
11
+
12
+ @implementation ScreenCaptureKitRecorder
13
+
14
+ + (BOOL)isScreenCaptureKitAvailable {
15
+ if (@available(macOS 14.0, *)) {
16
+ Class streamClass = NSClassFromString(@"SCStream");
17
+ Class recordingOutputClass = NSClassFromString(@"SCRecordingOutput");
18
+ return (streamClass != nil && recordingOutputClass != nil);
19
+ }
20
+ return NO;
21
+ }
22
+
23
+ + (BOOL)isRecording {
24
+ return g_scStream != nil;
25
+ }
26
+
27
+ + (BOOL)startRecordingWithConfiguration:(NSDictionary *)config
28
+ delegate:(id)delegate
29
+ error:(NSError **)error {
30
+ if (![self isScreenCaptureKitAvailable]) {
31
+ if (error) {
32
+ *error = [NSError errorWithDomain:@"ScreenCaptureKitRecorder"
33
+ code:-1
34
+ userInfo:@{NSLocalizedDescriptionKey: @"ScreenCaptureKit not available"}];
35
+ }
36
+ return NO;
37
+ }
38
+
39
+ if (g_scStream) {
40
+ return NO;
41
+ }
42
+
43
+ @try {
44
+ __block SCShareableContent *content = nil;
45
+ __block NSError *contentErr = nil;
46
+
47
+ dispatch_semaphore_t sem = dispatch_semaphore_create(0);
48
+ if (@available(macOS 13.0, *)) {
49
+ [SCShareableContent getShareableContentExcludingDesktopWindows:NO
50
+ onScreenWindowsOnly:YES
51
+ completionHandler:^(SCShareableContent * _Nullable shareableContent, NSError * _Nullable err) {
52
+ content = shareableContent;
53
+ contentErr = err;
54
+ dispatch_semaphore_signal(sem);
55
+ }];
56
+ } else {
57
+ dispatch_semaphore_signal(sem);
58
+ }
59
+ dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
60
+
61
+ if (!content || contentErr) {
62
+ if (error) { *error = contentErr ?: [NSError errorWithDomain:@"ScreenCaptureKitRecorder" code:-2 userInfo:nil]; }
63
+ return NO;
64
+ }
65
+
66
+ NSNumber *displayIdNumber = config[@"displayId"]; // CGDirectDisplayID
67
+ NSDictionary *captureArea = config[@"captureArea"]; // {x,y,width,height}
68
+ NSNumber *captureCursorNum = config[@"captureCursor"];
69
+ NSNumber *includeMicNum = config[@"includeMicrophone"];
70
+ NSNumber *includeSystemAudioNum = config[@"includeSystemAudio"];
71
+ NSArray<NSString *> *excludedBundleIds = config[@"excludedAppBundleIds"];
72
+ NSArray<NSNumber *> *excludedPIDs = config[@"excludedPIDs"];
73
+ NSArray<NSNumber *> *excludedWindowIds = config[@"excludedWindowIds"];
74
+ NSString *outputPath = config[@"outputPath"];
75
+
76
+ SCDisplay *targetDisplay = nil;
77
+ if (displayIdNumber) {
78
+ uint32_t wanted = (uint32_t)displayIdNumber.unsignedIntValue;
79
+ for (SCDisplay *d in content.displays) {
80
+ if (d.displayID == wanted) { targetDisplay = d; break; }
81
+ }
82
+ }
83
+ if (!targetDisplay) {
84
+ targetDisplay = content.displays.firstObject;
85
+ if (!targetDisplay) { return NO; }
86
+ }
87
+
88
+ NSMutableArray<SCRunningApplication*> *appsToExclude = [NSMutableArray array];
89
+ if (excludedBundleIds.count > 0) {
90
+ for (SCRunningApplication *app in content.applications) {
91
+ if ([excludedBundleIds containsObject:app.bundleIdentifier]) {
92
+ [appsToExclude addObject:app];
93
+ }
94
+ }
95
+ }
96
+ if (excludedPIDs.count > 0) {
97
+ for (SCRunningApplication *app in content.applications) {
98
+ if ([excludedPIDs containsObject:@(app.processID)]) {
99
+ [appsToExclude addObject:app];
100
+ }
101
+ }
102
+ }
103
+
104
+ NSMutableArray<SCWindow*> *windowsToExclude = [NSMutableArray array];
105
+ if (excludedWindowIds.count > 0) {
106
+ for (SCWindow *w in content.windows) {
107
+ if ([excludedWindowIds containsObject:@(w.windowID)]) {
108
+ [windowsToExclude addObject:w];
109
+ }
110
+ }
111
+ }
112
+
113
+ SCContentFilter *filter = nil;
114
+ if (appsToExclude.count > 0) {
115
+ if (@available(macOS 13.0, *)) {
116
+ filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingApplications:appsToExclude exceptingWindows:windowsToExclude];
117
+ }
118
+ }
119
+ if (!filter) {
120
+ if (@available(macOS 13.0, *)) {
121
+ filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:windowsToExclude];
122
+ }
123
+ }
124
+ if (!filter) { return NO; }
125
+
126
+ SCStreamConfiguration *cfg = [[SCStreamConfiguration alloc] init];
127
+ 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]);
132
+ cfg.sourceRect = src;
133
+ cfg.width = (int)src.size.width;
134
+ cfg.height = (int)src.size.height;
135
+ } else {
136
+ cfg.width = (int)targetDisplay.width;
137
+ cfg.height = (int)targetDisplay.height;
138
+ }
139
+ cfg.showsCursor = captureCursorNum.boolValue;
140
+ if (includeMicNum || includeSystemAudioNum) {
141
+ cfg.capturesAudio = YES;
142
+ }
143
+
144
+ g_scStream = [[SCStream alloc] initWithFilter:filter configuration:cfg delegate:nil];
145
+
146
+ if (@available(macOS 14.0, *)) {
147
+ SCRecordingOutputConfiguration *recCfg = [[SCRecordingOutputConfiguration alloc] init];
148
+ g_outputURL = [NSURL fileURLWithPath:outputPath];
149
+ recCfg.outputURL = g_outputURL;
150
+ recCfg.outputFileType = AVFileTypeQuickTimeMovie;
151
+ g_scRecordingOutput = [[SCRecordingOutput alloc] initWithConfiguration:recCfg delegate:(id<SCRecordingOutputDelegate>)delegate ?: (id<SCRecordingOutputDelegate>)self];
152
+ NSError *addErr = nil;
153
+ BOOL added = [g_scStream addRecordingOutput:g_scRecordingOutput error:&addErr];
154
+ if (!added) {
155
+ if (error) { *error = addErr ?: [NSError errorWithDomain:@"ScreenCaptureKitRecorder" code:-3 userInfo:nil]; }
156
+ g_scRecordingOutput = nil; g_scStream = nil; g_outputURL = nil;
157
+ return NO;
158
+ }
159
+
160
+ __block NSError *startErr = nil;
161
+ dispatch_semaphore_t startSem = dispatch_semaphore_create(0);
162
+ [g_scStream startCaptureWithCompletionHandler:^(NSError * _Nullable err) {
163
+ startErr = err;
164
+ dispatch_semaphore_signal(startSem);
165
+ }];
166
+ dispatch_semaphore_wait(startSem, DISPATCH_TIME_FOREVER);
167
+ if (startErr) {
168
+ if (error) { *error = startErr; }
169
+ [g_scStream removeRecordingOutput:g_scRecordingOutput error:nil];
170
+ g_scRecordingOutput = nil; g_scStream = nil; g_outputURL = nil;
171
+ return NO;
172
+ }
173
+ return YES;
174
+ }
175
+
176
+ return NO;
177
+ } @catch (__unused NSException *ex) {
178
+ return NO;
179
+ }
180
+ }
181
+
182
+ + (void)stopRecording {
183
+ if (!g_scStream) { return; }
184
+ @try {
185
+ dispatch_semaphore_t sem = dispatch_semaphore_create(0);
186
+ [g_scStream stopCaptureWithCompletionHandler:^(NSError * _Nullable error) {
187
+ dispatch_semaphore_signal(sem);
188
+ }];
189
+ dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
190
+ } @catch (__unused NSException *ex) {
191
+ }
192
+ if (g_scRecordingOutput) {
193
+ [g_scStream removeRecordingOutput:g_scRecordingOutput error:nil];
194
+ }
195
+ g_scRecordingOutput = nil;
196
+ g_scStream = nil;
197
+ g_outputURL = nil;
198
+ }
199
+
200
+ @end
201
+
202
+