node-mac-recorder 2.21.22 → 2.21.23

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.
@@ -44,8 +44,8 @@ class ElectronSafeMacRecorder extends EventEmitter {
44
44
  this.options = {
45
45
  includeMicrophone: false,
46
46
  includeSystemAudio: false,
47
- quality: "medium",
48
- frameRate: 30,
47
+ quality: "high",
48
+ frameRate: 60,
49
49
  captureArea: null,
50
50
  captureCursor: false,
51
51
  showClicks: false,
package/index.js CHANGED
@@ -43,8 +43,8 @@ class MacRecorder extends EventEmitter {
43
43
  this.options = {
44
44
  includeMicrophone: false, // Default olarak mikrofon kapalı
45
45
  includeSystemAudio: false, // Default olarak sistem sesi kapalı - kullanıcı explicit olarak açmalı
46
- quality: "medium",
47
- frameRate: 30,
46
+ quality: "high",
47
+ frameRate: 60,
48
48
  captureArea: null, // { x, y, width, height }
49
49
  captureCursor: false, // Default olarak cursor gizli
50
50
  showClicks: false,
@@ -181,6 +181,13 @@ class MacRecorder extends EventEmitter {
181
181
  if (options.captureCamera !== undefined) {
182
182
  this.options.captureCamera = options.captureCamera === true;
183
183
  }
184
+ if (options.frameRate !== undefined) {
185
+ const fps = parseInt(options.frameRate, 10);
186
+ if (!Number.isNaN(fps) && fps > 0) {
187
+ // Clamp reasonable range 1-120
188
+ this.options.frameRate = Math.min(Math.max(fps, 1), 120);
189
+ }
190
+ }
184
191
  if (options.cameraDeviceId !== undefined) {
185
192
  this.options.cameraDeviceId =
186
193
  typeof options.cameraDeviceId === "string" && options.cameraDeviceId.length > 0
@@ -497,6 +504,8 @@ class MacRecorder extends EventEmitter {
497
504
  captureCamera: this.options.captureCamera === true,
498
505
  cameraDeviceId: this.options.cameraDeviceId || null,
499
506
  sessionTimestamp,
507
+ frameRate: this.options.frameRate || 60,
508
+ quality: this.options.quality || "high",
500
509
  };
501
510
 
502
511
  if (cameraFilePath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.21.22",
3
+ "version": "2.21.23",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -34,7 +34,8 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
34
34
  bool includeMicrophone,
35
35
  bool includeSystemAudio,
36
36
  NSString* audioDeviceId,
37
- NSString* audioOutputPath) {
37
+ NSString* audioOutputPath,
38
+ double requestedFrameRate) {
38
39
 
39
40
  if (g_avIsRecording) {
40
41
  NSLog(@"❌ AVFoundation recording already in progress");
@@ -129,15 +130,20 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
129
130
  NSLog(@"🎬 ULTRA QUALITY AVFoundation: %dx%d, bitrate=%.2fMbps",
130
131
  (int)recordingSize.width, (int)recordingSize.height, bitrate / (1000.0 * 1000.0));
131
132
 
133
+ // Resolve target FPS
134
+ double fps = requestedFrameRate > 0 ? requestedFrameRate : 60.0;
135
+ if (fps < 1.0) fps = 1.0;
136
+ if (fps > 120.0) fps = 120.0;
137
+
132
138
  NSDictionary *videoSettings = @{
133
139
  AVVideoCodecKey: codecKey,
134
140
  AVVideoWidthKey: @((int)recordingSize.width),
135
141
  AVVideoHeightKey: @((int)recordingSize.height),
136
142
  AVVideoCompressionPropertiesKey: @{
137
143
  AVVideoAverageBitRateKey: @(bitrate),
138
- AVVideoMaxKeyFrameIntervalKey: @30,
144
+ AVVideoMaxKeyFrameIntervalKey: @((int)fps),
139
145
  AVVideoAllowFrameReorderingKey: @YES,
140
- AVVideoExpectedSourceFrameRateKey: @60,
146
+ AVVideoExpectedSourceFrameRateKey: @((int)fps),
141
147
  AVVideoQualityKey: @(0.95), // 0.0-1.0, higher is better
142
148
  AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
143
149
  AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC
@@ -266,7 +272,7 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
266
272
  }
267
273
  }
268
274
 
269
- // Start capture timer (10 FPS for Electron compatibility)
275
+ // Start capture timer using target FPS
270
276
  dispatch_queue_t captureQueue = dispatch_queue_create("AVFoundationCaptureQueue", DISPATCH_QUEUE_SERIAL);
271
277
  g_avTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, captureQueue);
272
278
 
@@ -275,7 +281,7 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
275
281
  return false;
276
282
  }
277
283
 
278
- uint64_t interval = NSEC_PER_SEC / 10; // 10 FPS for Electron stability
284
+ uint64_t interval = (uint64_t)(NSEC_PER_SEC / fps);
279
285
  dispatch_source_set_timer(g_avTimer, dispatch_time(DISPATCH_TIME_NOW, 0), interval, interval / 10);
280
286
 
281
287
  // Retain objects before passing to block to prevent deallocation
@@ -371,7 +377,7 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
371
377
 
372
378
  // Write frame only if input is ready
373
379
  if (localVideoInput && localVideoInput.readyForMoreMediaData) {
374
- CMTime frameTime = CMTimeAdd(g_avStartTime, CMTimeMakeWithSeconds(g_avFrameNumber / 10.0, 600));
380
+ CMTime frameTime = CMTimeAdd(g_avStartTime, CMTimeMakeWithSeconds(((double)g_avFrameNumber) / fps, 600));
375
381
  BOOL appendSuccess = [localPixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:frameTime];
376
382
  if (appendSuccess) {
377
383
  g_avFrameNumber++;
@@ -401,8 +407,8 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
401
407
  dispatch_resume(g_avTimer);
402
408
  g_avIsRecording = true;
403
409
 
404
- MRLog(@"🎥 AVFoundation recording started: %dx%d @ 10fps",
405
- (int)recordingSize.width, (int)recordingSize.height);
410
+ MRLog(@"🎥 AVFoundation recording started: %dx%d @ %.0ffps",
411
+ (int)recordingSize.width, (int)recordingSize.height, fps);
406
412
 
407
413
  return true;
408
414
 
@@ -121,12 +121,27 @@ static void initializeSafeQueue() {
121
121
  SCDisplay *targetDisplay = nil;
122
122
 
123
123
  if (displayId) {
124
+ // First, try matching by real CGDirectDisplayID
124
125
  for (SCDisplay *display in content.displays) {
125
126
  if (display.displayID == [displayId unsignedIntValue]) {
126
127
  targetDisplay = display;
127
128
  break;
128
129
  }
129
130
  }
131
+
132
+ // If not matched, treat provided value as index (0-based or 1-based)
133
+ if (!targetDisplay && content.displays.count > 0) {
134
+ NSUInteger count = content.displays.count;
135
+ NSUInteger idx0 = (NSUInteger)[displayId unsignedIntValue];
136
+ if (idx0 < count) {
137
+ targetDisplay = content.displays[idx0];
138
+ } else if ([displayId unsignedIntegerValue] > 0) {
139
+ NSUInteger idx1 = [displayId unsignedIntegerValue] - 1;
140
+ if (idx1 < count) {
141
+ targetDisplay = content.displays[idx1];
142
+ }
143
+ }
144
+ }
130
145
  }
131
146
 
132
147
  if (!targetDisplay && content.displays.count > 0) {
@@ -154,9 +169,45 @@ static void initializeSafeQueue() {
154
169
  }
155
170
 
156
171
  // Video configuration
157
- config.width = 1920;
158
- config.height = 1080;
159
- config.minimumFrameInterval = CMTimeMake(1, 30); // 30 FPS
172
+ // Prefer the target display's native resolution when available
173
+ if (filter && [filter isKindOfClass:[SCContentFilter class]]) {
174
+ // Try to infer dimensions from selected display or capture area
175
+ NSDictionary *captureArea = options[@"captureArea"];
176
+ if (captureArea) {
177
+ config.width = (size_t)[captureArea[@"width"] doubleValue];
178
+ config.height = (size_t)[captureArea[@"height"] doubleValue];
179
+ } else {
180
+ // Find the selected display again to get dimensions
181
+ NSNumber *displayId = options[@"displayId"];
182
+ if (displayId) {
183
+ for (SCDisplay *display in content.displays) {
184
+ if (display.displayID == [displayId unsignedIntValue]) {
185
+ config.width = (size_t)display.width;
186
+ config.height = (size_t)display.height;
187
+ break;
188
+ }
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ // Fallback default resolution if not set above
195
+ if (config.width == 0 || config.height == 0) {
196
+ config.width = 1920;
197
+ config.height = 1080;
198
+ }
199
+
200
+ // Frame rate from options (default 60)
201
+ NSInteger fps = 60;
202
+ if (options[@"frameRate"]) {
203
+ NSInteger v = [options[@"frameRate"] integerValue];
204
+ if (v > 0) {
205
+ if (v < 1) v = 1;
206
+ if (v > 120) v = 120;
207
+ fps = v;
208
+ }
209
+ }
210
+ config.minimumFrameInterval = CMTimeMake(1, (int)fps);
160
211
  config.queueDepth = 8;
161
212
 
162
213
  // Capture area if specified
@@ -19,7 +19,8 @@ extern "C" {
19
19
  bool includeMicrophone,
20
20
  bool includeSystemAudio,
21
21
  NSString* audioDeviceId,
22
- NSString* audioOutputPath);
22
+ NSString* audioOutputPath,
23
+ double frameRate);
23
24
  bool stopAVFoundationRecording();
24
25
  bool isAVFoundationRecording();
25
26
  NSString* getAVFoundationAudioPath();
@@ -204,6 +205,7 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
204
205
  NSString *cameraOutputPath = nil;
205
206
  int64_t sessionTimestamp = 0;
206
207
  NSString *audioOutputPath = nil;
208
+ double frameRate = 60.0;
207
209
 
208
210
  if (info.Length() > 1 && info[1].IsObject()) {
209
211
  Napi::Object options = info[1].As<Napi::Object>();
@@ -271,33 +273,57 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
271
273
  if (options.Has("sessionTimestamp") && options.Get("sessionTimestamp").IsNumber()) {
272
274
  sessionTimestamp = options.Get("sessionTimestamp").As<Napi::Number>().Int64Value();
273
275
  }
276
+
277
+ // Frame rate
278
+ if (options.Has("frameRate") && options.Get("frameRate").IsNumber()) {
279
+ double fps = options.Get("frameRate").As<Napi::Number>().DoubleValue();
280
+ if (fps > 0) {
281
+ // Clamp to reasonable range
282
+ if (fps < 1.0) fps = 1.0;
283
+ if (fps > 120.0) fps = 120.0;
284
+ frameRate = fps;
285
+ }
286
+ }
274
287
 
275
- // Display ID
288
+ // Display ID (accepts either real CGDirectDisplayID or index [0-based or 1-based])
276
289
  if (options.Has("displayId") && !options.Get("displayId").IsNull()) {
277
290
  double displayIdNum = options.Get("displayId").As<Napi::Number>().DoubleValue();
278
291
 
279
- // Use the display ID directly (not as an index)
280
- // The JavaScript layer passes the actual CGDirectDisplayID
281
- displayID = (CGDirectDisplayID)displayIdNum;
292
+ // First, assume the provided value is a real CGDirectDisplayID
293
+ CGDirectDisplayID candidateID = (CGDirectDisplayID)displayIdNum;
282
294
 
283
- // Verify that this display ID is valid
284
- uint32_t displayCount;
295
+ // Verify against active displays
296
+ uint32_t displayCount = 0;
285
297
  CGGetActiveDisplayList(0, NULL, &displayCount);
286
298
  if (displayCount > 0) {
287
299
  CGDirectDisplayID *displays = (CGDirectDisplayID*)malloc(displayCount * sizeof(CGDirectDisplayID));
288
300
  CGGetActiveDisplayList(displayCount, displays, &displayCount);
289
301
 
290
- bool validDisplay = false;
302
+ bool matchedByID = false;
291
303
  for (uint32_t i = 0; i < displayCount; i++) {
292
- if (displays[i] == displayID) {
293
- validDisplay = true;
304
+ if (displays[i] == candidateID) {
305
+ matchedByID = true;
306
+ displayID = candidateID;
294
307
  break;
295
308
  }
296
309
  }
297
310
 
298
- if (!validDisplay) {
299
- // Fallback to main display if invalid ID provided
300
- displayID = CGMainDisplayID();
311
+ if (!matchedByID) {
312
+ // Tolerant mapping: allow passing index instead of CGDirectDisplayID
313
+ // Try 0-based index
314
+ int idx0 = (int)displayIdNum;
315
+ if (idx0 >= 0 && idx0 < (int)displayCount) {
316
+ displayID = displays[idx0];
317
+ } else {
318
+ // Try 1-based index (common in user examples)
319
+ int idx1 = (int)displayIdNum - 1;
320
+ if (idx1 >= 0 && idx1 < (int)displayCount) {
321
+ displayID = displays[idx1];
322
+ } else {
323
+ // Fallback to main display
324
+ displayID = CGMainDisplayID();
325
+ }
326
+ }
301
327
  }
302
328
 
303
329
  free(displays);
@@ -400,6 +426,8 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
400
426
  if (sessionTimestamp != 0) {
401
427
  sckConfig[@"sessionTimestamp"] = @(sessionTimestamp);
402
428
  }
429
+ // Pass requested frame rate
430
+ sckConfig[@"frameRate"] = @(frameRate);
403
431
 
404
432
  if (!CGRectIsNull(captureRect)) {
405
433
  sckConfig[@"captureRect"] = @{
@@ -511,7 +539,8 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
511
539
  bool includeMicrophone,
512
540
  bool includeSystemAudio,
513
541
  NSString* audioDeviceId,
514
- NSString* audioOutputPath);
542
+ NSString* audioOutputPath,
543
+ double frameRate);
515
544
 
516
545
  // CRITICAL SYNC FIX: Start camera BEFORE screen recording for perfect sync
517
546
  // This ensures both capture their first frame at approximately the same time
@@ -529,7 +558,8 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
529
558
  // Now start screen recording immediately after camera
530
559
  MRLog(@"🎯 SYNC: Starting screen recording immediately");
531
560
  bool avResult = startAVFoundationRecording(outputPath, displayID, windowID, captureRect,
532
- captureCursor, includeMicrophone, includeSystemAudio, audioDeviceId, audioOutputPath);
561
+ captureCursor, includeMicrophone, includeSystemAudio,
562
+ audioDeviceId, audioOutputPath, frameRate);
533
563
 
534
564
  if (avResult) {
535
565
  MRLog(@"🎥 RECORDING METHOD: AVFoundation");
@@ -32,6 +32,7 @@ static BOOL g_audioWriterStarted = NO;
32
32
 
33
33
  static NSInteger g_configuredSampleRate = 48000;
34
34
  static NSInteger g_configuredChannelCount = 2;
35
+ static NSInteger g_targetFPS = 60;
35
36
 
36
37
  // Frame rate debugging
37
38
  static NSInteger g_frameCount = 0;
@@ -342,9 +343,9 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
342
343
 
343
344
  NSDictionary *compressionProps = @{
344
345
  AVVideoAverageBitRateKey: @(bitrate),
345
- AVVideoMaxKeyFrameIntervalKey: @30,
346
+ AVVideoMaxKeyFrameIntervalKey: @(MAX(1, g_targetFPS)),
346
347
  AVVideoAllowFrameReorderingKey: @YES,
347
- AVVideoExpectedSourceFrameRateKey: @60,
348
+ AVVideoExpectedSourceFrameRateKey: @(MAX(1, g_targetFPS)),
348
349
  AVVideoQualityKey: @(0.95), // 0.0-1.0, higher is better (0.95 = excellent)
349
350
  AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
350
351
  AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC
@@ -524,6 +525,17 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
524
525
  NSString *audioOutputPath = MRNormalizePath(config[@"audioOutputPath"]);
525
526
  NSNumber *sessionTimestampNumber = config[@"sessionTimestamp"];
526
527
 
528
+ // Extract requested frame rate
529
+ NSNumber *frameRateNumber = config[@"frameRate"];
530
+ if (frameRateNumber && [frameRateNumber respondsToSelector:@selector(intValue)]) {
531
+ NSInteger fps = [frameRateNumber intValue];
532
+ if (fps < 1) fps = 1;
533
+ if (fps > 120) fps = 120;
534
+ g_targetFPS = fps;
535
+ } else {
536
+ g_targetFPS = 60;
537
+ }
538
+
527
539
  MRLog(@"🎬 Starting PURE ScreenCaptureKit recording (NO AVFoundation)");
528
540
  MRLog(@"🔧 Config: cursor=%@ mic=%@ system=%@ display=%@ window=%@ crop=%@",
529
541
  captureCursor, includeMicrophone, includeSystemAudio, displayId, windowId, captureRect);
@@ -641,7 +653,7 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
641
653
  SCStreamConfiguration *streamConfig = [[SCStreamConfiguration alloc] init];
642
654
  streamConfig.width = recordingWidth;
643
655
  streamConfig.height = recordingHeight;
644
- streamConfig.minimumFrameInterval = CMTimeMake(1, 60); // 60 FPS for smooth recording
656
+ streamConfig.minimumFrameInterval = CMTimeMake(1, (int)MAX(1, g_targetFPS));
645
657
  streamConfig.pixelFormat = kCVPixelFormatType_32BGRA;
646
658
  streamConfig.scalesToFit = NO;
647
659
 
@@ -650,7 +662,7 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
650
662
  streamConfig.queueDepth = 8; // Larger queue for smoother capture
651
663
  }
652
664
 
653
- MRLog(@"🎬 ScreenCaptureKit config: %ldx%ld @ 60fps", (long)recordingWidth, (long)recordingHeight);
665
+ MRLog(@"🎬 ScreenCaptureKit config: %ldx%ld @ %ldfps", (long)recordingWidth, (long)recordingHeight, (long)g_targetFPS);
654
666
 
655
667
  BOOL shouldCaptureMic = includeMicrophone ? [includeMicrophone boolValue] : NO;
656
668
  BOOL shouldCaptureSystemAudio = includeSystemAudio ? [includeSystemAudio boolValue] : NO;
@@ -735,8 +747,8 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
735
747
  BOOL shouldShowCursor = captureCursor ? [captureCursor boolValue] : YES;
736
748
  streamConfig.showsCursor = shouldShowCursor;
737
749
 
738
- MRLog(@"🎥 Pure ScreenCapture config: %ldx%ld @ 30fps, cursor=%d",
739
- recordingWidth, recordingHeight, shouldShowCursor);
750
+ MRLog(@"🎥 Pure ScreenCapture config: %ldx%ld @ %ldfps, cursor=%d",
751
+ recordingWidth, recordingHeight, (long)g_targetFPS, shouldShowCursor);
740
752
 
741
753
  NSError *writerError = nil;
742
754
  if (![ScreenCaptureKitRecorder prepareVideoWriterWithWidth:recordingWidth height:recordingHeight error:&writerError]) {