node-mac-recorder 1.17.0 → 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;
@@ -302,13 +355,52 @@ class MacRecorder extends EventEmitter {
302
355
  this.emit("timeUpdate", elapsed);
303
356
  }, 1000);
304
357
 
305
- // Kayıt tam başladığı anda event emit et
306
- this.emit("recordingStarted", {
307
- outputPath: this.outputPath,
308
- timestamp: this.recordingStartTime,
309
- options: this.options
310
- });
311
-
358
+ // Native kayıt gerçekten başladığını kontrol etmek için polling başlat
359
+ let recordingStartedEmitted = false;
360
+ const checkRecordingStatus = setInterval(() => {
361
+ try {
362
+ const nativeStatus = nativeBinding.getRecordingStatus();
363
+ if (nativeStatus && !recordingStartedEmitted) {
364
+ recordingStartedEmitted = true;
365
+ clearInterval(checkRecordingStatus);
366
+
367
+ // Kayıt gerçekten başladığı anda event emit et
368
+ this.emit("recordingStarted", {
369
+ outputPath: this.outputPath,
370
+ timestamp: Date.now(), // Gerçek başlangıç zamanı
371
+ options: this.options,
372
+ nativeConfirmed: true,
373
+ });
374
+ }
375
+ } catch (error) {
376
+ // Native status check error - fallback
377
+ if (!recordingStartedEmitted) {
378
+ recordingStartedEmitted = true;
379
+ clearInterval(checkRecordingStatus);
380
+ this.emit("recordingStarted", {
381
+ outputPath: this.outputPath,
382
+ timestamp: this.recordingStartTime,
383
+ options: this.options,
384
+ nativeConfirmed: false,
385
+ });
386
+ }
387
+ }
388
+ }, 50); // Her 50ms kontrol et
389
+
390
+ // Timeout fallback - 5 saniye sonra hala başlamamışsa emit et
391
+ setTimeout(() => {
392
+ if (!recordingStartedEmitted) {
393
+ recordingStartedEmitted = true;
394
+ clearInterval(checkRecordingStatus);
395
+ this.emit("recordingStarted", {
396
+ outputPath: this.outputPath,
397
+ timestamp: this.recordingStartTime,
398
+ options: this.options,
399
+ nativeConfirmed: false,
400
+ });
401
+ }
402
+ }, 5000);
403
+
312
404
  this.emit("started", this.outputPath);
313
405
  resolve(this.outputPath);
314
406
  } else {
@@ -549,7 +641,7 @@ class MacRecorder extends EventEmitter {
549
641
  width: options.windowInfo.width,
550
642
  height: options.windowInfo.height,
551
643
  windowRelative: true,
552
- windowInfo: options.windowInfo
644
+ windowInfo: options.windowInfo,
553
645
  };
554
646
  } else if (this.recordingDisplayInfo) {
555
647
  // Recording başlatılmışsa o display'i kullan
@@ -605,7 +697,7 @@ class MacRecorder extends EventEmitter {
605
697
  if (this.cursorDisplayInfo.windowRelative) {
606
698
  // Window-relative koordinatlar
607
699
  coordinateSystem = "window-relative";
608
-
700
+
609
701
  // Window bounds kontrolü - cursor window dışındaysa kaydetme
610
702
  if (
611
703
  x < 0 ||
@@ -618,7 +710,7 @@ class MacRecorder extends EventEmitter {
618
710
  } else {
619
711
  // Display-relative koordinatlar
620
712
  coordinateSystem = "display-relative";
621
-
713
+
622
714
  // Display bounds kontrolü
623
715
  if (
624
716
  x < 0 ||
@@ -643,9 +735,9 @@ class MacRecorder extends EventEmitter {
643
735
  windowInfo: {
644
736
  width: this.cursorDisplayInfo.width,
645
737
  height: this.cursorDisplayInfo.height,
646
- originalWindow: this.cursorDisplayInfo.windowInfo
647
- }
648
- })
738
+ originalWindow: this.cursorDisplayInfo.windowInfo,
739
+ },
740
+ }),
649
741
  };
650
742
 
651
743
  // Sadece eventType değiştiğinde veya pozisyon değiştiğinde kaydet
@@ -892,6 +984,6 @@ class MacRecorder extends EventEmitter {
892
984
  }
893
985
 
894
986
  // WindowSelector modülünü de export edelim
895
- MacRecorder.WindowSelector = require('./window-selector');
987
+ MacRecorder.WindowSelector = require("./window-selector");
896
988
 
897
989
  module.exports = MacRecorder;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "1.17.0",
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
+