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 +1 -1
- package/src/screen_capture_kit.mm +82 -13
- package/test-noise-reduction.js +0 -80
package/package.json
CHANGED
|
@@ -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
|
-
|
|
1387
|
-
|
|
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 =
|
|
1390
|
-
recordingHeight =
|
|
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
|
-
|
|
1425
|
-
|
|
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 =
|
|
1428
|
-
recordingHeight =
|
|
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
|
-
|
|
1441
|
-
|
|
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 =
|
|
1444
|
-
recordingHeight =
|
|
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;
|
package/test-noise-reduction.js
DELETED
|
@@ -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
|
-
});
|