node-mac-recorder 2.17.8 → 2.17.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +387 -144
- package/package.json +1 -1
- package/src/mac_recorder.mm +113 -29
- package/src/screen_capture_kit.h +2 -1
- package/src/screen_capture_kit.mm +58 -2
package/index.js
CHANGED
|
@@ -31,6 +31,8 @@ class MacRecorder extends EventEmitter {
|
|
|
31
31
|
this.cursorCaptureFile = null;
|
|
32
32
|
this.cursorCaptureStartTime = null;
|
|
33
33
|
this.cursorCaptureFirstWrite = true;
|
|
34
|
+
this.cursorRecordingBaseTime = null;
|
|
35
|
+
this.cursorRecordingOffsetMs = 0;
|
|
34
36
|
this.lastCapturedData = null;
|
|
35
37
|
this.cursorDisplayInfo = null;
|
|
36
38
|
this.recordingDisplayInfo = null;
|
|
@@ -41,7 +43,7 @@ class MacRecorder extends EventEmitter {
|
|
|
41
43
|
quality: "medium",
|
|
42
44
|
frameRate: 30,
|
|
43
45
|
captureArea: null, // { x, y, width, height }
|
|
44
|
-
captureCursor: false, // Default
|
|
46
|
+
captureCursor: false, // Default: cursor hidden in video
|
|
45
47
|
showClicks: false,
|
|
46
48
|
displayId: null, // Hangi ekranı kaydedeceği (null = ana ekran)
|
|
47
49
|
windowId: null, // Hangi pencereyi kaydedeceği (null = tam ekran)
|
|
@@ -79,16 +81,29 @@ class MacRecorder extends EventEmitter {
|
|
|
79
81
|
*/
|
|
80
82
|
async getDisplays() {
|
|
81
83
|
const displays = nativeBinding.getDisplays();
|
|
82
|
-
return displays.map((display, index) =>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
return displays.map((display, index) => {
|
|
85
|
+
const logicalWidth = Number(display.width) || 0;
|
|
86
|
+
const logicalHeight = Number(display.height) || 0;
|
|
87
|
+
const pixelWidth = Number(display.pixelWidth) || logicalWidth;
|
|
88
|
+
const pixelHeight = Number(display.pixelHeight) || logicalHeight;
|
|
89
|
+
const scaleX = display.scaleX ? Number(display.scaleX) : (logicalWidth > 0 ? pixelWidth / logicalWidth : 1);
|
|
90
|
+
const scaleY = display.scaleY ? Number(display.scaleY) : (logicalHeight > 0 ? pixelHeight / logicalHeight : 1);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
id: display.id, // Use the actual display ID from native code
|
|
94
|
+
name: display.name,
|
|
95
|
+
width: logicalWidth,
|
|
96
|
+
height: logicalHeight,
|
|
97
|
+
pixelWidth,
|
|
98
|
+
pixelHeight,
|
|
99
|
+
scaleX,
|
|
100
|
+
scaleY,
|
|
101
|
+
x: display.x,
|
|
102
|
+
y: display.y,
|
|
103
|
+
isPrimary: display.isPrimary,
|
|
104
|
+
resolution: `${logicalWidth}x${logicalHeight}`,
|
|
105
|
+
};
|
|
106
|
+
});
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
/**
|
|
@@ -252,16 +267,29 @@ class MacRecorder extends EventEmitter {
|
|
|
252
267
|
|
|
253
268
|
// Recording için display bilgisini sakla (cursor capture için)
|
|
254
269
|
const targetDisplay = displays.find(d => d.id === targetDisplayId);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
270
|
+
if (targetDisplay) {
|
|
271
|
+
const logicalWidth = Number(targetDisplay.width) || parseInt(targetDisplay.resolution.split("x")[0]);
|
|
272
|
+
const logicalHeight = Number(targetDisplay.height) || parseInt(targetDisplay.resolution.split("x")[1]);
|
|
273
|
+
const pixelWidth = Number(targetDisplay.pixelWidth) || logicalWidth;
|
|
274
|
+
const pixelHeight = Number(targetDisplay.pixelHeight) || logicalHeight;
|
|
275
|
+
const scaleX = Number.isFinite(targetDisplay.scaleX) ? Number(targetDisplay.scaleX) : (logicalWidth > 0 ? pixelWidth / logicalWidth : 1);
|
|
276
|
+
const scaleY = Number.isFinite(targetDisplay.scaleY) ? Number(targetDisplay.scaleY) : (logicalHeight > 0 ? pixelHeight / logicalHeight : 1);
|
|
277
|
+
|
|
278
|
+
this.recordingDisplayInfo = {
|
|
279
|
+
displayId: targetDisplayId,
|
|
280
|
+
x: targetDisplay.x,
|
|
281
|
+
y: targetDisplay.y,
|
|
282
|
+
width: logicalWidth,
|
|
283
|
+
height: logicalHeight,
|
|
284
|
+
pixelWidth,
|
|
285
|
+
pixelHeight,
|
|
286
|
+
scaleX,
|
|
287
|
+
scaleY,
|
|
288
|
+
// Add scaling information for cursor coordinate transformation
|
|
289
|
+
logicalWidth,
|
|
290
|
+
logicalHeight,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
265
293
|
}
|
|
266
294
|
|
|
267
295
|
this.options.captureArea = {
|
|
@@ -289,15 +317,26 @@ class MacRecorder extends EventEmitter {
|
|
|
289
317
|
const displays = await this.getDisplays();
|
|
290
318
|
const targetDisplay = displays.find(d => d.id === this.options.displayId);
|
|
291
319
|
if (targetDisplay) {
|
|
320
|
+
const logicalWidth = Number(targetDisplay.width) || parseInt(targetDisplay.resolution.split("x")[0]);
|
|
321
|
+
const logicalHeight = Number(targetDisplay.height) || parseInt(targetDisplay.resolution.split("x")[1]);
|
|
322
|
+
const pixelWidth = Number(targetDisplay.pixelWidth) || logicalWidth;
|
|
323
|
+
const pixelHeight = Number(targetDisplay.pixelHeight) || logicalHeight;
|
|
324
|
+
const scaleX = Number.isFinite(targetDisplay.scaleX) ? Number(targetDisplay.scaleX) : (logicalWidth > 0 ? pixelWidth / logicalWidth : 1);
|
|
325
|
+
const scaleY = Number.isFinite(targetDisplay.scaleY) ? Number(targetDisplay.scaleY) : (logicalHeight > 0 ? pixelHeight / logicalHeight : 1);
|
|
326
|
+
|
|
292
327
|
this.recordingDisplayInfo = {
|
|
293
328
|
displayId: this.options.displayId,
|
|
294
329
|
x: targetDisplay.x,
|
|
295
330
|
y: targetDisplay.y,
|
|
296
|
-
width:
|
|
297
|
-
height:
|
|
331
|
+
width: logicalWidth,
|
|
332
|
+
height: logicalHeight,
|
|
333
|
+
pixelWidth,
|
|
334
|
+
pixelHeight,
|
|
335
|
+
scaleX,
|
|
336
|
+
scaleY,
|
|
298
337
|
// Add scaling information for cursor coordinate transformation
|
|
299
|
-
logicalWidth
|
|
300
|
-
logicalHeight
|
|
338
|
+
logicalWidth,
|
|
339
|
+
logicalHeight,
|
|
301
340
|
};
|
|
302
341
|
}
|
|
303
342
|
} catch (error) {
|
|
@@ -356,63 +395,108 @@ class MacRecorder extends EventEmitter {
|
|
|
356
395
|
this.isRecording = true;
|
|
357
396
|
this.recordingStartTime = Date.now();
|
|
358
397
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
+
}
|
|
417
|
+
}
|
|
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
|
|
389
464
|
this.startCursorCapture(cursorFilePath, {
|
|
390
465
|
windowRelative: true,
|
|
391
466
|
windowInfo: {
|
|
392
|
-
x:
|
|
393
|
-
y:
|
|
394
|
-
width:
|
|
395
|
-
height:
|
|
467
|
+
x: globalWindowX,
|
|
468
|
+
y: globalWindowY,
|
|
469
|
+
width: windowWidth,
|
|
470
|
+
height: windowHeight,
|
|
396
471
|
displayId: this.options.displayId,
|
|
472
|
+
captureArea: captureAreaForCursor,
|
|
397
473
|
originalWindow: targetWindow,
|
|
398
|
-
|
|
474
|
+
targetDisplay,
|
|
475
|
+
nativeFrame: nativeWindowFrame
|
|
399
476
|
}
|
|
400
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 => {
|
|
401
487
|
console.warn('Cursor tracking failed to start:', cursorError.message);
|
|
402
488
|
});
|
|
403
489
|
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
this.startCursorCapture(cursorFilePath, cursorOptions).catch(cursorError => {
|
|
415
|
-
console.warn('Cursor tracking failed to start:', cursorError.message);
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
startWindowCursorTracking();
|
|
493
|
+
} else {
|
|
494
|
+
// For display recording, use display-relative cursor tracking
|
|
495
|
+
this.startCursorCapture(cursorFilePath, {
|
|
496
|
+
displayRelative: true,
|
|
497
|
+
displayInfo: this.recordingDisplayInfo
|
|
498
|
+
}).catch(cursorError => {
|
|
499
|
+
console.warn('Display cursor tracking failed:', cursorError.message);
|
|
416
500
|
});
|
|
417
501
|
}
|
|
418
502
|
|
|
@@ -717,46 +801,157 @@ class MacRecorder extends EventEmitter {
|
|
|
717
801
|
|
|
718
802
|
// Koordinat sistemi belirle: window-relative, display-relative veya global
|
|
719
803
|
if (options.windowRelative && options.windowInfo) {
|
|
720
|
-
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
804
|
+
const windowInfo = options.windowInfo;
|
|
805
|
+
const targetDisplay = windowInfo.targetDisplay || this.recordingDisplayInfo || null;
|
|
806
|
+
const captureArea = windowInfo.captureArea || null;
|
|
807
|
+
const hasNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
808
|
+
const displayScaleX = targetDisplay && hasNumber(targetDisplay.scaleX)
|
|
809
|
+
? Number(targetDisplay.scaleX)
|
|
810
|
+
: (targetDisplay && hasNumber(targetDisplay.pixelWidth) && hasNumber(targetDisplay.width)
|
|
811
|
+
? targetDisplay.pixelWidth / targetDisplay.width
|
|
812
|
+
: 1);
|
|
813
|
+
const displayScaleY = targetDisplay && hasNumber(targetDisplay.scaleY)
|
|
814
|
+
? Number(targetDisplay.scaleY)
|
|
815
|
+
: (targetDisplay && hasNumber(targetDisplay.pixelHeight) && hasNumber(targetDisplay.height)
|
|
816
|
+
? targetDisplay.pixelHeight / targetDisplay.height
|
|
817
|
+
: 1);
|
|
818
|
+
|
|
819
|
+
let globalX = hasNumber(windowInfo.x) ? windowInfo.x : null;
|
|
820
|
+
let globalY = hasNumber(windowInfo.y) ? windowInfo.y : null;
|
|
821
|
+
|
|
822
|
+
if (captureArea && targetDisplay) {
|
|
823
|
+
if (!hasNumber(globalX) && hasNumber(captureArea.x) && hasNumber(targetDisplay.x)) {
|
|
824
|
+
globalX = targetDisplay.x + captureArea.x;
|
|
825
|
+
}
|
|
826
|
+
if (!hasNumber(globalY) && hasNumber(captureArea.y) && hasNumber(targetDisplay.y)) {
|
|
827
|
+
globalY = targetDisplay.y + captureArea.y;
|
|
828
|
+
}
|
|
744
829
|
}
|
|
830
|
+
|
|
831
|
+
if (!hasNumber(globalX)) {
|
|
832
|
+
if (captureArea && hasNumber(captureArea.x) && targetDisplay && hasNumber(targetDisplay.x)) {
|
|
833
|
+
globalX = targetDisplay.x + captureArea.x;
|
|
834
|
+
} else {
|
|
835
|
+
globalX = hasNumber(captureArea?.x) ? captureArea.x : 0;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (!hasNumber(globalY)) {
|
|
840
|
+
if (captureArea && hasNumber(captureArea.y) && targetDisplay && hasNumber(targetDisplay.y)) {
|
|
841
|
+
globalY = targetDisplay.y + captureArea.y;
|
|
842
|
+
} else {
|
|
843
|
+
globalY = hasNumber(captureArea?.y) ? captureArea.y : 0;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const displayOffsetX = captureArea && hasNumber(captureArea.x)
|
|
848
|
+
? captureArea.x
|
|
849
|
+
: (targetDisplay && hasNumber(globalX) && hasNumber(targetDisplay.x)
|
|
850
|
+
? globalX - targetDisplay.x
|
|
851
|
+
: globalX ?? 0);
|
|
852
|
+
const displayOffsetY = captureArea && hasNumber(captureArea.y)
|
|
853
|
+
? captureArea.y
|
|
854
|
+
: (targetDisplay && hasNumber(globalY) && hasNumber(targetDisplay.y)
|
|
855
|
+
? 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
|
+
}
|
|
885
|
+
|
|
886
|
+
this.cursorDisplayInfo = {
|
|
887
|
+
displayId:
|
|
888
|
+
windowInfo.displayId ??
|
|
889
|
+
targetDisplay?.displayId ??
|
|
890
|
+
targetDisplay?.id ??
|
|
891
|
+
null,
|
|
892
|
+
x: globalX,
|
|
893
|
+
y: globalY,
|
|
894
|
+
width: windowInfo.width,
|
|
895
|
+
height: windowInfo.height,
|
|
896
|
+
pixelWidth: windowPixelWidth,
|
|
897
|
+
pixelHeight: windowPixelHeight,
|
|
898
|
+
scaleX: displayScaleX,
|
|
899
|
+
scaleY: displayScaleY,
|
|
900
|
+
displayRelativeX: displayOffsetX,
|
|
901
|
+
displayRelativeY: displayOffsetY,
|
|
902
|
+
windowRelative: true,
|
|
903
|
+
windowInfo: {
|
|
904
|
+
...enrichedWindowInfo
|
|
905
|
+
},
|
|
906
|
+
targetDisplay,
|
|
907
|
+
captureArea: captureAreaInfo
|
|
908
|
+
};
|
|
909
|
+
} else if (options.displayRelative && options.displayInfo) {
|
|
910
|
+
// Display recording: Use display-relative coordinates
|
|
911
|
+
const pixelWidth = Number(options.displayInfo.pixelWidth) || Number(options.displayInfo.width);
|
|
912
|
+
const pixelHeight = Number(options.displayInfo.pixelHeight) || Number(options.displayInfo.height);
|
|
913
|
+
const scaleX = Number.isFinite(options.displayInfo.scaleX)
|
|
914
|
+
? Number(options.displayInfo.scaleX)
|
|
915
|
+
: (Number(options.displayInfo.width) > 0 ? pixelWidth / Number(options.displayInfo.width) : 1);
|
|
916
|
+
const scaleY = Number.isFinite(options.displayInfo.scaleY)
|
|
917
|
+
? Number(options.displayInfo.scaleY)
|
|
918
|
+
: (Number(options.displayInfo.height) > 0 ? pixelHeight / Number(options.displayInfo.height) : 1);
|
|
919
|
+
|
|
920
|
+
this.cursorDisplayInfo = {
|
|
921
|
+
displayId: options.displayInfo.displayId,
|
|
922
|
+
x: options.displayInfo.x,
|
|
923
|
+
y: options.displayInfo.y,
|
|
924
|
+
width: options.displayInfo.width,
|
|
925
|
+
height: options.displayInfo.height,
|
|
926
|
+
pixelWidth,
|
|
927
|
+
pixelHeight,
|
|
928
|
+
scaleX,
|
|
929
|
+
scaleY,
|
|
930
|
+
displayRelative: true
|
|
931
|
+
};
|
|
745
932
|
} else if (this.recordingDisplayInfo) {
|
|
746
|
-
//
|
|
933
|
+
// Fallback: Use recording display info if available
|
|
747
934
|
this.cursorDisplayInfo = this.recordingDisplayInfo;
|
|
748
935
|
} else {
|
|
749
|
-
// Main display
|
|
936
|
+
// Final fallback: Main display global coordinates
|
|
750
937
|
try {
|
|
751
938
|
const displays = await this.getDisplays();
|
|
752
939
|
const mainDisplay = displays.find((d) => d.isPrimary) || displays[0];
|
|
753
940
|
if (mainDisplay) {
|
|
754
941
|
this.cursorDisplayInfo = {
|
|
755
|
-
displayId:
|
|
942
|
+
displayId: mainDisplay.id,
|
|
756
943
|
x: mainDisplay.x,
|
|
757
944
|
y: mainDisplay.y,
|
|
758
|
-
width: parseInt(mainDisplay.resolution.split("x")[0]),
|
|
759
|
-
height: parseInt(mainDisplay.resolution.split("x")[1]),
|
|
945
|
+
width: Number(mainDisplay.width) || parseInt(mainDisplay.resolution.split("x")[0]),
|
|
946
|
+
height: Number(mainDisplay.height) || parseInt(mainDisplay.resolution.split("x")[1]),
|
|
947
|
+
pixelWidth: Number(mainDisplay.pixelWidth) || Number(mainDisplay.width) || parseInt(mainDisplay.resolution.split("x")[0]),
|
|
948
|
+
pixelHeight: Number(mainDisplay.pixelHeight) || Number(mainDisplay.height) || parseInt(mainDisplay.resolution.split("x")[1]),
|
|
949
|
+
scaleX: Number.isFinite(mainDisplay.scaleX)
|
|
950
|
+
? Number(mainDisplay.scaleX)
|
|
951
|
+
: ((Number(mainDisplay.width) || 0) > 0 ? (Number(mainDisplay.pixelWidth) || Number(mainDisplay.width)) / Number(mainDisplay.width) : 1),
|
|
952
|
+
scaleY: Number.isFinite(mainDisplay.scaleY)
|
|
953
|
+
? Number(mainDisplay.scaleY)
|
|
954
|
+
: ((Number(mainDisplay.height) || 0) > 0 ? (Number(mainDisplay.pixelHeight) || Number(mainDisplay.height)) / Number(mainDisplay.height) : 1),
|
|
760
955
|
};
|
|
761
956
|
}
|
|
762
957
|
} catch (error) {
|
|
@@ -772,86 +967,130 @@ class MacRecorder extends EventEmitter {
|
|
|
772
967
|
fs.writeFileSync(filepath, "[");
|
|
773
968
|
|
|
774
969
|
this.cursorCaptureFile = filepath;
|
|
775
|
-
|
|
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);
|
|
776
977
|
this.cursorCaptureFirstWrite = true;
|
|
777
978
|
this.lastCapturedData = null;
|
|
778
979
|
|
|
779
980
|
// JavaScript interval ile polling yap (daha sık - mouse event'leri yakalamak için)
|
|
780
981
|
this.cursorCaptureInterval = setInterval(() => {
|
|
781
982
|
try {
|
|
983
|
+
const now = Date.now();
|
|
782
984
|
const position = nativeBinding.getCursorPosition();
|
|
783
|
-
const
|
|
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);
|
|
784
988
|
|
|
785
|
-
//
|
|
989
|
+
// Transform coordinates based on recording type
|
|
786
990
|
let x = position.x;
|
|
787
991
|
let y = position.y;
|
|
788
992
|
let coordinateSystem = "global";
|
|
789
993
|
|
|
790
994
|
if (this.cursorDisplayInfo) {
|
|
791
995
|
if (this.cursorDisplayInfo.windowRelative) {
|
|
792
|
-
// Window recording:
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
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)) {
|
|
796
1002
|
const displayRelativeX = position.x - targetDisplay.x;
|
|
797
1003
|
const displayRelativeY = position.y - targetDisplay.y;
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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;
|
|
802
1015
|
} else {
|
|
803
|
-
// Fallback:
|
|
1016
|
+
// Fallback: global offsets
|
|
804
1017
|
x = position.x - this.cursorDisplayInfo.x;
|
|
805
1018
|
y = position.y - this.cursorDisplayInfo.y;
|
|
806
1019
|
}
|
|
807
|
-
|
|
808
|
-
|
|
1020
|
+
coordinateSystem = "window-relative";
|
|
1021
|
+
|
|
1022
|
+
// Window bounds check - skip if cursor is outside window
|
|
1023
|
+
if (x < 0 || y < 0 || x >= this.cursorDisplayInfo.width || y >= this.cursorDisplayInfo.height) {
|
|
1024
|
+
return; // Skip frame - cursor outside window
|
|
1025
|
+
}
|
|
1026
|
+
} else if (this.cursorDisplayInfo.displayRelative) {
|
|
1027
|
+
// Display recording: Transform global → display-relative coordinates
|
|
809
1028
|
x = position.x - this.cursorDisplayInfo.x;
|
|
810
1029
|
y = position.y - this.cursorDisplayInfo.y;
|
|
811
|
-
|
|
1030
|
+
coordinateSystem = "display-relative";
|
|
812
1031
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
// Window bounds kontrolü - cursor window dışındaysa kaydetme
|
|
818
|
-
if (
|
|
819
|
-
x < 0 ||
|
|
820
|
-
y < 0 ||
|
|
821
|
-
x >= this.cursorDisplayInfo.width ||
|
|
822
|
-
y >= this.cursorDisplayInfo.height
|
|
823
|
-
) {
|
|
824
|
-
return; // Bu frame'i skip et - cursor pencere dışında
|
|
1032
|
+
// Display bounds check - skip if cursor is outside display
|
|
1033
|
+
if (x < 0 || y < 0 || x >= this.cursorDisplayInfo.width || y >= this.cursorDisplayInfo.height) {
|
|
1034
|
+
return; // Skip frame - cursor outside display
|
|
825
1035
|
}
|
|
826
1036
|
} else {
|
|
827
|
-
//
|
|
1037
|
+
// Legacy fallback: Use global coordinates with basic offset
|
|
1038
|
+
x = position.x - this.cursorDisplayInfo.x;
|
|
1039
|
+
y = position.y - this.cursorDisplayInfo.y;
|
|
828
1040
|
coordinateSystem = "display-relative";
|
|
829
|
-
|
|
830
|
-
// Display bounds kontrolü
|
|
831
|
-
if (
|
|
832
|
-
x < 0 ||
|
|
833
|
-
y < 0 ||
|
|
834
|
-
x >= this.cursorDisplayInfo.width ||
|
|
835
|
-
y >= this.cursorDisplayInfo.height
|
|
836
|
-
) {
|
|
837
|
-
return; // Bu frame'i skip et - cursor display dışında
|
|
838
|
-
}
|
|
839
1041
|
}
|
|
840
1042
|
}
|
|
841
1043
|
|
|
1044
|
+
const logicalX = x;
|
|
1045
|
+
const logicalY = y;
|
|
1046
|
+
const scaleX = Number.isFinite(this.cursorDisplayInfo?.windowInfo?.scaleX)
|
|
1047
|
+
? Number(this.cursorDisplayInfo.windowInfo.scaleX)
|
|
1048
|
+
: (Number.isFinite(this.cursorDisplayInfo?.scaleX) ? Number(this.cursorDisplayInfo.scaleX) : 1);
|
|
1049
|
+
const scaleY = Number.isFinite(this.cursorDisplayInfo?.windowInfo?.scaleY)
|
|
1050
|
+
? Number(this.cursorDisplayInfo.windowInfo.scaleY)
|
|
1051
|
+
: (Number.isFinite(this.cursorDisplayInfo?.scaleY) ? Number(this.cursorDisplayInfo.scaleY) : 1);
|
|
1052
|
+
const pixelX = logicalX * scaleX;
|
|
1053
|
+
const pixelY = logicalY * scaleY;
|
|
1054
|
+
|
|
842
1055
|
const cursorData = {
|
|
843
|
-
x:
|
|
844
|
-
y:
|
|
845
|
-
|
|
846
|
-
|
|
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,
|
|
847
1066
|
cursorType: position.cursorType,
|
|
848
1067
|
type: position.eventType || "move",
|
|
849
1068
|
coordinateSystem: coordinateSystem,
|
|
1069
|
+
// Include recording context for window-relative coordinates
|
|
850
1070
|
...(this.cursorDisplayInfo?.windowRelative && {
|
|
851
1071
|
windowInfo: {
|
|
852
1072
|
width: this.cursorDisplayInfo.width,
|
|
853
1073
|
height: this.cursorDisplayInfo.height,
|
|
854
|
-
|
|
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
|
|
1082
|
+
}
|
|
1083
|
+
}),
|
|
1084
|
+
// Include display context for display-relative coordinates
|
|
1085
|
+
...(this.cursorDisplayInfo?.displayRelative && {
|
|
1086
|
+
displayInfo: {
|
|
1087
|
+
displayId: this.cursorDisplayInfo.displayId,
|
|
1088
|
+
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
|
|
855
1094
|
}
|
|
856
1095
|
})
|
|
857
1096
|
};
|
|
@@ -910,6 +1149,8 @@ class MacRecorder extends EventEmitter {
|
|
|
910
1149
|
this.cursorCaptureStartTime = null;
|
|
911
1150
|
this.cursorCaptureFirstWrite = true;
|
|
912
1151
|
this.cursorDisplayInfo = null;
|
|
1152
|
+
this.cursorRecordingBaseTime = null;
|
|
1153
|
+
this.cursorRecordingOffsetMs = 0;
|
|
913
1154
|
|
|
914
1155
|
this.emit("cursorCaptureStopped");
|
|
915
1156
|
resolve(true);
|
|
@@ -1035,6 +1276,8 @@ class MacRecorder extends EventEmitter {
|
|
|
1035
1276
|
isCapturing: !!this.cursorCaptureInterval,
|
|
1036
1277
|
outputFile: this.cursorCaptureFile || null,
|
|
1037
1278
|
startTime: this.cursorCaptureStartTime || null,
|
|
1279
|
+
recordingBaseTime: this.cursorRecordingBaseTime || null,
|
|
1280
|
+
recordingOffsetMs: this.cursorRecordingOffsetMs || 0,
|
|
1038
1281
|
displayInfo: this.cursorDisplayInfo || null,
|
|
1039
1282
|
};
|
|
1040
1283
|
}
|
package/package.json
CHANGED
package/src/mac_recorder.mm
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
#import <CoreGraphics/CoreGraphics.h>
|
|
5
5
|
#import <ImageIO/ImageIO.h>
|
|
6
6
|
#import <CoreAudio/CoreAudio.h>
|
|
7
|
+
#include <cstring>
|
|
7
8
|
|
|
8
9
|
// Import screen capture (ScreenCaptureKit only)
|
|
9
10
|
#import "screen_capture_kit.h"
|
|
@@ -59,6 +60,69 @@ void cleanupRecording() {
|
|
|
59
60
|
g_isRecording = false;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
static Napi::Value NSObjectToNapiValue(Napi::Env env, id value) {
|
|
64
|
+
if (!value || value == [NSNull null]) {
|
|
65
|
+
return env.Null();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if ([value isKindOfClass:[NSDictionary class]]) {
|
|
69
|
+
NSDictionary *dict = (NSDictionary *)value;
|
|
70
|
+
Napi::Object obj = Napi::Object::New(env);
|
|
71
|
+
|
|
72
|
+
for (id key in dict) {
|
|
73
|
+
id nestedValue = [dict objectForKey:key];
|
|
74
|
+
NSString *keyString = nil;
|
|
75
|
+
if ([key isKindOfClass:[NSString class]]) {
|
|
76
|
+
keyString = (NSString *)key;
|
|
77
|
+
} else {
|
|
78
|
+
keyString = [key description];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!keyString) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
obj.Set(Napi::String::New(env, [keyString UTF8String]), NSObjectToNapiValue(env, nestedValue));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return obj;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if ([value isKindOfClass:[NSArray class]]) {
|
|
92
|
+
NSArray *array = (NSArray *)value;
|
|
93
|
+
Napi::Array result = Napi::Array::New(env, array.count);
|
|
94
|
+
|
|
95
|
+
for (NSUInteger i = 0; i < array.count; i++) {
|
|
96
|
+
result.Set(i, NSObjectToNapiValue(env, array[i]));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if ([value isKindOfClass:[NSNumber class]]) {
|
|
103
|
+
NSNumber *number = (NSNumber *)value;
|
|
104
|
+
const char *type = [number objCType];
|
|
105
|
+
|
|
106
|
+
if (strcmp(type, @encode(BOOL)) == 0) {
|
|
107
|
+
return Napi::Boolean::New(env, [number boolValue]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return Napi::Number::New(env, [number doubleValue]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if ([value isKindOfClass:[NSString class]]) {
|
|
114
|
+
NSString *stringValue = (NSString *)value;
|
|
115
|
+
return Napi::String::New(env, [stringValue UTF8String]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if ([value isKindOfClass:[NSDate class]]) {
|
|
119
|
+
NSDate *dateValue = (NSDate *)value;
|
|
120
|
+
return Napi::Number::New(env, [dateValue timeIntervalSince1970] * 1000.0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return Napi::String::New(env, [[value description] UTF8String]);
|
|
124
|
+
}
|
|
125
|
+
|
|
62
126
|
// NAPI Function: Start Recording
|
|
63
127
|
Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
64
128
|
Napi::Env env = info.Env();
|
|
@@ -82,7 +146,7 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
82
146
|
|
|
83
147
|
// Options parsing
|
|
84
148
|
CGRect captureRect = CGRectNull;
|
|
85
|
-
bool captureCursor = false; // Default
|
|
149
|
+
bool captureCursor = false; // Default: cursor hidden in video
|
|
86
150
|
bool includeMicrophone = false; // Default olarak mikrofon kapalı
|
|
87
151
|
bool includeSystemAudio = true; // Default olarak sistem sesi açık
|
|
88
152
|
CGDirectDisplayID displayID = CGMainDisplayID(); // Default ana ekran
|
|
@@ -205,14 +269,12 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
205
269
|
NSLog(@"🔧 FORCE_AVFOUNDATION environment variable detected - skipping ScreenCaptureKit");
|
|
206
270
|
}
|
|
207
271
|
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
// macOS 14/13 → AVFoundation (including Electron)
|
|
211
|
-
if (isM15Plus && !forceAVFoundation) {
|
|
272
|
+
// Try ScreenCaptureKit on macOS 14+ (Electron supported) with robust fallback
|
|
273
|
+
if (isM14Plus && !forceAVFoundation) {
|
|
212
274
|
if (isElectron) {
|
|
213
|
-
NSLog(@"⚡ ELECTRON PRIORITY: macOS
|
|
275
|
+
NSLog(@"⚡ ELECTRON PRIORITY: macOS 14+ Electron → ScreenCaptureKit with full support (fallbacks enabled)");
|
|
214
276
|
} else {
|
|
215
|
-
NSLog(@"✅ macOS
|
|
277
|
+
NSLog(@"✅ macOS 14+ Node.js → ScreenCaptureKit available with full compatibility");
|
|
216
278
|
}
|
|
217
279
|
|
|
218
280
|
// Try ScreenCaptureKit with extensive safety measures
|
|
@@ -287,18 +349,14 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
287
349
|
// If we reach here, ScreenCaptureKit failed, so fall through to AVFoundation
|
|
288
350
|
NSLog(@"⏭️ ScreenCaptureKit failed - falling back to AVFoundation");
|
|
289
351
|
} else {
|
|
290
|
-
// macOS
|
|
352
|
+
// macOS 13 or forced AVFoundation → use AVFoundation (Electron supported!)
|
|
291
353
|
if (isElectron) {
|
|
292
|
-
if (
|
|
293
|
-
NSLog(@"⚡ ELECTRON PRIORITY: macOS 14/13 Electron → AVFoundation with full support");
|
|
294
|
-
} else if (isM13Plus) {
|
|
354
|
+
if (isM13Plus) {
|
|
295
355
|
NSLog(@"⚡ ELECTRON PRIORITY: macOS 13 Electron → AVFoundation with limited features");
|
|
296
356
|
}
|
|
297
357
|
} else {
|
|
298
|
-
if (
|
|
299
|
-
NSLog(@"🎯 macOS
|
|
300
|
-
} else if (isM14Plus) {
|
|
301
|
-
NSLog(@"🎯 macOS 14 Node.js → using AVFoundation (primary method)");
|
|
358
|
+
if (isM14Plus) {
|
|
359
|
+
NSLog(@"🎯 macOS 14+ Node.js with FORCE_AVFOUNDATION → using AVFoundation");
|
|
302
360
|
} else if (isM13Plus) {
|
|
303
361
|
NSLog(@"🎯 macOS 13 Node.js → using AVFoundation (limited features)");
|
|
304
362
|
}
|
|
@@ -575,19 +633,29 @@ Napi::Value GetDisplays(const Napi::CallbackInfo& info) {
|
|
|
575
633
|
|
|
576
634
|
for (uint32_t i = 0; i < displayCount; i++) {
|
|
577
635
|
CGDirectDisplayID displayID = activeDisplays[i];
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
636
|
+
CGRect displayBounds = CGDisplayBounds(displayID);
|
|
637
|
+
bool isPrimary = (displayID == CGMainDisplayID());
|
|
638
|
+
size_t pixelWidth = CGDisplayPixelsWide(displayID);
|
|
639
|
+
size_t pixelHeight = CGDisplayPixelsHigh(displayID);
|
|
640
|
+
double logicalWidth = displayBounds.size.width;
|
|
641
|
+
double logicalHeight = displayBounds.size.height;
|
|
642
|
+
double scaleX = (logicalWidth > 0.0) ? ((double)pixelWidth / logicalWidth) : 1.0;
|
|
643
|
+
double scaleY = (logicalHeight > 0.0) ? ((double)pixelHeight / logicalHeight) : 1.0;
|
|
644
|
+
|
|
645
|
+
NSDictionary *displayInfo = @{
|
|
646
|
+
@"id": @(displayID), // Direct CGDirectDisplayID
|
|
647
|
+
@"name": [NSString stringWithFormat:@"Display %u", (unsigned int)(i + 1)],
|
|
648
|
+
@"width": @((int)displayBounds.size.width),
|
|
649
|
+
@"height": @((int)displayBounds.size.height),
|
|
650
|
+
@"x": @((int)displayBounds.origin.x),
|
|
651
|
+
@"y": @((int)displayBounds.origin.y),
|
|
652
|
+
@"isPrimary": @(isPrimary),
|
|
653
|
+
@"pixelWidth": @((int)pixelWidth),
|
|
654
|
+
@"pixelHeight": @((int)pixelHeight),
|
|
655
|
+
@"scaleX": @((double)scaleX),
|
|
656
|
+
@"scaleY": @((double)scaleY)
|
|
657
|
+
};
|
|
658
|
+
[displays addObject:displayInfo];
|
|
591
659
|
}
|
|
592
660
|
Napi::Array result = Napi::Array::New(env, displays.count);
|
|
593
661
|
|
|
@@ -642,6 +710,21 @@ Napi::Value GetRecordingStatus(const Napi::CallbackInfo& info) {
|
|
|
642
710
|
return Napi::Boolean::New(env, isRecording);
|
|
643
711
|
}
|
|
644
712
|
|
|
713
|
+
Napi::Value GetActiveCaptureInfo(const Napi::CallbackInfo& info) {
|
|
714
|
+
Napi::Env env = info.Env();
|
|
715
|
+
|
|
716
|
+
if (@available(macOS 15.0, *)) {
|
|
717
|
+
NSDictionary *captureInfo = [ScreenCaptureKitRecorder currentCaptureInfo];
|
|
718
|
+
if (!captureInfo || captureInfo.count == 0) {
|
|
719
|
+
return env.Null();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return NSObjectToNapiValue(env, captureInfo);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return env.Null();
|
|
726
|
+
}
|
|
727
|
+
|
|
645
728
|
// NAPI Function: Get Window Thumbnail
|
|
646
729
|
Napi::Value GetWindowThumbnail(const Napi::CallbackInfo& info) {
|
|
647
730
|
Napi::Env env = info.Env();
|
|
@@ -982,6 +1065,7 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
982
1065
|
exports.Set(Napi::String::New(env, "getDisplays"), Napi::Function::New(env, GetDisplays));
|
|
983
1066
|
exports.Set(Napi::String::New(env, "getWindows"), Napi::Function::New(env, GetWindows));
|
|
984
1067
|
exports.Set(Napi::String::New(env, "getRecordingStatus"), Napi::Function::New(env, GetRecordingStatus));
|
|
1068
|
+
exports.Set(Napi::String::New(env, "getActiveCaptureInfo"), Napi::Function::New(env, GetActiveCaptureInfo));
|
|
985
1069
|
exports.Set(Napi::String::New(env, "checkPermissions"), Napi::Function::New(env, CheckPermissions));
|
|
986
1070
|
|
|
987
1071
|
// Thumbnail functions
|
|
@@ -997,4 +1081,4 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
997
1081
|
return exports;
|
|
998
1082
|
}
|
|
999
1083
|
|
|
1000
|
-
NODE_API_MODULE(mac_recorder, Init)
|
|
1084
|
+
NODE_API_MODULE(mac_recorder, Init)
|
package/src/screen_capture_kit.h
CHANGED
|
@@ -7,6 +7,7 @@ static id<SCStreamDelegate> API_AVAILABLE(macos(12.3)) g_streamDelegate = nil;
|
|
|
7
7
|
static BOOL g_isRecording = NO;
|
|
8
8
|
static BOOL g_isCleaningUp = NO; // Prevent recursive cleanup
|
|
9
9
|
static NSString *g_outputPath = nil;
|
|
10
|
+
static NSDictionary *g_currentCaptureInfo = nil;
|
|
10
11
|
|
|
11
12
|
@interface PureScreenCaptureDelegate : NSObject <SCStreamDelegate>
|
|
12
13
|
@end
|
|
@@ -58,6 +59,8 @@ static NSString *g_outputPath = nil;
|
|
|
58
59
|
g_isCleaningUp = NO;
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
g_currentCaptureInfo = nil;
|
|
63
|
+
|
|
61
64
|
NSString *outputPath = config[@"outputPath"];
|
|
62
65
|
if (!outputPath || [outputPath length] == 0) {
|
|
63
66
|
NSLog(@"❌ Invalid output path provided");
|
|
@@ -124,6 +127,34 @@ static NSString *g_outputPath = nil;
|
|
|
124
127
|
filter = [[SCContentFilter alloc] initWithDesktopIndependentWindow:targetWindow];
|
|
125
128
|
recordingWidth = (NSInteger)targetWindow.frame.size.width;
|
|
126
129
|
recordingHeight = (NSInteger)targetWindow.frame.size.height;
|
|
130
|
+
|
|
131
|
+
CGRect windowFrame = targetWindow.frame;
|
|
132
|
+
NSDictionary *frameDict = @{
|
|
133
|
+
@"x": @(windowFrame.origin.x),
|
|
134
|
+
@"y": @(windowFrame.origin.y),
|
|
135
|
+
@"width": @(windowFrame.size.width),
|
|
136
|
+
@"height": @(windowFrame.size.height)
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
NSMutableDictionary *windowInfo = [@{
|
|
140
|
+
@"id": @(targetWindow.windowID),
|
|
141
|
+
@"frame": frameDict
|
|
142
|
+
} mutableCopy];
|
|
143
|
+
|
|
144
|
+
if (targetWindow.title) {
|
|
145
|
+
windowInfo[@"title"] = targetWindow.title;
|
|
146
|
+
}
|
|
147
|
+
if (targetApp && targetApp.applicationName) {
|
|
148
|
+
windowInfo[@"appName"] = targetApp.applicationName;
|
|
149
|
+
}
|
|
150
|
+
if (targetApp && targetApp.bundleIdentifier) {
|
|
151
|
+
windowInfo[@"bundleId"] = targetApp.bundleIdentifier;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
g_currentCaptureInfo = @{
|
|
155
|
+
@"type": @"window",
|
|
156
|
+
@"window": windowInfo
|
|
157
|
+
};
|
|
127
158
|
} else {
|
|
128
159
|
NSLog(@"❌ Window ID %@ not found", windowId);
|
|
129
160
|
return;
|
|
@@ -163,6 +194,24 @@ static NSString *g_outputPath = nil;
|
|
|
163
194
|
filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:@[]];
|
|
164
195
|
recordingWidth = targetDisplay.width;
|
|
165
196
|
recordingHeight = targetDisplay.height;
|
|
197
|
+
|
|
198
|
+
CGRect displayFrame = targetDisplay.frame;
|
|
199
|
+
NSDictionary *displayFrameDict = @{
|
|
200
|
+
@"x": @(displayFrame.origin.x),
|
|
201
|
+
@"y": @(displayFrame.origin.y),
|
|
202
|
+
@"width": @(displayFrame.size.width),
|
|
203
|
+
@"height": @(displayFrame.size.height)
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
g_currentCaptureInfo = @{
|
|
207
|
+
@"type": @"display",
|
|
208
|
+
@"display": @{
|
|
209
|
+
@"id": @(targetDisplay.displayID),
|
|
210
|
+
@"frame": displayFrameDict,
|
|
211
|
+
@"width": @(targetDisplay.width),
|
|
212
|
+
@"height": @(targetDisplay.height)
|
|
213
|
+
}
|
|
214
|
+
};
|
|
166
215
|
}
|
|
167
216
|
|
|
168
217
|
// CROP AREA SUPPORT - Adjust dimensions and source rect
|
|
@@ -393,7 +442,8 @@ static NSString *g_outputPath = nil;
|
|
|
393
442
|
// SCRecordingOutput finalizes automatically
|
|
394
443
|
NSLog(@"✅ Pure recording output finalized");
|
|
395
444
|
}
|
|
396
|
-
|
|
445
|
+
|
|
446
|
+
g_currentCaptureInfo = nil;
|
|
397
447
|
[ScreenCaptureKitRecorder cleanupVideoWriter];
|
|
398
448
|
}
|
|
399
449
|
}
|
|
@@ -431,4 +481,10 @@ static NSString *g_outputPath = nil;
|
|
|
431
481
|
}
|
|
432
482
|
}
|
|
433
483
|
|
|
434
|
-
|
|
484
|
+
+ (NSDictionary *)currentCaptureInfo {
|
|
485
|
+
@synchronized([ScreenCaptureKitRecorder class]) {
|
|
486
|
+
return g_currentCaptureInfo;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
@end
|