node-mac-recorder 1.0.5 → 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 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
 
@@ -25,6 +26,7 @@ A powerful native macOS screen recording Node.js package with advanced window se
25
26
  - 🎯 **Automatic Coordinate Conversion** - Handle multi-display coordinate systems
26
27
  - 📐 **Display ID Detection** - Automatically select correct display for window recording
27
28
  - 🖼️ **Window Filtering** - Smart filtering of recordable windows
29
+ - 👁️ **Preview Thumbnails** - Generate window and display preview images
28
30
 
29
31
  ⚙️ **Customization Options**
30
32
 
@@ -202,6 +204,92 @@ console.log(status);
202
204
  // }
203
205
  ```
204
206
 
207
+ #### `getWindowThumbnail(windowId, options?)`
208
+
209
+ Captures a thumbnail preview of a specific window.
210
+
211
+ ```javascript
212
+ const thumbnail = await recorder.getWindowThumbnail(12345, {
213
+ maxWidth: 400, // Maximum width (default: 300)
214
+ maxHeight: 300, // Maximum height (default: 200)
215
+ });
216
+
217
+ // Returns: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
218
+ // Can be used directly in <img> tags or saved as file
219
+ ```
220
+
221
+ #### `getDisplayThumbnail(displayId, options?)`
222
+
223
+ Captures a thumbnail preview of a specific display.
224
+
225
+ ```javascript
226
+ const thumbnail = await recorder.getDisplayThumbnail(0, {
227
+ maxWidth: 400, // Maximum width (default: 300)
228
+ maxHeight: 300, // Maximum height (default: 200)
229
+ });
230
+
231
+ // Returns: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
232
+ // Perfect for display selection UI
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
+
205
293
  ## Usage Examples
206
294
 
207
295
  ### Window-Specific Recording
@@ -311,6 +399,163 @@ recorder.on("completed", (outputPath) => {
311
399
  await recorder.startRecording("./event-recording.mov");
312
400
  ```
313
401
 
402
+ ### Window Selection with Thumbnails
403
+
404
+ ```javascript
405
+ const recorder = new MacRecorder();
406
+
407
+ // Get windows with thumbnail previews
408
+ const windows = await recorder.getWindows();
409
+
410
+ console.log("Available windows with previews:");
411
+ for (const window of windows) {
412
+ console.log(`${window.appName} - ${window.name}`);
413
+
414
+ try {
415
+ // Generate thumbnail for each window
416
+ const thumbnail = await recorder.getWindowThumbnail(window.id, {
417
+ maxWidth: 200,
418
+ maxHeight: 150,
419
+ });
420
+
421
+ console.log(`Thumbnail: ${thumbnail.substring(0, 50)}...`);
422
+
423
+ // Use thumbnail in your UI:
424
+ // <img src="${thumbnail}" alt="Window Preview" />
425
+ } catch (error) {
426
+ console.log(`No preview available: ${error.message}`);
427
+ }
428
+ }
429
+ ```
430
+
431
+ ### Display Selection Interface
432
+
433
+ ```javascript
434
+ const recorder = new MacRecorder();
435
+
436
+ async function createDisplaySelector() {
437
+ const displays = await recorder.getDisplays();
438
+
439
+ const displayOptions = await Promise.all(
440
+ displays.map(async (display, index) => {
441
+ try {
442
+ const thumbnail = await recorder.getDisplayThumbnail(display.id);
443
+ return {
444
+ id: display.id,
445
+ name: `Display ${index + 1}`,
446
+ resolution: display.resolution,
447
+ thumbnail: thumbnail,
448
+ isPrimary: display.isPrimary,
449
+ };
450
+ } catch (error) {
451
+ return {
452
+ id: display.id,
453
+ name: `Display ${index + 1}`,
454
+ resolution: display.resolution,
455
+ thumbnail: null,
456
+ isPrimary: display.isPrimary,
457
+ };
458
+ }
459
+ })
460
+ );
461
+
462
+ return displayOptions;
463
+ }
464
+ ```
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
+
314
559
  ## Integration Examples
315
560
 
316
561
  ### Electron Integration
@@ -484,6 +729,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
484
729
 
485
730
  ### Latest Updates
486
731
 
732
+ - ✅ **Cursor Tracking**: Track mouse position, cursor types, and click events with JSON export
487
733
  - ✅ **Window Recording**: Automatic coordinate conversion for multi-display setups
488
734
  - ✅ **Audio Controls**: Separate microphone and system audio controls
489
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
@@ -355,6 +355,154 @@ class MacRecorder extends EventEmitter {
355
355
  });
356
356
  }
357
357
 
358
+ /**
359
+ * Pencere önizleme görüntüsü alır (Base64 PNG)
360
+ */
361
+ async getWindowThumbnail(windowId, options = {}) {
362
+ if (!windowId) {
363
+ throw new Error("Window ID is required");
364
+ }
365
+
366
+ const { maxWidth = 300, maxHeight = 200 } = options;
367
+
368
+ return new Promise((resolve, reject) => {
369
+ try {
370
+ const base64Image = nativeBinding.getWindowThumbnail(
371
+ windowId,
372
+ maxWidth,
373
+ maxHeight
374
+ );
375
+
376
+ if (base64Image) {
377
+ resolve(`data:image/png;base64,${base64Image}`);
378
+ } else {
379
+ reject(new Error("Failed to capture window thumbnail"));
380
+ }
381
+ } catch (error) {
382
+ reject(error);
383
+ }
384
+ });
385
+ }
386
+
387
+ /**
388
+ * Ekran önizleme görüntüsü alır (Base64 PNG)
389
+ */
390
+ async getDisplayThumbnail(displayId, options = {}) {
391
+ if (displayId === null || displayId === undefined) {
392
+ throw new Error("Display ID is required");
393
+ }
394
+
395
+ const { maxWidth = 300, maxHeight = 200 } = options;
396
+
397
+ return new Promise((resolve, reject) => {
398
+ try {
399
+ const base64Image = nativeBinding.getDisplayThumbnail(
400
+ displayId,
401
+ maxWidth,
402
+ maxHeight
403
+ );
404
+
405
+ if (base64Image) {
406
+ resolve(`data:image/png;base64,${base64Image}`);
407
+ } else {
408
+ reject(new Error("Failed to capture display thumbnail"));
409
+ }
410
+ } catch (error) {
411
+ reject(error);
412
+ }
413
+ });
414
+ }
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
+
358
506
  /**
359
507
  * Native modül bilgilerini döndürür
360
508
  */