node-mac-recorder 2.17.9 → 2.17.10

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/index.js CHANGED
@@ -31,6 +31,8 @@ class MacRecorder extends EventEmitter {
31
31
  this.cursorCaptureFile = null;
32
32
  this.cursorCaptureStartTime = null;
33
33
  this.cursorCaptureFirstWrite = true;
34
+ this.cursorRecordingBaseTime = null;
35
+ this.cursorRecordingOffsetMs = 0;
34
36
  this.lastCapturedData = null;
35
37
  this.cursorDisplayInfo = null;
36
38
  this.recordingDisplayInfo = null;
@@ -41,7 +43,7 @@ class MacRecorder extends EventEmitter {
41
43
  quality: "medium",
42
44
  frameRate: 30,
43
45
  captureArea: null, // { x, y, width, height }
44
- captureCursor: false, // Default olarak cursor gizli
46
+ captureCursor: false, // Default: cursor hidden in video
45
47
  showClicks: false,
46
48
  displayId: null, // Hangi ekranı kaydedeceği (null = ana ekran)
47
49
  windowId: null, // Hangi pencereyi kaydedeceği (null = tam ekran)
@@ -79,16 +81,29 @@ class MacRecorder extends EventEmitter {
79
81
  */
80
82
  async getDisplays() {
81
83
  const displays = nativeBinding.getDisplays();
82
- return displays.map((display, index) => ({
83
- id: display.id, // Use the actual display ID from native code
84
- name: display.name,
85
- width: display.width,
86
- height: display.height,
87
- x: display.x,
88
- y: display.y,
89
- isPrimary: display.isPrimary,
90
- resolution: `${display.width}x${display.height}`,
91
- }));
84
+ return displays.map((display, index) => {
85
+ const logicalWidth = Number(display.width) || 0;
86
+ const logicalHeight = Number(display.height) || 0;
87
+ const pixelWidth = Number(display.pixelWidth) || logicalWidth;
88
+ const pixelHeight = Number(display.pixelHeight) || logicalHeight;
89
+ const scaleX = display.scaleX ? Number(display.scaleX) : (logicalWidth > 0 ? pixelWidth / logicalWidth : 1);
90
+ const scaleY = display.scaleY ? Number(display.scaleY) : (logicalHeight > 0 ? pixelHeight / logicalHeight : 1);
91
+
92
+ return {
93
+ id: display.id, // Use the actual display ID from native code
94
+ name: display.name,
95
+ width: logicalWidth,
96
+ height: logicalHeight,
97
+ pixelWidth,
98
+ pixelHeight,
99
+ scaleX,
100
+ scaleY,
101
+ x: display.x,
102
+ y: display.y,
103
+ isPrimary: display.isPrimary,
104
+ resolution: `${logicalWidth}x${logicalHeight}`,
105
+ };
106
+ });
92
107
  }
93
108
 
94
109
  /**
@@ -252,16 +267,29 @@ class MacRecorder extends EventEmitter {
252
267
 
253
268
  // Recording için display bilgisini sakla (cursor capture için)
254
269
  const targetDisplay = displays.find(d => d.id === targetDisplayId);
255
- this.recordingDisplayInfo = {
256
- displayId: targetDisplayId,
257
- x: targetDisplay.x,
258
- y: targetDisplay.y,
259
- width: parseInt(targetDisplay.resolution.split("x")[0]),
260
- height: parseInt(targetDisplay.resolution.split("x")[1]),
261
- // Add scaling information for cursor coordinate transformation
262
- logicalWidth: parseInt(targetDisplay.resolution.split("x")[0]),
263
- logicalHeight: parseInt(targetDisplay.resolution.split("x")[1]),
264
- };
270
+ if (targetDisplay) {
271
+ const logicalWidth = Number(targetDisplay.width) || parseInt(targetDisplay.resolution.split("x")[0]);
272
+ const logicalHeight = Number(targetDisplay.height) || parseInt(targetDisplay.resolution.split("x")[1]);
273
+ const pixelWidth = Number(targetDisplay.pixelWidth) || logicalWidth;
274
+ const pixelHeight = Number(targetDisplay.pixelHeight) || logicalHeight;
275
+ const scaleX = Number.isFinite(targetDisplay.scaleX) ? Number(targetDisplay.scaleX) : (logicalWidth > 0 ? pixelWidth / logicalWidth : 1);
276
+ const scaleY = Number.isFinite(targetDisplay.scaleY) ? Number(targetDisplay.scaleY) : (logicalHeight > 0 ? pixelHeight / logicalHeight : 1);
277
+
278
+ this.recordingDisplayInfo = {
279
+ displayId: targetDisplayId,
280
+ x: targetDisplay.x,
281
+ y: targetDisplay.y,
282
+ width: logicalWidth,
283
+ height: logicalHeight,
284
+ pixelWidth,
285
+ pixelHeight,
286
+ scaleX,
287
+ scaleY,
288
+ // Add scaling information for cursor coordinate transformation
289
+ logicalWidth,
290
+ logicalHeight,
291
+ };
292
+ }
265
293
  }
266
294
 
267
295
  this.options.captureArea = {
@@ -289,15 +317,26 @@ class MacRecorder extends EventEmitter {
289
317
  const displays = await this.getDisplays();
290
318
  const targetDisplay = displays.find(d => d.id === this.options.displayId);
291
319
  if (targetDisplay) {
320
+ const logicalWidth = Number(targetDisplay.width) || parseInt(targetDisplay.resolution.split("x")[0]);
321
+ const logicalHeight = Number(targetDisplay.height) || parseInt(targetDisplay.resolution.split("x")[1]);
322
+ const pixelWidth = Number(targetDisplay.pixelWidth) || logicalWidth;
323
+ const pixelHeight = Number(targetDisplay.pixelHeight) || logicalHeight;
324
+ const scaleX = Number.isFinite(targetDisplay.scaleX) ? Number(targetDisplay.scaleX) : (logicalWidth > 0 ? pixelWidth / logicalWidth : 1);
325
+ const scaleY = Number.isFinite(targetDisplay.scaleY) ? Number(targetDisplay.scaleY) : (logicalHeight > 0 ? pixelHeight / logicalHeight : 1);
326
+
292
327
  this.recordingDisplayInfo = {
293
328
  displayId: this.options.displayId,
294
329
  x: targetDisplay.x,
295
330
  y: targetDisplay.y,
296
- width: parseInt(targetDisplay.resolution.split("x")[0]),
297
- height: parseInt(targetDisplay.resolution.split("x")[1]),
331
+ width: logicalWidth,
332
+ height: logicalHeight,
333
+ pixelWidth,
334
+ pixelHeight,
335
+ scaleX,
336
+ scaleY,
298
337
  // Add scaling information for cursor coordinate transformation
299
- logicalWidth: parseInt(targetDisplay.resolution.split("x")[0]),
300
- logicalHeight: parseInt(targetDisplay.resolution.split("x")[1]),
338
+ logicalWidth,
339
+ logicalHeight,
301
340
  };
302
341
  }
303
342
  } catch (error) {
@@ -356,51 +395,105 @@ class MacRecorder extends EventEmitter {
356
395
  this.isRecording = true;
357
396
  this.recordingStartTime = Date.now();
358
397
 
359
- // Start cursor tracking automatically with recording
360
- let cursorOptions = {};
361
-
362
- // For window recording, use simplified window-relative coordinates
363
- if (this.options.windowId) {
364
- // Use cached window info from the earlier window detection
365
- this.getWindows().then(windows => {
366
- const targetWindow = windows.find(w => w.id === this.options.windowId);
367
- if (targetWindow) {
368
- // Start cursor capture with simplified window-relative tracking
369
- this.startCursorCapture(cursorFilePath, {
370
- windowRelative: true,
371
- windowInfo: {
372
- // Use original global window coordinates for reference
373
- x: targetWindow.x,
374
- y: targetWindow.y,
375
- width: targetWindow.width,
376
- height: targetWindow.height,
377
- displayId: this.options.displayId,
378
- // Persist capture area so we can rebuild global offsets reliably
379
- captureArea: this.options.captureArea,
380
- // Keep a snapshot of the window details for debugging/analytics
381
- originalWindow: targetWindow,
382
- // Store display info for multi-display coordinate fixes
383
- targetDisplay: this.recordingDisplayInfo
398
+ // Start cursor tracking automatically with recording
399
+ // For window recording, align cursor coordinates against actual capture geometry
400
+ if (this.options.windowId) {
401
+ const waitForActiveCaptureInfo = async () => {
402
+ if (typeof nativeBinding.getActiveCaptureInfo !== "function") {
403
+ return null;
404
+ }
405
+
406
+ const maxAttempts = 30;
407
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
408
+ try {
409
+ const info = nativeBinding.getActiveCaptureInfo();
410
+ if (info && typeof info === "object" && Object.keys(info).length > 0) {
411
+ return info;
412
+ }
413
+ } catch (nativeInfoError) {
414
+ if (attempt === 0) {
415
+ console.warn('getActiveCaptureInfo failed:', nativeInfoError.message);
416
+ }
384
417
  }
385
- }).catch(cursorError => {
386
- console.warn('Window cursor tracking failed:', cursorError.message);
387
- // Fallback to display recording
388
- this.startCursorCapture(cursorFilePath).catch(fallbackError => {
389
- console.warn('Fallback cursor tracking failed:', fallbackError.message);
418
+
419
+ await new Promise((resolve) => setTimeout(resolve, 20));
420
+ }
421
+
422
+ return null;
423
+ };
424
+
425
+ const startWindowCursorTracking = async () => {
426
+ try {
427
+ const [windows, activeCaptureInfo] = await Promise.all([
428
+ this.getWindows(),
429
+ waitForActiveCaptureInfo()
430
+ ]);
431
+
432
+ const targetWindow = windows.find(w => w.id === this.options.windowId);
433
+ if (!targetWindow) {
434
+ throw new Error('Target window not found for cursor tracking');
435
+ }
436
+
437
+ const hasNumber = (value) => typeof value === "number" && Number.isFinite(value);
438
+ const targetDisplay = this.recordingDisplayInfo;
439
+ const nativeWindowFrame = activeCaptureInfo?.type === 'window' ? activeCaptureInfo.window?.frame : null;
440
+
441
+ const windowWidth = hasNumber(nativeWindowFrame?.width) ? nativeWindowFrame.width : targetWindow.width;
442
+ const windowHeight = hasNumber(nativeWindowFrame?.height) ? nativeWindowFrame.height : targetWindow.height;
443
+
444
+ let captureAreaForCursor = this.options.captureArea;
445
+ let globalWindowX = targetWindow.x;
446
+ let globalWindowY = targetWindow.y;
447
+
448
+ if (nativeWindowFrame && hasNumber(nativeWindowFrame.x) && hasNumber(nativeWindowFrame.y)) {
449
+ globalWindowX = nativeWindowFrame.x;
450
+ globalWindowY = nativeWindowFrame.y;
451
+
452
+ if (targetDisplay && hasNumber(targetDisplay.x) && hasNumber(targetDisplay.y)) {
453
+ captureAreaForCursor = {
454
+ x: nativeWindowFrame.x - targetDisplay.x,
455
+ y: nativeWindowFrame.y - targetDisplay.y,
456
+ width: windowWidth,
457
+ height: windowHeight
458
+ };
459
+ this.options.captureArea = captureAreaForCursor;
460
+ }
461
+ }
462
+
463
+ // Start cursor capture with geometry-aware window-relative tracking
464
+ this.startCursorCapture(cursorFilePath, {
465
+ windowRelative: true,
466
+ windowInfo: {
467
+ x: globalWindowX,
468
+ y: globalWindowY,
469
+ width: windowWidth,
470
+ height: windowHeight,
471
+ displayId: this.options.displayId,
472
+ captureArea: captureAreaForCursor,
473
+ originalWindow: targetWindow,
474
+ targetDisplay,
475
+ nativeFrame: nativeWindowFrame
476
+ }
477
+ }).catch(cursorError => {
478
+ console.warn('Window cursor tracking failed:', cursorError.message);
479
+ // Fallback to display recording
480
+ this.startCursorCapture(cursorFilePath).catch(fallbackError => {
481
+ console.warn('Fallback cursor tracking failed:', fallbackError.message);
482
+ });
390
483
  });
391
- });
392
- }
393
- }).catch(error => {
394
- console.warn('Could not get window info for cursor tracking:', error.message);
395
- // Fallback to display cursor tracking
396
- this.startCursorCapture(cursorFilePath).catch(cursorError => {
397
- console.warn('Cursor tracking failed to start:', cursorError.message);
398
- });
399
- });
400
- } else {
401
- // For display recording, use display-relative cursor tracking
402
- this.startCursorCapture(cursorFilePath, {
403
- displayRelative: true,
484
+ } catch (error) {
485
+ console.warn('Could not prepare window cursor tracking:', error.message);
486
+ this.startCursorCapture(cursorFilePath).catch(cursorError => {
487
+ console.warn('Cursor tracking failed to start:', cursorError.message);
488
+ });
489
+ }
490
+ };
491
+
492
+ startWindowCursorTracking();
493
+ } else {
494
+ // For display recording, use display-relative cursor tracking
495
+ this.startCursorCapture(cursorFilePath, {
496
+ displayRelative: true,
404
497
  displayInfo: this.recordingDisplayInfo
405
498
  }).catch(cursorError => {
406
499
  console.warn('Display cursor tracking failed:', cursorError.message);
@@ -712,6 +805,16 @@ class MacRecorder extends EventEmitter {
712
805
  const targetDisplay = windowInfo.targetDisplay || this.recordingDisplayInfo || null;
713
806
  const captureArea = windowInfo.captureArea || null;
714
807
  const hasNumber = (value) => typeof value === "number" && Number.isFinite(value);
808
+ const displayScaleX = targetDisplay && hasNumber(targetDisplay.scaleX)
809
+ ? Number(targetDisplay.scaleX)
810
+ : (targetDisplay && hasNumber(targetDisplay.pixelWidth) && hasNumber(targetDisplay.width)
811
+ ? targetDisplay.pixelWidth / targetDisplay.width
812
+ : 1);
813
+ const displayScaleY = targetDisplay && hasNumber(targetDisplay.scaleY)
814
+ ? Number(targetDisplay.scaleY)
815
+ : (targetDisplay && hasNumber(targetDisplay.pixelHeight) && hasNumber(targetDisplay.height)
816
+ ? targetDisplay.pixelHeight / targetDisplay.height
817
+ : 1);
715
818
 
716
819
  let globalX = hasNumber(windowInfo.x) ? windowInfo.x : null;
717
820
  let globalY = hasNumber(windowInfo.y) ? windowInfo.y : null;
@@ -745,12 +848,40 @@ class MacRecorder extends EventEmitter {
745
848
  ? captureArea.x
746
849
  : (targetDisplay && hasNumber(globalX) && hasNumber(targetDisplay.x)
747
850
  ? globalX - targetDisplay.x
748
- : globalX);
851
+ : globalX ?? 0);
749
852
  const displayOffsetY = captureArea && hasNumber(captureArea.y)
750
853
  ? captureArea.y
751
854
  : (targetDisplay && hasNumber(globalY) && hasNumber(targetDisplay.y)
752
855
  ? globalY - targetDisplay.y
753
- : globalY);
856
+ : globalY ?? 0);
857
+
858
+ const windowPixelWidth = hasNumber(windowInfo.width) ? windowInfo.width * displayScaleX : null;
859
+ const windowPixelHeight = hasNumber(windowInfo.height) ? windowInfo.height * displayScaleY : null;
860
+ const captureAreaInfo = captureArea
861
+ ? {
862
+ ...captureArea,
863
+ pixelX: hasNumber(captureArea.x) ? captureArea.x * displayScaleX : null,
864
+ pixelY: hasNumber(captureArea.y) ? captureArea.y * displayScaleY : null,
865
+ pixelWidth: hasNumber(captureArea.width) ? captureArea.width * displayScaleX : windowPixelWidth,
866
+ pixelHeight: hasNumber(captureArea.height) ? captureArea.height * displayScaleY : windowPixelHeight,
867
+ }
868
+ : null;
869
+
870
+ const enrichedWindowInfo = {
871
+ ...windowInfo,
872
+ globalX,
873
+ globalY,
874
+ displayOffsetX,
875
+ displayOffsetY,
876
+ scaleX: displayScaleX,
877
+ scaleY: displayScaleY,
878
+ pixelWidth: windowPixelWidth,
879
+ pixelHeight: windowPixelHeight,
880
+ };
881
+
882
+ if (captureAreaInfo) {
883
+ enrichedWindowInfo.captureArea = captureAreaInfo;
884
+ }
754
885
 
755
886
  this.cursorDisplayInfo = {
756
887
  displayId:
@@ -762,25 +893,40 @@ class MacRecorder extends EventEmitter {
762
893
  y: globalY,
763
894
  width: windowInfo.width,
764
895
  height: windowInfo.height,
896
+ pixelWidth: windowPixelWidth,
897
+ pixelHeight: windowPixelHeight,
898
+ scaleX: displayScaleX,
899
+ scaleY: displayScaleY,
900
+ displayRelativeX: displayOffsetX,
901
+ displayRelativeY: displayOffsetY,
765
902
  windowRelative: true,
766
903
  windowInfo: {
767
- ...windowInfo,
768
- globalX,
769
- globalY,
770
- displayOffsetX,
771
- displayOffsetY
904
+ ...enrichedWindowInfo
772
905
  },
773
906
  targetDisplay,
774
- captureArea
907
+ captureArea: captureAreaInfo
775
908
  };
776
909
  } else if (options.displayRelative && options.displayInfo) {
777
910
  // Display recording: Use display-relative coordinates
911
+ const pixelWidth = Number(options.displayInfo.pixelWidth) || Number(options.displayInfo.width);
912
+ const pixelHeight = Number(options.displayInfo.pixelHeight) || Number(options.displayInfo.height);
913
+ const scaleX = Number.isFinite(options.displayInfo.scaleX)
914
+ ? Number(options.displayInfo.scaleX)
915
+ : (Number(options.displayInfo.width) > 0 ? pixelWidth / Number(options.displayInfo.width) : 1);
916
+ const scaleY = Number.isFinite(options.displayInfo.scaleY)
917
+ ? Number(options.displayInfo.scaleY)
918
+ : (Number(options.displayInfo.height) > 0 ? pixelHeight / Number(options.displayInfo.height) : 1);
919
+
778
920
  this.cursorDisplayInfo = {
779
921
  displayId: options.displayInfo.displayId,
780
922
  x: options.displayInfo.x,
781
923
  y: options.displayInfo.y,
782
924
  width: options.displayInfo.width,
783
925
  height: options.displayInfo.height,
926
+ pixelWidth,
927
+ pixelHeight,
928
+ scaleX,
929
+ scaleY,
784
930
  displayRelative: true
785
931
  };
786
932
  } else if (this.recordingDisplayInfo) {
@@ -796,8 +942,16 @@ class MacRecorder extends EventEmitter {
796
942
  displayId: mainDisplay.id,
797
943
  x: mainDisplay.x,
798
944
  y: mainDisplay.y,
799
- width: parseInt(mainDisplay.resolution.split("x")[0]),
800
- height: parseInt(mainDisplay.resolution.split("x")[1]),
945
+ width: Number(mainDisplay.width) || parseInt(mainDisplay.resolution.split("x")[0]),
946
+ height: Number(mainDisplay.height) || parseInt(mainDisplay.resolution.split("x")[1]),
947
+ pixelWidth: Number(mainDisplay.pixelWidth) || Number(mainDisplay.width) || parseInt(mainDisplay.resolution.split("x")[0]),
948
+ pixelHeight: Number(mainDisplay.pixelHeight) || Number(mainDisplay.height) || parseInt(mainDisplay.resolution.split("x")[1]),
949
+ scaleX: Number.isFinite(mainDisplay.scaleX)
950
+ ? Number(mainDisplay.scaleX)
951
+ : ((Number(mainDisplay.width) || 0) > 0 ? (Number(mainDisplay.pixelWidth) || Number(mainDisplay.width)) / Number(mainDisplay.width) : 1),
952
+ scaleY: Number.isFinite(mainDisplay.scaleY)
953
+ ? Number(mainDisplay.scaleY)
954
+ : ((Number(mainDisplay.height) || 0) > 0 ? (Number(mainDisplay.pixelHeight) || Number(mainDisplay.height)) / Number(mainDisplay.height) : 1),
801
955
  };
802
956
  }
803
957
  } catch (error) {
@@ -813,15 +967,24 @@ class MacRecorder extends EventEmitter {
813
967
  fs.writeFileSync(filepath, "[");
814
968
 
815
969
  this.cursorCaptureFile = filepath;
816
- this.cursorCaptureStartTime = Date.now();
970
+ const captureStart = Date.now();
971
+ const recordingBase = this.isRecording && this.recordingStartTime
972
+ ? this.recordingStartTime
973
+ : captureStart;
974
+ this.cursorCaptureStartTime = captureStart;
975
+ this.cursorRecordingBaseTime = recordingBase;
976
+ this.cursorRecordingOffsetMs = Math.max(0, captureStart - recordingBase);
817
977
  this.cursorCaptureFirstWrite = true;
818
978
  this.lastCapturedData = null;
819
979
 
820
980
  // JavaScript interval ile polling yap (daha sık - mouse event'leri yakalamak için)
821
981
  this.cursorCaptureInterval = setInterval(() => {
822
982
  try {
983
+ const now = Date.now();
823
984
  const position = nativeBinding.getCursorPosition();
824
- const timestamp = Date.now() - this.cursorCaptureStartTime;
985
+ const captureTimestamp = this.cursorCaptureStartTime ? now - this.cursorCaptureStartTime : 0;
986
+ const recordingBase = this.cursorRecordingBaseTime || this.cursorCaptureStartTime || now;
987
+ const recordingTimestamp = Math.max(0, now - recordingBase);
825
988
 
826
989
  // Transform coordinates based on recording type
827
990
  let x = position.x;
@@ -830,9 +993,30 @@ class MacRecorder extends EventEmitter {
830
993
 
831
994
  if (this.cursorDisplayInfo) {
832
995
  if (this.cursorDisplayInfo.windowRelative) {
833
- // Window recording: Transform global → window-relative coordinates
834
- x = position.x - this.cursorDisplayInfo.x;
835
- y = position.y - this.cursorDisplayInfo.y;
996
+ // Window recording: base on display-relative offsets when available
997
+ const captureArea = this.cursorDisplayInfo.captureArea || this.cursorDisplayInfo.windowInfo?.captureArea;
998
+ const targetDisplay = this.cursorDisplayInfo.targetDisplay || this.recordingDisplayInfo;
999
+ const hasCoord = (value) => typeof value === "number" && Number.isFinite(value);
1000
+
1001
+ if (targetDisplay && captureArea && hasCoord(captureArea.x) && hasCoord(captureArea.y)) {
1002
+ const displayRelativeX = position.x - targetDisplay.x;
1003
+ const displayRelativeY = position.y - targetDisplay.y;
1004
+ x = displayRelativeX - captureArea.x;
1005
+ y = displayRelativeY - captureArea.y;
1006
+ } else if (
1007
+ hasCoord(this.cursorDisplayInfo.displayRelativeX) &&
1008
+ hasCoord(this.cursorDisplayInfo.displayRelativeY) &&
1009
+ targetDisplay
1010
+ ) {
1011
+ const displayRelativeX = position.x - targetDisplay.x;
1012
+ const displayRelativeY = position.y - targetDisplay.y;
1013
+ x = displayRelativeX - this.cursorDisplayInfo.displayRelativeX;
1014
+ y = displayRelativeY - this.cursorDisplayInfo.displayRelativeY;
1015
+ } else {
1016
+ // Fallback: global offsets
1017
+ x = position.x - this.cursorDisplayInfo.x;
1018
+ y = position.y - this.cursorDisplayInfo.y;
1019
+ }
836
1020
  coordinateSystem = "window-relative";
837
1021
 
838
1022
  // Window bounds check - skip if cursor is outside window
@@ -857,11 +1041,28 @@ class MacRecorder extends EventEmitter {
857
1041
  }
858
1042
  }
859
1043
 
1044
+ const logicalX = x;
1045
+ const logicalY = y;
1046
+ const scaleX = Number.isFinite(this.cursorDisplayInfo?.windowInfo?.scaleX)
1047
+ ? Number(this.cursorDisplayInfo.windowInfo.scaleX)
1048
+ : (Number.isFinite(this.cursorDisplayInfo?.scaleX) ? Number(this.cursorDisplayInfo.scaleX) : 1);
1049
+ const scaleY = Number.isFinite(this.cursorDisplayInfo?.windowInfo?.scaleY)
1050
+ ? Number(this.cursorDisplayInfo.windowInfo.scaleY)
1051
+ : (Number.isFinite(this.cursorDisplayInfo?.scaleY) ? Number(this.cursorDisplayInfo.scaleY) : 1);
1052
+ const pixelX = logicalX * scaleX;
1053
+ const pixelY = logicalY * scaleY;
1054
+
860
1055
  const cursorData = {
861
- x: x,
862
- y: y,
863
- timestamp: timestamp,
864
- unixTimeMs: Date.now(),
1056
+ x: logicalX,
1057
+ y: logicalY,
1058
+ pixelX,
1059
+ pixelY,
1060
+ scaleX,
1061
+ scaleY,
1062
+ timestamp: recordingTimestamp,
1063
+ captureTimestamp,
1064
+ recordingOffsetMs: this.cursorRecordingOffsetMs,
1065
+ unixTimeMs: now,
865
1066
  cursorType: position.cursorType,
866
1067
  type: position.eventType || "move",
867
1068
  coordinateSystem: coordinateSystem,
@@ -870,7 +1071,14 @@ class MacRecorder extends EventEmitter {
870
1071
  windowInfo: {
871
1072
  width: this.cursorDisplayInfo.width,
872
1073
  height: this.cursorDisplayInfo.height,
873
- displayId: this.cursorDisplayInfo.displayId
1074
+ pixelWidth: this.cursorDisplayInfo.pixelWidth ?? (this.cursorDisplayInfo.width * scaleX),
1075
+ pixelHeight: this.cursorDisplayInfo.pixelHeight ?? (this.cursorDisplayInfo.height * scaleY),
1076
+ displayId: this.cursorDisplayInfo.displayId,
1077
+ displayOffsetX: this.cursorDisplayInfo.displayRelativeX,
1078
+ displayOffsetY: this.cursorDisplayInfo.displayRelativeY,
1079
+ scaleX,
1080
+ scaleY,
1081
+ captureArea: this.cursorDisplayInfo.captureArea || null
874
1082
  }
875
1083
  }),
876
1084
  // Include display context for display-relative coordinates
@@ -878,7 +1086,11 @@ class MacRecorder extends EventEmitter {
878
1086
  displayInfo: {
879
1087
  displayId: this.cursorDisplayInfo.displayId,
880
1088
  width: this.cursorDisplayInfo.width,
881
- height: this.cursorDisplayInfo.height
1089
+ height: this.cursorDisplayInfo.height,
1090
+ pixelWidth: this.cursorDisplayInfo.pixelWidth ?? (this.cursorDisplayInfo.width * scaleX),
1091
+ pixelHeight: this.cursorDisplayInfo.pixelHeight ?? (this.cursorDisplayInfo.height * scaleY),
1092
+ scaleX,
1093
+ scaleY
882
1094
  }
883
1095
  })
884
1096
  };
@@ -937,6 +1149,8 @@ class MacRecorder extends EventEmitter {
937
1149
  this.cursorCaptureStartTime = null;
938
1150
  this.cursorCaptureFirstWrite = true;
939
1151
  this.cursorDisplayInfo = null;
1152
+ this.cursorRecordingBaseTime = null;
1153
+ this.cursorRecordingOffsetMs = 0;
940
1154
 
941
1155
  this.emit("cursorCaptureStopped");
942
1156
  resolve(true);
@@ -1062,6 +1276,8 @@ class MacRecorder extends EventEmitter {
1062
1276
  isCapturing: !!this.cursorCaptureInterval,
1063
1277
  outputFile: this.cursorCaptureFile || null,
1064
1278
  startTime: this.cursorCaptureStartTime || null,
1279
+ recordingBaseTime: this.cursorRecordingBaseTime || null,
1280
+ recordingOffsetMs: this.cursorRecordingOffsetMs || 0,
1065
1281
  displayInfo: this.cursorDisplayInfo || null,
1066
1282
  };
1067
1283
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.17.9",
3
+ "version": "2.17.10",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -4,6 +4,7 @@
4
4
  #import <CoreGraphics/CoreGraphics.h>
5
5
  #import <ImageIO/ImageIO.h>
6
6
  #import <CoreAudio/CoreAudio.h>
7
+ #include <cstring>
7
8
 
8
9
  // Import screen capture (ScreenCaptureKit only)
9
10
  #import "screen_capture_kit.h"
@@ -59,6 +60,69 @@ void cleanupRecording() {
59
60
  g_isRecording = false;
60
61
  }
61
62
 
63
+ static Napi::Value NSObjectToNapiValue(Napi::Env env, id value) {
64
+ if (!value || value == [NSNull null]) {
65
+ return env.Null();
66
+ }
67
+
68
+ if ([value isKindOfClass:[NSDictionary class]]) {
69
+ NSDictionary *dict = (NSDictionary *)value;
70
+ Napi::Object obj = Napi::Object::New(env);
71
+
72
+ for (id key in dict) {
73
+ id nestedValue = [dict objectForKey:key];
74
+ NSString *keyString = nil;
75
+ if ([key isKindOfClass:[NSString class]]) {
76
+ keyString = (NSString *)key;
77
+ } else {
78
+ keyString = [key description];
79
+ }
80
+
81
+ if (!keyString) {
82
+ continue;
83
+ }
84
+
85
+ obj.Set(Napi::String::New(env, [keyString UTF8String]), NSObjectToNapiValue(env, nestedValue));
86
+ }
87
+
88
+ return obj;
89
+ }
90
+
91
+ if ([value isKindOfClass:[NSArray class]]) {
92
+ NSArray *array = (NSArray *)value;
93
+ Napi::Array result = Napi::Array::New(env, array.count);
94
+
95
+ for (NSUInteger i = 0; i < array.count; i++) {
96
+ result.Set(i, NSObjectToNapiValue(env, array[i]));
97
+ }
98
+
99
+ return result;
100
+ }
101
+
102
+ if ([value isKindOfClass:[NSNumber class]]) {
103
+ NSNumber *number = (NSNumber *)value;
104
+ const char *type = [number objCType];
105
+
106
+ if (strcmp(type, @encode(BOOL)) == 0) {
107
+ return Napi::Boolean::New(env, [number boolValue]);
108
+ }
109
+
110
+ return Napi::Number::New(env, [number doubleValue]);
111
+ }
112
+
113
+ if ([value isKindOfClass:[NSString class]]) {
114
+ NSString *stringValue = (NSString *)value;
115
+ return Napi::String::New(env, [stringValue UTF8String]);
116
+ }
117
+
118
+ if ([value isKindOfClass:[NSDate class]]) {
119
+ NSDate *dateValue = (NSDate *)value;
120
+ return Napi::Number::New(env, [dateValue timeIntervalSince1970] * 1000.0);
121
+ }
122
+
123
+ return Napi::String::New(env, [[value description] UTF8String]);
124
+ }
125
+
62
126
  // NAPI Function: Start Recording
63
127
  Napi::Value StartRecording(const Napi::CallbackInfo& info) {
64
128
  Napi::Env env = info.Env();
@@ -82,7 +146,7 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
82
146
 
83
147
  // Options parsing
84
148
  CGRect captureRect = CGRectNull;
85
- bool captureCursor = false; // Default olarak cursor gizli
149
+ bool captureCursor = false; // Default: cursor hidden in video
86
150
  bool includeMicrophone = false; // Default olarak mikrofon kapalı
87
151
  bool includeSystemAudio = true; // Default olarak sistem sesi açık
88
152
  CGDirectDisplayID displayID = CGMainDisplayID(); // Default ana ekran
@@ -205,14 +269,12 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
205
269
  NSLog(@"🔧 FORCE_AVFOUNDATION environment variable detected - skipping ScreenCaptureKit");
206
270
  }
207
271
 
208
- // Electron-first priority: This application is built for Electron.js
209
- // macOS 15+ ScreenCaptureKit (including Electron)
210
- // macOS 14/13 → AVFoundation (including Electron)
211
- if (isM15Plus && !forceAVFoundation) {
272
+ // Try ScreenCaptureKit on macOS 14+ (Electron supported) with robust fallback
273
+ if (isM14Plus && !forceAVFoundation) {
212
274
  if (isElectron) {
213
- NSLog(@"⚡ ELECTRON PRIORITY: macOS 15+ Electron → ScreenCaptureKit with full support");
275
+ NSLog(@"⚡ ELECTRON PRIORITY: macOS 14+ Electron → ScreenCaptureKit with full support (fallbacks enabled)");
214
276
  } else {
215
- NSLog(@"✅ macOS 15+ Node.js → ScreenCaptureKit available with full compatibility");
277
+ NSLog(@"✅ macOS 14+ Node.js → ScreenCaptureKit available with full compatibility");
216
278
  }
217
279
 
218
280
  // Try ScreenCaptureKit with extensive safety measures
@@ -287,18 +349,14 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
287
349
  // If we reach here, ScreenCaptureKit failed, so fall through to AVFoundation
288
350
  NSLog(@"⏭️ ScreenCaptureKit failed - falling back to AVFoundation");
289
351
  } else {
290
- // macOS 14/13 or forced AVFoundation → ALWAYS use AVFoundation (Electron supported!)
352
+ // macOS 13 or forced AVFoundation → use AVFoundation (Electron supported!)
291
353
  if (isElectron) {
292
- if (isM14Plus) {
293
- NSLog(@"⚡ ELECTRON PRIORITY: macOS 14/13 Electron → AVFoundation with full support");
294
- } else if (isM13Plus) {
354
+ if (isM13Plus) {
295
355
  NSLog(@"⚡ ELECTRON PRIORITY: macOS 13 Electron → AVFoundation with limited features");
296
356
  }
297
357
  } else {
298
- if (isM15Plus) {
299
- NSLog(@"🎯 macOS 15+ Node.js with FORCE_AVFOUNDATION → using AVFoundation");
300
- } else if (isM14Plus) {
301
- NSLog(@"🎯 macOS 14 Node.js → using AVFoundation (primary method)");
358
+ if (isM14Plus) {
359
+ NSLog(@"🎯 macOS 14+ Node.js with FORCE_AVFOUNDATION → using AVFoundation");
302
360
  } else if (isM13Plus) {
303
361
  NSLog(@"🎯 macOS 13 Node.js → using AVFoundation (limited features)");
304
362
  }
@@ -575,19 +633,29 @@ Napi::Value GetDisplays(const Napi::CallbackInfo& info) {
575
633
 
576
634
  for (uint32_t i = 0; i < displayCount; i++) {
577
635
  CGDirectDisplayID displayID = activeDisplays[i];
578
- CGRect displayBounds = CGDisplayBounds(displayID);
579
- bool isPrimary = (displayID == CGMainDisplayID());
580
-
581
- NSDictionary *displayInfo = @{
582
- @"id": @(displayID), // Direct CGDirectDisplayID
583
- @"name": [NSString stringWithFormat:@"Display %u", (unsigned int)(i + 1)],
584
- @"width": @((int)displayBounds.size.width),
585
- @"height": @((int)displayBounds.size.height),
586
- @"x": @((int)displayBounds.origin.x),
587
- @"y": @((int)displayBounds.origin.y),
588
- @"isPrimary": @(isPrimary)
589
- };
590
- [displays addObject:displayInfo];
636
+ CGRect displayBounds = CGDisplayBounds(displayID);
637
+ bool isPrimary = (displayID == CGMainDisplayID());
638
+ size_t pixelWidth = CGDisplayPixelsWide(displayID);
639
+ size_t pixelHeight = CGDisplayPixelsHigh(displayID);
640
+ double logicalWidth = displayBounds.size.width;
641
+ double logicalHeight = displayBounds.size.height;
642
+ double scaleX = (logicalWidth > 0.0) ? ((double)pixelWidth / logicalWidth) : 1.0;
643
+ double scaleY = (logicalHeight > 0.0) ? ((double)pixelHeight / logicalHeight) : 1.0;
644
+
645
+ NSDictionary *displayInfo = @{
646
+ @"id": @(displayID), // Direct CGDirectDisplayID
647
+ @"name": [NSString stringWithFormat:@"Display %u", (unsigned int)(i + 1)],
648
+ @"width": @((int)displayBounds.size.width),
649
+ @"height": @((int)displayBounds.size.height),
650
+ @"x": @((int)displayBounds.origin.x),
651
+ @"y": @((int)displayBounds.origin.y),
652
+ @"isPrimary": @(isPrimary),
653
+ @"pixelWidth": @((int)pixelWidth),
654
+ @"pixelHeight": @((int)pixelHeight),
655
+ @"scaleX": @((double)scaleX),
656
+ @"scaleY": @((double)scaleY)
657
+ };
658
+ [displays addObject:displayInfo];
591
659
  }
592
660
  Napi::Array result = Napi::Array::New(env, displays.count);
593
661
 
@@ -642,6 +710,21 @@ Napi::Value GetRecordingStatus(const Napi::CallbackInfo& info) {
642
710
  return Napi::Boolean::New(env, isRecording);
643
711
  }
644
712
 
713
+ Napi::Value GetActiveCaptureInfo(const Napi::CallbackInfo& info) {
714
+ Napi::Env env = info.Env();
715
+
716
+ if (@available(macOS 15.0, *)) {
717
+ NSDictionary *captureInfo = [ScreenCaptureKitRecorder currentCaptureInfo];
718
+ if (!captureInfo || captureInfo.count == 0) {
719
+ return env.Null();
720
+ }
721
+
722
+ return NSObjectToNapiValue(env, captureInfo);
723
+ }
724
+
725
+ return env.Null();
726
+ }
727
+
645
728
  // NAPI Function: Get Window Thumbnail
646
729
  Napi::Value GetWindowThumbnail(const Napi::CallbackInfo& info) {
647
730
  Napi::Env env = info.Env();
@@ -982,6 +1065,7 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
982
1065
  exports.Set(Napi::String::New(env, "getDisplays"), Napi::Function::New(env, GetDisplays));
983
1066
  exports.Set(Napi::String::New(env, "getWindows"), Napi::Function::New(env, GetWindows));
984
1067
  exports.Set(Napi::String::New(env, "getRecordingStatus"), Napi::Function::New(env, GetRecordingStatus));
1068
+ exports.Set(Napi::String::New(env, "getActiveCaptureInfo"), Napi::Function::New(env, GetActiveCaptureInfo));
985
1069
  exports.Set(Napi::String::New(env, "checkPermissions"), Napi::Function::New(env, CheckPermissions));
986
1070
 
987
1071
  // Thumbnail functions
@@ -997,4 +1081,4 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
997
1081
  return exports;
998
1082
  }
999
1083
 
1000
- NODE_API_MODULE(mac_recorder, Init)
1084
+ NODE_API_MODULE(mac_recorder, Init)
@@ -15,5 +15,6 @@ API_AVAILABLE(macos(15.0))
15
15
  + (void)finalizeRecording;
16
16
  + (void)finalizeVideoWriter;
17
17
  + (void)cleanupVideoWriter;
18
+ + (NSDictionary *)currentCaptureInfo;
18
19
 
19
- @end
20
+ @end
@@ -7,6 +7,7 @@ static id<SCStreamDelegate> API_AVAILABLE(macos(12.3)) g_streamDelegate = nil;
7
7
  static BOOL g_isRecording = NO;
8
8
  static BOOL g_isCleaningUp = NO; // Prevent recursive cleanup
9
9
  static NSString *g_outputPath = nil;
10
+ static NSDictionary *g_currentCaptureInfo = nil;
10
11
 
11
12
  @interface PureScreenCaptureDelegate : NSObject <SCStreamDelegate>
12
13
  @end
@@ -58,6 +59,8 @@ static NSString *g_outputPath = nil;
58
59
  g_isCleaningUp = NO;
59
60
  }
60
61
 
62
+ g_currentCaptureInfo = nil;
63
+
61
64
  NSString *outputPath = config[@"outputPath"];
62
65
  if (!outputPath || [outputPath length] == 0) {
63
66
  NSLog(@"❌ Invalid output path provided");
@@ -124,6 +127,34 @@ static NSString *g_outputPath = nil;
124
127
  filter = [[SCContentFilter alloc] initWithDesktopIndependentWindow:targetWindow];
125
128
  recordingWidth = (NSInteger)targetWindow.frame.size.width;
126
129
  recordingHeight = (NSInteger)targetWindow.frame.size.height;
130
+
131
+ CGRect windowFrame = targetWindow.frame;
132
+ NSDictionary *frameDict = @{
133
+ @"x": @(windowFrame.origin.x),
134
+ @"y": @(windowFrame.origin.y),
135
+ @"width": @(windowFrame.size.width),
136
+ @"height": @(windowFrame.size.height)
137
+ };
138
+
139
+ NSMutableDictionary *windowInfo = [@{
140
+ @"id": @(targetWindow.windowID),
141
+ @"frame": frameDict
142
+ } mutableCopy];
143
+
144
+ if (targetWindow.title) {
145
+ windowInfo[@"title"] = targetWindow.title;
146
+ }
147
+ if (targetApp && targetApp.applicationName) {
148
+ windowInfo[@"appName"] = targetApp.applicationName;
149
+ }
150
+ if (targetApp && targetApp.bundleIdentifier) {
151
+ windowInfo[@"bundleId"] = targetApp.bundleIdentifier;
152
+ }
153
+
154
+ g_currentCaptureInfo = @{
155
+ @"type": @"window",
156
+ @"window": windowInfo
157
+ };
127
158
  } else {
128
159
  NSLog(@"❌ Window ID %@ not found", windowId);
129
160
  return;
@@ -163,6 +194,24 @@ static NSString *g_outputPath = nil;
163
194
  filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:@[]];
164
195
  recordingWidth = targetDisplay.width;
165
196
  recordingHeight = targetDisplay.height;
197
+
198
+ CGRect displayFrame = targetDisplay.frame;
199
+ NSDictionary *displayFrameDict = @{
200
+ @"x": @(displayFrame.origin.x),
201
+ @"y": @(displayFrame.origin.y),
202
+ @"width": @(displayFrame.size.width),
203
+ @"height": @(displayFrame.size.height)
204
+ };
205
+
206
+ g_currentCaptureInfo = @{
207
+ @"type": @"display",
208
+ @"display": @{
209
+ @"id": @(targetDisplay.displayID),
210
+ @"frame": displayFrameDict,
211
+ @"width": @(targetDisplay.width),
212
+ @"height": @(targetDisplay.height)
213
+ }
214
+ };
166
215
  }
167
216
 
168
217
  // CROP AREA SUPPORT - Adjust dimensions and source rect
@@ -393,7 +442,8 @@ static NSString *g_outputPath = nil;
393
442
  // SCRecordingOutput finalizes automatically
394
443
  NSLog(@"✅ Pure recording output finalized");
395
444
  }
396
-
445
+
446
+ g_currentCaptureInfo = nil;
397
447
  [ScreenCaptureKitRecorder cleanupVideoWriter];
398
448
  }
399
449
  }
@@ -431,4 +481,10 @@ static NSString *g_outputPath = nil;
431
481
  }
432
482
  }
433
483
 
434
- @end
484
+ + (NSDictionary *)currentCaptureInfo {
485
+ @synchronized([ScreenCaptureKitRecorder class]) {
486
+ return g_currentCaptureInfo;
487
+ }
488
+ }
489
+
490
+ @end