node-mac-recorder 1.1.0 → 1.2.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/README.md +153 -0
- package/binding.gyp +6 -2
- package/cursor-test.js +176 -0
- package/index.js +90 -0
- package/manual-cursor-data.json +352 -0
- package/package.json +1 -1
- package/src/cursor_tracker.mm +399 -0
- package/src/mac_recorder.mm +6 -0
- package/preview-test.js +0 -329
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ A powerful native macOS screen recording Node.js package with advanced window se
|
|
|
11
11
|
- 🎯 **Area Selection** - Record custom screen regions
|
|
12
12
|
- 🖱️ **Multi-Display Support** - Automatic display detection and selection
|
|
13
13
|
- 🎨 **Cursor Control** - Toggle cursor visibility in recordings
|
|
14
|
+
- 🖱️ **Cursor Tracking** - Track mouse position, cursor types, and click events
|
|
14
15
|
|
|
15
16
|
🎵 **Granular Audio Controls**
|
|
16
17
|
|
|
@@ -231,6 +232,64 @@ const thumbnail = await recorder.getDisplayThumbnail(0, {
|
|
|
231
232
|
// Perfect for display selection UI
|
|
232
233
|
```
|
|
233
234
|
|
|
235
|
+
### Cursor Tracking Methods
|
|
236
|
+
|
|
237
|
+
#### `startCursorTracking(outputPath)`
|
|
238
|
+
|
|
239
|
+
Starts tracking cursor movements and saves data to JSON file.
|
|
240
|
+
|
|
241
|
+
```javascript
|
|
242
|
+
await recorder.startCursorTracking("./cursor-data.json");
|
|
243
|
+
// Cursor tracking started - will record position, cursor type, and events
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
#### `stopCursorTracking()`
|
|
247
|
+
|
|
248
|
+
Stops cursor tracking and saves collected data.
|
|
249
|
+
|
|
250
|
+
```javascript
|
|
251
|
+
await recorder.stopCursorTracking();
|
|
252
|
+
// Data saved to specified JSON file
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
#### `getCursorPosition()`
|
|
256
|
+
|
|
257
|
+
Gets current cursor position and type.
|
|
258
|
+
|
|
259
|
+
```javascript
|
|
260
|
+
const position = recorder.getCursorPosition();
|
|
261
|
+
console.log(position);
|
|
262
|
+
// {
|
|
263
|
+
// x: 1234,
|
|
264
|
+
// y: 567,
|
|
265
|
+
// cursorType: "default" // "default", "pointer", "grabbing", "text"
|
|
266
|
+
// }
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
#### `getCursorTrackingStatus()`
|
|
270
|
+
|
|
271
|
+
Returns cursor tracking status and data count.
|
|
272
|
+
|
|
273
|
+
```javascript
|
|
274
|
+
const status = recorder.getCursorTrackingStatus();
|
|
275
|
+
console.log(status);
|
|
276
|
+
// {
|
|
277
|
+
// isTracking: true,
|
|
278
|
+
// dataCount: 1250,
|
|
279
|
+
// hasEventTap: true,
|
|
280
|
+
// hasRunLoopSource: true
|
|
281
|
+
// }
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
#### `saveCursorData(outputPath)`
|
|
285
|
+
|
|
286
|
+
Manually saves current cursor data to file.
|
|
287
|
+
|
|
288
|
+
```javascript
|
|
289
|
+
await recorder.saveCursorData("./cursor-backup.json");
|
|
290
|
+
// Data saved to file
|
|
291
|
+
```
|
|
292
|
+
|
|
234
293
|
## Usage Examples
|
|
235
294
|
|
|
236
295
|
### Window-Specific Recording
|
|
@@ -404,6 +463,99 @@ async function createDisplaySelector() {
|
|
|
404
463
|
}
|
|
405
464
|
```
|
|
406
465
|
|
|
466
|
+
### Cursor Tracking Usage
|
|
467
|
+
|
|
468
|
+
```javascript
|
|
469
|
+
const recorder = new MacRecorder();
|
|
470
|
+
|
|
471
|
+
async function trackUserInteraction() {
|
|
472
|
+
// Start cursor tracking
|
|
473
|
+
await recorder.startCursorTracking("./user-interactions.json");
|
|
474
|
+
console.log("Cursor tracking started...");
|
|
475
|
+
|
|
476
|
+
// Monitor real-time cursor position
|
|
477
|
+
const monitorInterval = setInterval(() => {
|
|
478
|
+
const position = recorder.getCursorPosition();
|
|
479
|
+
console.log(
|
|
480
|
+
`Cursor: ${position.x}, ${position.y} (${position.cursorType})`
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
const status = recorder.getCursorTrackingStatus();
|
|
484
|
+
console.log(`Tracking status: ${status.dataCount} positions recorded`);
|
|
485
|
+
}, 100); // Check every 100ms
|
|
486
|
+
|
|
487
|
+
// Track for 10 seconds
|
|
488
|
+
setTimeout(async () => {
|
|
489
|
+
clearInterval(monitorInterval);
|
|
490
|
+
|
|
491
|
+
// Stop tracking and save data
|
|
492
|
+
await recorder.stopCursorTracking();
|
|
493
|
+
console.log("Cursor tracking completed!");
|
|
494
|
+
|
|
495
|
+
// Load and analyze the data
|
|
496
|
+
const fs = require("fs");
|
|
497
|
+
const data = JSON.parse(
|
|
498
|
+
fs.readFileSync("./user-interactions.json", "utf8")
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
console.log(`Total interactions recorded: ${data.length}`);
|
|
502
|
+
|
|
503
|
+
// Analyze cursor types
|
|
504
|
+
const cursorTypes = {};
|
|
505
|
+
data.forEach((item) => {
|
|
506
|
+
cursorTypes[item.cursorType] = (cursorTypes[item.cursorType] || 0) + 1;
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
console.log("Cursor types distribution:", cursorTypes);
|
|
510
|
+
|
|
511
|
+
// Analyze event types
|
|
512
|
+
const eventTypes = {};
|
|
513
|
+
data.forEach((item) => {
|
|
514
|
+
eventTypes[item.type] = (eventTypes[item.type] || 0) + 1;
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
console.log("Event types distribution:", eventTypes);
|
|
518
|
+
}, 10000);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
trackUserInteraction();
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### Combined Screen Recording + Cursor Tracking
|
|
525
|
+
|
|
526
|
+
```javascript
|
|
527
|
+
const recorder = new MacRecorder();
|
|
528
|
+
|
|
529
|
+
async function recordWithCursorTracking() {
|
|
530
|
+
// Start both screen recording and cursor tracking
|
|
531
|
+
await Promise.all([
|
|
532
|
+
recorder.startRecording("./screen-recording.mov", {
|
|
533
|
+
captureCursor: false, // Don't show cursor in video
|
|
534
|
+
includeSystemAudio: true,
|
|
535
|
+
quality: "high",
|
|
536
|
+
}),
|
|
537
|
+
recorder.startCursorTracking("./cursor-data.json"),
|
|
538
|
+
]);
|
|
539
|
+
|
|
540
|
+
console.log("Recording screen and tracking cursor...");
|
|
541
|
+
|
|
542
|
+
// Record for 30 seconds
|
|
543
|
+
setTimeout(async () => {
|
|
544
|
+
await Promise.all([
|
|
545
|
+
recorder.stopRecording(),
|
|
546
|
+
recorder.stopCursorTracking(),
|
|
547
|
+
]);
|
|
548
|
+
|
|
549
|
+
console.log("Screen recording and cursor tracking completed!");
|
|
550
|
+
console.log("Files created:");
|
|
551
|
+
console.log("- screen-recording.mov");
|
|
552
|
+
console.log("- cursor-data.json");
|
|
553
|
+
}, 30000);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
recordWithCursorTracking();
|
|
557
|
+
```
|
|
558
|
+
|
|
407
559
|
## Integration Examples
|
|
408
560
|
|
|
409
561
|
### Electron Integration
|
|
@@ -577,6 +729,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
577
729
|
|
|
578
730
|
### Latest Updates
|
|
579
731
|
|
|
732
|
+
- ✅ **Cursor Tracking**: Track mouse position, cursor types, and click events with JSON export
|
|
580
733
|
- ✅ **Window Recording**: Automatic coordinate conversion for multi-display setups
|
|
581
734
|
- ✅ **Audio Controls**: Separate microphone and system audio controls
|
|
582
735
|
- ✅ **Display Selection**: Multi-monitor support with automatic detection
|
package/binding.gyp
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
"sources": [
|
|
6
6
|
"src/mac_recorder.mm",
|
|
7
7
|
"src/screen_capture.mm",
|
|
8
|
-
"src/audio_capture.mm"
|
|
8
|
+
"src/audio_capture.mm",
|
|
9
|
+
"src/cursor_tracker.mm"
|
|
9
10
|
],
|
|
10
11
|
"include_dirs": [
|
|
11
12
|
"<!@(node -p \"require('node-addon-api').include\")"
|
|
@@ -30,7 +31,10 @@
|
|
|
30
31
|
"-framework CoreVideo",
|
|
31
32
|
"-framework Foundation",
|
|
32
33
|
"-framework AppKit",
|
|
33
|
-
"-framework ScreenCaptureKit"
|
|
34
|
+
"-framework ScreenCaptureKit",
|
|
35
|
+
"-framework ApplicationServices",
|
|
36
|
+
"-framework Carbon",
|
|
37
|
+
"-framework Accessibility"
|
|
34
38
|
]
|
|
35
39
|
},
|
|
36
40
|
"defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ]
|
package/cursor-test.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
const MacRecorder = require("./index");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
|
|
5
|
+
async function testCursorTracking() {
|
|
6
|
+
console.log("🎯 Cursor Tracking Test Başlatılıyor...\n");
|
|
7
|
+
|
|
8
|
+
const recorder = new MacRecorder();
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
// Cursor tracking başlat
|
|
12
|
+
const outputPath = path.join(__dirname, "cursor-data.json");
|
|
13
|
+
console.log("📍 Cursor tracking başlatılıyor...");
|
|
14
|
+
console.log("📁 Output dosyası:", outputPath);
|
|
15
|
+
|
|
16
|
+
await recorder.startCursorTracking(outputPath);
|
|
17
|
+
console.log("✅ Cursor tracking başlatıldı!");
|
|
18
|
+
|
|
19
|
+
// Durum kontrolü
|
|
20
|
+
const status = recorder.getCursorTrackingStatus();
|
|
21
|
+
console.log("📊 Tracking durumu:", status);
|
|
22
|
+
|
|
23
|
+
// 5 saniye bekle ve pozisyon örnekleri al
|
|
24
|
+
console.log(
|
|
25
|
+
"\n🎬 5 saniye boyunca cursor hareketlerinizi takip ediyoruz..."
|
|
26
|
+
);
|
|
27
|
+
console.log("💡 Fare hareket ettirin, tıklayın ve sürükleyin!");
|
|
28
|
+
|
|
29
|
+
// Manuel data collection - JavaScript tarafında polling
|
|
30
|
+
const manualData = [];
|
|
31
|
+
const startTime = Date.now();
|
|
32
|
+
|
|
33
|
+
for (let i = 5; i > 0; i--) {
|
|
34
|
+
console.log(`⏳ ${i} saniye kaldı...`);
|
|
35
|
+
|
|
36
|
+
// 100ms'de bir pozisyon al (10 FPS)
|
|
37
|
+
for (let j = 0; j < 10; j++) {
|
|
38
|
+
const position = recorder.getCursorPosition();
|
|
39
|
+
const timestamp = Date.now() - startTime;
|
|
40
|
+
|
|
41
|
+
manualData.push({
|
|
42
|
+
x: position.x,
|
|
43
|
+
y: position.y,
|
|
44
|
+
timestamp: timestamp,
|
|
45
|
+
cursorType: position.cursorType,
|
|
46
|
+
type: "move",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Son pozisyonu göster
|
|
53
|
+
if (manualData.length > 0) {
|
|
54
|
+
const lastPos = manualData[manualData.length - 1];
|
|
55
|
+
console.log(
|
|
56
|
+
`📌 Pozisyon: x=${lastPos.x}, y=${lastPos.y}, tip=${lastPos.cursorType}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Manuel veriyi dosyaya kaydet
|
|
62
|
+
console.log(
|
|
63
|
+
`\n📝 Manuel data collection: ${manualData.length} pozisyon toplandı`
|
|
64
|
+
);
|
|
65
|
+
const manualPath = path.join(__dirname, "manual-cursor-data.json");
|
|
66
|
+
fs.writeFileSync(manualPath, JSON.stringify(manualData, null, 2));
|
|
67
|
+
console.log(`📄 Manuel veriler kaydedildi: ${manualPath}`);
|
|
68
|
+
|
|
69
|
+
// Tracking durdur
|
|
70
|
+
console.log("\n🛑 Cursor tracking durduruluyor...");
|
|
71
|
+
await recorder.stopCursorTracking();
|
|
72
|
+
console.log("✅ Cursor tracking durduruldu!");
|
|
73
|
+
|
|
74
|
+
// Final durum kontrolü
|
|
75
|
+
const finalStatus = recorder.getCursorTrackingStatus();
|
|
76
|
+
console.log("📊 Final durumu:", finalStatus);
|
|
77
|
+
|
|
78
|
+
// Dosya kontrolü
|
|
79
|
+
if (fs.existsSync(outputPath)) {
|
|
80
|
+
const data = JSON.parse(fs.readFileSync(outputPath, "utf8"));
|
|
81
|
+
console.log(
|
|
82
|
+
`\n📄 JSON dosyası oluşturuldu: ${data.length} adet cursor verisi kaydedildi`
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// İlk birkaç veriyi göster
|
|
86
|
+
if (data.length > 0) {
|
|
87
|
+
console.log("\n📝 İlk 3 cursor verisi:");
|
|
88
|
+
data.slice(0, 3).forEach((item, index) => {
|
|
89
|
+
console.log(
|
|
90
|
+
`${index + 1}. x:${item.x}, y:${item.y}, timestamp:${
|
|
91
|
+
item.timestamp
|
|
92
|
+
}, cursorType:${item.cursorType}, type:${item.type}`
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (data.length > 3) {
|
|
97
|
+
console.log(`... ve ${data.length - 3} adet daha`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Cursor tipleri istatistiği
|
|
102
|
+
const cursorTypes = {};
|
|
103
|
+
const eventTypes = {};
|
|
104
|
+
data.forEach((item) => {
|
|
105
|
+
cursorTypes[item.cursorType] = (cursorTypes[item.cursorType] || 0) + 1;
|
|
106
|
+
eventTypes[item.type] = (eventTypes[item.type] || 0) + 1;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
console.log("\n📈 Cursor Tipleri İstatistiği:");
|
|
110
|
+
Object.keys(cursorTypes).forEach((type) => {
|
|
111
|
+
console.log(` ${type}: ${cursorTypes[type]} adet`);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
console.log("\n🎭 Event Tipleri İstatistiği:");
|
|
115
|
+
Object.keys(eventTypes).forEach((type) => {
|
|
116
|
+
console.log(` ${type}: ${eventTypes[type]} adet`);
|
|
117
|
+
});
|
|
118
|
+
} else {
|
|
119
|
+
console.log("❌ JSON dosyası oluşturulamadı!");
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error("❌ Hata:", error.message);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log("\n🎉 Test tamamlandı!");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Ek fonksiyonlar test et
|
|
129
|
+
async function testCursorPositionOnly() {
|
|
130
|
+
console.log("\n🎯 Anlık Cursor Pozisyon Testi...");
|
|
131
|
+
|
|
132
|
+
const recorder = new MacRecorder();
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
for (let i = 0; i < 5; i++) {
|
|
136
|
+
const position = recorder.getCursorPosition();
|
|
137
|
+
console.log(
|
|
138
|
+
`📌 Pozisyon ${i + 1}: x=${position.x}, y=${position.y}, tip=${
|
|
139
|
+
position.cursorType
|
|
140
|
+
}`
|
|
141
|
+
);
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error("❌ Pozisyon alma hatası:", error.message);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Test menüsü
|
|
150
|
+
async function main() {
|
|
151
|
+
console.log("🚀 MacRecorder Cursor Tracking Test Menüsü\n");
|
|
152
|
+
|
|
153
|
+
const args = process.argv.slice(2);
|
|
154
|
+
|
|
155
|
+
if (args.includes("--position")) {
|
|
156
|
+
await testCursorPositionOnly();
|
|
157
|
+
} else if (args.includes("--full")) {
|
|
158
|
+
await testCursorTracking();
|
|
159
|
+
} else {
|
|
160
|
+
console.log("Kullanım:");
|
|
161
|
+
console.log(
|
|
162
|
+
" node cursor-test.js --full # Tam cursor tracking testi (5 saniye)"
|
|
163
|
+
);
|
|
164
|
+
console.log(
|
|
165
|
+
" node cursor-test.js --position # Sadece anlık pozisyon testi"
|
|
166
|
+
);
|
|
167
|
+
console.log("\nÖrnek:");
|
|
168
|
+
console.log(" node cursor-test.js --full");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (require.main === module) {
|
|
173
|
+
main().catch(console.error);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = { testCursorTracking, testCursorPositionOnly };
|
package/index.js
CHANGED
|
@@ -413,6 +413,96 @@ class MacRecorder extends EventEmitter {
|
|
|
413
413
|
});
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Cursor tracking başlatır
|
|
418
|
+
*/
|
|
419
|
+
async startCursorTracking(outputPath) {
|
|
420
|
+
if (!outputPath) {
|
|
421
|
+
throw new Error("Output path is required");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return new Promise((resolve, reject) => {
|
|
425
|
+
try {
|
|
426
|
+
const success = nativeBinding.startCursorTracking(outputPath);
|
|
427
|
+
if (success) {
|
|
428
|
+
this.emit("cursorTrackingStarted", outputPath);
|
|
429
|
+
resolve(true);
|
|
430
|
+
} else {
|
|
431
|
+
reject(
|
|
432
|
+
new Error(
|
|
433
|
+
"Failed to start cursor tracking. Check permissions and try again."
|
|
434
|
+
)
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
} catch (error) {
|
|
438
|
+
reject(error);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Cursor tracking durdurur ve JSON dosyasını kaydeder
|
|
445
|
+
*/
|
|
446
|
+
async stopCursorTracking() {
|
|
447
|
+
return new Promise((resolve, reject) => {
|
|
448
|
+
try {
|
|
449
|
+
const success = nativeBinding.stopCursorTracking();
|
|
450
|
+
if (success) {
|
|
451
|
+
this.emit("cursorTrackingStopped");
|
|
452
|
+
resolve(true);
|
|
453
|
+
} else {
|
|
454
|
+
reject(new Error("Failed to stop cursor tracking"));
|
|
455
|
+
}
|
|
456
|
+
} catch (error) {
|
|
457
|
+
reject(error);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Anlık cursor pozisyonunu ve tipini döndürür
|
|
464
|
+
*/
|
|
465
|
+
getCursorPosition() {
|
|
466
|
+
try {
|
|
467
|
+
return nativeBinding.getCursorPosition();
|
|
468
|
+
} catch (error) {
|
|
469
|
+
throw new Error("Failed to get cursor position: " + error.message);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Cursor tracking durumunu döndürür
|
|
475
|
+
*/
|
|
476
|
+
getCursorTrackingStatus() {
|
|
477
|
+
try {
|
|
478
|
+
return nativeBinding.getCursorTrackingStatus();
|
|
479
|
+
} catch (error) {
|
|
480
|
+
throw new Error("Failed to get cursor tracking status: " + error.message);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Cursor verilerini JSON dosyasına kaydeder
|
|
486
|
+
*/
|
|
487
|
+
async saveCursorData(outputPath) {
|
|
488
|
+
if (!outputPath) {
|
|
489
|
+
throw new Error("Output path is required");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return new Promise((resolve, reject) => {
|
|
493
|
+
try {
|
|
494
|
+
const success = nativeBinding.saveCursorData(outputPath);
|
|
495
|
+
if (success) {
|
|
496
|
+
resolve(true);
|
|
497
|
+
} else {
|
|
498
|
+
reject(new Error("Failed to save cursor data"));
|
|
499
|
+
}
|
|
500
|
+
} catch (error) {
|
|
501
|
+
reject(error);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
416
506
|
/**
|
|
417
507
|
* Native modül bilgilerini döndürür
|
|
418
508
|
*/
|