node-mac-recorder 2.1.2 → 2.2.1

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.
@@ -1,6 +1,10 @@
1
1
  #import <napi.h>
2
+ #import <ScreenCaptureKit/ScreenCaptureKit.h>
3
+ <<<<<<< HEAD
4
+ =======
2
5
  #import <AVFoundation/AVFoundation.h>
3
6
  #import <CoreMedia/CoreMedia.h>
7
+ >>>>>>> screencapture
4
8
  #import <AppKit/AppKit.h>
5
9
  #import <Foundation/Foundation.h>
6
10
  #import <CoreGraphics/CoreGraphics.h>
@@ -17,57 +21,234 @@ Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports);
17
21
  // Window selector function declarations
18
22
  Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports);
19
23
 
20
- @interface MacRecorderDelegate : NSObject <AVCaptureFileOutputRecordingDelegate>
24
+ <<<<<<< HEAD
25
+ @interface MacRecorderDelegate : NSObject
26
+ =======
27
+ // ScreenCaptureKit Recording Delegate
28
+ API_AVAILABLE(macos(12.3))
29
+ @interface SCKRecorderDelegate : NSObject <SCStreamDelegate, SCStreamOutput>
30
+ >>>>>>> screencapture
21
31
  @property (nonatomic, copy) void (^completionHandler)(NSURL *outputURL, NSError *error);
32
+ @property (nonatomic, copy) void (^startedHandler)(void);
33
+ @property (nonatomic, strong) AVAssetWriter *assetWriter;
34
+ @property (nonatomic, strong) AVAssetWriterInput *videoInput;
35
+ @property (nonatomic, strong) AVAssetWriterInput *audioInput;
36
+ @property (nonatomic, strong) NSURL *outputURL;
37
+ @property (nonatomic, assign) BOOL isWriting;
38
+ @property (nonatomic, assign) CMTime startTime;
39
+ @property (nonatomic, assign) BOOL hasStartTime;
40
+ @property (nonatomic, assign) BOOL startAttempted;
41
+ @property (nonatomic, assign) BOOL startFailed;
22
42
  @end
23
43
 
44
+ <<<<<<< HEAD
24
45
  @implementation MacRecorderDelegate
25
- - (void)captureOutput:(AVCaptureFileOutput *)output
26
- didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
27
- fromConnections:(NSArray<AVCaptureConnection *> *)connections
28
- error:(NSError *)error {
46
+ - (void)recordingDidStart {
47
+ NSLog(@"[mac_recorder] ScreenCaptureKit recording started");
48
+ }
49
+ - (void)recordingDidFinish:(NSURL *)outputURL error:(NSError *)error {
50
+ if (error) {
51
+ NSLog(@"[mac_recorder] ScreenCaptureKit recording finished with error: %@", error.localizedDescription);
52
+ } else {
53
+ NSLog(@"[mac_recorder] ScreenCaptureKit recording finished OK → %@", outputURL.path);
54
+ }
55
+ if (self.completionHandler) {
56
+ self.completionHandler(outputURL, error);
57
+ =======
58
+ @implementation SCKRecorderDelegate
59
+
60
+ // Standard SCStreamDelegate method - should be called automatically
61
+ - (void)stream:(SCStream *)stream didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type {
62
+ NSLog(@"📹 SCStreamDelegate received sample buffer of type: %ld", (long)type);
63
+ [self handleSampleBuffer:sampleBuffer ofType:type fromStream:stream];
64
+ }
65
+
66
+ - (void)stream:(SCStream *)stream didStopWithError:(NSError *)error {
67
+ NSLog(@"🛑 Stream stopped with error: %@", error ? error.localizedDescription : @"none");
29
68
  if (self.completionHandler) {
30
- self.completionHandler(outputFileURL, error);
69
+ self.completionHandler(self.outputURL, error);
70
+ >>>>>>> screencapture
71
+ }
72
+ }
73
+
74
+
75
+ // Main sample buffer handler (renamed to avoid conflicts)
76
+ - (void)handleSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type fromStream:(SCStream *)stream {
77
+ NSLog(@"📹 Handling sample buffer of type: %ld", (long)type);
78
+
79
+ if (!self.isWriting || !self.assetWriter) {
80
+ NSLog(@"⚠️ Not writing or no asset writer available");
81
+ return;
82
+ }
83
+ if (self.startFailed) {
84
+ NSLog(@"⚠️ Asset writer start previously failed; ignoring buffers");
85
+ return;
86
+ }
87
+
88
+ // Start asset writer on first sample buffer
89
+ if (!self.hasStartTime) {
90
+ NSLog(@"🚀 Starting asset writer with first sample buffer");
91
+ if (self.startAttempted) {
92
+ // Another thread already attempted start; wait for success/fail flag to flip
93
+ return;
94
+ }
95
+ self.startAttempted = YES;
96
+ if (![self.assetWriter startWriting]) {
97
+ NSLog(@"❌ Failed to start asset writer: %@", self.assetWriter.error.localizedDescription);
98
+ self.startFailed = YES;
99
+ return;
100
+ }
101
+
102
+ NSLog(@"✅ Asset writer started successfully");
103
+ self.startTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
104
+ self.hasStartTime = YES;
105
+ [self.assetWriter startSessionAtSourceTime:self.startTime];
106
+ NSLog(@"✅ Asset writer session started at time: %lld", self.startTime.value);
107
+ }
108
+
109
+ switch (type) {
110
+ case SCStreamOutputTypeScreen: {
111
+ NSLog(@"📺 Processing screen sample buffer");
112
+ if (self.videoInput && self.videoInput.isReadyForMoreMediaData) {
113
+ BOOL success = [self.videoInput appendSampleBuffer:sampleBuffer];
114
+ NSLog(@"📺 Video sample buffer appended: %@", success ? @"SUCCESS" : @"FAILED");
115
+ } else {
116
+ NSLog(@"⚠️ Video input not ready for more data");
117
+ }
118
+ break;
119
+ }
120
+ case SCStreamOutputTypeAudio: {
121
+ NSLog(@"🔊 Processing audio sample buffer");
122
+ if (self.audioInput && self.audioInput.isReadyForMoreMediaData) {
123
+ BOOL success = [self.audioInput appendSampleBuffer:sampleBuffer];
124
+ NSLog(@"🔊 Audio sample buffer appended: %@", success ? @"SUCCESS" : @"FAILED");
125
+ } else {
126
+ NSLog(@"⚠️ Audio input not ready for more data (or no audio input)");
127
+ }
128
+ break;
129
+ }
130
+ case SCStreamOutputTypeMicrophone: {
131
+ NSLog(@"🎤 Processing microphone sample buffer");
132
+ if (self.audioInput && self.audioInput.isReadyForMoreMediaData) {
133
+ BOOL success = [self.audioInput appendSampleBuffer:sampleBuffer];
134
+ NSLog(@"🎤 Microphone sample buffer appended: %@", success ? @"SUCCESS" : @"FAILED");
135
+ } else {
136
+ NSLog(@"⚠️ Microphone input not ready for more data (or no audio input)");
137
+ }
138
+ break;
139
+ }
31
140
  }
32
141
  }
142
+
33
143
  @end
34
144
 
145
+ <<<<<<< HEAD
35
146
  // Global state for recording
36
- static AVCaptureSession *g_captureSession = nil;
37
- static AVCaptureMovieFileOutput *g_movieFileOutput = nil;
38
- static AVCaptureScreenInput *g_screenInput = nil;
39
- static AVCaptureDeviceInput *g_audioInput = nil;
40
147
  static MacRecorderDelegate *g_delegate = nil;
41
148
  static bool g_isRecording = false;
42
149
 
43
150
  // Helper function to cleanup recording resources
44
151
  void cleanupRecording() {
45
- if (g_captureSession) {
46
- [g_captureSession stopRunning];
47
- g_captureSession = nil;
48
- }
49
- g_movieFileOutput = nil;
50
- g_screenInput = nil;
51
- g_audioInput = nil;
52
152
  g_delegate = nil;
153
+ =======
154
+ // Global state for ScreenCaptureKit recording
155
+ static SCStream *g_scStream = nil;
156
+ static SCKRecorderDelegate *g_scDelegate = nil;
157
+ static bool g_isRecording = false;
158
+
159
+ // Helper function to cleanup ScreenCaptureKit recording resources
160
+ void cleanupSCKRecording() {
161
+ NSLog(@"🛑 Cleaning up ScreenCaptureKit recording");
162
+
163
+ if (g_scStream) {
164
+ NSLog(@"🛑 Stopping SCStream");
165
+ [g_scStream stopCaptureWithCompletionHandler:^(NSError * _Nullable error) {
166
+ if (error) {
167
+ NSLog(@"❌ Error stopping SCStream: %@", error.localizedDescription);
168
+ } else {
169
+ NSLog(@"✅ SCStream stopped successfully");
170
+ }
171
+ }];
172
+ g_scStream = nil;
173
+ }
174
+
175
+ if (g_scDelegate) {
176
+ if (g_scDelegate.assetWriter && g_scDelegate.isWriting) {
177
+ NSLog(@"🛑 Finishing asset writer (status: %ld)", (long)g_scDelegate.assetWriter.status);
178
+ g_scDelegate.isWriting = NO;
179
+
180
+ // Only mark inputs as finished if asset writer is actually writing
181
+ if (g_scDelegate.assetWriter.status == AVAssetWriterStatusWriting) {
182
+ if (g_scDelegate.videoInput) {
183
+ [g_scDelegate.videoInput markAsFinished];
184
+ }
185
+ if (g_scDelegate.audioInput) {
186
+ [g_scDelegate.audioInput markAsFinished];
187
+ }
188
+
189
+ [g_scDelegate.assetWriter finishWritingWithCompletionHandler:^{
190
+ NSLog(@"✅ Asset writer finished. Status: %ld", (long)g_scDelegate.assetWriter.status);
191
+ if (g_scDelegate.assetWriter.error) {
192
+ NSLog(@"❌ Asset writer error: %@", g_scDelegate.assetWriter.error.localizedDescription);
193
+ }
194
+ }];
195
+ } else {
196
+ NSLog(@"⚠️ Asset writer not in writing status, cannot finish normally");
197
+ if (g_scDelegate.assetWriter.status == AVAssetWriterStatusFailed) {
198
+ NSLog(@"❌ Asset writer failed: %@", g_scDelegate.assetWriter.error.localizedDescription);
199
+ }
200
+ }
201
+ }
202
+ g_scDelegate = nil;
203
+ }
204
+ >>>>>>> screencapture
53
205
  g_isRecording = false;
54
206
  }
55
207
 
56
- // NAPI Function: Start Recording
208
+ // Check if ScreenCaptureKit is available
209
+ bool isScreenCaptureKitAvailable() {
210
+ if (@available(macOS 12.3, *)) {
211
+ return true;
212
+ }
213
+ return false;
214
+ }
215
+
216
+ // NAPI Function: Start Recording with ScreenCaptureKit
57
217
  Napi::Value StartRecording(const Napi::CallbackInfo& info) {
58
218
  Napi::Env env = info.Env();
59
219
 
220
+ if (!isScreenCaptureKitAvailable()) {
221
+ NSLog(@"ScreenCaptureKit requires macOS 12.3 or later");
222
+ return Napi::Boolean::New(env, false);
223
+ }
224
+
60
225
  if (info.Length() < 1) {
61
- Napi::TypeError::New(env, "Output path required").ThrowAsJavaScriptException();
62
- return env.Null();
226
+ NSLog(@"Output path required");
227
+ return Napi::Boolean::New(env, false);
63
228
  }
64
229
 
65
230
  if (g_isRecording) {
231
+ NSLog(@"⚠️ Already recording");
66
232
  return Napi::Boolean::New(env, false);
67
233
  }
68
234
 
235
+ // Verify permissions before starting
236
+ if (!CGPreflightScreenCaptureAccess()) {
237
+ NSLog(@"❌ Screen recording permission not granted - requesting access");
238
+ bool requestResult = CGRequestScreenCaptureAccess();
239
+ NSLog(@"📋 Permission request result: %@", requestResult ? @"SUCCESS" : @"FAILED");
240
+
241
+ if (!CGPreflightScreenCaptureAccess()) {
242
+ NSLog(@"❌ Screen recording permission still not available");
243
+ return Napi::Boolean::New(env, false);
244
+ }
245
+ }
246
+ NSLog(@"✅ Screen recording permission verified");
247
+
69
248
  std::string outputPath = info[0].As<Napi::String>().Utf8Value();
249
+ NSLog(@"[mac_recorder] StartRecording: output=%@", [NSString stringWithUTF8String:outputPath.c_str()]);
70
250
 
251
+ <<<<<<< HEAD
71
252
  // Options parsing (shared)
72
253
  CGRect captureRect = CGRectNull;
73
254
  bool captureCursor = false; // Default olarak cursor gizli
@@ -82,44 +263,29 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
82
263
  NSMutableArray<NSNumber*> *excludedPIDs = [NSMutableArray array];
83
264
  NSMutableArray<NSNumber*> *excludedWindowIds = [NSMutableArray array];
84
265
  bool autoExcludeSelf = false;
266
+ =======
267
+ // Default options
268
+ bool captureCursor = false;
269
+ bool includeSystemAudio = true;
270
+ CGDirectDisplayID displayID = 0; // Will be set to first available display
271
+ uint32_t windowID = 0;
272
+ CGRect captureRect = CGRectNull;
273
+ >>>>>>> screencapture
85
274
 
275
+ // Parse options
86
276
  if (info.Length() > 1 && info[1].IsObject()) {
87
277
  Napi::Object options = info[1].As<Napi::Object>();
88
278
 
89
- // Capture area
90
- if (options.Has("captureArea") && options.Get("captureArea").IsObject()) {
91
- Napi::Object rectObj = options.Get("captureArea").As<Napi::Object>();
92
- if (rectObj.Has("x") && rectObj.Has("y") && rectObj.Has("width") && rectObj.Has("height")) {
93
- captureRect = CGRectMake(
94
- rectObj.Get("x").As<Napi::Number>().DoubleValue(),
95
- rectObj.Get("y").As<Napi::Number>().DoubleValue(),
96
- rectObj.Get("width").As<Napi::Number>().DoubleValue(),
97
- rectObj.Get("height").As<Napi::Number>().DoubleValue()
98
- );
99
- }
100
- }
101
-
102
- // Capture cursor
103
279
  if (options.Has("captureCursor")) {
104
280
  captureCursor = options.Get("captureCursor").As<Napi::Boolean>();
105
281
  }
106
282
 
107
- // Microphone
108
- if (options.Has("includeMicrophone")) {
109
- includeMicrophone = options.Get("includeMicrophone").As<Napi::Boolean>();
110
- }
111
-
112
- // Audio device ID
113
- if (options.Has("audioDeviceId") && !options.Get("audioDeviceId").IsNull()) {
114
- std::string deviceId = options.Get("audioDeviceId").As<Napi::String>().Utf8Value();
115
- audioDeviceId = [NSString stringWithUTF8String:deviceId.c_str()];
116
- }
117
283
 
118
- // System audio
119
284
  if (options.Has("includeSystemAudio")) {
120
285
  includeSystemAudio = options.Get("includeSystemAudio").As<Napi::Boolean>();
121
286
  }
122
287
 
288
+ <<<<<<< HEAD
123
289
  // System audio device ID
124
290
  if (options.Has("systemAudioDeviceId") && !options.Get("systemAudioDeviceId").IsNull()) {
125
291
  std::string sysDeviceId = options.Get("systemAudioDeviceId").As<Napi::String>().Utf8Value();
@@ -164,48 +330,38 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
164
330
  }
165
331
 
166
332
  // Display ID
333
+ =======
334
+ >>>>>>> screencapture
167
335
  if (options.Has("displayId") && !options.Get("displayId").IsNull()) {
168
- double displayIdNum = options.Get("displayId").As<Napi::Number>().DoubleValue();
169
-
170
- // Use the display ID directly (not as an index)
171
- // The JavaScript layer passes the actual CGDirectDisplayID
172
- displayID = (CGDirectDisplayID)displayIdNum;
173
-
174
- // Verify that this display ID is valid
175
- uint32_t displayCount;
176
- CGGetActiveDisplayList(0, NULL, &displayCount);
177
- if (displayCount > 0) {
178
- CGDirectDisplayID *displays = (CGDirectDisplayID*)malloc(displayCount * sizeof(CGDirectDisplayID));
179
- CGGetActiveDisplayList(displayCount, displays, &displayCount);
180
-
181
- bool validDisplay = false;
182
- for (uint32_t i = 0; i < displayCount; i++) {
183
- if (displays[i] == displayID) {
184
- validDisplay = true;
185
- break;
186
- }
187
- }
188
-
189
- if (!validDisplay) {
190
- // Fallback to main display if invalid ID provided
191
- displayID = CGMainDisplayID();
192
- }
193
-
194
- free(displays);
336
+ uint32_t tempDisplayID = options.Get("displayId").As<Napi::Number>().Uint32Value();
337
+ if (tempDisplayID != 0) {
338
+ displayID = tempDisplayID;
195
339
  }
196
340
  }
197
341
 
198
- // Window ID için gelecekte kullanım (şimdilik captureArea ile hallediliyor)
199
342
  if (options.Has("windowId") && !options.Get("windowId").IsNull()) {
200
- // WindowId belirtilmiş ama captureArea JavaScript tarafında ayarlanıyor
201
- // Bu parametre gelecekte native level pencere seçimi için kullanılabilir
343
+ windowID = options.Get("windowId").As<Napi::Number>().Uint32Value();
344
+ }
345
+
346
+ if (options.Has("captureArea") && options.Get("captureArea").IsObject()) {
347
+ Napi::Object rectObj = options.Get("captureArea").As<Napi::Object>();
348
+ if (rectObj.Has("x") && rectObj.Has("y") && rectObj.Has("width") && rectObj.Has("height")) {
349
+ captureRect = CGRectMake(
350
+ rectObj.Get("x").As<Napi::Number>().DoubleValue(),
351
+ rectObj.Get("y").As<Napi::Number>().DoubleValue(),
352
+ rectObj.Get("width").As<Napi::Number>().DoubleValue(),
353
+ rectObj.Get("height").As<Napi::Number>().DoubleValue()
354
+ );
355
+ }
202
356
  }
203
357
  }
204
358
 
359
+ <<<<<<< HEAD
205
360
  @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]) {
361
+ // Always prefer ScreenCaptureKit if available
362
+ NSLog(@"[mac_recorder] Checking ScreenCaptureKit availability");
363
+ if (@available(macOS 12.3, *)) {
364
+ if ([ScreenCaptureKitRecorder isScreenCaptureKitAvailable]) {
209
365
  NSMutableDictionary *scConfig = [@{} mutableCopy];
210
366
  scConfig[@"displayId"] = @(displayID);
211
367
  if (!CGRectIsNull(captureRect)) {
@@ -233,321 +389,352 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
233
389
  scConfig[@"outputPath"] = [NSString stringWithUTF8String:outputPathStr.c_str()];
234
390
 
235
391
  NSError *scErr = nil;
236
- BOOL ok = [ScreenCaptureKitRecorder startRecordingWithConfiguration:scConfig delegate:nil error:&scErr];
392
+ NSLog(@"[mac_recorder] Using ScreenCaptureKit path (displayId=%u)", displayID);
393
+
394
+ // Create and set up delegate
395
+ g_delegate = [[MacRecorderDelegate alloc] init];
396
+
397
+ BOOL ok = [ScreenCaptureKitRecorder startRecordingWithConfiguration:scConfig delegate:g_delegate error:&scErr];
237
398
  if (ok) {
238
399
  g_isRecording = true;
400
+ NSLog(@"[mac_recorder] ScreenCaptureKit startRecording → OK");
239
401
  return Napi::Boolean::New(env, true);
240
402
  }
241
- // If SC failed, fall through to AVFoundation as fallback
403
+ NSLog(@"[mac_recorder] ScreenCaptureKit startRecording FAIL: %@", scErr.localizedDescription);
404
+ cleanupRecording();
405
+ return Napi::Boolean::New(env, false);
406
+ }
407
+ } else {
408
+ NSLog(@"[mac_recorder] ScreenCaptureKit not available");
409
+ cleanupRecording();
410
+ return Napi::Boolean::New(env, false);
242
411
  }
243
- // Create capture session
244
- g_captureSession = [[AVCaptureSession alloc] init];
245
- [g_captureSession beginConfiguration];
246
-
247
- // Set session preset
248
- g_captureSession.sessionPreset = AVCaptureSessionPresetHigh;
249
412
 
250
- // Create screen input with selected display
251
- g_screenInput = [[AVCaptureScreenInput alloc] initWithDisplayID:displayID];
413
+ } @catch (NSException *exception) {
414
+ cleanupRecording();
415
+ return Napi::Boolean::New(env, false);
416
+ =======
417
+ // Create output URL
418
+ NSURL *outputURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:outputPath.c_str()]];
419
+ NSLog(@"📁 Output URL: %@", outputURL.absoluteString);
420
+
421
+ // Remove existing file if present to avoid AVAssetWriter "Cannot Save" error
422
+ NSFileManager *fm = [NSFileManager defaultManager];
423
+ if ([fm fileExistsAtPath:outputURL.path]) {
424
+ NSError *rmErr = nil;
425
+ [fm removeItemAtURL:outputURL error:&rmErr];
426
+ if (rmErr) {
427
+ NSLog(@"⚠️ Failed to remove existing output file (%@): %@", outputURL.path, rmErr.localizedDescription);
428
+ }
429
+ }
430
+
431
+ // Get shareable content
432
+ dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
433
+ __block NSError *contentError = nil;
434
+ __block SCShareableContent *shareableContent = nil;
435
+
436
+ [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent * _Nullable content, NSError * _Nullable error) {
437
+ shareableContent = content;
438
+ contentError = error;
439
+ dispatch_semaphore_signal(semaphore);
440
+ }];
441
+
442
+ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
443
+
444
+ if (contentError) {
445
+ NSLog(@"ScreenCaptureKit error: %@", contentError.localizedDescription);
446
+ NSLog(@"This is likely due to missing screen recording permissions");
447
+ return Napi::Boolean::New(env, false);
448
+ }
449
+
450
+ // Find target display or window
451
+ SCContentFilter *contentFilter = nil;
452
+
453
+ if (windowID > 0) {
454
+ // Window recording
455
+ SCWindow *targetWindow = nil;
456
+ for (SCWindow *window in shareableContent.windows) {
457
+ if (window.windowID == windowID) {
458
+ targetWindow = window;
459
+ break;
460
+ }
461
+ }
252
462
 
253
- if (!CGRectIsNull(captureRect)) {
254
- g_screenInput.cropRect = captureRect;
463
+ if (!targetWindow) {
464
+ NSLog(@"Window not found with ID: %u", windowID);
465
+ return Napi::Boolean::New(env, false);
255
466
  }
256
467
 
257
- // Set cursor capture
258
- g_screenInput.capturesCursor = captureCursor;
468
+ contentFilter = [[SCContentFilter alloc] initWithDesktopIndependentWindow:targetWindow];
469
+ } else {
470
+ // Display recording
471
+ NSLog(@"🔍 Selecting display among %lu available displays", (unsigned long)shareableContent.displays.count);
259
472
 
260
- if ([g_captureSession canAddInput:g_screenInput]) {
261
- [g_captureSession addInput:g_screenInput];
262
- } else {
263
- cleanupRecording();
264
- return Napi::Boolean::New(env, false);
473
+ SCDisplay *targetDisplay = nil;
474
+
475
+ // Log all available displays first
476
+ for (SCDisplay *display in shareableContent.displays) {
477
+ NSLog(@"📺 Available display: ID=%u, width=%d, height=%d", display.displayID, (int)display.width, (int)display.height);
265
478
  }
266
479
 
267
- // Add microphone input if requested
268
- if (includeMicrophone) {
269
- AVCaptureDevice *audioDevice = nil;
270
-
271
- if (audioDeviceId) {
272
- // Try to find the specified device
273
- NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
274
- NSLog(@"[DEBUG] Looking for audio device with ID: %@", audioDeviceId);
275
- NSLog(@"[DEBUG] Available audio devices:");
276
- for (AVCaptureDevice *device in devices) {
277
- NSLog(@"[DEBUG] - Device: %@ (ID: %@)", device.localizedName, device.uniqueID);
278
- if ([device.uniqueID isEqualToString:audioDeviceId]) {
279
- NSLog(@"[DEBUG] Found matching device: %@", device.localizedName);
280
- audioDevice = device;
281
- break;
282
- }
283
- }
284
-
285
- if (!audioDevice) {
286
- NSLog(@"[DEBUG] Specified audio device not found, falling back to default");
480
+ if (displayID != 0) {
481
+ // Look for specific display ID
482
+ for (SCDisplay *display in shareableContent.displays) {
483
+ if (display.displayID == displayID) {
484
+ targetDisplay = display;
485
+ break;
287
486
  }
288
487
  }
289
488
 
290
- // Fallback to default device if specified device not found
291
- if (!audioDevice) {
292
- audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
293
- NSLog(@"[DEBUG] Using default audio device: %@ (ID: %@)", audioDevice.localizedName, audioDevice.uniqueID);
489
+ if (!targetDisplay) {
490
+ NSLog(@"❌ Display not found with ID: %u", displayID);
294
491
  }
295
-
296
- if (audioDevice) {
297
- NSError *error;
298
- g_audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:&error];
299
- if (g_audioInput && [g_captureSession canAddInput:g_audioInput]) {
300
- [g_captureSession addInput:g_audioInput];
301
- NSLog(@"[DEBUG] Successfully added audio input device");
302
- } else {
303
- NSLog(@"[DEBUG] Failed to add audio input device: %@", error);
304
- }
492
+ }
493
+
494
+ // If no specific display was requested or found, use the first available
495
+ if (!targetDisplay) {
496
+ if (shareableContent.displays.count > 0) {
497
+ targetDisplay = shareableContent.displays.firstObject;
498
+ NSLog(@" Using first available display: ID=%u, %dx%d", targetDisplay.displayID, (int)targetDisplay.width, (int)targetDisplay.height);
499
+ } else {
500
+ NSLog(@" No displays available at all");
501
+ return Napi::Boolean::New(env, false);
305
502
  }
503
+ } else {
504
+ NSLog(@"✅ Using specified display: ID=%u, %dx%d", targetDisplay.displayID, (int)targetDisplay.width, (int)targetDisplay.height);
306
505
  }
307
506
 
308
- // System audio configuration
309
- if (includeSystemAudio) {
310
- // Enable audio capture in screen input
311
- g_screenInput.capturesMouseClicks = YES;
312
-
313
- // Try to add system audio input using Core Audio
314
- // This approach captures system audio by creating a virtual audio device
315
- if (@available(macOS 10.15, *)) {
316
- // Configure screen input for better audio capture
317
- g_screenInput.capturesCursor = captureCursor;
318
- g_screenInput.capturesMouseClicks = YES;
319
-
320
- // Try to find and add system audio device (like Soundflower, BlackHole, etc.)
321
- NSArray *audioDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
322
- AVCaptureDevice *systemAudioDevice = nil;
323
-
324
- // If specific system audio device ID is provided, try to find it first
325
- if (systemAudioDeviceId) {
326
- for (AVCaptureDevice *device in audioDevices) {
327
- if ([device.uniqueID isEqualToString:systemAudioDeviceId]) {
328
- systemAudioDevice = device;
329
- NSLog(@"[DEBUG] Found specified system audio device: %@ (ID: %@)", device.localizedName, device.uniqueID);
330
- break;
331
- }
332
- }
333
- }
334
-
335
- // If no specific device found or specified, look for known system audio devices
336
- if (!systemAudioDevice) {
337
- for (AVCaptureDevice *device in audioDevices) {
338
- NSString *deviceName = [device.localizedName lowercaseString];
339
- // Check for common system audio capture devices
340
- if ([deviceName containsString:@"soundflower"] ||
341
- [deviceName containsString:@"blackhole"] ||
342
- [deviceName containsString:@"loopback"] ||
343
- [deviceName containsString:@"system audio"] ||
344
- [deviceName containsString:@"aggregate"]) {
345
- systemAudioDevice = device;
346
- NSLog(@"[DEBUG] Auto-detected system audio device: %@", device.localizedName);
347
- break;
507
+ // Update displayID for subsequent use
508
+ displayID = targetDisplay.displayID;
509
+
510
+ // Build exclusion windows array if provided
511
+ NSMutableArray<SCWindow *> *excluded = [NSMutableArray array];
512
+ BOOL excludeCurrentApp = NO;
513
+ if (info.Length() > 1 && info[1].IsObject()) {
514
+ Napi::Object options = info[1].As<Napi::Object>();
515
+ if (options.Has("excludeCurrentApp")) {
516
+ excludeCurrentApp = options.Get("excludeCurrentApp").As<Napi::Boolean>();
517
+ }
518
+ if (options.Has("excludeWindowIds") && options.Get("excludeWindowIds").IsArray()) {
519
+ Napi::Array arr = options.Get("excludeWindowIds").As<Napi::Array>();
520
+ for (uint32_t i = 0; i < arr.Length(); i++) {
521
+ Napi::Value v = arr.Get(i);
522
+ if (v.IsNumber()) {
523
+ uint32_t wid = v.As<Napi::Number>().Uint32Value();
524
+ for (SCWindow *w in shareableContent.windows) {
525
+ if (w.windowID == wid) {
526
+ [excluded addObject:w];
527
+ break;
528
+ }
348
529
  }
349
530
  }
350
531
  }
351
-
352
- // If we found a system audio device, add it as an additional input
353
- if (systemAudioDevice && !includeMicrophone) {
354
- // Only add system audio device if microphone is not already added
355
- NSError *error;
356
- AVCaptureDeviceInput *systemAudioInput = [[AVCaptureDeviceInput alloc] initWithDevice:systemAudioDevice error:&error];
357
- if (systemAudioInput && [g_captureSession canAddInput:systemAudioInput]) {
358
- [g_captureSession addInput:systemAudioInput];
359
- NSLog(@"[DEBUG] Successfully added system audio device: %@", systemAudioDevice.localizedName);
360
- } else if (error) {
361
- NSLog(@"[DEBUG] Failed to add system audio device: %@", error.localizedDescription);
362
- }
363
- } else if (includeSystemAudio && !systemAudioDevice) {
364
- NSLog(@"[DEBUG] System audio requested but no suitable device found. Available devices:");
365
- for (AVCaptureDevice *device in audioDevices) {
366
- NSLog(@"[DEBUG] - %@ (ID: %@)", device.localizedName, device.uniqueID);
367
- }
368
- }
369
532
  }
370
- } else {
371
- // Explicitly disable audio capture if not requested
372
- g_screenInput.capturesMouseClicks = NO;
373
533
  }
374
534
 
375
- // Create movie file output
376
- g_movieFileOutput = [[AVCaptureMovieFileOutput alloc] init];
377
- if ([g_captureSession canAddOutput:g_movieFileOutput]) {
378
- [g_captureSession addOutput:g_movieFileOutput];
379
- } else {
380
- cleanupRecording();
381
- return Napi::Boolean::New(env, false);
535
+ if (excludeCurrentApp) {
536
+ pid_t pid = [[NSProcessInfo processInfo] processIdentifier];
537
+ for (SCWindow *w in shareableContent.windows) {
538
+ if (w.owningApplication && w.owningApplication.processID == pid) {
539
+ [excluded addObject:w];
540
+ }
541
+ }
382
542
  }
383
543
 
384
- [g_captureSession commitConfiguration];
385
-
386
- // Start session
387
- [g_captureSession startRunning];
388
-
389
- // Create delegate
390
- g_delegate = [[MacRecorderDelegate alloc] init];
391
-
392
- // Start recording
393
- NSURL *outputURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:outputPath.c_str()]];
394
- [g_movieFileOutput startRecordingToOutputFileURL:outputURL recordingDelegate:g_delegate];
395
-
396
- g_isRecording = true;
397
- return Napi::Boolean::New(env, true);
398
-
399
- } @catch (NSException *exception) {
400
- cleanupRecording();
401
- return Napi::Boolean::New(env, false);
544
+ contentFilter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:excluded];
545
+ NSLog(@"✅ Content filter created for display recording");
546
+ >>>>>>> screencapture
402
547
  }
403
- }
404
-
405
- // NAPI Function: Stop Recording
406
- Napi::Value StopRecording(const Napi::CallbackInfo& info) {
407
- Napi::Env env = info.Env();
408
548
 
549
+ <<<<<<< HEAD
409
550
  if (!g_isRecording) {
410
551
  return Napi::Boolean::New(env, false);
552
+ =======
553
+ // Get actual display dimensions for proper video configuration
554
+ CGRect displayBounds = CGDisplayBounds(displayID);
555
+ NSSize videoSize = NSMakeSize(displayBounds.size.width, displayBounds.size.height);
556
+
557
+ // Create stream configuration
558
+ SCStreamConfiguration *config = [[SCStreamConfiguration alloc] init];
559
+ config.width = videoSize.width;
560
+ config.height = videoSize.height;
561
+ config.minimumFrameInterval = CMTimeMake(1, 30); // 30 FPS
562
+
563
+ // Try a more compatible pixel format
564
+ config.pixelFormat = kCVPixelFormatType_32BGRA;
565
+
566
+ NSLog(@"📐 Stream configuration: %dx%d, FPS=30, cursor=%@", (int)config.width, (int)config.height, captureCursor ? @"YES" : @"NO");
567
+
568
+ if (@available(macOS 13.0, *)) {
569
+ config.capturesAudio = includeSystemAudio;
570
+ config.excludesCurrentProcessAudio = YES;
571
+ NSLog(@"🔊 Audio configuration: capture=%@, excludeProcess=%@", includeSystemAudio ? @"YES" : @"NO", @"YES");
572
+ } else {
573
+ NSLog(@"⚠️ macOS 13.0+ features not available");
574
+ >>>>>>> screencapture
411
575
  }
576
+ config.showsCursor = captureCursor;
412
577
 
578
+ <<<<<<< HEAD
413
579
  @try {
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];
580
+ NSLog(@"[mac_recorder] StopRecording called");
581
+
582
+ // Stop ScreenCaptureKit recording
583
+ NSLog(@"[mac_recorder] Stopping ScreenCaptureKit stream");
584
+ if (@available(macOS 12.3, *)) {
585
+ [ScreenCaptureKitRecorder stopRecording];
586
+ }
422
587
  g_isRecording = false;
588
+ cleanupRecording();
589
+ NSLog(@"[mac_recorder] ScreenCaptureKit stopped");
423
590
  return Napi::Boolean::New(env, true);
424
591
 
425
592
  } @catch (NSException *exception) {
426
593
  cleanupRecording();
594
+ =======
595
+ if (!CGRectIsNull(captureRect)) {
596
+ config.sourceRect = captureRect;
597
+ // Update video size if capture rect is specified
598
+ videoSize = NSMakeSize(captureRect.size.width, captureRect.size.height);
599
+ }
600
+
601
+ // Create delegate
602
+ g_scDelegate = [[SCKRecorderDelegate alloc] init];
603
+ g_scDelegate.outputURL = outputURL;
604
+ g_scDelegate.hasStartTime = NO;
605
+ g_scDelegate.startAttempted = NO;
606
+ g_scDelegate.startFailed = NO;
607
+
608
+ // Setup AVAssetWriter
609
+ NSError *writerError = nil;
610
+ g_scDelegate.assetWriter = [[AVAssetWriter alloc] initWithURL:outputURL fileType:AVFileTypeQuickTimeMovie error:&writerError];
611
+
612
+ if (writerError) {
613
+ NSLog(@"❌ Failed to create asset writer: %@", writerError.localizedDescription);
614
+ >>>>>>> screencapture
427
615
  return Napi::Boolean::New(env, false);
428
616
  }
429
- }
430
-
431
-
432
-
433
- // NAPI Function: Get Windows List
434
- Napi::Value GetWindows(const Napi::CallbackInfo& info) {
435
- Napi::Env env = info.Env();
436
- Napi::Array windowArray = Napi::Array::New(env);
437
617
 
618
+ NSLog(@"✅ Asset writer created successfully");
619
+
620
+ // Video input settings using actual dimensions
621
+ NSLog(@"📺 Setting up video input: %dx%d", (int)videoSize.width, (int)videoSize.height);
622
+ NSDictionary *videoSettings = @{
623
+ AVVideoCodecKey: AVVideoCodecTypeH264,
624
+ AVVideoWidthKey: @((NSInteger)videoSize.width),
625
+ AVVideoHeightKey: @((NSInteger)videoSize.height)
626
+ };
627
+
628
+ g_scDelegate.videoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
629
+ g_scDelegate.videoInput.expectsMediaDataInRealTime = YES;
630
+
631
+ if ([g_scDelegate.assetWriter canAddInput:g_scDelegate.videoInput]) {
632
+ [g_scDelegate.assetWriter addInput:g_scDelegate.videoInput];
633
+ NSLog(@"✅ Video input added to asset writer");
634
+ } else {
635
+ NSLog(@"❌ Cannot add video input to asset writer");
636
+ }
637
+
638
+ <<<<<<< HEAD
438
639
  @try {
439
- // Get window list
440
- CFArrayRef windowList = CGWindowListCopyWindowInfo(
441
- kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
442
- kCGNullWindowID
443
- );
640
+ NSMutableArray *devices = [NSMutableArray array];
444
641
 
445
- if (!windowList) {
446
- return windowArray;
642
+ // Use CoreAudio to get audio devices since we're removing AVFoundation
643
+ AudioObjectPropertyAddress propertyAddress = {
644
+ kAudioHardwarePropertyDevices,
645
+ kAudioObjectPropertyScopeGlobal,
646
+ kAudioObjectPropertyElementMain
647
+ };
648
+
649
+ UInt32 dataSize = 0;
650
+ OSStatus status = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize);
651
+ if (status != noErr) {
652
+ return Napi::Array::New(env, 0);
447
653
  }
448
654
 
449
- CFIndex windowCount = CFArrayGetCount(windowList);
450
- uint32_t arrayIndex = 0;
655
+ UInt32 deviceCount = dataSize / sizeof(AudioDeviceID);
656
+ AudioDeviceID *audioDevices = (AudioDeviceID *)malloc(dataSize);
451
657
 
452
- for (CFIndex i = 0; i < windowCount; i++) {
453
- CFDictionaryRef window = (CFDictionaryRef)CFArrayGetValueAtIndex(windowList, i);
454
-
455
- // Get window ID
456
- CFNumberRef windowIDRef = (CFNumberRef)CFDictionaryGetValue(window, kCGWindowNumber);
457
- if (!windowIDRef) continue;
658
+ status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize, audioDevices);
659
+ if (status != noErr) {
660
+ free(audioDevices);
661
+ return Napi::Array::New(env, 0);
662
+ }
663
+
664
+ for (UInt32 i = 0; i < deviceCount; i++) {
665
+ AudioDeviceID deviceID = audioDevices[i];
458
666
 
459
- uint32_t windowID;
460
- CFNumberGetValue(windowIDRef, kCFNumberSInt32Type, &windowID);
667
+ // Check if device has input streams
668
+ AudioObjectPropertyAddress streamsAddress = {
669
+ kAudioDevicePropertyStreams,
670
+ kAudioDevicePropertyScopeInput,
671
+ kAudioObjectPropertyElementMain
672
+ };
461
673
 
462
- // Get window name
463
- CFStringRef windowNameRef = (CFStringRef)CFDictionaryGetValue(window, kCGWindowName);
464
- std::string windowName = "";
465
- if (windowNameRef) {
466
- const char* windowNameCStr = CFStringGetCStringPtr(windowNameRef, kCFStringEncodingUTF8);
467
- if (windowNameCStr) {
468
- windowName = std::string(windowNameCStr);
469
- } else {
470
- // Fallback for non-ASCII characters
471
- CFIndex length = CFStringGetLength(windowNameRef);
472
- CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
473
- char* buffer = (char*)malloc(maxSize);
474
- if (CFStringGetCString(windowNameRef, buffer, maxSize, kCFStringEncodingUTF8)) {
475
- windowName = std::string(buffer);
476
- }
477
- free(buffer);
478
- }
674
+ UInt32 streamsSize = 0;
675
+ status = AudioObjectGetPropertyDataSize(deviceID, &streamsAddress, 0, NULL, &streamsSize);
676
+ if (status != noErr || streamsSize == 0) {
677
+ continue; // Skip output-only devices
479
678
  }
480
679
 
481
- // Get application name
482
- CFStringRef appNameRef = (CFStringRef)CFDictionaryGetValue(window, kCGWindowOwnerName);
483
- std::string appName = "";
484
- if (appNameRef) {
485
- const char* appNameCStr = CFStringGetCStringPtr(appNameRef, kCFStringEncodingUTF8);
486
- if (appNameCStr) {
487
- appName = std::string(appNameCStr);
488
- } else {
489
- CFIndex length = CFStringGetLength(appNameRef);
490
- CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
491
- char* buffer = (char*)malloc(maxSize);
492
- if (CFStringGetCString(appNameRef, buffer, maxSize, kCFStringEncodingUTF8)) {
493
- appName = std::string(buffer);
494
- }
495
- free(buffer);
496
- }
497
- }
680
+ // Get device name
681
+ AudioObjectPropertyAddress nameAddress = {
682
+ kAudioDevicePropertyDeviceNameCFString,
683
+ kAudioObjectPropertyScopeGlobal,
684
+ kAudioObjectPropertyElementMain
685
+ };
498
686
 
499
- // Get window bounds
500
- CFDictionaryRef boundsRef = (CFDictionaryRef)CFDictionaryGetValue(window, kCGWindowBounds);
501
- CGRect bounds = CGRectZero;
502
- if (boundsRef) {
503
- CGRectMakeWithDictionaryRepresentation(boundsRef, &bounds);
687
+ CFStringRef deviceNameRef = NULL;
688
+ UInt32 nameSize = sizeof(CFStringRef);
689
+ status = AudioObjectGetPropertyData(deviceID, &nameAddress, 0, NULL, &nameSize, &deviceNameRef);
690
+
691
+ NSString *deviceName = @"Unknown Device";
692
+ if (status == noErr && deviceNameRef) {
693
+ deviceName = (__bridge NSString *)deviceNameRef;
504
694
  }
505
695
 
506
- // Skip windows without name or very small windows
507
- if (windowName.empty() || bounds.size.width < 50 || bounds.size.height < 50) {
508
- continue;
696
+ // Get device UID
697
+ AudioObjectPropertyAddress uidAddress = {
698
+ kAudioDevicePropertyDeviceUID,
699
+ kAudioObjectPropertyScopeGlobal,
700
+ kAudioObjectPropertyElementMain
701
+ };
702
+
703
+ CFStringRef deviceUIDRef = NULL;
704
+ UInt32 uidSize = sizeof(CFStringRef);
705
+ status = AudioObjectGetPropertyData(deviceID, &uidAddress, 0, NULL, &uidSize, &deviceUIDRef);
706
+
707
+ NSString *deviceUID = [NSString stringWithFormat:@"%u", deviceID];
708
+ if (status == noErr && deviceUIDRef) {
709
+ deviceUID = (__bridge NSString *)deviceUIDRef;
509
710
  }
510
711
 
511
- // Create window object
512
- Napi::Object windowObj = Napi::Object::New(env);
513
- windowObj.Set("id", Napi::Number::New(env, windowID));
514
- windowObj.Set("name", Napi::String::New(env, windowName));
515
- windowObj.Set("appName", Napi::String::New(env, appName));
516
- windowObj.Set("x", Napi::Number::New(env, bounds.origin.x));
517
- windowObj.Set("y", Napi::Number::New(env, bounds.origin.y));
518
- windowObj.Set("width", Napi::Number::New(env, bounds.size.width));
519
- windowObj.Set("height", Napi::Number::New(env, bounds.size.height));
712
+ // Check if this is the default input device
713
+ AudioObjectPropertyAddress defaultAddress = {
714
+ kAudioHardwarePropertyDefaultInputDevice,
715
+ kAudioObjectPropertyScopeGlobal,
716
+ kAudioObjectPropertyElementMain
717
+ };
718
+
719
+ AudioDeviceID defaultDeviceID = kAudioDeviceUnknown;
720
+ UInt32 defaultSize = sizeof(AudioDeviceID);
721
+ AudioObjectGetPropertyData(kAudioObjectSystemObject, &defaultAddress, 0, NULL, &defaultSize, &defaultDeviceID);
722
+
723
+ BOOL isDefault = (deviceID == defaultDeviceID);
520
724
 
521
- windowArray.Set(arrayIndex++, windowObj);
522
- }
523
-
524
- CFRelease(windowList);
525
- return windowArray;
526
-
527
- } @catch (NSException *exception) {
528
- return windowArray;
529
- }
530
- }
531
-
532
- // NAPI Function: Get Audio Devices
533
- Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
534
- Napi::Env env = info.Env();
535
-
536
- @try {
537
- NSMutableArray *devices = [NSMutableArray array];
538
-
539
- // Get all audio devices
540
- NSArray *audioDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
541
-
542
- for (AVCaptureDevice *device in audioDevices) {
543
725
  [devices addObject:@{
544
- @"id": device.uniqueID,
545
- @"name": device.localizedName,
546
- @"manufacturer": device.manufacturer ?: @"Unknown",
547
- @"isDefault": @([device isEqual:[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]])
726
+ @"id": deviceUID,
727
+ @"name": deviceName,
728
+ @"manufacturer": @"Unknown",
729
+ @"isDefault": @(isDefault)
548
730
  }];
731
+
732
+ if (deviceNameRef) CFRelease(deviceNameRef);
733
+ if (deviceUIDRef) CFRelease(deviceUIDRef);
549
734
  }
550
735
 
736
+ free(audioDevices);
737
+
551
738
  // Convert to NAPI array
552
739
  Napi::Array result = Napi::Array::New(env, devices.count);
553
740
  for (NSUInteger i = 0; i < devices.count; i++) {
@@ -558,338 +745,337 @@ Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
558
745
  deviceObj.Set("manufacturer", Napi::String::New(env, [device[@"manufacturer"] UTF8String]));
559
746
  deviceObj.Set("isDefault", Napi::Boolean::New(env, [device[@"isDefault"] boolValue]));
560
747
  result[i] = deviceObj;
748
+ =======
749
+ // Audio input settings (if needed)
750
+ if (includeSystemAudio) {
751
+ NSDictionary *audioSettings = @{
752
+ AVFormatIDKey: @(kAudioFormatMPEG4AAC),
753
+ AVSampleRateKey: @44100,
754
+ AVNumberOfChannelsKey: @2
755
+ };
756
+
757
+ g_scDelegate.audioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:audioSettings];
758
+ g_scDelegate.audioInput.expectsMediaDataInRealTime = YES;
759
+
760
+ if ([g_scDelegate.assetWriter canAddInput:g_scDelegate.audioInput]) {
761
+ [g_scDelegate.assetWriter addInput:g_scDelegate.audioInput];
561
762
  }
562
-
563
- return result;
564
-
565
- } @catch (NSException *exception) {
566
- return Napi::Array::New(env, 0);
567
763
  }
764
+
765
+ // Create callback queue for the delegate
766
+ dispatch_queue_t delegateQueue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);
767
+
768
+ // Create and start stream first
769
+ g_scStream = [[SCStream alloc] initWithFilter:contentFilter configuration:config delegate:g_scDelegate];
770
+
771
+ // Attach outputs to actually receive sample buffers
772
+ NSLog(@"✅ Setting up stream output callback for sample buffers");
773
+ dispatch_queue_t outputQueue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);
774
+ NSError *outputError = nil;
775
+ BOOL addedScreenOutput = [g_scStream addStreamOutput:g_scDelegate type:SCStreamOutputTypeScreen sampleHandlerQueue:outputQueue error:&outputError];
776
+ if (addedScreenOutput) {
777
+ NSLog(@"✅ Screen output attached to SCStream");
778
+ } else {
779
+ NSLog(@"❌ Failed to attach screen output to SCStream: %@", outputError.localizedDescription);
780
+ }
781
+ if (includeSystemAudio) {
782
+ outputError = nil;
783
+ BOOL addedAudioOutput = [g_scStream addStreamOutput:g_scDelegate type:SCStreamOutputTypeAudio sampleHandlerQueue:outputQueue error:&outputError];
784
+ if (addedAudioOutput) {
785
+ NSLog(@"✅ Audio output attached to SCStream");
786
+ } else {
787
+ NSLog(@"⚠️ Failed to attach audio output to SCStream (audio may be disabled): %@", outputError.localizedDescription);
788
+ >>>>>>> screencapture
789
+ }
790
+ }
791
+
792
+ if (!g_scStream) {
793
+ NSLog(@"❌ Failed to create SCStream");
794
+ return Napi::Boolean::New(env, false);
795
+ }
796
+
797
+ NSLog(@"✅ SCStream created successfully");
798
+
799
+ // Add callback queue for sample buffers (this might be important)
800
+ if (@available(macOS 14.0, *)) {
801
+ // In macOS 14+, we can set a specific queue
802
+ // For now, we'll rely on the default behavior
803
+ }
804
+
805
+ // Start capture and wait for it to begin
806
+ dispatch_semaphore_t startSemaphore = dispatch_semaphore_create(0);
807
+ __block NSError *startError = nil;
808
+
809
+ NSLog(@"🚀 Starting ScreenCaptureKit capture");
810
+ [g_scStream startCaptureWithCompletionHandler:^(NSError * _Nullable error) {
811
+ startError = error;
812
+ dispatch_semaphore_signal(startSemaphore);
813
+ }];
814
+
815
+ dispatch_semaphore_wait(startSemaphore, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC));
816
+
817
+ if (startError) {
818
+ NSLog(@"❌ Failed to start capture: %@", startError.localizedDescription);
819
+ return Napi::Boolean::New(env, false);
820
+ }
821
+
822
+ NSLog(@"✅ ScreenCaptureKit capture started successfully");
823
+
824
+ // Mark that we're ready to write (asset writer will be started in first sample buffer)
825
+ g_scDelegate.isWriting = YES;
826
+ g_isRecording = true;
827
+
828
+ // Wait a moment to see if we get any sample buffers
829
+ NSLog(@"⏱️ Waiting 1 second for sample buffers to arrive...");
830
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
831
+ if (g_scDelegate && !g_scDelegate.hasStartTime) {
832
+ NSLog(@"⚠️ No sample buffers received after 1 second - this might indicate a permission or configuration issue");
833
+ } else if (g_scDelegate && g_scDelegate.hasStartTime) {
834
+ NSLog(@"✅ Sample buffers are being received successfully");
835
+ }
836
+ });
837
+
838
+ NSLog(@"🎬 Recording initialized successfully");
839
+ return Napi::Boolean::New(env, true);
568
840
  }
569
841
 
570
- // NAPI Function: Get Displays
571
- Napi::Value GetDisplays(const Napi::CallbackInfo& info) {
842
+ // NAPI Function: Stop Recording
843
+ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
572
844
  Napi::Env env = info.Env();
573
845
 
574
- @try {
575
- NSArray *displays = [ScreenCapture getAvailableDisplays];
576
- Napi::Array result = Napi::Array::New(env, displays.count);
577
-
578
- NSLog(@"Found %lu displays", (unsigned long)displays.count);
579
-
580
- for (NSUInteger i = 0; i < displays.count; i++) {
581
- NSDictionary *display = displays[i];
582
- NSLog(@"Display %lu: ID=%u, Name=%@, Size=%@x%@",
583
- (unsigned long)i,
584
- [display[@"id"] unsignedIntValue],
585
- display[@"name"],
586
- display[@"width"],
587
- display[@"height"]);
588
-
589
- Napi::Object displayObj = Napi::Object::New(env);
590
- displayObj.Set("id", Napi::Number::New(env, [display[@"id"] unsignedIntValue]));
591
- displayObj.Set("name", Napi::String::New(env, [display[@"name"] UTF8String]));
592
- displayObj.Set("width", Napi::Number::New(env, [display[@"width"] doubleValue]));
593
- displayObj.Set("height", Napi::Number::New(env, [display[@"height"] doubleValue]));
594
- displayObj.Set("x", Napi::Number::New(env, [display[@"x"] doubleValue]));
595
- displayObj.Set("y", Napi::Number::New(env, [display[@"y"] doubleValue]));
596
- displayObj.Set("isPrimary", Napi::Boolean::New(env, [display[@"isPrimary"] boolValue]));
597
- result[i] = displayObj;
598
- }
599
-
600
- return result;
601
-
602
- } @catch (NSException *exception) {
603
- NSLog(@"Exception in GetDisplays: %@", exception);
604
- return Napi::Array::New(env, 0);
846
+ if (!g_isRecording) {
847
+ return Napi::Boolean::New(env, false);
605
848
  }
849
+
850
+ cleanupSCKRecording();
851
+ return Napi::Boolean::New(env, true);
606
852
  }
607
853
 
608
854
  // NAPI Function: Get Recording Status
609
- Napi::Value GetRecordingStatus(const Napi::CallbackInfo& info) {
855
+ Napi::Value IsRecording(const Napi::CallbackInfo& info) {
610
856
  Napi::Env env = info.Env();
611
857
  return Napi::Boolean::New(env, g_isRecording);
612
858
  }
613
859
 
614
- // NAPI Function: Get Window Thumbnail
615
- Napi::Value GetWindowThumbnail(const Napi::CallbackInfo& info) {
860
+ // NAPI Function: Get Displays
861
+ Napi::Value GetDisplays(const Napi::CallbackInfo& info) {
616
862
  Napi::Env env = info.Env();
617
863
 
618
- if (info.Length() < 1) {
619
- Napi::TypeError::New(env, "Window ID is required").ThrowAsJavaScriptException();
620
- return env.Null();
864
+ if (!isScreenCaptureKitAvailable()) {
865
+ // Fallback to legacy method
866
+ return GetAvailableDisplays(info);
621
867
  }
622
868
 
623
- uint32_t windowID = info[0].As<Napi::Number>().Uint32Value();
869
+ // Use ScreenCaptureKit
870
+ dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
871
+ __block SCShareableContent *shareableContent = nil;
872
+ __block NSError *error = nil;
624
873
 
625
- // Optional parameters
626
- int maxWidth = 300; // Default thumbnail width
627
- int maxHeight = 200; // Default thumbnail height
874
+ [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent * _Nullable content, NSError * _Nullable err) {
875
+ shareableContent = content;
876
+ error = err;
877
+ dispatch_semaphore_signal(semaphore);
878
+ }];
628
879
 
629
- if (info.Length() >= 2 && !info[1].IsNull()) {
630
- maxWidth = info[1].As<Napi::Number>().Int32Value();
631
- }
632
- if (info.Length() >= 3 && !info[2].IsNull()) {
633
- maxHeight = info[2].As<Napi::Number>().Int32Value();
880
+ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
881
+
882
+ if (error) {
883
+ NSLog(@"Failed to get displays: %@", error.localizedDescription);
884
+ return Napi::Array::New(env, 0);
634
885
  }
635
886
 
636
- @try {
637
- // Create window image
638
- CGImageRef windowImage = CGWindowListCreateImage(
639
- CGRectNull,
640
- kCGWindowListOptionIncludingWindow,
641
- windowID,
642
- kCGWindowImageBoundsIgnoreFraming | kCGWindowImageShouldBeOpaque
643
- );
644
-
645
- if (!windowImage) {
646
- return env.Null();
647
- }
648
-
649
- // Get original dimensions
650
- size_t originalWidth = CGImageGetWidth(windowImage);
651
- size_t originalHeight = CGImageGetHeight(windowImage);
652
-
653
- // Calculate scaled dimensions maintaining aspect ratio
654
- double scaleX = (double)maxWidth / originalWidth;
655
- double scaleY = (double)maxHeight / originalHeight;
656
- double scale = std::min(scaleX, scaleY);
657
-
658
- size_t thumbnailWidth = (size_t)(originalWidth * scale);
659
- size_t thumbnailHeight = (size_t)(originalHeight * scale);
660
-
661
- // Create scaled image
662
- CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
663
- CGContextRef context = CGBitmapContextCreate(
664
- NULL,
665
- thumbnailWidth,
666
- thumbnailHeight,
667
- 8,
668
- thumbnailWidth * 4,
669
- colorSpace,
670
- kCGImageAlphaPremultipliedLast
671
- );
672
-
673
- if (context) {
674
- CGContextDrawImage(context, CGRectMake(0, 0, thumbnailWidth, thumbnailHeight), windowImage);
675
- CGImageRef thumbnailImage = CGBitmapContextCreateImage(context);
676
-
677
- if (thumbnailImage) {
678
- // Convert to PNG data
679
- NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:thumbnailImage];
680
- NSData *pngData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
681
-
682
- if (pngData) {
683
- // Convert to Base64
684
- NSString *base64String = [pngData base64EncodedStringWithOptions:0];
685
- std::string base64Std = [base64String UTF8String];
686
-
687
- CGImageRelease(thumbnailImage);
688
- CGContextRelease(context);
689
- CGColorSpaceRelease(colorSpace);
690
- CGImageRelease(windowImage);
691
-
692
- return Napi::String::New(env, base64Std);
693
- }
694
-
695
- CGImageRelease(thumbnailImage);
696
- }
697
-
698
- CGContextRelease(context);
699
- }
700
-
701
- CGColorSpaceRelease(colorSpace);
702
- CGImageRelease(windowImage);
703
-
704
- return env.Null();
705
-
706
- } @catch (NSException *exception) {
707
- return env.Null();
887
+ Napi::Array displaysArray = Napi::Array::New(env);
888
+ uint32_t index = 0;
889
+
890
+ for (SCDisplay *display in shareableContent.displays) {
891
+ Napi::Object displayObj = Napi::Object::New(env);
892
+ displayObj.Set("id", Napi::Number::New(env, display.displayID));
893
+ displayObj.Set("width", Napi::Number::New(env, display.width));
894
+ displayObj.Set("height", Napi::Number::New(env, display.height));
895
+ displayObj.Set("frame", Napi::Object::New(env)); // TODO: Add frame details
896
+
897
+ displaysArray.Set(index++, displayObj);
708
898
  }
899
+
900
+ return displaysArray;
709
901
  }
710
902
 
711
- // NAPI Function: Get Display Thumbnail
712
- Napi::Value GetDisplayThumbnail(const Napi::CallbackInfo& info) {
903
+
904
+ // NAPI Function: Get Windows
905
+ Napi::Value GetWindows(const Napi::CallbackInfo& info) {
713
906
  Napi::Env env = info.Env();
714
907
 
715
- if (info.Length() < 1) {
716
- Napi::TypeError::New(env, "Display ID is required").ThrowAsJavaScriptException();
717
- return env.Null();
908
+ if (!isScreenCaptureKitAvailable()) {
909
+ // Use legacy CGWindowList method
910
+ return GetWindowList(info);
718
911
  }
719
912
 
720
- uint32_t displayID = info[0].As<Napi::Number>().Uint32Value();
913
+ // Use ScreenCaptureKit
914
+ dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
915
+ __block SCShareableContent *shareableContent = nil;
916
+ __block NSError *error = nil;
721
917
 
722
- // Optional parameters
723
- int maxWidth = 300; // Default thumbnail width
724
- int maxHeight = 200; // Default thumbnail height
918
+ [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent * _Nullable content, NSError * _Nullable err) {
919
+ shareableContent = content;
920
+ error = err;
921
+ dispatch_semaphore_signal(semaphore);
922
+ }];
725
923
 
726
- if (info.Length() >= 2 && !info[1].IsNull()) {
727
- maxWidth = info[1].As<Napi::Number>().Int32Value();
728
- }
729
- if (info.Length() >= 3 && !info[2].IsNull()) {
730
- maxHeight = info[2].As<Napi::Number>().Int32Value();
924
+ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
925
+
926
+ if (error) {
927
+ NSLog(@"Failed to get windows: %@", error.localizedDescription);
928
+ return Napi::Array::New(env, 0);
731
929
  }
732
930
 
733
- @try {
734
- // Verify display exists
735
- CGDirectDisplayID activeDisplays[32];
736
- uint32_t displayCount;
737
- CGError err = CGGetActiveDisplayList(32, activeDisplays, &displayCount);
738
-
739
- if (err != kCGErrorSuccess) {
740
- NSLog(@"Failed to get active display list: %d", err);
741
- return env.Null();
742
- }
743
-
744
- bool displayFound = false;
745
- for (uint32_t i = 0; i < displayCount; i++) {
746
- if (activeDisplays[i] == displayID) {
747
- displayFound = true;
748
- break;
749
- }
750
- }
751
-
752
- if (!displayFound) {
753
- NSLog(@"Display ID %u not found in active displays", displayID);
754
- return env.Null();
755
- }
756
-
757
- // Create display image
758
- CGImageRef displayImage = CGDisplayCreateImage(displayID);
759
-
760
- if (!displayImage) {
761
- NSLog(@"CGDisplayCreateImage failed for display ID: %u", displayID);
762
- return env.Null();
763
- }
764
-
765
- // Get original dimensions
766
- size_t originalWidth = CGImageGetWidth(displayImage);
767
- size_t originalHeight = CGImageGetHeight(displayImage);
768
-
769
- NSLog(@"Original dimensions: %zux%zu", originalWidth, originalHeight);
770
-
771
- // Calculate scaled dimensions maintaining aspect ratio
772
- double scaleX = (double)maxWidth / originalWidth;
773
- double scaleY = (double)maxHeight / originalHeight;
774
- double scale = std::min(scaleX, scaleY);
775
-
776
- size_t thumbnailWidth = (size_t)(originalWidth * scale);
777
- size_t thumbnailHeight = (size_t)(originalHeight * scale);
778
-
779
- NSLog(@"Thumbnail dimensions: %zux%zu (scale: %f)", thumbnailWidth, thumbnailHeight, scale);
780
-
781
- // Create scaled image
782
- CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
783
- CGContextRef context = CGBitmapContextCreate(
784
- NULL,
785
- thumbnailWidth,
786
- thumbnailHeight,
787
- 8,
788
- thumbnailWidth * 4,
789
- colorSpace,
790
- kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big
791
- );
792
-
793
- if (!context) {
794
- NSLog(@"Failed to create bitmap context");
795
- CGImageRelease(displayImage);
796
- CGColorSpaceRelease(colorSpace);
797
- return env.Null();
798
- }
799
-
800
- // Set interpolation quality for better scaling
801
- CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
802
-
803
- // Draw the image
804
- CGContextDrawImage(context, CGRectMake(0, 0, thumbnailWidth, thumbnailHeight), displayImage);
805
- CGImageRef thumbnailImage = CGBitmapContextCreateImage(context);
806
-
807
- if (!thumbnailImage) {
808
- NSLog(@"Failed to create thumbnail image");
809
- CGContextRelease(context);
810
- CGImageRelease(displayImage);
811
- CGColorSpaceRelease(colorSpace);
812
- return env.Null();
813
- }
814
-
815
- // Convert to PNG data
816
- NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:thumbnailImage];
817
- NSDictionary *properties = @{NSImageCompressionFactor: @0.8};
818
- NSData *pngData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:properties];
819
-
820
- if (!pngData) {
821
- NSLog(@"Failed to convert image to PNG data");
822
- CGImageRelease(thumbnailImage);
823
- CGContextRelease(context);
824
- CGImageRelease(displayImage);
825
- CGColorSpaceRelease(colorSpace);
826
- return env.Null();
931
+ Napi::Array windowsArray = Napi::Array::New(env);
932
+ uint32_t index = 0;
933
+
934
+ for (SCWindow *window in shareableContent.windows) {
935
+ if (window.isOnScreen && window.frame.size.width > 50 && window.frame.size.height > 50) {
936
+ Napi::Object windowObj = Napi::Object::New(env);
937
+ windowObj.Set("id", Napi::Number::New(env, window.windowID));
938
+ windowObj.Set("title", Napi::String::New(env, window.title ? [window.title UTF8String] : ""));
939
+ windowObj.Set("ownerName", Napi::String::New(env, window.owningApplication.applicationName ? [window.owningApplication.applicationName UTF8String] : ""));
940
+ windowObj.Set("bounds", Napi::Object::New(env)); // TODO: Add bounds details
941
+
942
+ windowsArray.Set(index++, windowObj);
827
943
  }
828
-
829
- // Convert to Base64
830
- NSString *base64String = [pngData base64EncodedStringWithOptions:0];
831
- std::string base64Std = [base64String UTF8String];
832
-
833
- NSLog(@"Successfully created thumbnail with base64 length: %lu", (unsigned long)base64Std.length());
834
-
835
- // Cleanup
836
- CGImageRelease(thumbnailImage);
837
- CGContextRelease(context);
838
- CGColorSpaceRelease(colorSpace);
839
- CGImageRelease(displayImage);
840
-
841
- return Napi::String::New(env, base64Std);
842
-
843
- } @catch (NSException *exception) {
844
- NSLog(@"Exception in GetDisplayThumbnail: %@", exception);
845
- return env.Null();
846
944
  }
945
+
946
+ return windowsArray;
847
947
  }
848
948
 
849
949
  // NAPI Function: Check Permissions
850
950
  Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
851
951
  Napi::Env env = info.Env();
852
952
 
953
+ <<<<<<< HEAD
853
954
  @try {
854
- // Check screen recording permission
955
+ // Check screen recording permission using ScreenCaptureKit
855
956
  bool hasScreenPermission = true;
856
957
 
857
- if (@available(macOS 10.15, *)) {
858
- // Try to create a display stream to test permissions
859
- CGDisplayStreamRef stream = CGDisplayStreamCreate(
860
- CGMainDisplayID(),
861
- 1, 1,
862
- kCVPixelFormatType_32BGRA,
863
- nil,
864
- ^(CGDisplayStreamFrameStatus status, uint64_t displayTime, IOSurfaceRef frameSurface, CGDisplayStreamUpdateRef updateRef) {
865
- // Empty handler
866
- }
867
- );
868
-
869
- if (stream) {
870
- CFRelease(stream);
871
- hasScreenPermission = true;
872
- } else {
958
+ if (@available(macOS 12.3, *)) {
959
+ // Try to get shareable content to test ScreenCaptureKit permissions
960
+ @try {
961
+ SCShareableContent *content = [SCShareableContent currentShareableContent];
962
+ hasScreenPermission = (content != nil && content.displays.count > 0);
963
+ } @catch (NSException *exception) {
873
964
  hasScreenPermission = false;
965
+ =======
966
+ // Check screen recording permission
967
+ bool hasPermission = CGPreflightScreenCaptureAccess();
968
+
969
+ // If we don't have permission, try to request it
970
+ if (!hasPermission) {
971
+ NSLog(@"⚠️ Screen recording permission not granted, requesting access");
972
+ bool requestResult = CGRequestScreenCaptureAccess();
973
+ NSLog(@"📋 Permission request result: %@", requestResult ? @"SUCCESS" : @"FAILED");
974
+
975
+ // Check again after request
976
+ hasPermission = CGPreflightScreenCaptureAccess();
977
+ }
978
+
979
+ return Napi::Boolean::New(env, hasPermission);
980
+ }
981
+
982
+ // NAPI Function: Get Audio Devices
983
+ Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
984
+ Napi::Env env = info.Env();
985
+
986
+ Napi::Array devices = Napi::Array::New(env);
987
+ uint32_t index = 0;
988
+
989
+ AudioObjectPropertyAddress propertyAddress = {
990
+ kAudioHardwarePropertyDevices,
991
+ kAudioObjectPropertyScopeGlobal,
992
+ kAudioObjectPropertyElementMain
993
+ };
994
+
995
+ UInt32 dataSize = 0;
996
+ OSStatus status = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize);
997
+
998
+ if (status != noErr) {
999
+ return devices;
1000
+ }
1001
+
1002
+ UInt32 deviceCount = dataSize / sizeof(AudioDeviceID);
1003
+ AudioDeviceID *audioDevices = (AudioDeviceID *)malloc(dataSize);
1004
+
1005
+ status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize, audioDevices);
1006
+
1007
+ if (status == noErr) {
1008
+ for (UInt32 i = 0; i < deviceCount; ++i) {
1009
+ AudioDeviceID deviceID = audioDevices[i];
1010
+
1011
+ // Get device name
1012
+ CFStringRef deviceName = NULL;
1013
+ UInt32 size = sizeof(deviceName);
1014
+ AudioObjectPropertyAddress nameAddress = {
1015
+ kAudioDevicePropertyDeviceNameCFString,
1016
+ kAudioDevicePropertyScopeInput,
1017
+ kAudioObjectPropertyElementMain
1018
+ };
1019
+
1020
+ status = AudioObjectGetPropertyData(deviceID, &nameAddress, 0, NULL, &size, &deviceName);
1021
+
1022
+ if (status == noErr && deviceName) {
1023
+ Napi::Object deviceObj = Napi::Object::New(env);
1024
+ deviceObj.Set("id", Napi::String::New(env, std::to_string(deviceID)));
1025
+
1026
+ const char *name = CFStringGetCStringPtr(deviceName, kCFStringEncodingUTF8);
1027
+ if (name) {
1028
+ deviceObj.Set("name", Napi::String::New(env, name));
1029
+ } else {
1030
+ deviceObj.Set("name", Napi::String::New(env, "Unknown Device"));
1031
+ }
1032
+
1033
+ devices.Set(index++, deviceObj);
1034
+ CFRelease(deviceName);
1035
+ >>>>>>> screencapture
1036
+ }
1037
+ } else {
1038
+ // Fallback for older macOS versions
1039
+ if (@available(macOS 10.15, *)) {
1040
+ // Try to create a display stream to test permissions
1041
+ CGDisplayStreamRef stream = CGDisplayStreamCreate(
1042
+ CGMainDisplayID(),
1043
+ 1, 1,
1044
+ kCVPixelFormatType_32BGRA,
1045
+ nil,
1046
+ ^(CGDisplayStreamFrameStatus status, uint64_t displayTime, IOSurfaceRef frameSurface, CGDisplayStreamUpdateRef updateRef) {
1047
+ // Empty handler
1048
+ }
1049
+ );
1050
+
1051
+ if (stream) {
1052
+ CFRelease(stream);
1053
+ hasScreenPermission = true;
1054
+ } else {
1055
+ hasScreenPermission = false;
1056
+ }
874
1057
  }
875
1058
  }
1059
+ <<<<<<< HEAD
876
1060
 
877
- // Check audio permission
1061
+ // For audio permission, we'll use a simpler check since we're using CoreAudio
878
1062
  bool hasAudioPermission = true;
879
- if (@available(macOS 10.14, *)) {
880
- AVAuthorizationStatus audioStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
881
- hasAudioPermission = (audioStatus == AVAuthorizationStatusAuthorized);
882
- }
883
1063
 
884
1064
  return Napi::Boolean::New(env, hasScreenPermission && hasAudioPermission);
885
1065
 
886
1066
  } @catch (NSException *exception) {
887
1067
  return Napi::Boolean::New(env, false);
1068
+ =======
1069
+ >>>>>>> screencapture
888
1070
  }
1071
+
1072
+ free(audioDevices);
1073
+ return devices;
889
1074
  }
890
1075
 
891
- // Initialize NAPI Module
1076
+ // Initialize the addon
892
1077
  Napi::Object Init(Napi::Env env, Napi::Object exports) {
1078
+ <<<<<<< HEAD
893
1079
  exports.Set(Napi::String::New(env, "startRecording"), Napi::Function::New(env, StartRecording));
894
1080
  exports.Set(Napi::String::New(env, "stopRecording"), Napi::Function::New(env, StopRecording));
895
1081
 
@@ -901,13 +1087,25 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
901
1087
  // ScreenCaptureKit availability (optional for clients)
902
1088
  exports.Set(Napi::String::New(env, "isScreenCaptureKitAvailable"), Napi::Function::New(env, [](const Napi::CallbackInfo& info){
903
1089
  Napi::Env env = info.Env();
904
- bool available = [ScreenCaptureKitRecorder isScreenCaptureKitAvailable];
905
- return Napi::Boolean::New(env, available);
1090
+ if (@available(macOS 12.3, *)) {
1091
+ bool available = [ScreenCaptureKitRecorder isScreenCaptureKitAvailable];
1092
+ return Napi::Boolean::New(env, available);
1093
+ }
1094
+ return Napi::Boolean::New(env, false);
906
1095
  }));
907
1096
 
908
1097
  // Thumbnail functions
909
1098
  exports.Set(Napi::String::New(env, "getWindowThumbnail"), Napi::Function::New(env, GetWindowThumbnail));
910
1099
  exports.Set(Napi::String::New(env, "getDisplayThumbnail"), Napi::Function::New(env, GetDisplayThumbnail));
1100
+ =======
1101
+ exports.Set("startRecording", Napi::Function::New(env, StartRecording));
1102
+ exports.Set("stopRecording", Napi::Function::New(env, StopRecording));
1103
+ exports.Set("isRecording", Napi::Function::New(env, IsRecording));
1104
+ exports.Set("getDisplays", Napi::Function::New(env, GetDisplays));
1105
+ exports.Set("getWindows", Napi::Function::New(env, GetWindows));
1106
+ exports.Set("checkPermissions", Napi::Function::New(env, CheckPermissions));
1107
+ exports.Set("getAudioDevices", Napi::Function::New(env, GetAudioDevices));
1108
+ >>>>>>> screencapture
911
1109
 
912
1110
  // Initialize cursor tracker
913
1111
  InitCursorTracker(env, exports);
@@ -918,4 +1116,4 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
918
1116
  return exports;
919
1117
  }
920
1118
 
921
- NODE_API_MODULE(mac_recorder, Init)
1119
+ NODE_API_MODULE(mac_recorder, Init)