node-mac-recorder 2.22.5 → 2.22.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.22.5",
3
+ "version": "2.22.7",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -800,7 +800,10 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
800
800
  AVVideoExpectedSourceFrameRateKey: @(MAX(1, g_targetFPS)),
801
801
  AVVideoQualityKey: qualityHint, // 0.0-1.0, higher is better
802
802
  AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
803
- AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC
803
+ AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC,
804
+ // Add data rate limits for consistent quality during motion
805
+ AVVideoAverageNonDroppableFrameRateKey: @(MAX(1, g_targetFPS)),
806
+ AVVideoMaxKeyFrameIntervalDurationKey: @(1.0) // Keyframe at least every second
804
807
  };
805
808
 
806
809
  NSDictionary *videoSettings = @{
@@ -1383,11 +1386,46 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1383
1386
  }
1384
1387
 
1385
1388
  if (targetWindow && targetApp) {
1386
- MRLog(@"🪟 Recording window: %@ (%ux%u)",
1387
- targetWindow.title, (unsigned)targetWindow.frame.size.width, (unsigned)targetWindow.frame.size.height);
1389
+ CGFloat windowLogicalWidth = targetWindow.frame.size.width;
1390
+ CGFloat windowLogicalHeight = targetWindow.frame.size.height;
1391
+
1392
+ // Find display containing this window to get scale factor
1393
+ CGFloat scaleFactor = 1.0;
1394
+ CGPoint windowCenter = CGPointMake(
1395
+ targetWindow.frame.origin.x + windowLogicalWidth / 2.0,
1396
+ targetWindow.frame.origin.y + windowLogicalHeight / 2.0
1397
+ );
1398
+ for (SCDisplay *disp in content.displays) {
1399
+ CGRect dispBounds = CGRectMake(disp.frame.origin.x, disp.frame.origin.y,
1400
+ disp.frame.size.width, disp.frame.size.height);
1401
+ if (CGRectContainsPoint(dispBounds, windowCenter)) {
1402
+ NSInteger physW = (NSInteger)CGDisplayPixelsWide(disp.displayID);
1403
+ NSInteger physH = (NSInteger)CGDisplayPixelsHigh(disp.displayID);
1404
+ CGFloat scX = (disp.width > 0) ? (CGFloat)physW / (CGFloat)disp.width : 1.0;
1405
+ CGFloat scY = (disp.height > 0) ? (CGFloat)physH / (CGFloat)disp.height : 1.0;
1406
+ scaleFactor = MAX(scX, scY);
1407
+ break;
1408
+ }
1409
+ }
1410
+ // Fallback: use main display scale factor
1411
+ if (scaleFactor == 1.0 && content.displays.count > 0) {
1412
+ SCDisplay *mainDisp = content.displays.firstObject;
1413
+ NSInteger physW = (NSInteger)CGDisplayPixelsWide(mainDisp.displayID);
1414
+ NSInteger physH = (NSInteger)CGDisplayPixelsHigh(mainDisp.displayID);
1415
+ CGFloat scX = (mainDisp.width > 0) ? (CGFloat)physW / (CGFloat)mainDisp.width : 1.0;
1416
+ CGFloat scY = (mainDisp.height > 0) ? (CGFloat)physH / (CGFloat)mainDisp.height : 1.0;
1417
+ scaleFactor = MAX(scX, scY);
1418
+ }
1419
+
1420
+ NSInteger physicalWindowWidth = (NSInteger)(windowLogicalWidth * scaleFactor);
1421
+ NSInteger physicalWindowHeight = (NSInteger)(windowLogicalHeight * scaleFactor);
1422
+
1423
+ MRLog(@"🪟 Recording window: %@ logical=%ux%u, physical=%ldx%ld, scale=%.2fx",
1424
+ targetWindow.title, (unsigned)windowLogicalWidth, (unsigned)windowLogicalHeight,
1425
+ (long)physicalWindowWidth, (long)physicalWindowHeight, scaleFactor);
1388
1426
  filter = [[SCContentFilter alloc] initWithDesktopIndependentWindow:targetWindow];
1389
- recordingWidth = (NSInteger)targetWindow.frame.size.width;
1390
- recordingHeight = (NSInteger)targetWindow.frame.size.height;
1427
+ recordingWidth = physicalWindowWidth;
1428
+ recordingHeight = physicalWindowHeight;
1391
1429
  } else {
1392
1430
  NSLog(@"❌ Window ID %@ not found", windowId);
1393
1431
  SCKFailScheduling();
@@ -1421,11 +1459,24 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1421
1459
  }
1422
1460
 
1423
1461
  if (targetDisplay) {
1424
- MRLog(@"🖥️ Recording display %u (%dx%d)",
1425
- targetDisplay.displayID, (int)targetDisplay.width, (int)targetDisplay.height);
1462
+ // Use physical pixel dimensions for Retina displays (not logical points)
1463
+ CGDirectDisplayID displayID = targetDisplay.displayID;
1464
+ NSInteger physicalWidth = (NSInteger)CGDisplayPixelsWide(displayID);
1465
+ NSInteger physicalHeight = (NSInteger)CGDisplayPixelsHigh(displayID);
1466
+ NSInteger logicalWidth = targetDisplay.width;
1467
+ NSInteger logicalHeight = targetDisplay.height;
1468
+
1469
+ CGFloat scaleX = (logicalWidth > 0) ? (CGFloat)physicalWidth / (CGFloat)logicalWidth : 1.0;
1470
+ CGFloat scaleY = (logicalHeight > 0) ? (CGFloat)physicalHeight / (CGFloat)logicalHeight : 1.0;
1471
+ CGFloat scaleFactor = MAX(scaleX, scaleY);
1472
+
1473
+ MRLog(@"🖥️ Recording display %u: logical=%dx%d, physical=%ldx%ld, scale=%.2fx",
1474
+ displayID, (int)logicalWidth, (int)logicalHeight,
1475
+ (long)physicalWidth, (long)physicalHeight, scaleFactor);
1476
+
1426
1477
  filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:@[]];
1427
- recordingWidth = targetDisplay.width;
1428
- recordingHeight = targetDisplay.height;
1478
+ recordingWidth = physicalWidth;
1479
+ recordingHeight = physicalHeight;
1429
1480
  } else {
1430
1481
  NSLog(@"❌ No display available");
1431
1482
  SCKFailScheduling();
@@ -1437,11 +1488,22 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1437
1488
  CGFloat cropWidth = [captureRect[@"width"] doubleValue];
1438
1489
  CGFloat cropHeight = [captureRect[@"height"] doubleValue];
1439
1490
  if (cropWidth > 0 && cropHeight > 0) {
1440
- MRLog(@"🔲 Crop area specified: %.0fx%.0f at (%.0f,%.0f)",
1441
- cropWidth, cropHeight,
1491
+ // Scale crop dimensions for Retina displays
1492
+ CGFloat cropScaleFactor = 1.0;
1493
+ if (targetDisplay) {
1494
+ NSInteger physW = (NSInteger)CGDisplayPixelsWide(targetDisplay.displayID);
1495
+ NSInteger physH = (NSInteger)CGDisplayPixelsHigh(targetDisplay.displayID);
1496
+ CGFloat scX = (targetDisplay.width > 0) ? (CGFloat)physW / (CGFloat)targetDisplay.width : 1.0;
1497
+ CGFloat scY = (targetDisplay.height > 0) ? (CGFloat)physH / (CGFloat)targetDisplay.height : 1.0;
1498
+ cropScaleFactor = MAX(scX, scY);
1499
+ }
1500
+ NSInteger physicalCropWidth = (NSInteger)(cropWidth * cropScaleFactor);
1501
+ NSInteger physicalCropHeight = (NSInteger)(cropHeight * cropScaleFactor);
1502
+ MRLog(@"🔲 Crop area: logical=%.0fx%.0f, physical=%ldx%ld, scale=%.2fx at (%.0f,%.0f)",
1503
+ cropWidth, cropHeight, (long)physicalCropWidth, (long)physicalCropHeight, cropScaleFactor,
1442
1504
  [captureRect[@"x"] doubleValue], [captureRect[@"y"] doubleValue]);
1443
- recordingWidth = (NSInteger)cropWidth;
1444
- recordingHeight = (NSInteger)cropHeight;
1505
+ recordingWidth = physicalCropWidth;
1506
+ recordingHeight = physicalCropHeight;
1445
1507
  }
1446
1508
  }
1447
1509
 
@@ -1474,6 +1536,13 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1474
1536
  if (@available(macOS 13.0, *)) {
1475
1537
  streamConfig.queueDepth = 8;
1476
1538
  }
1539
+ if (@available(macOS 14.0, *)) {
1540
+ // Use best capture resolution for maximum quality on Retina displays
1541
+ streamConfig.captureResolution = SCCaptureResolutionBest;
1542
+ // Make stream opaque to avoid alpha channel overhead
1543
+ streamConfig.shouldBeOpaque = YES;
1544
+ MRLog(@"🎯 Using SCCaptureResolutionBest + shouldBeOpaque for maximum quality (macOS 14+)");
1545
+ }
1477
1546
 
1478
1547
  BOOL shouldCaptureMic = includeMicrophone ? [includeMicrophone boolValue] : NO;
1479
1548
  BOOL shouldCaptureSystemAudio = includeSystemAudio ? [includeSystemAudio boolValue] : NO;
@@ -1,80 +0,0 @@
1
- const MacRecorder = require('./index.js');
2
- const path = require('path');
3
- const fs = require('fs');
4
-
5
- // Test output directory
6
- const outputDir = path.join(__dirname, 'test-output');
7
- if (!fs.existsSync(outputDir)) {
8
- fs.mkdirSync(outputDir, { recursive: true });
9
- }
10
-
11
- const timestamp = Date.now();
12
- const outputPath = path.join(outputDir, `noise-reduction-test-${timestamp}.mov`);
13
-
14
- console.log('🎙️ Starting microphone recording with noise reduction...');
15
- console.log('📝 Instructions:');
16
- console.log(' 1. Speak into your microphone (normal voice)');
17
- console.log(' 2. Type on your keyboard (heavy typing)');
18
- console.log(' 3. Click your mouse multiple times');
19
- console.log(' 4. Speak again to compare');
20
- console.log('');
21
- console.log('⏱️ Recording for 15 seconds...');
22
- console.log('');
23
-
24
- const recorder = new MacRecorder();
25
-
26
- recorder.on('recordingStarted', () => {
27
- console.log('✅ Recording started!');
28
- console.log('🎤 Custom noise reduction is ACTIVE');
29
- console.log('');
30
- console.log('Starting countdown:');
31
-
32
- let countdown = 15;
33
- const interval = setInterval(() => {
34
- process.stdout.write(`\r⏱️ ${countdown} seconds remaining... `);
35
- countdown--;
36
-
37
- if (countdown < 0) {
38
- clearInterval(interval);
39
- process.stdout.write('\r');
40
- console.log('⏱️ Time up! Stopping...');
41
- console.log('');
42
-
43
- recorder.stopRecording()
44
- .then(() => {
45
- console.log('✅ Recording saved to:', outputPath);
46
- console.log('');
47
- console.log('🎧 Play the recording to verify:');
48
- console.log(` open "${outputPath}"`);
49
- console.log('');
50
- console.log('Expected results:');
51
- console.log(' ✅ Voice should be clear');
52
- console.log(' ✅ Keyboard typing should be significantly reduced');
53
- console.log(' ✅ Mouse clicks should be filtered out');
54
- process.exit(0);
55
- })
56
- .catch(err => {
57
- console.error('❌ Error stopping:', err);
58
- process.exit(1);
59
- });
60
- }
61
- }, 1000);
62
- });
63
-
64
- recorder.on('stopped', () => {
65
- console.log('🛑 Recording stopped');
66
- });
67
-
68
- recorder.on('completed', (result) => {
69
- console.log('✅ Recording completed:', result);
70
- });
71
-
72
- // Start recording with microphone
73
- recorder.startRecording(outputPath, {
74
- includeMicrophone: true,
75
- includeSystemAudio: false,
76
- fps: 1 // Minimal FPS since we're only testing audio
77
- }).catch(err => {
78
- console.error('❌ Failed to start recording:', err);
79
- process.exit(1);
80
- });