node-mac-recorder 2.16.32 β 2.17.0
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 +3 -1
- package/cursor-macbook-test.js +63 -0
- package/cursor-simple-test.js +26 -0
- package/index.js +26 -0
- package/package.json +1 -1
- package/src/cursor_tracker.mm +82 -12
- package/test-integrated-recording.js +120 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const MacRecorder = require('./index.js');
|
|
2
|
+
|
|
3
|
+
async function testMacBookCursor() {
|
|
4
|
+
console.log('π₯οΈ Testing cursor on MacBook internal display...\n');
|
|
5
|
+
|
|
6
|
+
const recorder = new MacRecorder();
|
|
7
|
+
|
|
8
|
+
// Get displays
|
|
9
|
+
const displays = await recorder.getDisplays();
|
|
10
|
+
console.log('πΊ Available displays:');
|
|
11
|
+
displays.forEach((display, index) => {
|
|
12
|
+
console.log(` Display ${index}: ${display.resolution} at (${display.x}, ${display.y}) - Primary: ${display.isPrimary}`);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
console.log('\nπ― Move your mouse to the PRIMARY MacBook display and press any key...');
|
|
16
|
+
console.log('(Make sure cursor is on the built-in MacBook screen, not external monitor)\n');
|
|
17
|
+
|
|
18
|
+
// Wait for keypress
|
|
19
|
+
await new Promise((resolve) => {
|
|
20
|
+
process.stdin.setRawMode(true);
|
|
21
|
+
process.stdin.resume();
|
|
22
|
+
process.stdin.once('data', () => {
|
|
23
|
+
process.stdin.setRawMode(false);
|
|
24
|
+
resolve();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
console.log('π Testing cursor position on MacBook display:');
|
|
29
|
+
for (let i = 0; i < 3; i++) {
|
|
30
|
+
const position = recorder.getCursorPosition();
|
|
31
|
+
console.log(`\n Test ${i+1}:`);
|
|
32
|
+
console.log(` Cursor: (${position.x}, ${position.y})`);
|
|
33
|
+
console.log(` Cursor type: ${position.cursorType}`);
|
|
34
|
+
|
|
35
|
+
// Check which display cursor is on
|
|
36
|
+
const primaryDisplay = displays.find(d => d.isPrimary);
|
|
37
|
+
if (primaryDisplay) {
|
|
38
|
+
const isOnPrimary = position.x >= primaryDisplay.x &&
|
|
39
|
+
position.x < primaryDisplay.x + parseInt(primaryDisplay.resolution.split('x')[0]) &&
|
|
40
|
+
position.y >= primaryDisplay.y &&
|
|
41
|
+
position.y < primaryDisplay.y + parseInt(primaryDisplay.resolution.split('x')[1]);
|
|
42
|
+
|
|
43
|
+
console.log(` On primary display: ${isOnPrimary ? 'β
YES' : 'β NO'}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (position.scaleFactor) {
|
|
47
|
+
console.log(` Scale factor: ${position.scaleFactor}x`);
|
|
48
|
+
if (position.displayInfo) {
|
|
49
|
+
console.log(` Display logical: ${position.displayInfo.logicalWidth}x${position.displayInfo.logicalHeight}`);
|
|
50
|
+
console.log(` Display physical: ${position.displayInfo.physicalWidth}x${position.displayInfo.physicalHeight}`);
|
|
51
|
+
console.log(` Raw cursor: (${position.rawX}, ${position.rawY})`);
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
console.log(` β οΈ No scaling info detected`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
testMacBookCursor().catch(console.error);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const MacRecorder = require('./index.js');
|
|
2
|
+
|
|
3
|
+
console.log('π― Simple cursor test - Move your mouse to MacBook screen...\n');
|
|
4
|
+
|
|
5
|
+
const recorder = new MacRecorder();
|
|
6
|
+
|
|
7
|
+
// Test 10 cursor positions
|
|
8
|
+
for (let i = 0; i < 10; i++) {
|
|
9
|
+
setTimeout(() => {
|
|
10
|
+
const position = recorder.getCursorPosition();
|
|
11
|
+
console.log(`${i+1}. Cursor: (${position.x}, ${position.y}), Scale: ${position.scaleFactor || 'none'}`);
|
|
12
|
+
|
|
13
|
+
if (position.displayInfo && position.scaleFactor > 1.1) {
|
|
14
|
+
console.log(` π SCALING DETECTED! ${position.scaleFactor}x`);
|
|
15
|
+
console.log(` Logical: ${position.displayInfo.logicalWidth}x${position.displayInfo.logicalHeight}`);
|
|
16
|
+
console.log(` Physical: ${position.displayInfo.physicalWidth}x${position.displayInfo.physicalHeight}`);
|
|
17
|
+
console.log(` Raw cursor: (${position.rawX}, ${position.rawY})`);
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (i === 9) {
|
|
22
|
+
console.log('\nβ οΈ No scaling detected. Try moving mouse to MacBook internal display.');
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
}, i * 500);
|
|
26
|
+
}
|
package/index.js
CHANGED
|
@@ -315,6 +315,11 @@ class MacRecorder extends EventEmitter {
|
|
|
315
315
|
|
|
316
316
|
return new Promise((resolve, reject) => {
|
|
317
317
|
try {
|
|
318
|
+
// Create cursor file path with timestamp in the same directory as video
|
|
319
|
+
const timestamp = Date.now();
|
|
320
|
+
const outputDir = path.dirname(outputPath);
|
|
321
|
+
const cursorFilePath = path.join(outputDir, `temp_cursor_${timestamp}.json`);
|
|
322
|
+
|
|
318
323
|
// Native kayΔ±t baΕlat
|
|
319
324
|
const recordingOptions = {
|
|
320
325
|
includeMicrophone: this.options.includeMicrophone === true, // Only if explicitly enabled
|
|
@@ -351,6 +356,20 @@ class MacRecorder extends EventEmitter {
|
|
|
351
356
|
this.isRecording = true;
|
|
352
357
|
this.recordingStartTime = Date.now();
|
|
353
358
|
|
|
359
|
+
// Start cursor tracking automatically with recording
|
|
360
|
+
this.startCursorCapture(cursorFilePath, {
|
|
361
|
+
windowRelative: !!this.options.windowId,
|
|
362
|
+
windowInfo: this.options.windowId ? {
|
|
363
|
+
x: this.options.captureArea?.x || 0,
|
|
364
|
+
y: this.options.captureArea?.y || 0,
|
|
365
|
+
width: this.options.captureArea?.width || 1920,
|
|
366
|
+
height: this.options.captureArea?.height || 1080,
|
|
367
|
+
displayId: this.options.displayId
|
|
368
|
+
} : null
|
|
369
|
+
}).catch(cursorError => {
|
|
370
|
+
console.warn('Cursor tracking failed to start:', cursorError.message);
|
|
371
|
+
});
|
|
372
|
+
|
|
354
373
|
// Timer baΕlat (progress tracking iΓ§in)
|
|
355
374
|
this.recordingTimer = setInterval(() => {
|
|
356
375
|
const elapsed = Math.floor(
|
|
@@ -441,6 +460,13 @@ class MacRecorder extends EventEmitter {
|
|
|
441
460
|
success = true; // Assume success to avoid throwing
|
|
442
461
|
}
|
|
443
462
|
|
|
463
|
+
// Stop cursor tracking automatically
|
|
464
|
+
if (this.cursorCaptureInterval) {
|
|
465
|
+
this.stopCursorCapture().catch(cursorError => {
|
|
466
|
+
console.warn('Cursor tracking failed to stop:', cursorError.message);
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
444
470
|
// Timer durdur
|
|
445
471
|
if (this.recordingTimer) {
|
|
446
472
|
clearInterval(this.recordingTimer);
|
package/package.json
CHANGED
package/src/cursor_tracker.mm
CHANGED
|
@@ -488,40 +488,110 @@ NSDictionary* getDisplayScalingInfo(CGPoint globalPoint) {
|
|
|
488
488
|
|
|
489
489
|
// Check if point is within this display
|
|
490
490
|
if (isInBounds) {
|
|
491
|
-
// Get
|
|
491
|
+
// CRITICAL FIX: Get REAL physical dimensions using multiple detection methods
|
|
492
|
+
// Method 1: CGDisplayCreateImage (may be scaled on some systems)
|
|
493
|
+
CGImageRef testImage = CGDisplayCreateImage(displayID);
|
|
494
|
+
CGSize imageSize = CGSizeMake(CGImageGetWidth(testImage), CGImageGetHeight(testImage));
|
|
495
|
+
CGImageRelease(testImage);
|
|
496
|
+
|
|
497
|
+
// Method 2: Native display mode detection for true physical resolution
|
|
498
|
+
CGSize actualPhysicalSize = imageSize;
|
|
499
|
+
CFArrayRef displayModes = CGDisplayCopyAllDisplayModes(displayID, NULL);
|
|
500
|
+
if (displayModes) {
|
|
501
|
+
CFIndex modeCount = CFArrayGetCount(displayModes);
|
|
502
|
+
CGSize maxResolution = CGSizeMake(0, 0);
|
|
503
|
+
|
|
504
|
+
// Find the highest resolution mode (native resolution)
|
|
505
|
+
for (CFIndex i = 0; i < modeCount; i++) {
|
|
506
|
+
CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(displayModes, i);
|
|
507
|
+
CGSize modeSize = CGSizeMake(CGDisplayModeGetWidth(mode), CGDisplayModeGetHeight(mode));
|
|
508
|
+
|
|
509
|
+
if (modeSize.width > maxResolution.width ||
|
|
510
|
+
(modeSize.width == maxResolution.width && modeSize.height > maxResolution.height)) {
|
|
511
|
+
maxResolution = modeSize;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Use the max resolution if it's significantly higher than image size
|
|
516
|
+
if (maxResolution.width > imageSize.width * 1.5 || maxResolution.height > imageSize.height * 1.5) {
|
|
517
|
+
actualPhysicalSize = maxResolution;
|
|
518
|
+
NSLog(@"π Using display mode detection: %.0fx%.0f (was %.0fx%.0f)",
|
|
519
|
+
maxResolution.width, maxResolution.height, imageSize.width, imageSize.height);
|
|
520
|
+
} else {
|
|
521
|
+
actualPhysicalSize = imageSize;
|
|
522
|
+
NSLog(@"π Using image size detection: %.0fx%.0f", imageSize.width, imageSize.height);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
CFRelease(displayModes);
|
|
526
|
+
} else {
|
|
527
|
+
actualPhysicalSize = imageSize;
|
|
528
|
+
}
|
|
529
|
+
|
|
492
530
|
CGSize logicalSize = displayBounds.size;
|
|
493
|
-
CGSize
|
|
531
|
+
CGSize reportedPhysicalSize = CGSizeMake(CGDisplayPixelsWide(displayID), CGDisplayPixelsHigh(displayID));
|
|
494
532
|
|
|
495
|
-
NSLog(@"π
|
|
496
|
-
|
|
533
|
+
NSLog(@"π REAL scaling info:");
|
|
534
|
+
NSLog(@" Logical: %.0fx%.0f", logicalSize.width, logicalSize.height);
|
|
535
|
+
NSLog(@" Reported physical: %.0fx%.0f", reportedPhysicalSize.width, reportedPhysicalSize.height);
|
|
536
|
+
NSLog(@" ACTUAL physical: %.0fx%.0f", actualPhysicalSize.width, actualPhysicalSize.height);
|
|
497
537
|
|
|
498
|
-
CGFloat scaleX =
|
|
499
|
-
CGFloat scaleY =
|
|
538
|
+
CGFloat scaleX = actualPhysicalSize.width / logicalSize.width;
|
|
539
|
+
CGFloat scaleY = actualPhysicalSize.height / logicalSize.height;
|
|
500
540
|
CGFloat scaleFactor = MAX(scaleX, scaleY);
|
|
501
541
|
|
|
502
|
-
NSLog(@"π
|
|
542
|
+
NSLog(@"π REAL scale factors: X=%.2f, Y=%.2f, Final=%.2f", scaleX, scaleY, scaleFactor);
|
|
503
543
|
|
|
504
544
|
return @{
|
|
505
545
|
@"displayID": @(displayID),
|
|
506
546
|
@"logicalSize": [NSValue valueWithSize:NSMakeSize(logicalSize.width, logicalSize.height)],
|
|
507
|
-
@"physicalSize": [NSValue valueWithSize:NSMakeSize(
|
|
547
|
+
@"physicalSize": [NSValue valueWithSize:NSMakeSize(actualPhysicalSize.width, actualPhysicalSize.height)],
|
|
508
548
|
@"scaleFactor": @(scaleFactor),
|
|
509
549
|
@"displayBounds": [NSValue valueWithRect:NSMakeRect(displayBounds.origin.x, displayBounds.origin.y, displayBounds.size.width, displayBounds.size.height)]
|
|
510
550
|
};
|
|
511
551
|
}
|
|
512
552
|
}
|
|
513
553
|
|
|
514
|
-
// Fallback to main display
|
|
554
|
+
// Fallback to main display with REAL physical dimensions
|
|
515
555
|
CGDirectDisplayID mainDisplay = CGMainDisplayID();
|
|
516
556
|
CGRect displayBounds = CGDisplayBounds(mainDisplay);
|
|
557
|
+
|
|
558
|
+
// Get REAL physical dimensions using multiple detection methods
|
|
559
|
+
CGImageRef testImage = CGDisplayCreateImage(mainDisplay);
|
|
560
|
+
CGSize imageSize = CGSizeMake(CGImageGetWidth(testImage), CGImageGetHeight(testImage));
|
|
561
|
+
CGImageRelease(testImage);
|
|
562
|
+
|
|
563
|
+
// Try display mode detection for true native resolution
|
|
564
|
+
CGSize actualPhysicalSize = imageSize;
|
|
565
|
+
CFArrayRef displayModes = CGDisplayCopyAllDisplayModes(mainDisplay, NULL);
|
|
566
|
+
if (displayModes) {
|
|
567
|
+
CFIndex modeCount = CFArrayGetCount(displayModes);
|
|
568
|
+
CGSize maxResolution = CGSizeMake(0, 0);
|
|
569
|
+
|
|
570
|
+
for (CFIndex i = 0; i < modeCount; i++) {
|
|
571
|
+
CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(displayModes, i);
|
|
572
|
+
CGSize modeSize = CGSizeMake(CGDisplayModeGetWidth(mode), CGDisplayModeGetHeight(mode));
|
|
573
|
+
|
|
574
|
+
if (modeSize.width > maxResolution.width ||
|
|
575
|
+
(modeSize.width == maxResolution.width && modeSize.height > maxResolution.height)) {
|
|
576
|
+
maxResolution = modeSize;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (maxResolution.width > imageSize.width * 1.5 || maxResolution.height > imageSize.height * 1.5) {
|
|
581
|
+
actualPhysicalSize = maxResolution;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
CFRelease(displayModes);
|
|
585
|
+
}
|
|
586
|
+
|
|
517
587
|
CGSize logicalSize = displayBounds.size;
|
|
518
|
-
|
|
588
|
+
CGFloat scaleFactor = MAX(actualPhysicalSize.width / logicalSize.width, actualPhysicalSize.height / logicalSize.height);
|
|
519
589
|
|
|
520
590
|
return @{
|
|
521
591
|
@"displayID": @(mainDisplay),
|
|
522
592
|
@"logicalSize": [NSValue valueWithSize:NSMakeSize(logicalSize.width, logicalSize.height)],
|
|
523
|
-
@"physicalSize": [NSValue valueWithSize:NSMakeSize(
|
|
524
|
-
@"scaleFactor": @(
|
|
593
|
+
@"physicalSize": [NSValue valueWithSize:NSMakeSize(actualPhysicalSize.width, actualPhysicalSize.height)],
|
|
594
|
+
@"scaleFactor": @(scaleFactor),
|
|
525
595
|
@"displayBounds": [NSValue valueWithRect:NSMakeRect(displayBounds.origin.x, displayBounds.origin.y, displayBounds.size.width, displayBounds.size.height)]
|
|
526
596
|
};
|
|
527
597
|
} @catch (NSException *exception) {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const MacRecorder = require('./index.js');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
// Test output directory
|
|
8
|
+
const outputDir = './test-output';
|
|
9
|
+
if (!fs.existsSync(outputDir)) {
|
|
10
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const recorder = new MacRecorder();
|
|
14
|
+
|
|
15
|
+
async function testIntegratedRecording() {
|
|
16
|
+
console.log('π¬ Testing integrated screen recording with automatic cursor tracking...');
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
// Test permissions first
|
|
20
|
+
const permissions = await recorder.checkPermissions();
|
|
21
|
+
console.log('π Permissions:', permissions);
|
|
22
|
+
|
|
23
|
+
if (!permissions.screenRecording) {
|
|
24
|
+
console.log('β Screen recording permission required');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Get displays and windows
|
|
29
|
+
const displays = await recorder.getDisplays();
|
|
30
|
+
const windows = await recorder.getWindows();
|
|
31
|
+
|
|
32
|
+
console.log(`πΊ Found ${displays.length} displays and ${windows.length} windows`);
|
|
33
|
+
|
|
34
|
+
// Setup recording options
|
|
35
|
+
const timestamp = Date.now();
|
|
36
|
+
const videoPath = path.join(outputDir, `integrated-test-${timestamp}.mov`);
|
|
37
|
+
|
|
38
|
+
console.log(`π₯ Starting recording to: ${videoPath}`);
|
|
39
|
+
console.log('π Cursor tracking will start automatically and create a JSON file in the same directory');
|
|
40
|
+
|
|
41
|
+
// Start recording (cursor tracking will start automatically)
|
|
42
|
+
// Use the primary display (displayId: 1) where cursor is located
|
|
43
|
+
await recorder.startRecording(videoPath, {
|
|
44
|
+
includeMicrophone: false,
|
|
45
|
+
includeSystemAudio: false,
|
|
46
|
+
captureCursor: true,
|
|
47
|
+
displayId: 1 // Use primary display where cursor is located
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
console.log('β
Recording started successfully!');
|
|
51
|
+
console.log('π±οΈ Cursor tracking started automatically');
|
|
52
|
+
console.log('β° Recording for 5 seconds...');
|
|
53
|
+
console.log('π‘ Move your mouse around to generate cursor data');
|
|
54
|
+
|
|
55
|
+
// Wait for 5 seconds
|
|
56
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
57
|
+
|
|
58
|
+
console.log('π Stopping recording...');
|
|
59
|
+
|
|
60
|
+
// Stop recording (cursor tracking will stop automatically)
|
|
61
|
+
const result = await recorder.stopRecording();
|
|
62
|
+
|
|
63
|
+
console.log('β
Recording stopped:', result);
|
|
64
|
+
console.log('π Checking output files...');
|
|
65
|
+
|
|
66
|
+
// Wait a moment for files to be written
|
|
67
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
68
|
+
|
|
69
|
+
// Check for video file
|
|
70
|
+
if (fs.existsSync(videoPath)) {
|
|
71
|
+
const videoStats = fs.statSync(videoPath);
|
|
72
|
+
console.log(`πΉ Video file created: ${videoPath} (${videoStats.size} bytes)`);
|
|
73
|
+
} else {
|
|
74
|
+
console.log('β Video file not found');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check for cursor file
|
|
78
|
+
const files = fs.readdirSync(outputDir);
|
|
79
|
+
const cursorFile = files.find(f => f.startsWith('temp_cursor_') && f.endsWith('.json'));
|
|
80
|
+
|
|
81
|
+
if (cursorFile) {
|
|
82
|
+
const cursorPath = path.join(outputDir, cursorFile);
|
|
83
|
+
const cursorStats = fs.statSync(cursorPath);
|
|
84
|
+
console.log(`π±οΈ Cursor file created: ${cursorFile} (${cursorStats.size} bytes)`);
|
|
85
|
+
|
|
86
|
+
// Read and validate cursor data
|
|
87
|
+
try {
|
|
88
|
+
const cursorData = JSON.parse(fs.readFileSync(cursorPath, 'utf8'));
|
|
89
|
+
console.log(`π Cursor data points: ${cursorData.length}`);
|
|
90
|
+
|
|
91
|
+
if (cursorData.length > 0) {
|
|
92
|
+
const first = cursorData[0];
|
|
93
|
+
const last = cursorData[cursorData.length - 1];
|
|
94
|
+
console.log(`π First cursor position: (${first.x}, ${first.y}) at ${first.timestamp}ms`);
|
|
95
|
+
console.log(`π Last cursor position: (${last.x}, ${last.y}) at ${last.timestamp}ms`);
|
|
96
|
+
console.log(`π Coordinate system: ${first.coordinateSystem || 'global'}`);
|
|
97
|
+
}
|
|
98
|
+
} catch (parseError) {
|
|
99
|
+
console.log('β οΈ Could not parse cursor data:', parseError.message);
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
console.log('β Cursor file not found');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log('β
Test completed successfully!');
|
|
106
|
+
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error('β Test failed:', error.message);
|
|
109
|
+
console.error(error.stack);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Run the test
|
|
114
|
+
testIntegratedRecording().then(() => {
|
|
115
|
+
console.log('π All tests finished');
|
|
116
|
+
process.exit(0);
|
|
117
|
+
}).catch(error => {
|
|
118
|
+
console.error('π₯ Fatal error:', error);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
});
|