node-mac-recorder 2.1.3 → 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,5 +1,10 @@
1
1
  #import <napi.h>
2
2
  #import <ScreenCaptureKit/ScreenCaptureKit.h>
3
+ <<<<<<< HEAD
4
+ =======
5
+ #import <AVFoundation/AVFoundation.h>
6
+ #import <CoreMedia/CoreMedia.h>
7
+ >>>>>>> screencapture
3
8
  #import <AppKit/AppKit.h>
4
9
  #import <Foundation/Foundation.h>
5
10
  #import <CoreGraphics/CoreGraphics.h>
@@ -16,10 +21,27 @@ Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports);
16
21
  // Window selector function declarations
17
22
  Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports);
18
23
 
24
+ <<<<<<< HEAD
19
25
  @interface MacRecorderDelegate : NSObject
26
+ =======
27
+ // ScreenCaptureKit Recording Delegate
28
+ API_AVAILABLE(macos(12.3))
29
+ @interface SCKRecorderDelegate : NSObject <SCStreamDelegate, SCStreamOutput>
30
+ >>>>>>> screencapture
20
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;
21
42
  @end
22
43
 
44
+ <<<<<<< HEAD
23
45
  @implementation MacRecorderDelegate
24
46
  - (void)recordingDidStart {
25
47
  NSLog(@"[mac_recorder] ScreenCaptureKit recording started");
@@ -32,10 +54,95 @@ Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports);
32
54
  }
33
55
  if (self.completionHandler) {
34
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");
68
+ if (self.completionHandler) {
69
+ self.completionHandler(self.outputURL, error);
70
+ >>>>>>> screencapture
35
71
  }
36
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
+ }
140
+ }
141
+ }
142
+
37
143
  @end
38
144
 
145
+ <<<<<<< HEAD
39
146
  // Global state for recording
40
147
  static MacRecorderDelegate *g_delegate = nil;
41
148
  static bool g_isRecording = false;
@@ -43,25 +150,105 @@ static bool g_isRecording = false;
43
150
  // Helper function to cleanup recording resources
44
151
  void cleanupRecording() {
45
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
46
205
  g_isRecording = false;
47
206
  }
48
207
 
49
- // 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
50
217
  Napi::Value StartRecording(const Napi::CallbackInfo& info) {
51
218
  Napi::Env env = info.Env();
52
219
 
220
+ if (!isScreenCaptureKitAvailable()) {
221
+ NSLog(@"ScreenCaptureKit requires macOS 12.3 or later");
222
+ return Napi::Boolean::New(env, false);
223
+ }
224
+
53
225
  if (info.Length() < 1) {
54
- Napi::TypeError::New(env, "Output path required").ThrowAsJavaScriptException();
55
- return env.Null();
226
+ NSLog(@"Output path required");
227
+ return Napi::Boolean::New(env, false);
56
228
  }
57
229
 
58
230
  if (g_isRecording) {
231
+ NSLog(@"⚠️ Already recording");
59
232
  return Napi::Boolean::New(env, false);
60
233
  }
61
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
+
62
248
  std::string outputPath = info[0].As<Napi::String>().Utf8Value();
63
249
  NSLog(@"[mac_recorder] StartRecording: output=%@", [NSString stringWithUTF8String:outputPath.c_str()]);
64
250
 
251
+ <<<<<<< HEAD
65
252
  // Options parsing (shared)
66
253
  CGRect captureRect = CGRectNull;
67
254
  bool captureCursor = false; // Default olarak cursor gizli
@@ -76,44 +263,29 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
76
263
  NSMutableArray<NSNumber*> *excludedPIDs = [NSMutableArray array];
77
264
  NSMutableArray<NSNumber*> *excludedWindowIds = [NSMutableArray array];
78
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
79
274
 
275
+ // Parse options
80
276
  if (info.Length() > 1 && info[1].IsObject()) {
81
277
  Napi::Object options = info[1].As<Napi::Object>();
82
278
 
83
- // Capture area
84
- if (options.Has("captureArea") && options.Get("captureArea").IsObject()) {
85
- Napi::Object rectObj = options.Get("captureArea").As<Napi::Object>();
86
- if (rectObj.Has("x") && rectObj.Has("y") && rectObj.Has("width") && rectObj.Has("height")) {
87
- captureRect = CGRectMake(
88
- rectObj.Get("x").As<Napi::Number>().DoubleValue(),
89
- rectObj.Get("y").As<Napi::Number>().DoubleValue(),
90
- rectObj.Get("width").As<Napi::Number>().DoubleValue(),
91
- rectObj.Get("height").As<Napi::Number>().DoubleValue()
92
- );
93
- }
94
- }
95
-
96
- // Capture cursor
97
279
  if (options.Has("captureCursor")) {
98
280
  captureCursor = options.Get("captureCursor").As<Napi::Boolean>();
99
281
  }
100
282
 
101
- // Microphone
102
- if (options.Has("includeMicrophone")) {
103
- includeMicrophone = options.Get("includeMicrophone").As<Napi::Boolean>();
104
- }
105
-
106
- // Audio device ID
107
- if (options.Has("audioDeviceId") && !options.Get("audioDeviceId").IsNull()) {
108
- std::string deviceId = options.Get("audioDeviceId").As<Napi::String>().Utf8Value();
109
- audioDeviceId = [NSString stringWithUTF8String:deviceId.c_str()];
110
- }
111
283
 
112
- // System audio
113
284
  if (options.Has("includeSystemAudio")) {
114
285
  includeSystemAudio = options.Get("includeSystemAudio").As<Napi::Boolean>();
115
286
  }
116
287
 
288
+ <<<<<<< HEAD
117
289
  // System audio device ID
118
290
  if (options.Has("systemAudioDeviceId") && !options.Get("systemAudioDeviceId").IsNull()) {
119
291
  std::string sysDeviceId = options.Get("systemAudioDeviceId").As<Napi::String>().Utf8Value();
@@ -158,44 +330,33 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
158
330
  }
159
331
 
160
332
  // Display ID
333
+ =======
334
+ >>>>>>> screencapture
161
335
  if (options.Has("displayId") && !options.Get("displayId").IsNull()) {
162
- double displayIdNum = options.Get("displayId").As<Napi::Number>().DoubleValue();
163
-
164
- // Use the display ID directly (not as an index)
165
- // The JavaScript layer passes the actual CGDirectDisplayID
166
- displayID = (CGDirectDisplayID)displayIdNum;
167
-
168
- // Verify that this display ID is valid
169
- uint32_t displayCount;
170
- CGGetActiveDisplayList(0, NULL, &displayCount);
171
- if (displayCount > 0) {
172
- CGDirectDisplayID *displays = (CGDirectDisplayID*)malloc(displayCount * sizeof(CGDirectDisplayID));
173
- CGGetActiveDisplayList(displayCount, displays, &displayCount);
174
-
175
- bool validDisplay = false;
176
- for (uint32_t i = 0; i < displayCount; i++) {
177
- if (displays[i] == displayID) {
178
- validDisplay = true;
179
- break;
180
- }
181
- }
182
-
183
- if (!validDisplay) {
184
- // Fallback to main display if invalid ID provided
185
- displayID = CGMainDisplayID();
186
- }
187
-
188
- free(displays);
336
+ uint32_t tempDisplayID = options.Get("displayId").As<Napi::Number>().Uint32Value();
337
+ if (tempDisplayID != 0) {
338
+ displayID = tempDisplayID;
189
339
  }
190
340
  }
191
341
 
192
- // Window ID için gelecekte kullanım (şimdilik captureArea ile hallediliyor)
193
342
  if (options.Has("windowId") && !options.Get("windowId").IsNull()) {
194
- // WindowId belirtilmiş ama captureArea JavaScript tarafında ayarlanıyor
195
- // 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
+ }
196
356
  }
197
357
  }
198
358
 
359
+ <<<<<<< HEAD
199
360
  @try {
200
361
  // Always prefer ScreenCaptureKit if available
201
362
  NSLog(@"[mac_recorder] Checking ScreenCaptureKit availability");
@@ -252,17 +413,169 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
252
413
  } @catch (NSException *exception) {
253
414
  cleanupRecording();
254
415
  return Napi::Boolean::New(env, false);
255
- }
256
- }
416
+ =======
417
+ // Create output URL
418
+ NSURL *outputURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:outputPath.c_str()]];
419
+ NSLog(@"📁 Output URL: %@", outputURL.absoluteString);
257
420
 
258
- // NAPI Function: Stop Recording
259
- Napi::Value StopRecording(const Napi::CallbackInfo& info) {
260
- Napi::Env env = info.Env();
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
+ }
462
+
463
+ if (!targetWindow) {
464
+ NSLog(@"Window not found with ID: %u", windowID);
465
+ return Napi::Boolean::New(env, false);
466
+ }
467
+
468
+ contentFilter = [[SCContentFilter alloc] initWithDesktopIndependentWindow:targetWindow];
469
+ } else {
470
+ // Display recording
471
+ NSLog(@"🔍 Selecting display among %lu available displays", (unsigned long)shareableContent.displays.count);
472
+
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);
478
+ }
479
+
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;
486
+ }
487
+ }
488
+
489
+ if (!targetDisplay) {
490
+ NSLog(@"❌ Display not found with ID: %u", displayID);
491
+ }
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);
502
+ }
503
+ } else {
504
+ NSLog(@"✅ Using specified display: ID=%u, %dx%d", targetDisplay.displayID, (int)targetDisplay.width, (int)targetDisplay.height);
505
+ }
506
+
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
+ }
529
+ }
530
+ }
531
+ }
532
+ }
533
+ }
534
+
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
+ }
542
+ }
543
+
544
+ contentFilter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:excluded];
545
+ NSLog(@"✅ Content filter created for display recording");
546
+ >>>>>>> screencapture
547
+ }
261
548
 
549
+ <<<<<<< HEAD
262
550
  if (!g_isRecording) {
263
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
264
575
  }
576
+ config.showsCursor = captureCursor;
265
577
 
578
+ <<<<<<< HEAD
266
579
  @try {
267
580
  NSLog(@"[mac_recorder] StopRecording called");
268
581
 
@@ -278,115 +591,51 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
278
591
 
279
592
  } @catch (NSException *exception) {
280
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
281
615
  return Napi::Boolean::New(env, false);
282
616
  }
283
- }
284
-
285
-
286
-
287
- // NAPI Function: Get Windows List
288
- Napi::Value GetWindows(const Napi::CallbackInfo& info) {
289
- Napi::Env env = info.Env();
290
- Napi::Array windowArray = Napi::Array::New(env);
291
617
 
292
- @try {
293
- // Get window list
294
- CFArrayRef windowList = CGWindowListCopyWindowInfo(
295
- kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
296
- kCGNullWindowID
297
- );
298
-
299
- if (!windowList) {
300
- return windowArray;
301
- }
302
-
303
- CFIndex windowCount = CFArrayGetCount(windowList);
304
- uint32_t arrayIndex = 0;
305
-
306
- for (CFIndex i = 0; i < windowCount; i++) {
307
- CFDictionaryRef window = (CFDictionaryRef)CFArrayGetValueAtIndex(windowList, i);
308
-
309
- // Get window ID
310
- CFNumberRef windowIDRef = (CFNumberRef)CFDictionaryGetValue(window, kCGWindowNumber);
311
- if (!windowIDRef) continue;
312
-
313
- uint32_t windowID;
314
- CFNumberGetValue(windowIDRef, kCFNumberSInt32Type, &windowID);
315
-
316
- // Get window name
317
- CFStringRef windowNameRef = (CFStringRef)CFDictionaryGetValue(window, kCGWindowName);
318
- std::string windowName = "";
319
- if (windowNameRef) {
320
- const char* windowNameCStr = CFStringGetCStringPtr(windowNameRef, kCFStringEncodingUTF8);
321
- if (windowNameCStr) {
322
- windowName = std::string(windowNameCStr);
323
- } else {
324
- // Fallback for non-ASCII characters
325
- CFIndex length = CFStringGetLength(windowNameRef);
326
- CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
327
- char* buffer = (char*)malloc(maxSize);
328
- if (CFStringGetCString(windowNameRef, buffer, maxSize, kCFStringEncodingUTF8)) {
329
- windowName = std::string(buffer);
330
- }
331
- free(buffer);
332
- }
333
- }
334
-
335
- // Get application name
336
- CFStringRef appNameRef = (CFStringRef)CFDictionaryGetValue(window, kCGWindowOwnerName);
337
- std::string appName = "";
338
- if (appNameRef) {
339
- const char* appNameCStr = CFStringGetCStringPtr(appNameRef, kCFStringEncodingUTF8);
340
- if (appNameCStr) {
341
- appName = std::string(appNameCStr);
342
- } else {
343
- CFIndex length = CFStringGetLength(appNameRef);
344
- CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
345
- char* buffer = (char*)malloc(maxSize);
346
- if (CFStringGetCString(appNameRef, buffer, maxSize, kCFStringEncodingUTF8)) {
347
- appName = std::string(buffer);
348
- }
349
- free(buffer);
350
- }
351
- }
352
-
353
- // Get window bounds
354
- CFDictionaryRef boundsRef = (CFDictionaryRef)CFDictionaryGetValue(window, kCGWindowBounds);
355
- CGRect bounds = CGRectZero;
356
- if (boundsRef) {
357
- CGRectMakeWithDictionaryRepresentation(boundsRef, &bounds);
358
- }
359
-
360
- // Skip windows without name or very small windows
361
- if (windowName.empty() || bounds.size.width < 50 || bounds.size.height < 50) {
362
- continue;
363
- }
364
-
365
- // Create window object
366
- Napi::Object windowObj = Napi::Object::New(env);
367
- windowObj.Set("id", Napi::Number::New(env, windowID));
368
- windowObj.Set("name", Napi::String::New(env, windowName));
369
- windowObj.Set("appName", Napi::String::New(env, appName));
370
- windowObj.Set("x", Napi::Number::New(env, bounds.origin.x));
371
- windowObj.Set("y", Napi::Number::New(env, bounds.origin.y));
372
- windowObj.Set("width", Napi::Number::New(env, bounds.size.width));
373
- windowObj.Set("height", Napi::Number::New(env, bounds.size.height));
374
-
375
- windowArray.Set(arrayIndex++, windowObj);
376
- }
377
-
378
- CFRelease(windowList);
379
- return windowArray;
380
-
381
- } @catch (NSException *exception) {
382
- return windowArray;
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");
383
636
  }
384
- }
385
-
386
- // NAPI Function: Get Audio Devices
387
- Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
388
- Napi::Env env = info.Env();
389
637
 
638
+ <<<<<<< HEAD
390
639
  @try {
391
640
  NSMutableArray *devices = [NSMutableArray array];
392
641
 
@@ -496,298 +745,212 @@ Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
496
745
  deviceObj.Set("manufacturer", Napi::String::New(env, [device[@"manufacturer"] UTF8String]));
497
746
  deviceObj.Set("isDefault", Napi::Boolean::New(env, [device[@"isDefault"] boolValue]));
498
747
  result[i] = deviceObj;
499
- }
748
+ =======
749
+ // Audio input settings (if needed)
750
+ if (includeSystemAudio) {
751
+ NSDictionary *audioSettings = @{
752
+ AVFormatIDKey: @(kAudioFormatMPEG4AAC),
753
+ AVSampleRateKey: @44100,
754
+ AVNumberOfChannelsKey: @2
755
+ };
500
756
 
501
- return result;
757
+ g_scDelegate.audioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:audioSettings];
758
+ g_scDelegate.audioInput.expectsMediaDataInRealTime = YES;
502
759
 
503
- } @catch (NSException *exception) {
504
- return Napi::Array::New(env, 0);
760
+ if ([g_scDelegate.assetWriter canAddInput:g_scDelegate.audioInput]) {
761
+ [g_scDelegate.assetWriter addInput:g_scDelegate.audioInput];
762
+ }
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);
505
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);
506
840
  }
507
841
 
508
- // NAPI Function: Get Displays
509
- Napi::Value GetDisplays(const Napi::CallbackInfo& info) {
842
+ // NAPI Function: Stop Recording
843
+ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
510
844
  Napi::Env env = info.Env();
511
845
 
512
- @try {
513
- NSArray *displays = [ScreenCapture getAvailableDisplays];
514
- Napi::Array result = Napi::Array::New(env, displays.count);
515
-
516
- NSLog(@"Found %lu displays", (unsigned long)displays.count);
517
-
518
- for (NSUInteger i = 0; i < displays.count; i++) {
519
- NSDictionary *display = displays[i];
520
- NSLog(@"Display %lu: ID=%u, Name=%@, Size=%@x%@",
521
- (unsigned long)i,
522
- [display[@"id"] unsignedIntValue],
523
- display[@"name"],
524
- display[@"width"],
525
- display[@"height"]);
526
-
527
- Napi::Object displayObj = Napi::Object::New(env);
528
- displayObj.Set("id", Napi::Number::New(env, [display[@"id"] unsignedIntValue]));
529
- displayObj.Set("name", Napi::String::New(env, [display[@"name"] UTF8String]));
530
- displayObj.Set("width", Napi::Number::New(env, [display[@"width"] doubleValue]));
531
- displayObj.Set("height", Napi::Number::New(env, [display[@"height"] doubleValue]));
532
- displayObj.Set("x", Napi::Number::New(env, [display[@"x"] doubleValue]));
533
- displayObj.Set("y", Napi::Number::New(env, [display[@"y"] doubleValue]));
534
- displayObj.Set("isPrimary", Napi::Boolean::New(env, [display[@"isPrimary"] boolValue]));
535
- result[i] = displayObj;
536
- }
537
-
538
- return result;
539
-
540
- } @catch (NSException *exception) {
541
- NSLog(@"Exception in GetDisplays: %@", exception);
542
- return Napi::Array::New(env, 0);
846
+ if (!g_isRecording) {
847
+ return Napi::Boolean::New(env, false);
543
848
  }
849
+
850
+ cleanupSCKRecording();
851
+ return Napi::Boolean::New(env, true);
544
852
  }
545
853
 
546
854
  // NAPI Function: Get Recording Status
547
- Napi::Value GetRecordingStatus(const Napi::CallbackInfo& info) {
855
+ Napi::Value IsRecording(const Napi::CallbackInfo& info) {
548
856
  Napi::Env env = info.Env();
549
857
  return Napi::Boolean::New(env, g_isRecording);
550
858
  }
551
859
 
552
- // NAPI Function: Get Window Thumbnail
553
- Napi::Value GetWindowThumbnail(const Napi::CallbackInfo& info) {
860
+ // NAPI Function: Get Displays
861
+ Napi::Value GetDisplays(const Napi::CallbackInfo& info) {
554
862
  Napi::Env env = info.Env();
555
863
 
556
- if (info.Length() < 1) {
557
- Napi::TypeError::New(env, "Window ID is required").ThrowAsJavaScriptException();
558
- return env.Null();
864
+ if (!isScreenCaptureKitAvailable()) {
865
+ // Fallback to legacy method
866
+ return GetAvailableDisplays(info);
559
867
  }
560
868
 
561
- 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;
562
873
 
563
- // Optional parameters
564
- int maxWidth = 300; // Default thumbnail width
565
- 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
+ }];
566
879
 
567
- if (info.Length() >= 2 && !info[1].IsNull()) {
568
- maxWidth = info[1].As<Napi::Number>().Int32Value();
569
- }
570
- if (info.Length() >= 3 && !info[2].IsNull()) {
571
- 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);
572
885
  }
573
886
 
574
- @try {
575
- // Create window image
576
- CGImageRef windowImage = CGWindowListCreateImage(
577
- CGRectNull,
578
- kCGWindowListOptionIncludingWindow,
579
- windowID,
580
- kCGWindowImageBoundsIgnoreFraming | kCGWindowImageShouldBeOpaque
581
- );
582
-
583
- if (!windowImage) {
584
- return env.Null();
585
- }
586
-
587
- // Get original dimensions
588
- size_t originalWidth = CGImageGetWidth(windowImage);
589
- size_t originalHeight = CGImageGetHeight(windowImage);
590
-
591
- // Calculate scaled dimensions maintaining aspect ratio
592
- double scaleX = (double)maxWidth / originalWidth;
593
- double scaleY = (double)maxHeight / originalHeight;
594
- double scale = std::min(scaleX, scaleY);
595
-
596
- size_t thumbnailWidth = (size_t)(originalWidth * scale);
597
- size_t thumbnailHeight = (size_t)(originalHeight * scale);
598
-
599
- // Create scaled image
600
- CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
601
- CGContextRef context = CGBitmapContextCreate(
602
- NULL,
603
- thumbnailWidth,
604
- thumbnailHeight,
605
- 8,
606
- thumbnailWidth * 4,
607
- colorSpace,
608
- kCGImageAlphaPremultipliedLast
609
- );
610
-
611
- if (context) {
612
- CGContextDrawImage(context, CGRectMake(0, 0, thumbnailWidth, thumbnailHeight), windowImage);
613
- CGImageRef thumbnailImage = CGBitmapContextCreateImage(context);
614
-
615
- if (thumbnailImage) {
616
- // Convert to PNG data
617
- NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:thumbnailImage];
618
- NSData *pngData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
619
-
620
- if (pngData) {
621
- // Convert to Base64
622
- NSString *base64String = [pngData base64EncodedStringWithOptions:0];
623
- std::string base64Std = [base64String UTF8String];
624
-
625
- CGImageRelease(thumbnailImage);
626
- CGContextRelease(context);
627
- CGColorSpaceRelease(colorSpace);
628
- CGImageRelease(windowImage);
629
-
630
- return Napi::String::New(env, base64Std);
631
- }
632
-
633
- CGImageRelease(thumbnailImage);
634
- }
635
-
636
- CGContextRelease(context);
637
- }
638
-
639
- CGColorSpaceRelease(colorSpace);
640
- CGImageRelease(windowImage);
641
-
642
- return env.Null();
643
-
644
- } @catch (NSException *exception) {
645
- 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);
646
898
  }
899
+
900
+ return displaysArray;
647
901
  }
648
902
 
649
- // NAPI Function: Get Display Thumbnail
650
- Napi::Value GetDisplayThumbnail(const Napi::CallbackInfo& info) {
903
+
904
+ // NAPI Function: Get Windows
905
+ Napi::Value GetWindows(const Napi::CallbackInfo& info) {
651
906
  Napi::Env env = info.Env();
652
907
 
653
- if (info.Length() < 1) {
654
- Napi::TypeError::New(env, "Display ID is required").ThrowAsJavaScriptException();
655
- return env.Null();
908
+ if (!isScreenCaptureKitAvailable()) {
909
+ // Use legacy CGWindowList method
910
+ return GetWindowList(info);
656
911
  }
657
912
 
658
- 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;
659
917
 
660
- // Optional parameters
661
- int maxWidth = 300; // Default thumbnail width
662
- 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
+ }];
663
923
 
664
- if (info.Length() >= 2 && !info[1].IsNull()) {
665
- maxWidth = info[1].As<Napi::Number>().Int32Value();
666
- }
667
- if (info.Length() >= 3 && !info[2].IsNull()) {
668
- 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);
669
929
  }
670
930
 
671
- @try {
672
- // Verify display exists
673
- CGDirectDisplayID activeDisplays[32];
674
- uint32_t displayCount;
675
- CGError err = CGGetActiveDisplayList(32, activeDisplays, &displayCount);
676
-
677
- if (err != kCGErrorSuccess) {
678
- NSLog(@"Failed to get active display list: %d", err);
679
- return env.Null();
680
- }
681
-
682
- bool displayFound = false;
683
- for (uint32_t i = 0; i < displayCount; i++) {
684
- if (activeDisplays[i] == displayID) {
685
- displayFound = true;
686
- break;
687
- }
688
- }
689
-
690
- if (!displayFound) {
691
- NSLog(@"Display ID %u not found in active displays", displayID);
692
- return env.Null();
693
- }
694
-
695
- // Create display image
696
- CGImageRef displayImage = CGDisplayCreateImage(displayID);
697
-
698
- if (!displayImage) {
699
- NSLog(@"CGDisplayCreateImage failed for display ID: %u", displayID);
700
- return env.Null();
701
- }
702
-
703
- // Get original dimensions
704
- size_t originalWidth = CGImageGetWidth(displayImage);
705
- size_t originalHeight = CGImageGetHeight(displayImage);
706
-
707
- NSLog(@"Original dimensions: %zux%zu", originalWidth, originalHeight);
708
-
709
- // Calculate scaled dimensions maintaining aspect ratio
710
- double scaleX = (double)maxWidth / originalWidth;
711
- double scaleY = (double)maxHeight / originalHeight;
712
- double scale = std::min(scaleX, scaleY);
713
-
714
- size_t thumbnailWidth = (size_t)(originalWidth * scale);
715
- size_t thumbnailHeight = (size_t)(originalHeight * scale);
716
-
717
- NSLog(@"Thumbnail dimensions: %zux%zu (scale: %f)", thumbnailWidth, thumbnailHeight, scale);
718
-
719
- // Create scaled image
720
- CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
721
- CGContextRef context = CGBitmapContextCreate(
722
- NULL,
723
- thumbnailWidth,
724
- thumbnailHeight,
725
- 8,
726
- thumbnailWidth * 4,
727
- colorSpace,
728
- kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big
729
- );
730
-
731
- if (!context) {
732
- NSLog(@"Failed to create bitmap context");
733
- CGImageRelease(displayImage);
734
- CGColorSpaceRelease(colorSpace);
735
- return env.Null();
736
- }
737
-
738
- // Set interpolation quality for better scaling
739
- CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
740
-
741
- // Draw the image
742
- CGContextDrawImage(context, CGRectMake(0, 0, thumbnailWidth, thumbnailHeight), displayImage);
743
- CGImageRef thumbnailImage = CGBitmapContextCreateImage(context);
744
-
745
- if (!thumbnailImage) {
746
- NSLog(@"Failed to create thumbnail image");
747
- CGContextRelease(context);
748
- CGImageRelease(displayImage);
749
- CGColorSpaceRelease(colorSpace);
750
- return env.Null();
751
- }
752
-
753
- // Convert to PNG data
754
- NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:thumbnailImage];
755
- NSDictionary *properties = @{NSImageCompressionFactor: @0.8};
756
- NSData *pngData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:properties];
757
-
758
- if (!pngData) {
759
- NSLog(@"Failed to convert image to PNG data");
760
- CGImageRelease(thumbnailImage);
761
- CGContextRelease(context);
762
- CGImageRelease(displayImage);
763
- CGColorSpaceRelease(colorSpace);
764
- 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);
765
943
  }
766
-
767
- // Convert to Base64
768
- NSString *base64String = [pngData base64EncodedStringWithOptions:0];
769
- std::string base64Std = [base64String UTF8String];
770
-
771
- NSLog(@"Successfully created thumbnail with base64 length: %lu", (unsigned long)base64Std.length());
772
-
773
- // Cleanup
774
- CGImageRelease(thumbnailImage);
775
- CGContextRelease(context);
776
- CGColorSpaceRelease(colorSpace);
777
- CGImageRelease(displayImage);
778
-
779
- return Napi::String::New(env, base64Std);
780
-
781
- } @catch (NSException *exception) {
782
- NSLog(@"Exception in GetDisplayThumbnail: %@", exception);
783
- return env.Null();
784
944
  }
945
+
946
+ return windowsArray;
785
947
  }
786
948
 
787
949
  // NAPI Function: Check Permissions
788
950
  Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
789
951
  Napi::Env env = info.Env();
790
952
 
953
+ <<<<<<< HEAD
791
954
  @try {
792
955
  // Check screen recording permission using ScreenCaptureKit
793
956
  bool hasScreenPermission = true;
@@ -799,6 +962,77 @@ Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
799
962
  hasScreenPermission = (content != nil && content.displays.count > 0);
800
963
  } @catch (NSException *exception) {
801
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
802
1036
  }
803
1037
  } else {
804
1038
  // Fallback for older macOS versions
@@ -822,6 +1056,7 @@ Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
822
1056
  }
823
1057
  }
824
1058
  }
1059
+ <<<<<<< HEAD
825
1060
 
826
1061
  // For audio permission, we'll use a simpler check since we're using CoreAudio
827
1062
  bool hasAudioPermission = true;
@@ -830,11 +1065,17 @@ Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
830
1065
 
831
1066
  } @catch (NSException *exception) {
832
1067
  return Napi::Boolean::New(env, false);
1068
+ =======
1069
+ >>>>>>> screencapture
833
1070
  }
1071
+
1072
+ free(audioDevices);
1073
+ return devices;
834
1074
  }
835
1075
 
836
- // Initialize NAPI Module
1076
+ // Initialize the addon
837
1077
  Napi::Object Init(Napi::Env env, Napi::Object exports) {
1078
+ <<<<<<< HEAD
838
1079
  exports.Set(Napi::String::New(env, "startRecording"), Napi::Function::New(env, StartRecording));
839
1080
  exports.Set(Napi::String::New(env, "stopRecording"), Napi::Function::New(env, StopRecording));
840
1081
 
@@ -856,6 +1097,15 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
856
1097
  // Thumbnail functions
857
1098
  exports.Set(Napi::String::New(env, "getWindowThumbnail"), Napi::Function::New(env, GetWindowThumbnail));
858
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
859
1109
 
860
1110
  // Initialize cursor tracker
861
1111
  InitCursorTracker(env, exports);
@@ -866,4 +1116,4 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
866
1116
  return exports;
867
1117
  }
868
1118
 
869
- NODE_API_MODULE(mac_recorder, Init)
1119
+ NODE_API_MODULE(mac_recorder, Init)