node-mac-recorder 2.21.41 → 2.21.43

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.
@@ -160,8 +160,13 @@ static bool startCameraWithConfirmation(bool captureCamera,
160
160
  if (!startCameraIfRequested(true, cameraOutputPathRef, cameraDeviceId, screenOutputPath, sessionTimestampMs)) {
161
161
  return false;
162
162
  }
163
- double cameraWaitTimeout = 3.0;
163
+ double cameraWaitTimeout = 8.0; // allow slower devices (e.g., Continuity) to spin up
164
164
  if (!waitForCameraRecordingStart(cameraWaitTimeout)) {
165
+ double cameraStartTs = currentCameraRecordingStartTime();
166
+ if (cameraStartTs > 0 || isCameraRecording()) {
167
+ MRLog(@"⚠️ Camera did not confirm start within %.1fs but appears to be running; continuing", cameraWaitTimeout);
168
+ return true;
169
+ }
165
170
  MRLog(@"❌ Camera did not signal recording start within %.1fs", cameraWaitTimeout);
166
171
  stopCameraRecording();
167
172
  return false;
@@ -238,6 +243,11 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
238
243
  cleanupRecording();
239
244
  MRStoreActiveStopLimit(-1.0);
240
245
 
246
+ // CRITICAL FIX: Reset sync stop limit to prevent consecutive recording issues
247
+ // The sync stop limit persists from previous recording and causes camera to stop early
248
+ MRSyncSetStopLimitSeconds(-1.0);
249
+ MRLog(@"✅ Stop limit reset to unlimited for new recording");
250
+
241
251
  if (g_isRecording) {
242
252
  MRLog(@"⚠️ Still recording after cleanup - forcing stop");
243
253
  return Napi::Boolean::New(env, false);
@@ -270,6 +280,7 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
270
280
  int64_t sessionTimestamp = 0;
271
281
  NSString *audioOutputPath = nil;
272
282
  double frameRate = 60.0;
283
+ NSString *qualityPreset = @"high";
273
284
  bool mixAudio = true; // Default: mix mic+system into single track when possible
274
285
  double mixMicGain = 0.8; // default mic priority
275
286
  double mixSystemGain = 0.4; // default system lower
@@ -353,6 +364,18 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
353
364
  }
354
365
  }
355
366
 
367
+ // Quality preset (low, medium, high)
368
+ if (options.Has("quality") && options.Get("quality").IsString()) {
369
+ std::string qualityStd = options.Get("quality").As<Napi::String>().Utf8Value();
370
+ NSString *qualityStr = [NSString stringWithUTF8String:qualityStd.c_str()];
371
+ if (qualityStr && [qualityStr length] > 0) {
372
+ NSString *normalized = [[qualityStr stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] lowercaseString];
373
+ if ([normalized length] > 0) {
374
+ qualityPreset = normalized;
375
+ }
376
+ }
377
+ }
378
+
356
379
  // Optional: allow caller to prefer ScreenCaptureKit when available (macOS 15+)
357
380
  if (options.Has("preferScreenCaptureKit")) {
358
381
  preferScreenCaptureKitOption = options.Get("preferScreenCaptureKit").As<Napi::Boolean>();
@@ -566,6 +589,8 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
566
589
  sckConfig[@"includeMicrophone"] = @((screenCaptureSupportsMic && captureMicrophone) ? YES : NO);
567
590
  sckConfig[@"audioDeviceId"] = audioDeviceId;
568
591
  sckConfig[@"outputPath"] = [NSString stringWithUTF8String:outputPath.c_str()];
592
+ // Let ScreenCaptureKit know camera is active so it can adjust FPS/resource usage
593
+ sckConfig[@"captureCamera"] = @(captureCamera);
569
594
  if (audioOutputPath) {
570
595
  sckConfig[@"audioOutputPath"] = audioOutputPath;
571
596
  }
@@ -580,6 +605,7 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
580
605
  sckConfig[@"mixAudio"] = @(mixAudio);
581
606
  sckConfig[@"mixMicGain"] = @((double)mixMicGain);
582
607
  sckConfig[@"mixSystemGain"] = @((double)mixSystemGain);
608
+ sckConfig[@"quality"] = qualityPreset ?: @"high";
583
609
 
584
610
  if (!CGRectIsNull(captureRect)) {
585
611
  sckConfig[@"captureRect"] = @{
@@ -162,9 +162,74 @@ static float g_mixSystemGain = 0.4f;
162
162
  static NSInteger g_configuredSampleRate = 48000;
163
163
  static NSInteger g_configuredChannelCount = 2;
164
164
  static NSInteger g_targetFPS = 60;
165
+ static NSString *g_qualityPreset = @"high";
165
166
  static NSInteger g_frameCount = 0;
166
167
  static CFAbsoluteTime g_firstFrameTime = 0;
167
168
 
169
+ // Quality helpers
170
+ static NSString *SCKNormalizeQualityPreset(id preset) {
171
+ if (![preset isKindOfClass:[NSString class]]) {
172
+ return @"high";
173
+ }
174
+ NSString *trimmed = [[(NSString *)preset stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] lowercaseString];
175
+ if ([trimmed length] == 0) {
176
+ return @"high";
177
+ }
178
+ if ([trimmed isEqualToString:@"low"] || [trimmed isEqualToString:@"medium"] || [trimmed isEqualToString:@"high"]) {
179
+ return trimmed;
180
+ }
181
+ return @"high";
182
+ }
183
+
184
+ static CGFloat SCKQualityScaleForPreset(NSString *preset) {
185
+ NSString *normalized = SCKNormalizeQualityPreset(preset);
186
+ if ([normalized isEqualToString:@"low"]) {
187
+ return 0.5;
188
+ }
189
+ if ([normalized isEqualToString:@"medium"]) {
190
+ return 0.75;
191
+ }
192
+ return 1.0; // High = full resolution
193
+ }
194
+
195
+ static void SCKQualityBitrateForDimensions(NSString *preset,
196
+ NSInteger width,
197
+ NSInteger height,
198
+ NSInteger *bitrateOut,
199
+ NSInteger *multiplierOut,
200
+ NSInteger *minOut,
201
+ NSInteger *maxOut) {
202
+ NSString *normalized = SCKNormalizeQualityPreset(preset);
203
+
204
+ NSInteger multiplier = 30;
205
+ NSInteger minBitrate = 30 * 1000 * 1000;
206
+ NSInteger maxBitrate = 120 * 1000 * 1000;
207
+
208
+ if ([normalized isEqualToString:@"low"]) {
209
+ multiplier = 10;
210
+ minBitrate = 10 * 1000 * 1000;
211
+ maxBitrate = 45 * 1000 * 1000;
212
+ } else if ([normalized isEqualToString:@"medium"]) {
213
+ multiplier = 18;
214
+ minBitrate = 18 * 1000 * 1000;
215
+ maxBitrate = 80 * 1000 * 1000;
216
+ } else { // high/default
217
+ multiplier = 45;
218
+ minBitrate = 50 * 1000 * 1000;
219
+ maxBitrate = 200 * 1000 * 1000;
220
+ }
221
+
222
+ double base = ((double)MAX(1, width)) * ((double)MAX(1, height)) * (double)multiplier;
223
+ NSInteger bitrate = (NSInteger)base;
224
+ if (bitrate < minBitrate) bitrate = minBitrate;
225
+ if (bitrate > maxBitrate) bitrate = maxBitrate;
226
+
227
+ if (bitrateOut) *bitrateOut = bitrate;
228
+ if (multiplierOut) *multiplierOut = multiplier;
229
+ if (minOut) *minOut = minBitrate;
230
+ if (maxOut) *maxOut = maxBitrate;
231
+ }
232
+
168
233
  static dispatch_queue_t ScreenCaptureControlQueue(void);
169
234
  static void SCKMarkSchedulingComplete(void);
170
235
  static void SCKFailScheduling(void);
@@ -705,23 +770,35 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
705
770
  return NO;
706
771
  }
707
772
 
708
- // QUALITY FIX: ULTRA HIGH quality for screen recording
709
- // ProMotion displays may run at 10Hz (low power) = 10 FPS capture
710
- // Solution: Use VERY HIGH bitrate so each frame is perfect quality
711
- // Use 30x multiplier for ULTRA quality (was 6x - way too low!)
712
- NSInteger bitrate = (NSInteger)(width * height * 30);
713
- bitrate = MAX(bitrate, 30 * 1000 * 1000); // Minimum 30 Mbps for crystal clear screen recording
714
- bitrate = MIN(bitrate, 120 * 1000 * 1000); // Maximum 120 Mbps for ultra quality
715
-
716
- MRLog(@"🎬 ULTRA QUALITY Screen encoder: %ldx%ld, bitrate=%.2fMbps",
717
- (long)width, (long)height, bitrate / (1000.0 * 1000.0));
773
+ NSInteger bitrate = 0;
774
+ NSInteger bitrateMultiplier = 0;
775
+ NSInteger minBitrate = 0;
776
+ NSInteger maxBitrate = 0;
777
+ NSString *normalizedQuality = SCKNormalizeQualityPreset(g_qualityPreset);
778
+ SCKQualityBitrateForDimensions(normalizedQuality, width, height, &bitrate, &bitrateMultiplier, &minBitrate, &maxBitrate);
779
+
780
+ NSNumber *qualityHint = @0.95;
781
+ if ([normalizedQuality isEqualToString:@"medium"]) {
782
+ qualityHint = @0.9;
783
+ } else if ([normalizedQuality isEqualToString:@"low"]) {
784
+ qualityHint = @0.85;
785
+ }
786
+
787
+ MRLog(@"🎬 Screen encoder (%@): %ldx%ld, multiplier=%ld, bitrate=%.2fMbps (min=%ldMbps max=%ldMbps)",
788
+ normalizedQuality,
789
+ (long)width,
790
+ (long)height,
791
+ (long)bitrateMultiplier,
792
+ bitrate / (1000.0 * 1000.0),
793
+ (long)(minBitrate / (1000 * 1000)),
794
+ (long)(maxBitrate / (1000 * 1000)));
718
795
 
719
796
  NSDictionary *compressionProps = @{
720
797
  AVVideoAverageBitRateKey: @(bitrate),
721
798
  AVVideoMaxKeyFrameIntervalKey: @(MAX(1, g_targetFPS)),
722
799
  AVVideoAllowFrameReorderingKey: @YES,
723
800
  AVVideoExpectedSourceFrameRateKey: @(MAX(1, g_targetFPS)),
724
- AVVideoQualityKey: @(0.95), // 0.0-1.0, higher is better (0.95 = excellent)
801
+ AVVideoQualityKey: qualityHint, // 0.0-1.0, higher is better
725
802
  AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
726
803
  AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC
727
804
  };
@@ -1250,6 +1327,8 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1250
1327
  if (g_mixSystemGain < 0.f) g_mixSystemGain = 0.f;
1251
1328
  if (g_mixSystemGain > 2.f) g_mixSystemGain = 2.f;
1252
1329
  }
1330
+ g_qualityPreset = SCKNormalizeQualityPreset(config[@"quality"]);
1331
+ MRLog(@"🎚️ Requested quality preset: %@", g_qualityPreset);
1253
1332
  NSNumber *captureCamera = config[@"captureCamera"];
1254
1333
 
1255
1334
  if (frameRateNumber && [frameRateNumber respondsToSelector:@selector(intValue)]) {
@@ -1366,6 +1445,26 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1366
1445
  }
1367
1446
  }
1368
1447
 
1448
+ CGFloat qualityScale = SCKQualityScaleForPreset(g_qualityPreset);
1449
+ if (qualityScale < 0.999 && recordingWidth > 0 && recordingHeight > 0) {
1450
+ NSInteger scaledWidth = MAX(1, (NSInteger)((double)recordingWidth * qualityScale + 0.5));
1451
+ NSInteger scaledHeight = MAX(1, (NSInteger)((double)recordingHeight * qualityScale + 0.5));
1452
+ MRLog(@"🎚️ Quality '%@': scaling output to %ldx%ld (%.0f%% of source %ldx%ld)",
1453
+ g_qualityPreset,
1454
+ (long)scaledWidth,
1455
+ (long)scaledHeight,
1456
+ qualityScale * 100.0,
1457
+ (long)recordingWidth,
1458
+ (long)recordingHeight);
1459
+ recordingWidth = scaledWidth;
1460
+ recordingHeight = scaledHeight;
1461
+ } else {
1462
+ MRLog(@"🎚️ Quality '%@': using full resolution %ldx%ld",
1463
+ g_qualityPreset,
1464
+ (long)recordingWidth,
1465
+ (long)recordingHeight);
1466
+ }
1467
+
1369
1468
  SCStreamConfiguration *streamConfig = [[SCStreamConfiguration alloc] init];
1370
1469
  streamConfig.width = recordingWidth;
1371
1470
  streamConfig.height = recordingHeight;
@@ -1 +0,0 @@
1
- []
@@ -1 +0,0 @@
1
- []
package/cursor-data.json DELETED
@@ -1 +0,0 @@
1
- [{"x":1151,"y":726,"timestamp":20,"cursorType":"text","type":"move"}