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.
- package/.claude/settings.local.json +4 -1
- package/package.json +2 -3
- package/src/camera_recorder.mm +429 -880
- package/src/mac_recorder.mm +27 -1
- package/src/screen_capture_kit.mm +110 -11
- package/cursor-data-1751364226346.json +0 -1
- package/cursor-data-1751364314136.json +0 -1
- package/cursor-data.json +0 -1
package/src/mac_recorder.mm
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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:
|
|
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"}
|