node-mac-recorder 2.17.10 → 2.17.12

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.
@@ -4,7 +4,9 @@
4
4
  "Bash(node:*)",
5
5
  "Bash(chmod:*)",
6
6
  "Bash(cat:*)",
7
- "Bash(git checkout:*)"
7
+ "Bash(git checkout:*)",
8
+ "WebSearch",
9
+ "WebFetch(domain:stackoverflow.com)"
8
10
  ],
9
11
  "deny": [],
10
12
  "ask": []
package/index.js CHANGED
@@ -31,8 +31,6 @@ 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;
36
34
  this.lastCapturedData = null;
37
35
  this.cursorDisplayInfo = null;
38
36
  this.recordingDisplayInfo = null;
@@ -43,7 +41,7 @@ class MacRecorder extends EventEmitter {
43
41
  quality: "medium",
44
42
  frameRate: 30,
45
43
  captureArea: null, // { x, y, width, height }
46
- captureCursor: false, // Default: cursor hidden in video
44
+ captureCursor: false, // Default olarak cursor gizli
47
45
  showClicks: false,
48
46
  displayId: null, // Hangi ekranı kaydedeceği (null = ana ekran)
49
47
  windowId: null, // Hangi pencereyi kaydedeceği (null = tam ekran)
@@ -81,29 +79,16 @@ class MacRecorder extends EventEmitter {
81
79
  */
82
80
  async getDisplays() {
83
81
  const displays = nativeBinding.getDisplays();
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
- });
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
+ }));
107
92
  }
108
93
 
109
94
  /**
@@ -267,29 +252,16 @@ class MacRecorder extends EventEmitter {
267
252
 
268
253
  // Recording için display bilgisini sakla (cursor capture için)
269
254
  const targetDisplay = displays.find(d => d.id === targetDisplayId);
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
- }
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
+ };
293
265
  }
294
266
 
295
267
  this.options.captureArea = {
@@ -317,26 +289,15 @@ class MacRecorder extends EventEmitter {
317
289
  const displays = await this.getDisplays();
318
290
  const targetDisplay = displays.find(d => d.id === this.options.displayId);
319
291
  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
-
327
292
  this.recordingDisplayInfo = {
328
293
  displayId: this.options.displayId,
329
294
  x: targetDisplay.x,
330
295
  y: targetDisplay.y,
331
- width: logicalWidth,
332
- height: logicalHeight,
333
- pixelWidth,
334
- pixelHeight,
335
- scaleX,
336
- scaleY,
296
+ width: parseInt(targetDisplay.resolution.split("x")[0]),
297
+ height: parseInt(targetDisplay.resolution.split("x")[1]),
337
298
  // Add scaling information for cursor coordinate transformation
338
- logicalWidth,
339
- logicalHeight,
299
+ logicalWidth: parseInt(targetDisplay.resolution.split("x")[0]),
300
+ logicalHeight: parseInt(targetDisplay.resolution.split("x")[1]),
340
301
  };
341
302
  }
342
303
  } catch (error) {
@@ -395,105 +356,51 @@ class MacRecorder extends EventEmitter {
395
356
  this.isRecording = true;
396
357
  this.recordingStartTime = Date.now();
397
358
 
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
- }
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
417
384
  }
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
- });
483
- });
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);
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);
488
390
  });
489
- }
490
- };
491
-
492
- startWindowCursorTracking();
493
- } else {
494
- // For display recording, use display-relative cursor tracking
495
- this.startCursorCapture(cursorFilePath, {
496
- displayRelative: true,
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,
497
404
  displayInfo: this.recordingDisplayInfo
498
405
  }).catch(cursorError => {
499
406
  console.warn('Display cursor tracking failed:', cursorError.message);
@@ -805,16 +712,6 @@ class MacRecorder extends EventEmitter {
805
712
  const targetDisplay = windowInfo.targetDisplay || this.recordingDisplayInfo || null;
806
713
  const captureArea = windowInfo.captureArea || null;
807
714
  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);
818
715
 
819
716
  let globalX = hasNumber(windowInfo.x) ? windowInfo.x : null;
820
717
  let globalY = hasNumber(windowInfo.y) ? windowInfo.y : null;
@@ -848,40 +745,12 @@ class MacRecorder extends EventEmitter {
848
745
  ? captureArea.x
849
746
  : (targetDisplay && hasNumber(globalX) && hasNumber(targetDisplay.x)
850
747
  ? globalX - targetDisplay.x
851
- : globalX ?? 0);
748
+ : globalX);
852
749
  const displayOffsetY = captureArea && hasNumber(captureArea.y)
853
750
  ? captureArea.y
854
751
  : (targetDisplay && hasNumber(globalY) && hasNumber(targetDisplay.y)
855
752
  ? globalY - targetDisplay.y
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
- }
753
+ : globalY);
885
754
 
886
755
  this.cursorDisplayInfo = {
887
756
  displayId:
@@ -893,40 +762,25 @@ class MacRecorder extends EventEmitter {
893
762
  y: globalY,
894
763
  width: windowInfo.width,
895
764
  height: windowInfo.height,
896
- pixelWidth: windowPixelWidth,
897
- pixelHeight: windowPixelHeight,
898
- scaleX: displayScaleX,
899
- scaleY: displayScaleY,
900
- displayRelativeX: displayOffsetX,
901
- displayRelativeY: displayOffsetY,
902
765
  windowRelative: true,
903
766
  windowInfo: {
904
- ...enrichedWindowInfo
767
+ ...windowInfo,
768
+ globalX,
769
+ globalY,
770
+ displayOffsetX,
771
+ displayOffsetY
905
772
  },
906
773
  targetDisplay,
907
- captureArea: captureAreaInfo
774
+ captureArea
908
775
  };
909
776
  } else if (options.displayRelative && options.displayInfo) {
910
777
  // 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
-
920
778
  this.cursorDisplayInfo = {
921
779
  displayId: options.displayInfo.displayId,
922
780
  x: options.displayInfo.x,
923
781
  y: options.displayInfo.y,
924
782
  width: options.displayInfo.width,
925
783
  height: options.displayInfo.height,
926
- pixelWidth,
927
- pixelHeight,
928
- scaleX,
929
- scaleY,
930
784
  displayRelative: true
931
785
  };
932
786
  } else if (this.recordingDisplayInfo) {
@@ -942,16 +796,8 @@ class MacRecorder extends EventEmitter {
942
796
  displayId: mainDisplay.id,
943
797
  x: mainDisplay.x,
944
798
  y: mainDisplay.y,
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),
799
+ width: parseInt(mainDisplay.resolution.split("x")[0]),
800
+ height: parseInt(mainDisplay.resolution.split("x")[1]),
955
801
  };
956
802
  }
957
803
  } catch (error) {
@@ -967,24 +813,15 @@ class MacRecorder extends EventEmitter {
967
813
  fs.writeFileSync(filepath, "[");
968
814
 
969
815
  this.cursorCaptureFile = filepath;
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);
816
+ this.cursorCaptureStartTime = Date.now();
977
817
  this.cursorCaptureFirstWrite = true;
978
818
  this.lastCapturedData = null;
979
819
 
980
820
  // JavaScript interval ile polling yap (daha sık - mouse event'leri yakalamak için)
981
821
  this.cursorCaptureInterval = setInterval(() => {
982
822
  try {
983
- const now = Date.now();
984
823
  const position = nativeBinding.getCursorPosition();
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);
824
+ const timestamp = Date.now() - this.cursorCaptureStartTime;
988
825
 
989
826
  // Transform coordinates based on recording type
990
827
  let x = position.x;
@@ -993,30 +830,9 @@ class MacRecorder extends EventEmitter {
993
830
 
994
831
  if (this.cursorDisplayInfo) {
995
832
  if (this.cursorDisplayInfo.windowRelative) {
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
- }
833
+ // Window recording: Transform global → window-relative coordinates
834
+ x = position.x - this.cursorDisplayInfo.x;
835
+ y = position.y - this.cursorDisplayInfo.y;
1020
836
  coordinateSystem = "window-relative";
1021
837
 
1022
838
  // Window bounds check - skip if cursor is outside window
@@ -1041,28 +857,11 @@ class MacRecorder extends EventEmitter {
1041
857
  }
1042
858
  }
1043
859
 
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
-
1055
860
  const cursorData = {
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,
861
+ x: x,
862
+ y: y,
863
+ timestamp: timestamp,
864
+ unixTimeMs: Date.now(),
1066
865
  cursorType: position.cursorType,
1067
866
  type: position.eventType || "move",
1068
867
  coordinateSystem: coordinateSystem,
@@ -1071,14 +870,7 @@ class MacRecorder extends EventEmitter {
1071
870
  windowInfo: {
1072
871
  width: this.cursorDisplayInfo.width,
1073
872
  height: this.cursorDisplayInfo.height,
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
873
+ displayId: this.cursorDisplayInfo.displayId
1082
874
  }
1083
875
  }),
1084
876
  // Include display context for display-relative coordinates
@@ -1086,11 +878,7 @@ class MacRecorder extends EventEmitter {
1086
878
  displayInfo: {
1087
879
  displayId: this.cursorDisplayInfo.displayId,
1088
880
  width: this.cursorDisplayInfo.width,
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
881
+ height: this.cursorDisplayInfo.height
1094
882
  }
1095
883
  })
1096
884
  };
@@ -1149,8 +937,6 @@ class MacRecorder extends EventEmitter {
1149
937
  this.cursorCaptureStartTime = null;
1150
938
  this.cursorCaptureFirstWrite = true;
1151
939
  this.cursorDisplayInfo = null;
1152
- this.cursorRecordingBaseTime = null;
1153
- this.cursorRecordingOffsetMs = 0;
1154
940
 
1155
941
  this.emit("cursorCaptureStopped");
1156
942
  resolve(true);
@@ -1276,8 +1062,6 @@ class MacRecorder extends EventEmitter {
1276
1062
  isCapturing: !!this.cursorCaptureInterval,
1277
1063
  outputFile: this.cursorCaptureFile || null,
1278
1064
  startTime: this.cursorCaptureStartTime || null,
1279
- recordingBaseTime: this.cursorRecordingBaseTime || null,
1280
- recordingOffsetMs: this.cursorRecordingOffsetMs || 0,
1281
1065
  displayInfo: this.cursorDisplayInfo || null,
1282
1066
  };
1283
1067
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.17.10",
3
+ "version": "2.17.12",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -35,9 +35,17 @@ NSDictionary* getDisplayScalingInfo(CGPoint globalPoint);
35
35
 
36
36
  static CursorTimerTarget *g_timerTarget = nil;
37
37
 
38
- // Global cursor state tracking
38
+ // Enhanced cursor state tracking with stability
39
39
  static NSString *g_lastDetectedCursorType = nil;
40
+ static NSString *g_stableCursorType = @"default";
40
41
  static int g_cursorTypeCounter = 0;
42
+ static NSTimeInterval g_lastCursorCheckTime = 0;
43
+ static int g_sameCursorDetectionCount = 0;
44
+ static NSString *g_pendingCursorType = nil;
45
+
46
+ // Cursor stability constants
47
+ static const NSTimeInterval CURSOR_STABILITY_THRESHOLD = 0.1; // 100ms
48
+ static const int CURSOR_CONFIRMATION_COUNT = 2; // Need 2 consecutive detections
41
49
 
42
50
  // Mouse button state tracking
43
51
  static bool g_leftMouseDown = false;
@@ -49,255 +57,145 @@ static CGEventRef eventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEv
49
57
  return event;
50
58
  }
51
59
 
52
- // Cursor type detection helper - sistem genelindeki cursor type'ı al
60
+ // Enhanced cursor type detection with multi-layer approach and stability
61
+ NSString* detectCursorTypeFromNSCursor() {
62
+ @try {
63
+ NSCursor *currentCursor = [NSCursor currentSystemCursor];
64
+ if (!currentCursor) {
65
+ return @"default";
66
+ }
67
+
68
+ // Compare with known system cursors using identity comparison
69
+ if (currentCursor == [NSCursor arrowCursor]) {
70
+ return @"default";
71
+ } else if (currentCursor == [NSCursor IBeamCursor]) {
72
+ return @"text";
73
+ } else if (currentCursor == [NSCursor pointingHandCursor]) {
74
+ return @"pointer";
75
+ } else if (currentCursor == [NSCursor resizeLeftRightCursor]) {
76
+ return @"col-resize";
77
+ } else if (currentCursor == [NSCursor resizeUpDownCursor]) {
78
+ return @"ns-resize";
79
+ } else if (currentCursor == [NSCursor crosshairCursor]) {
80
+ return @"crosshair";
81
+ } else if (currentCursor == [NSCursor openHandCursor]) {
82
+ return @"grab";
83
+ } else if (currentCursor == [NSCursor closedHandCursor]) {
84
+ return @"grabbing";
85
+ } else if (currentCursor == [NSCursor operationNotAllowedCursor]) {
86
+ return @"not-allowed";
87
+ }
88
+
89
+ // Fallback to image-based comparison for custom cursors
90
+ NSImage *cursorImage = [currentCursor image];
91
+ if (cursorImage) {
92
+ NSSize imageSize = [cursorImage size];
93
+
94
+ // Text cursors typically have I-beam shape (narrow width)
95
+ if (imageSize.width < 8 && imageSize.height > 15) {
96
+ return @"text";
97
+ }
98
+ // Pointer cursors are typically hand-shaped
99
+ else if (imageSize.width > 15 && imageSize.height > 15) {
100
+ return @"pointer";
101
+ }
102
+ }
103
+
104
+ return @"default";
105
+ } @catch (NSException *exception) {
106
+ return @"default";
107
+ }
108
+ }
109
+
110
+ // Improved cursor type detection with stability and multi-layer approach
53
111
  NSString* getCursorType() {
54
112
  @autoreleasepool {
55
113
  g_cursorTypeCounter++;
56
-
57
- @try {
58
- // ACCESSIBILITY API BASED CURSOR DETECTION
59
- // Determine cursor type based on the UI element under the cursor
60
-
61
- CGPoint cursorPos = CGEventGetLocation(CGEventCreate(NULL));
62
- AXUIElementRef systemWide = AXUIElementCreateSystemWide();
63
- AXUIElementRef elementAtPosition = NULL;
64
- AXError error = AXUIElementCopyElementAtPosition(systemWide, cursorPos.x, cursorPos.y, &elementAtPosition);
65
-
66
- NSString *cursorType = @"default"; // Default fallback
67
-
68
- if (error == kAXErrorSuccess && elementAtPosition) {
69
- CFStringRef role = NULL;
70
- error = AXUIElementCopyAttributeValue(elementAtPosition, kAXRoleAttribute, (CFTypeRef*)&role);
71
-
72
- if (error == kAXErrorSuccess && role) {
73
- NSString *elementRole = (__bridge_transfer NSString*)role;
74
- NSLog(@"🎯 ELEMENT ROLE: %@", elementRole);
75
-
76
- // TEXT CURSORS
77
- if ([elementRole isEqualToString:@"AXTextField"] ||
78
- [elementRole isEqualToString:@"AXTextArea"] ||
79
- [elementRole isEqualToString:@"AXStaticText"] ||
80
- [elementRole isEqualToString:@"AXSearchField"]) {
81
- cursorType = @"text";
82
- }
83
- // POINTER CURSORS (clickable elements)
84
- else if ([elementRole isEqualToString:@"AXLink"] ||
85
- [elementRole isEqualToString:@"AXButton"] ||
86
- [elementRole isEqualToString:@"AXMenuItem"] ||
87
- [elementRole isEqualToString:@"AXRadioButton"] ||
88
- [elementRole isEqualToString:@"AXCheckBox"] ||
89
- [elementRole isEqualToString:@"AXPopUpButton"] ||
90
- [elementRole isEqualToString:@"AXTab"]) {
91
- cursorType = @"pointer";
92
- }
93
- // GRAB CURSORS (draggable elements)
94
- else if ([elementRole isEqualToString:@"AXImage"] ||
95
- [elementRole isEqualToString:@"AXGroup"]) {
96
- // Check if element is draggable
97
- CFBooleanRef draggable = NULL;
98
- error = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXMovable"), (CFTypeRef*)&draggable);
99
- if (error == kAXErrorSuccess && draggable && CFBooleanGetValue(draggable)) {
100
- cursorType = @"grab";
101
- } else {
102
- cursorType = @"default";
103
- }
104
- }
105
- // PROGRESS CURSORS (loading/busy elements)
106
- else if ([elementRole isEqualToString:@"AXProgressIndicator"] ||
107
- [elementRole isEqualToString:@"AXBusyIndicator"]) {
108
- cursorType = @"progress";
109
- }
110
- // HELP CURSORS (help buttons/tooltips)
111
- else if ([elementRole isEqualToString:@"AXHelpTag"] ||
112
- [elementRole isEqualToString:@"AXTooltip"]) {
113
- cursorType = @"help";
114
- }
115
- // RESIZE CURSORS - sadece AXSplitter için
116
- else if ([elementRole isEqualToString:@"AXSplitter"]) {
117
- // Get splitter orientation to determine resize direction
118
- CFStringRef orientation = NULL;
119
- error = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXOrientation"), (CFTypeRef*)&orientation);
120
- if (error == kAXErrorSuccess && orientation) {
121
- NSString *orientationStr = (__bridge_transfer NSString*)orientation;
122
- if ([orientationStr isEqualToString:@"AXHorizontalOrientation"]) {
123
- cursorType = @"ns-resize"; // Yatay splitter -> dikey hareket (north-south)
124
- } else if ([orientationStr isEqualToString:@"AXVerticalOrientation"]) {
125
- cursorType = @"col-resize"; // Dikey splitter -> yatay hareket (east-west)
126
- } else {
127
- cursorType = @"default"; // Bilinmeyen orientation
128
- }
129
- } else {
130
- cursorType = @"default"; // Orientation alınamazsa default
131
- }
132
- }
133
- // SCROLL CURSORS - hep default olsun, all-scroll görünmesin
134
- else if ([elementRole isEqualToString:@"AXScrollBar"]) {
135
- cursorType = @"default"; // ScrollBar'lar için de default
136
- }
137
- // AXScrollArea - hep default
138
- else if ([elementRole isEqualToString:@"AXScrollArea"]) {
139
- cursorType = @"default"; // ScrollArea her zaman default
140
- }
141
- // CROSSHAIR CURSORS (drawing/selection tools)
142
- else if ([elementRole isEqualToString:@"AXCanvas"] ||
143
- [elementRole isEqualToString:@"AXDrawingArea"]) {
144
- cursorType = @"crosshair";
145
- }
146
- // ZOOM CURSORS (zoom controls)
147
- else if ([elementRole isEqualToString:@"AXZoomButton"]) {
148
- cursorType = @"zoom-in";
149
- }
150
- // NOT-ALLOWED CURSORS (disabled elements)
151
- else if ([elementRole isEqualToString:@"AXStaticText"] ||
152
- [elementRole isEqualToString:@"AXGroup"]) {
153
- // Check if element is disabled/readonly
154
- CFBooleanRef enabled = NULL;
155
- error = AXUIElementCopyAttributeValue(elementAtPosition, kAXEnabledAttribute, (CFTypeRef*)&enabled);
156
- if (error == kAXErrorSuccess && enabled && !CFBooleanGetValue(enabled)) {
157
- cursorType = @"not-allowed";
158
- }
159
- }
160
- // WINDOW BORDER RESIZE - sadece pencere kenarlarında
161
- else if ([elementRole isEqualToString:@"AXWindow"]) {
162
- // Check window attributes to see if it's resizable
163
- CFBooleanRef resizable = NULL;
164
- AXError resizableError = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXResizeButton"), (CFTypeRef*)&resizable);
165
-
166
- // Sadece resize edilebilir pencereler için cursor değişimi
167
- if (resizableError == kAXErrorSuccess || true) { // AXResizeButton bulunamazsa da devam et
168
- CFTypeRef position = NULL;
169
- CFTypeRef size = NULL;
170
- error = AXUIElementCopyAttributeValue(elementAtPosition, kAXPositionAttribute, &position);
171
- AXError sizeError = AXUIElementCopyAttributeValue(elementAtPosition, kAXSizeAttribute, &size);
172
-
173
- if (error == kAXErrorSuccess && sizeError == kAXErrorSuccess && position && size) {
174
- CGPoint windowPos;
175
- CGSize windowSize;
176
- AXValueGetValue((AXValueRef)position, kAXValueTypeCGPoint, &windowPos);
177
- AXValueGetValue((AXValueRef)size, kAXValueTypeCGSize, &windowSize);
178
-
179
- CGFloat x = cursorPos.x - windowPos.x;
180
- CGFloat y = cursorPos.y - windowPos.y;
181
- CGFloat w = windowSize.width;
182
- CGFloat h = windowSize.height;
183
- CGFloat edge = 3.0; // Daha küçük edge detection (3px)
184
-
185
- // Sadece çok kenar köşelerde resize cursor'ı göster
186
- BOOL isOnBorder = NO;
187
-
188
- // Corner resize detection - çok dar alanda, doğru açılar
189
- if (x <= edge && y <= edge) {
190
- cursorType = @"nwse-resize"; // Sol üst köşe - northwest-southeast
191
- isOnBorder = YES;
192
- }
193
- else if (x >= w-edge && y <= edge) {
194
- cursorType = @"nesw-resize"; // Sağ üst köşe - northeast-southwest
195
- isOnBorder = YES;
196
- }
197
- else if (x <= edge && y >= h-edge) {
198
- cursorType = @"nesw-resize"; // Sol alt köşe - southwest-northeast
199
- isOnBorder = YES;
200
- }
201
- else if (x >= w-edge && y >= h-edge) {
202
- cursorType = @"nwse-resize"; // Sağ alt köşe - southeast-northwest
203
- isOnBorder = YES;
204
- }
205
- // Edge resize detection - sadece çok kenarlarda
206
- else if (x <= edge && y > edge && y < h-edge) {
207
- cursorType = @"col-resize"; // Sol kenar - column resize (yatay)
208
- isOnBorder = YES;
209
- }
210
- else if (x >= w-edge && y > edge && y < h-edge) {
211
- cursorType = @"col-resize"; // Sağ kenar - column resize (yatay)
212
- isOnBorder = YES;
213
- }
214
- else if (y <= edge && x > edge && x < w-edge) {
215
- cursorType = @"ns-resize"; // Üst kenar - north-south resize (dikey)
216
- isOnBorder = YES;
217
- }
218
- else if (y >= h-edge && x > edge && x < w-edge) {
219
- cursorType = @"ns-resize"; // Alt kenar - north-south resize (dikey)
220
- isOnBorder = YES;
221
- }
222
-
223
- // Eğer border'da değilse default
224
- if (!isOnBorder) {
225
- cursorType = @"default";
226
- }
227
-
228
- if (position) CFRelease(position);
229
- if (size) CFRelease(size);
230
- } else {
231
- cursorType = @"default";
232
- }
233
- } else {
234
- cursorType = @"default";
235
- }
236
- }
237
- // HER DURUM İÇİN DEFAULT FALLBACK
238
- else {
239
- // Bilinmeyen elementler için her zaman default
240
- cursorType = @"default";
241
- }
114
+ NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
242
115
 
243
- // Check subroles for additional context
244
- CFStringRef subrole = NULL;
245
- error = AXUIElementCopyAttributeValue(elementAtPosition, kAXSubroleAttribute, (CFTypeRef*)&subrole);
246
- if (error == kAXErrorSuccess && subrole) {
247
- NSString *elementSubrole = (__bridge_transfer NSString*)subrole;
248
- NSLog(@"🎯 ELEMENT SUBROLE: %@", elementSubrole);
249
-
250
- // Subrole override'ları - sadece çok spesifik durumlar için
251
- if ([elementSubrole isEqualToString:@"AXCloseButton"] ||
252
- [elementSubrole isEqualToString:@"AXMinimizeButton"] ||
253
- [elementSubrole isEqualToString:@"AXZoomButton"] ||
254
- [elementSubrole isEqualToString:@"AXToolbarButton"]) {
255
- cursorType = @"pointer";
256
- }
257
- // Copy/alias subroles - sadece bu durumlar için override
258
- else if ([elementSubrole isEqualToString:@"AXFileDrop"] ||
259
- [elementSubrole isEqualToString:@"AXDropTarget"]) {
260
- cursorType = @"copy";
261
- }
262
- // Alias/shortcut subroles
263
- else if ([elementSubrole isEqualToString:@"AXAlias"] ||
264
- [elementSubrole isEqualToString:@"AXShortcut"]) {
265
- cursorType = @"alias";
266
- }
267
- // Grabbing state (being dragged) - sadece gerçek drag sırasında
268
- else if ([elementSubrole isEqualToString:@"AXDragging"] ||
269
- [elementSubrole isEqualToString:@"AXMoving"]) {
270
- cursorType = @"grabbing";
116
+ @try {
117
+ // Layer 1: Fast NSCursor detection (most reliable)
118
+ NSString *nsCursorType = detectCursorTypeFromNSCursor();
119
+
120
+ // Layer 2: Accessibility API for context (when NSCursor isn't enough)
121
+ NSString *contextualCursorType = nsCursorType;
122
+
123
+ // Only use expensive Accessibility API if NSCursor gives us "default"
124
+ if ([nsCursorType isEqualToString:@"default"]) {
125
+ CGPoint cursorPos = CGEventGetLocation(CGEventCreate(NULL));
126
+ AXUIElementRef systemWide = AXUIElementCreateSystemWide();
127
+ AXUIElementRef elementAtPosition = NULL;
128
+ AXError error = AXUIElementCopyElementAtPosition(systemWide, cursorPos.x, cursorPos.y, &elementAtPosition);
129
+
130
+ if (error == kAXErrorSuccess && elementAtPosition) {
131
+ CFStringRef role = NULL;
132
+ error = AXUIElementCopyAttributeValue(elementAtPosition, kAXRoleAttribute, (CFTypeRef*)&role);
133
+
134
+ if (error == kAXErrorSuccess && role) {
135
+ NSString *elementRole = (__bridge_transfer NSString*)role;
136
+
137
+ // Simplified, high-confidence role mappings only
138
+ if ([elementRole isEqualToString:@"AXTextField"] ||
139
+ [elementRole isEqualToString:@"AXTextArea"] ||
140
+ [elementRole isEqualToString:@"AXSearchField"]) {
141
+ contextualCursorType = @"text";
271
142
  }
272
- // Zoom controls - sadece spesifik zoom butonları için
273
- else if ([elementSubrole isEqualToString:@"AXZoomIn"]) {
274
- cursorType = @"zoom-in";
143
+ else if ([elementRole isEqualToString:@"AXLink"] ||
144
+ [elementRole isEqualToString:@"AXButton"] ||
145
+ [elementRole isEqualToString:@"AXMenuItem"]) {
146
+ contextualCursorType = @"pointer";
275
147
  }
276
- else if ([elementSubrole isEqualToString:@"AXZoomOut"]) {
277
- cursorType = @"zoom-out";
148
+ else if ([elementRole isEqualToString:@"AXProgressIndicator"]) {
149
+ contextualCursorType = @"progress";
278
150
  }
279
- // Subrole'dan bir şey bulamazsa role-based cursor'ı koruyoruz
280
151
  }
152
+ CFRelease(elementAtPosition);
281
153
  }
154
+ if (systemWide) CFRelease(systemWide);
155
+ }
282
156
 
283
- CFRelease(elementAtPosition);
157
+ // Layer 3: Stability filtering to prevent oscillation
158
+ NSString *detectedCursorType = contextualCursorType;
159
+
160
+ // Time-based stability check
161
+ if (currentTime - g_lastCursorCheckTime > CURSOR_STABILITY_THRESHOLD) {
162
+ // Enough time has passed, reset counters
163
+ g_sameCursorDetectionCount = 0;
164
+ g_pendingCursorType = detectedCursorType;
284
165
  }
285
166
 
286
- if (systemWide) {
287
- CFRelease(systemWide);
167
+ // Check if detected cursor matches pending cursor
168
+ if ([detectedCursorType isEqualToString:g_pendingCursorType]) {
169
+ g_sameCursorDetectionCount++;
170
+
171
+ // If we have enough confirmations, update stable cursor
172
+ if (g_sameCursorDetectionCount >= CURSOR_CONFIRMATION_COUNT) {
173
+ g_stableCursorType = detectedCursorType;
174
+ g_lastDetectedCursorType = detectedCursorType;
175
+ }
176
+ } else {
177
+ // Different cursor detected, start new pending
178
+ g_pendingCursorType = detectedCursorType;
179
+ g_sameCursorDetectionCount = 1;
288
180
  }
289
181
 
290
- // Son güvence - eğer cursorType hala nil veya geçersizse default'a çevir
291
- if (!cursorType || [cursorType length] == 0) {
292
- cursorType = @"default";
182
+ g_lastCursorCheckTime = currentTime;
183
+
184
+ // Final validation
185
+ NSString *finalCursorType = g_stableCursorType;
186
+ if (!finalCursorType || [finalCursorType length] == 0) {
187
+ finalCursorType = @"default";
293
188
  }
294
189
 
295
- NSLog(@"🎯 FINAL CURSOR TYPE: %@", cursorType);
296
- return cursorType;
297
-
190
+ // Debug logging for stability tracking
191
+ NSLog(@"🎯 CURSOR DETECTION - NSCursor: %@, Contextual: %@, Stable: %@, Count: %d",
192
+ nsCursorType, contextualCursorType, finalCursorType, g_sameCursorDetectionCount);
193
+
194
+ return finalCursorType;
195
+
298
196
  } @catch (NSException *exception) {
299
197
  NSLog(@"Error in getCursorType: %@", exception);
300
- return @"default";
198
+ return g_stableCursorType ?: @"default";
301
199
  }
302
200
  }
303
201
  }
@@ -501,7 +399,11 @@ void cleanupCursorTracking() {
501
399
  g_outputPath = nil;
502
400
  g_debugCallbackCount = 0;
503
401
  g_lastDetectedCursorType = nil;
402
+ g_stableCursorType = @"default";
504
403
  g_cursorTypeCounter = 0;
404
+ g_lastCursorCheckTime = 0;
405
+ g_sameCursorDetectionCount = 0;
406
+ g_pendingCursorType = nil;
505
407
  g_isFirstWrite = true;
506
408
  }
507
409
 
@@ -4,7 +4,6 @@
4
4
  #import <CoreGraphics/CoreGraphics.h>
5
5
  #import <ImageIO/ImageIO.h>
6
6
  #import <CoreAudio/CoreAudio.h>
7
- #include <cstring>
8
7
 
9
8
  // Import screen capture (ScreenCaptureKit only)
10
9
  #import "screen_capture_kit.h"
@@ -60,69 +59,6 @@ void cleanupRecording() {
60
59
  g_isRecording = false;
61
60
  }
62
61
 
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
-
126
62
  // NAPI Function: Start Recording
127
63
  Napi::Value StartRecording(const Napi::CallbackInfo& info) {
128
64
  Napi::Env env = info.Env();
@@ -146,7 +82,7 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
146
82
 
147
83
  // Options parsing
148
84
  CGRect captureRect = CGRectNull;
149
- bool captureCursor = false; // Default: cursor hidden in video
85
+ bool captureCursor = false; // Default olarak cursor gizli
150
86
  bool includeMicrophone = false; // Default olarak mikrofon kapalı
151
87
  bool includeSystemAudio = true; // Default olarak sistem sesi açık
152
88
  CGDirectDisplayID displayID = CGMainDisplayID(); // Default ana ekran
@@ -269,12 +205,14 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
269
205
  NSLog(@"🔧 FORCE_AVFOUNDATION environment variable detected - skipping ScreenCaptureKit");
270
206
  }
271
207
 
272
- // Try ScreenCaptureKit on macOS 14+ (Electron supported) with robust fallback
273
- if (isM14Plus && !forceAVFoundation) {
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) {
274
212
  if (isElectron) {
275
- NSLog(@"⚡ ELECTRON PRIORITY: macOS 14+ Electron → ScreenCaptureKit with full support (fallbacks enabled)");
213
+ NSLog(@"⚡ ELECTRON PRIORITY: macOS 15+ Electron → ScreenCaptureKit with full support");
276
214
  } else {
277
- NSLog(@"✅ macOS 14+ Node.js → ScreenCaptureKit available with full compatibility");
215
+ NSLog(@"✅ macOS 15+ Node.js → ScreenCaptureKit available with full compatibility");
278
216
  }
279
217
 
280
218
  // Try ScreenCaptureKit with extensive safety measures
@@ -349,14 +287,18 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
349
287
  // If we reach here, ScreenCaptureKit failed, so fall through to AVFoundation
350
288
  NSLog(@"⏭️ ScreenCaptureKit failed - falling back to AVFoundation");
351
289
  } else {
352
- // macOS 13 or forced AVFoundation → use AVFoundation (Electron supported!)
290
+ // macOS 14/13 or forced AVFoundation → ALWAYS use AVFoundation (Electron supported!)
353
291
  if (isElectron) {
354
- if (isM13Plus) {
292
+ if (isM14Plus) {
293
+ NSLog(@"⚡ ELECTRON PRIORITY: macOS 14/13 Electron → AVFoundation with full support");
294
+ } else if (isM13Plus) {
355
295
  NSLog(@"⚡ ELECTRON PRIORITY: macOS 13 Electron → AVFoundation with limited features");
356
296
  }
357
297
  } else {
358
- if (isM14Plus) {
359
- NSLog(@"🎯 macOS 14+ Node.js with FORCE_AVFOUNDATION → using AVFoundation");
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)");
360
302
  } else if (isM13Plus) {
361
303
  NSLog(@"🎯 macOS 13 Node.js → using AVFoundation (limited features)");
362
304
  }
@@ -633,29 +575,19 @@ Napi::Value GetDisplays(const Napi::CallbackInfo& info) {
633
575
 
634
576
  for (uint32_t i = 0; i < displayCount; i++) {
635
577
  CGDirectDisplayID displayID = activeDisplays[i];
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];
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];
659
591
  }
660
592
  Napi::Array result = Napi::Array::New(env, displays.count);
661
593
 
@@ -710,21 +642,6 @@ Napi::Value GetRecordingStatus(const Napi::CallbackInfo& info) {
710
642
  return Napi::Boolean::New(env, isRecording);
711
643
  }
712
644
 
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
-
728
645
  // NAPI Function: Get Window Thumbnail
729
646
  Napi::Value GetWindowThumbnail(const Napi::CallbackInfo& info) {
730
647
  Napi::Env env = info.Env();
@@ -1065,7 +982,6 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
1065
982
  exports.Set(Napi::String::New(env, "getDisplays"), Napi::Function::New(env, GetDisplays));
1066
983
  exports.Set(Napi::String::New(env, "getWindows"), Napi::Function::New(env, GetWindows));
1067
984
  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));
1069
985
  exports.Set(Napi::String::New(env, "checkPermissions"), Napi::Function::New(env, CheckPermissions));
1070
986
 
1071
987
  // Thumbnail functions
@@ -1081,4 +997,4 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
1081
997
  return exports;
1082
998
  }
1083
999
 
1084
- NODE_API_MODULE(mac_recorder, Init)
1000
+ NODE_API_MODULE(mac_recorder, Init)
@@ -15,6 +15,5 @@ API_AVAILABLE(macos(15.0))
15
15
  + (void)finalizeRecording;
16
16
  + (void)finalizeVideoWriter;
17
17
  + (void)cleanupVideoWriter;
18
- + (NSDictionary *)currentCaptureInfo;
19
18
 
20
- @end
19
+ @end
@@ -7,7 +7,6 @@ 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;
11
10
 
12
11
  @interface PureScreenCaptureDelegate : NSObject <SCStreamDelegate>
13
12
  @end
@@ -59,8 +58,6 @@ static NSDictionary *g_currentCaptureInfo = nil;
59
58
  g_isCleaningUp = NO;
60
59
  }
61
60
 
62
- g_currentCaptureInfo = nil;
63
-
64
61
  NSString *outputPath = config[@"outputPath"];
65
62
  if (!outputPath || [outputPath length] == 0) {
66
63
  NSLog(@"❌ Invalid output path provided");
@@ -127,34 +124,6 @@ static NSDictionary *g_currentCaptureInfo = nil;
127
124
  filter = [[SCContentFilter alloc] initWithDesktopIndependentWindow:targetWindow];
128
125
  recordingWidth = (NSInteger)targetWindow.frame.size.width;
129
126
  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
- };
158
127
  } else {
159
128
  NSLog(@"❌ Window ID %@ not found", windowId);
160
129
  return;
@@ -194,24 +163,6 @@ static NSDictionary *g_currentCaptureInfo = nil;
194
163
  filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:@[]];
195
164
  recordingWidth = targetDisplay.width;
196
165
  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
- };
215
166
  }
216
167
 
217
168
  // CROP AREA SUPPORT - Adjust dimensions and source rect
@@ -442,8 +393,7 @@ static NSDictionary *g_currentCaptureInfo = nil;
442
393
  // SCRecordingOutput finalizes automatically
443
394
  NSLog(@"✅ Pure recording output finalized");
444
395
  }
445
-
446
- g_currentCaptureInfo = nil;
396
+
447
397
  [ScreenCaptureKitRecorder cleanupVideoWriter];
448
398
  }
449
399
  }
@@ -481,10 +431,4 @@ static NSDictionary *g_currentCaptureInfo = nil;
481
431
  }
482
432
  }
483
433
 
484
- + (NSDictionary *)currentCaptureInfo {
485
- @synchronized([ScreenCaptureKitRecorder class]) {
486
- return g_currentCaptureInfo;
487
- }
488
- }
489
-
490
- @end
434
+ @end