node-mac-recorder 2.22.23 → 2.22.32

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.
@@ -1,636 +0,0 @@
1
- "use strict";
2
-
3
- const fs = require("fs");
4
- const { resolveCursorDisplayInfo } = require("./displayInfo");
5
-
6
- const IS_ELECTRON = !!(
7
- process &&
8
- process.versions &&
9
- process.versions.electron
10
- );
11
-
12
- const NATIVE_TEXT_INPUT_SAMPLE_MS = 120;
13
- const NATIVE_TEXT_INPUT_GRACE_MS = 600;
14
-
15
- const ELECTRON_SYNTH_GRACE_MS = 400;
16
- const ELECTRON_SYNTH_THROTTLE_MS = 220;
17
-
18
- const DEFAULT_CURSOR_INTERVAL_MS = IS_ELECTRON ? 50 : 20;
19
-
20
- function shouldCaptureCursorSample(lastCapturedData, currentData) {
21
- if (!lastCapturedData) {
22
- return true;
23
- }
24
- const last = lastCapturedData;
25
- if (currentData.type !== last.type) {
26
- return true;
27
- }
28
- if (
29
- Math.abs(currentData.x - last.x) >= 2 ||
30
- Math.abs(currentData.y - last.y) >= 2
31
- ) {
32
- return true;
33
- }
34
- if (currentData.cursorType !== last.cursorType) {
35
- return true;
36
- }
37
- return false;
38
- }
39
-
40
- function appendCursorJsonLine(recorder, filepath, obj) {
41
- const jsonString = JSON.stringify(obj);
42
- if (recorder.cursorCaptureFirstWrite) {
43
- fs.appendFileSync(filepath, jsonString);
44
- recorder.cursorCaptureFirstWrite = false;
45
- } else {
46
- fs.appendFileSync(filepath, "," + jsonString);
47
- }
48
- }
49
-
50
- function packDisplayInfoForExport(di) {
51
- if (!di) return {};
52
- return {
53
- displayId: di.displayId,
54
- displayX: Number.isFinite(Number(di.displayX))
55
- ? Number(di.displayX)
56
- : Number(di.x) || 0,
57
- displayY: Number.isFinite(Number(di.displayY))
58
- ? Number(di.displayY)
59
- : Number(di.y) || 0,
60
- width: di.displayWidth ?? di.width,
61
- height: di.displayHeight ?? di.height,
62
- };
63
- }
64
-
65
- function transformGlobalToVideo(globalX, globalY, d) {
66
- if (!d || !d.videoRelative) {
67
- return {
68
- x: globalX,
69
- y: globalY,
70
- coordinateSystem: "global",
71
- outsideVideo: false,
72
- };
73
- }
74
- const displayRelativeX = globalX - d.displayX;
75
- const displayRelativeY = globalY - d.displayY;
76
- const x = displayRelativeX - d.videoOffsetX;
77
- const y = displayRelativeY - d.videoOffsetY;
78
- const outsideVideo =
79
- x < 0 ||
80
- y < 0 ||
81
- x >= d.videoWidth ||
82
- y >= d.videoHeight;
83
- return {
84
- x,
85
- y,
86
- coordinateSystem: outsideVideo
87
- ? "video-relative-outside"
88
- : "video-relative",
89
- outsideVideo,
90
- };
91
- }
92
-
93
- function transformInputFrameGlobal(ifr, d) {
94
- if (!ifr || typeof ifr !== "object") {
95
- return {};
96
- }
97
- const ox = Number(ifr.x);
98
- const oy = Number(ifr.y);
99
- const tw = transformGlobalToVideo(ox, oy, d);
100
- return {
101
- x: tw.x,
102
- y: tw.y,
103
- width: Number(ifr.width) || 0,
104
- height: Number(ifr.height) || 0,
105
- };
106
- }
107
-
108
- // Electron: AX caret cache — deferred refresh ile crash-safe
109
- const ELECTRON_AX_CACHE_MAX_AGE_MS = 500;
110
-
111
- function deferredRefreshElectronAXCache(recorder, nativeBinding) {
112
- if (recorder._axDeferredPending) return;
113
- if (!nativeBinding || typeof nativeBinding.getTextInputSnapshot !== "function") return;
114
-
115
- recorder._axDeferredPending = true;
116
- setImmediate(() => {
117
- recorder._axDeferredPending = false;
118
- try {
119
- const snap = nativeBinding.getTextInputSnapshot();
120
- if (snap && Number.isFinite(snap.caretX) && Number.isFinite(snap.caretY)) {
121
- const d = recorder.cursorDisplayInfo;
122
- const caretT = transformGlobalToVideo(snap.caretX, snap.caretY, d);
123
- recorder._axCaretCache = {
124
- caretX: caretT.x,
125
- caretY: caretT.y,
126
- inputFrame: snap.inputFrame
127
- ? transformInputFrameGlobal(snap.inputFrame, d)
128
- : null,
129
- csys: d && d.videoRelative ? "video-relative" : "global",
130
- wallMs: Date.now(),
131
- };
132
- }
133
- } catch {
134
- // AX API hata verirse mevcut cache'i koru
135
- }
136
- });
137
- }
138
-
139
- /** @returns {boolean} Dosyaya textInput satırı yazıldı mı */
140
- function tryAppendSyntheticTextInputRow(recorder, nativeBinding, filepath, cursorData, timestamp) {
141
- if (!IS_ELECTRON) {
142
- return false;
143
- }
144
- const ct = cursorData.cursorType || "";
145
- if (ct !== "text" && ct !== "vertical-text") {
146
- return false;
147
- }
148
- if (timestamp < ELECTRON_SYNTH_GRACE_MS) {
149
- return false;
150
- }
151
- const wall = Date.now();
152
- if (
153
- wall - (recorder._electronSynthWallMs || 0) <
154
- ELECTRON_SYNTH_THROTTLE_MS
155
- ) {
156
- // Throttle aktif olsa bile AX cache yenilensin
157
- deferredRefreshElectronAXCache(recorder, nativeBinding);
158
- return false;
159
- }
160
- const vx = cursorData.x;
161
- const vy = cursorData.y;
162
- if (!Number.isFinite(vx) || !Number.isFinite(vy)) {
163
- return false;
164
- }
165
-
166
- // Cached AX caret varsa gerçek caret pozisyonunu kullan
167
- let caretX = vx;
168
- let caretY = vy;
169
- let inputFrame = { x: vx - 1, y: vy - 9, width: 2, height: 18 };
170
- let coordinateSystem = cursorData.coordinateSystem;
171
-
172
- const cache = recorder._axCaretCache;
173
- if (
174
- cache &&
175
- Number.isFinite(cache.caretX) &&
176
- Number.isFinite(cache.caretY) &&
177
- (wall - (cache.wallMs || 0)) < ELECTRON_AX_CACHE_MAX_AGE_MS
178
- ) {
179
- caretX = cache.caretX;
180
- caretY = cache.caretY;
181
- if (cache.inputFrame) inputFrame = cache.inputFrame;
182
- coordinateSystem = cache.csys || coordinateSystem;
183
- }
184
-
185
- // Sonraki frame için AX cache'i yenile (deferred — crash-safe)
186
- deferredRefreshElectronAXCache(recorder, nativeBinding);
187
-
188
- const tiRow = {
189
- x: vx,
190
- y: vy,
191
- timestamp,
192
- unixTimeMs: wall,
193
- cursorType: "text",
194
- type: "textInput",
195
- caretX,
196
- caretY,
197
- inputFrame,
198
- coordinateSystem,
199
- recordingType: cursorData.recordingType,
200
- videoInfo: cursorData.videoInfo || {},
201
- displayInfo: cursorData.displayInfo || {},
202
- };
203
-
204
- if (cursorData.location) {
205
- tiRow.location = cursorData.location;
206
- }
207
- if (cursorData.windowRelative) {
208
- tiRow.windowRelative = cursorData.windowRelative;
209
- }
210
-
211
- if (
212
- recorder.cursorCaptureFirstWrite &&
213
- recorder.cursorCaptureSessionTimestamp
214
- ) {
215
- tiRow._syncMetadata = {
216
- videoStartTime: recorder.cursorCaptureSessionTimestamp,
217
- cursorStartTime: recorder.cursorCaptureStartTime,
218
- offsetMs:
219
- recorder.cursorCaptureStartTime -
220
- recorder.cursorCaptureSessionTimestamp,
221
- };
222
- }
223
-
224
- const le = recorder._lastTextInputEmitted;
225
- if (
226
- le &&
227
- Math.abs(le.caretX - tiRow.caretX) < 0.75 &&
228
- Math.abs(le.caretY - tiRow.caretY) < 0.75 &&
229
- timestamp - le.timestamp < 220
230
- ) {
231
- return false;
232
- }
233
- recorder._lastTextInputEmitted = {
234
- caretX: tiRow.caretX,
235
- caretY: tiRow.caretY,
236
- timestamp,
237
- };
238
- recorder._electronSynthWallMs = wall;
239
-
240
- appendCursorJsonLine(recorder, filepath, tiRow);
241
- return true;
242
- }
243
-
244
- function tryAppendTextInput(
245
- recorder,
246
- nativeBinding,
247
- filepath,
248
- position,
249
- timestamp,
250
- ) {
251
- if (IS_ELECTRON) {
252
- return;
253
- }
254
- if (typeof nativeBinding.getTextInputSnapshot !== "function") {
255
- return;
256
- }
257
- if (timestamp < NATIVE_TEXT_INPUT_GRACE_MS) {
258
- return;
259
- }
260
- const ct = position.cursorType || "";
261
- if (ct !== "text" && ct !== "vertical-text") {
262
- return;
263
- }
264
- const wall = Date.now();
265
- if (
266
- wall - (recorder._tiSampleWallMs || 0) <
267
- NATIVE_TEXT_INPUT_SAMPLE_MS
268
- ) {
269
- return;
270
- }
271
- recorder._tiSampleWallMs = wall;
272
-
273
- let snap = null;
274
- try {
275
- snap = nativeBinding.getTextInputSnapshot();
276
- } catch {
277
- return;
278
- }
279
- if (
280
- !snap ||
281
- !Number.isFinite(snap.caretX) ||
282
- !Number.isFinite(snap.caretY)
283
- ) {
284
- return;
285
- }
286
-
287
- const d = recorder.cursorDisplayInfo;
288
- const caretT = transformGlobalToVideo(snap.caretX, snap.caretY, d);
289
- const mouseGX = position.x;
290
- const mouseGY = position.y;
291
- const mouseT = transformGlobalToVideo(mouseGX, mouseGY, d);
292
-
293
- const inputFrameVid = transformInputFrameGlobal(snap.inputFrame, d);
294
-
295
- const tiRow = {
296
- x: mouseT.x,
297
- y: mouseT.y,
298
- timestamp,
299
- unixTimeMs: wall,
300
- cursorType: "text",
301
- type: "textInput",
302
- caretX: caretT.x,
303
- caretY: caretT.y,
304
- inputFrame: inputFrameVid,
305
- coordinateSystem: caretT.coordinateSystem,
306
- recordingType: d?.recordingType || "display",
307
- videoInfo: d
308
- ? {
309
- width: d.videoWidth,
310
- height: d.videoHeight,
311
- offsetX: d.videoOffsetX,
312
- offsetY: d.videoOffsetY,
313
- }
314
- : {},
315
- displayInfo: packDisplayInfoForExport(d),
316
- };
317
-
318
- if (
319
- recorder.cursorCaptureFirstWrite &&
320
- recorder.cursorCaptureSessionTimestamp
321
- ) {
322
- tiRow._syncMetadata = {
323
- videoStartTime: recorder.cursorCaptureSessionTimestamp,
324
- cursorStartTime: recorder.cursorCaptureStartTime,
325
- offsetMs:
326
- recorder.cursorCaptureStartTime -
327
- recorder.cursorCaptureSessionTimestamp,
328
- };
329
- }
330
-
331
- const le = recorder._lastTextInputEmitted;
332
- if (
333
- le &&
334
- Math.abs(le.caretX - tiRow.caretX) < 0.75 &&
335
- Math.abs(le.caretY - tiRow.caretY) < 0.75 &&
336
- timestamp - le.timestamp < 220
337
- ) {
338
- return;
339
- }
340
- recorder._lastTextInputEmitted = {
341
- caretX: tiRow.caretX,
342
- caretY: tiRow.caretY,
343
- timestamp,
344
- };
345
-
346
- const jsonString = JSON.stringify(tiRow);
347
- if (recorder.cursorCaptureFirstWrite) {
348
- fs.appendFileSync(filepath, jsonString);
349
- recorder.cursorCaptureFirstWrite = false;
350
- } else {
351
- fs.appendFileSync(filepath, "," + jsonString);
352
- }
353
- }
354
-
355
- function queueDeferredTextInputSample(
356
- recorder,
357
- nativeBinding,
358
- filepath,
359
- position,
360
- timestamp,
361
- ) {
362
- if (IS_ELECTRON) {
363
- return;
364
- }
365
- if (typeof nativeBinding.getTextInputSnapshot !== "function") {
366
- return;
367
- }
368
- if (timestamp < NATIVE_TEXT_INPUT_GRACE_MS) {
369
- return;
370
- }
371
- const ct = position.cursorType || "";
372
- if (ct !== "text" && ct !== "vertical-text") {
373
- return;
374
- }
375
- if (recorder._tiDeferredPending) {
376
- return;
377
- }
378
- recorder._tiDeferredPending = true;
379
- const pos = {
380
- x: position.x,
381
- y: position.y,
382
- cursorType: position.cursorType,
383
- eventType: position.eventType,
384
- };
385
- const ts = timestamp;
386
- setImmediate(() => {
387
- recorder._tiDeferredPending = false;
388
- if (!recorder.cursorCaptureFile || recorder.cursorCaptureFile !== filepath) {
389
- return;
390
- }
391
- try {
392
- tryAppendTextInput(recorder, nativeBinding, filepath, pos, ts);
393
- } catch {
394
- /* ignore */
395
- }
396
- });
397
- }
398
-
399
- async function startCursorCapture(recorder, nativeBinding, intervalOrFilepath, options = {}) {
400
- let filepath;
401
- let interval = DEFAULT_CURSOR_INTERVAL_MS;
402
-
403
- if (typeof intervalOrFilepath === "number") {
404
- interval = Math.max(10, intervalOrFilepath);
405
- filepath = `cursor-data-${Date.now()}.json`;
406
- } else if (typeof intervalOrFilepath === "string") {
407
- filepath = intervalOrFilepath;
408
- } else {
409
- throw new Error("Parameter must be interval (number) or filepath (string)");
410
- }
411
-
412
- if (recorder.cursorCaptureInterval) {
413
- throw new Error("Cursor capture is already running");
414
- }
415
-
416
- const syncStartTime = options.startTimestamp || Date.now();
417
-
418
- if (options.multiWindowBounds && options.multiWindowBounds.length > 0) {
419
- try {
420
- const allWindows = await recorder.getWindows();
421
- for (const windowInfo of options.multiWindowBounds) {
422
- const windowData = allWindows.find(
423
- (w) => w.id === windowInfo.windowId,
424
- );
425
- if (windowData) {
426
- windowInfo.bounds = {
427
- x: windowData.x || 0,
428
- y: windowData.y || 0,
429
- width: windowData.width || 0,
430
- height: windowData.height || 0,
431
- };
432
- }
433
- }
434
- } catch (error) {
435
- console.warn(
436
- "Failed to fetch window bounds for multi-window cursor tracking:",
437
- error.message,
438
- );
439
- }
440
- }
441
-
442
- await resolveCursorDisplayInfo(recorder, options);
443
-
444
- return new Promise((resolve, reject) => {
445
- try {
446
- fs.writeFileSync(filepath, "[");
447
-
448
- recorder.cursorCaptureFile = filepath;
449
- recorder.cursorCaptureStartTime = syncStartTime;
450
- recorder.cursorCaptureFirstWrite = true;
451
- recorder.lastCapturedData = null;
452
- recorder.cursorCaptureSessionTimestamp = recorder.sessionTimestamp;
453
- recorder._tiSampleWallMs = 0;
454
- recorder._electronSynthWallMs = 0;
455
- recorder._lastTextInputEmitted = null;
456
- recorder._tiDeferredPending = false;
457
-
458
- recorder.cursorCaptureInterval = setInterval(() => {
459
- try {
460
- const position = nativeBinding.getCursorPosition();
461
- const timestamp =
462
- Date.now() - recorder.cursorCaptureStartTime;
463
-
464
- let x = position.x;
465
- let y = position.y;
466
- let coordinateSystem = "global";
467
-
468
- const di = recorder.cursorDisplayInfo;
469
- if (di && di.videoRelative) {
470
- const displayRelativeX = position.x - di.displayX;
471
- const displayRelativeY = position.y - di.displayY;
472
- x = displayRelativeX - di.videoOffsetX;
473
- y = displayRelativeY - di.videoOffsetY;
474
- coordinateSystem = "video-relative";
475
- const outsideVideo =
476
- x < 0 ||
477
- y < 0 ||
478
- x >= di.videoWidth ||
479
- y >= di.videoHeight;
480
- if (outsideVideo) {
481
- coordinateSystem = "video-relative-outside";
482
- }
483
- }
484
-
485
- const cursorData = {
486
- x,
487
- y,
488
- timestamp,
489
- unixTimeMs: Date.now(),
490
- cursorType: position.cursorType,
491
- type: position.eventType || "move",
492
- coordinateSystem,
493
- recordingType: di?.recordingType || "display",
494
- videoInfo: di
495
- ? {
496
- width: di.videoWidth,
497
- height: di.videoHeight,
498
- offsetX: di.videoOffsetX,
499
- offsetY: di.videoOffsetY,
500
- }
501
- : {},
502
- displayInfo: packDisplayInfoForExport(di),
503
- };
504
-
505
- if (di?.multiWindowBounds && di.multiWindowBounds.length > 0) {
506
- const location = { hover: null, click: null };
507
- let windowRelativeCoords = null;
508
- for (const windowInfo of di.multiWindowBounds) {
509
- if (windowInfo.bounds) {
510
- const { x: wx, y: wy, width: ww, height: wh } =
511
- windowInfo.bounds;
512
- if (
513
- position.x >= wx &&
514
- position.x <= wx + ww &&
515
- position.y >= wy &&
516
- position.y <= wy + wh
517
- ) {
518
- location.hover = windowInfo.windowId;
519
- windowRelativeCoords = {
520
- windowId: windowInfo.windowId,
521
- x: position.x - wx,
522
- y: position.y - wy,
523
- windowWidth: ww,
524
- windowHeight: wh,
525
- };
526
- const eventType = position.eventType || "";
527
- if (
528
- eventType === "mousedown" ||
529
- eventType === "mouseup" ||
530
- eventType === "drag" ||
531
- eventType === "rightmousedown" ||
532
- eventType === "rightmouseup" ||
533
- eventType === "rightdrag"
534
- ) {
535
- location.click = windowInfo.windowId;
536
- }
537
- break;
538
- }
539
- }
540
- }
541
- cursorData.location = location;
542
- if (windowRelativeCoords) {
543
- cursorData.windowRelative = windowRelativeCoords;
544
- }
545
- }
546
-
547
- if (
548
- recorder.cursorCaptureFirstWrite &&
549
- recorder.cursorCaptureSessionTimestamp
550
- ) {
551
- cursorData._syncMetadata = {
552
- videoStartTime: recorder.cursorCaptureSessionTimestamp,
553
- cursorStartTime: recorder.cursorCaptureStartTime,
554
- offsetMs:
555
- recorder.cursorCaptureStartTime -
556
- recorder.cursorCaptureSessionTimestamp,
557
- };
558
- }
559
-
560
- let wroteMoveSample = false;
561
- if (shouldCaptureCursorSample(recorder.lastCapturedData, cursorData)) {
562
- appendCursorJsonLine(recorder, filepath, cursorData);
563
- recorder.lastCapturedData = { ...cursorData };
564
- wroteMoveSample = true;
565
- }
566
-
567
- if (IS_ELECTRON) {
568
- const textInputWritten = tryAppendSyntheticTextInputRow(
569
- recorder,
570
- nativeBinding,
571
- filepath,
572
- cursorData,
573
- timestamp,
574
- );
575
- if (textInputWritten && !wroteMoveSample) {
576
- appendCursorJsonLine(recorder, filepath, cursorData);
577
- recorder.lastCapturedData = { ...cursorData };
578
- }
579
- } else {
580
- queueDeferredTextInputSample(
581
- recorder,
582
- nativeBinding,
583
- filepath,
584
- position,
585
- timestamp,
586
- );
587
- }
588
- } catch (error) {
589
- console.error("Cursor capture error:", error);
590
- }
591
- }, interval);
592
-
593
- recorder.emit("cursorCaptureStarted", filepath);
594
- resolve(true);
595
- } catch (error) {
596
- reject(error);
597
- }
598
- });
599
- }
600
-
601
- async function stopCursorCapture(recorder) {
602
- return new Promise((resolve, reject) => {
603
- try {
604
- if (!recorder.cursorCaptureInterval) {
605
- return resolve(false);
606
- }
607
- clearInterval(recorder.cursorCaptureInterval);
608
- recorder.cursorCaptureInterval = null;
609
-
610
- if (recorder.cursorCaptureFile) {
611
- fs.appendFileSync(recorder.cursorCaptureFile, "]");
612
- recorder.cursorCaptureFile = null;
613
- }
614
-
615
- recorder.lastCapturedData = null;
616
- recorder.cursorCaptureStartTime = null;
617
- recorder.cursorCaptureFirstWrite = true;
618
- recorder.cursorDisplayInfo = null;
619
- recorder._tiSampleWallMs = 0;
620
- recorder._electronSynthWallMs = 0;
621
- recorder._lastTextInputEmitted = null;
622
- recorder._tiDeferredPending = false;
623
-
624
- recorder.emit("cursorCaptureStopped");
625
- resolve(true);
626
- } catch (error) {
627
- reject(error);
628
- }
629
- });
630
- }
631
-
632
- module.exports = {
633
- startCursorCapture,
634
- stopCursorCapture,
635
- shouldCaptureCursorSample,
636
- };
@@ -1,3 +0,0 @@
1
- #import <Foundation/Foundation.h>
2
-
3
- NSDictionary *_Nullable MRTextInputSnapshotDictionary(void);