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.
- package/.claude/settings.local.json +3 -1
- package/index.js +89 -305
- package/package.json +1 -1
- package/src/cursor_tracker.mm +134 -232
- package/src/mac_recorder.mm +29 -113
- package/src/screen_capture_kit.h +1 -2
- package/src/screen_capture_kit.mm +2 -58
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
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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:
|
|
332
|
-
height:
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
|
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
|
|
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
|
-
...
|
|
767
|
+
...windowInfo,
|
|
768
|
+
globalX,
|
|
769
|
+
globalY,
|
|
770
|
+
displayOffsetX,
|
|
771
|
+
displayOffsetY
|
|
905
772
|
},
|
|
906
773
|
targetDisplay,
|
|
907
|
-
captureArea
|
|
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:
|
|
946
|
-
height:
|
|
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
|
-
|
|
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
|
|
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:
|
|
997
|
-
|
|
998
|
-
|
|
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:
|
|
1057
|
-
y:
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
-
|
|
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
package/src/cursor_tracker.mm
CHANGED
|
@@ -35,9 +35,17 @@ NSDictionary* getDisplayScalingInfo(CGPoint globalPoint);
|
|
|
35
35
|
|
|
36
36
|
static CursorTimerTarget *g_timerTarget = nil;
|
|
37
37
|
|
|
38
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
143
|
+
else if ([elementRole isEqualToString:@"AXLink"] ||
|
|
144
|
+
[elementRole isEqualToString:@"AXButton"] ||
|
|
145
|
+
[elementRole isEqualToString:@"AXMenuItem"]) {
|
|
146
|
+
contextualCursorType = @"pointer";
|
|
275
147
|
}
|
|
276
|
-
else if ([
|
|
277
|
-
|
|
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
|
-
|
|
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
|
|
287
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
296
|
-
|
|
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
|
|
package/src/mac_recorder.mm
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
273
|
-
|
|
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
|
|
213
|
+
NSLog(@"⚡ ELECTRON PRIORITY: macOS 15+ Electron → ScreenCaptureKit with full support");
|
|
276
214
|
} else {
|
|
277
|
-
NSLog(@"✅ macOS
|
|
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 (
|
|
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 (
|
|
359
|
-
NSLog(@"🎯 macOS
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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)
|
package/src/screen_capture_kit.h
CHANGED
|
@@ -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
|
-
|
|
485
|
-
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
486
|
-
return g_currentCaptureInfo;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
@end
|
|
434
|
+
@end
|