node-mac-recorder 2.4.11 → 2.4.13

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,518 +0,0 @@
1
- #import <napi.h>
2
- #import <AppKit/AppKit.h>
3
- #import <Foundation/Foundation.h>
4
- #import <CoreGraphics/CoreGraphics.h>
5
- #import <ApplicationServices/ApplicationServices.h>
6
- #import <Carbon/Carbon.h>
7
- #import <Accessibility/Accessibility.h>
8
-
9
- // Global state for cursor tracking
10
- static bool g_isCursorTracking = false;
11
- static CFMachPortRef g_eventTap = NULL;
12
- static CFRunLoopSourceRef g_runLoopSource = NULL;
13
- static NSDate *g_trackingStartTime = nil;
14
- static NSString *g_outputPath = nil;
15
- static NSTimer *g_cursorTimer = nil;
16
- static int g_debugCallbackCount = 0;
17
- static NSFileHandle *g_fileHandle = nil;
18
- static bool g_isFirstWrite = true;
19
-
20
- // Forward declaration
21
- void cursorTimerCallback();
22
- void writeToFile(NSDictionary *cursorData);
23
-
24
- // Timer helper class
25
- @interface CursorTimerTarget : NSObject
26
- - (void)timerCallback:(NSTimer *)timer;
27
- @end
28
-
29
- @implementation CursorTimerTarget
30
- - (void)timerCallback:(NSTimer *)timer {
31
- cursorTimerCallback();
32
- }
33
- @end
34
-
35
- static CursorTimerTarget *g_timerTarget = nil;
36
-
37
- // Global cursor state tracking
38
- static NSString *g_lastDetectedCursorType = nil;
39
- static int g_cursorTypeCounter = 0;
40
-
41
- // Mouse button state tracking
42
- static bool g_leftMouseDown = false;
43
- static bool g_rightMouseDown = false;
44
- static NSString *g_lastEventType = @"move";
45
-
46
- // Event tap callback
47
- static CGEventRef eventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *userInfo) {
48
- return event;
49
- }
50
-
51
- // Cursor type detection helper - sistem genelindeki cursor type'ı al
52
- NSString* getCursorType() {
53
- @autoreleasepool {
54
- g_cursorTypeCounter++;
55
-
56
- @try {
57
- // Get current cursor info
58
- NSCursor *currentCursor = [NSCursor currentSystemCursor];
59
- NSString *cursorType = @"default";
60
-
61
- // Get cursor image info
62
- NSImage *cursorImage = [currentCursor image];
63
- NSPoint hotSpot = [currentCursor hotSpot];
64
- NSSize imageSize = [cursorImage size];
65
-
66
- // Check cursor type by comparing with standard cursors
67
- if ([currentCursor isEqual:[NSCursor pointingHandCursor]] ||
68
- (hotSpot.x >= 5 && hotSpot.x <= 7 && hotSpot.y >= 0 && hotSpot.y <= 4) ||
69
- (hotSpot.x >= 12 && hotSpot.x <= 14 && hotSpot.y >= 7 && hotSpot.y <= 9)) {
70
- return @"pointer";
71
- } else if ([currentCursor isEqual:[NSCursor IBeamCursor]] ||
72
- (hotSpot.x >= 3 && hotSpot.x <= 5 && hotSpot.y >= 8 && hotSpot.y <= 10 &&
73
- imageSize.width <= 10 && imageSize.height >= 16)) {
74
- return @"text";
75
- } else if ([currentCursor isEqual:[NSCursor resizeLeftRightCursor]]) {
76
- return @"ew-resize";
77
- } else if ([currentCursor isEqual:[NSCursor resizeUpDownCursor]]) {
78
- return @"ns-resize";
79
- } else if ([currentCursor isEqual:[NSCursor openHandCursor]] ||
80
- [currentCursor isEqual:[NSCursor closedHandCursor]]) {
81
- return @"grabbing";
82
- }
83
-
84
- // Check if we're in a drag operation
85
- CGEventRef event = CGEventCreate(NULL);
86
- if (event) {
87
- CGEventType eventType = (CGEventType)CGEventGetType(event);
88
- if (eventType == kCGEventLeftMouseDragged ||
89
- eventType == kCGEventRightMouseDragged) {
90
- CFRelease(event);
91
- return @"grabbing";
92
- }
93
- CFRelease(event);
94
- }
95
-
96
- // Get the window under the cursor
97
- CGPoint cursorPos = CGEventGetLocation(CGEventCreate(NULL));
98
- AXUIElementRef systemWide = AXUIElementCreateSystemWide();
99
- AXUIElementRef elementAtPosition = NULL;
100
- AXError error = AXUIElementCopyElementAtPosition(systemWide, cursorPos.x, cursorPos.y, &elementAtPosition);
101
-
102
- if (error == kAXErrorSuccess && elementAtPosition) {
103
- CFStringRef role = NULL;
104
- error = AXUIElementCopyAttributeValue(elementAtPosition, kAXRoleAttribute, (CFTypeRef*)&role);
105
-
106
- if (error == kAXErrorSuccess && role) {
107
- NSString *elementRole = (__bridge_transfer NSString*)role;
108
-
109
- // Check for clickable elements that should show pointer cursor
110
- if ([elementRole isEqualToString:@"AXLink"] ||
111
- [elementRole isEqualToString:@"AXButton"] ||
112
- [elementRole isEqualToString:@"AXMenuItem"] ||
113
- [elementRole isEqualToString:@"AXRadioButton"] ||
114
- [elementRole isEqualToString:@"AXCheckBox"]) {
115
- return @"pointer";
116
- }
117
-
118
- // Check subrole for additional pointer cursor elements
119
- CFStringRef subrole = NULL;
120
- error = AXUIElementCopyAttributeValue(elementAtPosition, kAXSubroleAttribute, (CFTypeRef*)&subrole);
121
- if (error == kAXErrorSuccess && subrole) {
122
- NSString *elementSubrole = (__bridge_transfer NSString*)subrole;
123
-
124
- if ([elementSubrole isEqualToString:@"AXClickable"] ||
125
- [elementSubrole isEqualToString:@"AXDisclosureTriangle"] ||
126
- [elementSubrole isEqualToString:@"AXToolbarButton"] ||
127
- [elementSubrole isEqualToString:@"AXCloseButton"] ||
128
- [elementSubrole isEqualToString:@"AXMinimizeButton"] ||
129
- [elementSubrole isEqualToString:@"AXZoomButton"]) {
130
- return @"pointer";
131
- }
132
- }
133
-
134
- // Check for text elements
135
- if ([elementRole isEqualToString:@"AXTextField"] ||
136
- [elementRole isEqualToString:@"AXTextArea"] ||
137
- [elementRole isEqualToString:@"AXStaticText"]) {
138
- return @"text";
139
- }
140
- }
141
-
142
- CFRelease(elementAtPosition);
143
- }
144
-
145
- if (systemWide) {
146
- CFRelease(systemWide);
147
- }
148
-
149
- return cursorType;
150
-
151
- } @catch (NSException *exception) {
152
- NSLog(@"Error in getCursorType: %@", exception);
153
- return @"default";
154
- }
155
- }
156
- }
157
-
158
- // Dosyaya yazma helper fonksiyonu
159
- void writeToFile(NSDictionary *cursorData) {
160
- @autoreleasepool {
161
- if (!g_fileHandle || !cursorData) {
162
- return;
163
- }
164
-
165
- @try {
166
- NSError *error;
167
- NSData *jsonData = [NSJSONSerialization dataWithJSONObject:cursorData
168
- options:0
169
- error:&error];
170
- if (jsonData && !error) {
171
- NSString *jsonString = [[[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] autorelease];
172
-
173
- if (g_isFirstWrite) {
174
- // İlk yazma - array başlat
175
- [g_fileHandle writeData:[@"[" dataUsingEncoding:NSUTF8StringEncoding]];
176
- [g_fileHandle writeData:[jsonString dataUsingEncoding:NSUTF8StringEncoding]];
177
- g_isFirstWrite = false;
178
- } else {
179
- // Sonraki yazmalar - virgül + json
180
- [g_fileHandle writeData:[@"," dataUsingEncoding:NSUTF8StringEncoding]];
181
- [g_fileHandle writeData:[jsonString dataUsingEncoding:NSUTF8StringEncoding]];
182
- }
183
-
184
- [g_fileHandle synchronizeFile];
185
- }
186
- } @catch (NSException *exception) {
187
- // Hata durumunda sessizce devam et
188
- }
189
- }
190
- }
191
-
192
- // Event callback for mouse events
193
- CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
194
- @autoreleasepool {
195
- g_debugCallbackCount++; // Callback çağrıldığını say
196
-
197
- if (!g_isCursorTracking || !g_trackingStartTime || !g_fileHandle) {
198
- return event;
199
- }
200
-
201
- CGPoint location = CGEventGetLocation(event);
202
- NSDate *currentDate = [NSDate date];
203
- NSTimeInterval timestamp = [currentDate timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
204
- NSTimeInterval unixTimeMs = [currentDate timeIntervalSince1970] * 1000; // unix timestamp in milliseconds
205
- NSString *cursorType = getCursorType();
206
- NSString *eventType = @"move";
207
-
208
- // Event tipini belirle
209
- switch (type) {
210
- case kCGEventLeftMouseDown:
211
- case kCGEventRightMouseDown:
212
- case kCGEventOtherMouseDown:
213
- eventType = @"mousedown";
214
- break;
215
- case kCGEventLeftMouseUp:
216
- case kCGEventRightMouseUp:
217
- case kCGEventOtherMouseUp:
218
- eventType = @"mouseup";
219
- break;
220
- case kCGEventLeftMouseDragged:
221
- case kCGEventRightMouseDragged:
222
- case kCGEventOtherMouseDragged:
223
- eventType = @"drag";
224
- break;
225
- case kCGEventMouseMoved:
226
- default:
227
- eventType = @"move";
228
- break;
229
- }
230
-
231
- // Cursor data oluştur
232
- NSDictionary *cursorInfo = @{
233
- @"x": @((int)location.x),
234
- @"y": @((int)location.y),
235
- @"timestamp": @(timestamp),
236
- @"unixTimeMs": @(unixTimeMs),
237
- @"cursorType": cursorType,
238
- @"type": eventType
239
- };
240
-
241
- // Direkt dosyaya yaz
242
- writeToFile(cursorInfo);
243
-
244
- return event;
245
- }
246
- }
247
-
248
- // Timer callback for periodic cursor position updates
249
- void cursorTimerCallback() {
250
- @autoreleasepool {
251
- g_debugCallbackCount++; // Timer callback çağrıldığını say
252
-
253
- if (!g_isCursorTracking || !g_trackingStartTime || !g_fileHandle) {
254
- return;
255
- }
256
-
257
- // CGEventGetLocation direkt global koordinat verir - çoklu ekran desteği için daha doğru
258
- CGEventRef event = CGEventCreate(NULL);
259
- CGPoint location = CGEventGetLocation(event);
260
- if (event) {
261
- CFRelease(event);
262
- }
263
-
264
- NSDate *currentDate = [NSDate date];
265
- NSTimeInterval timestamp = [currentDate timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
266
- NSTimeInterval unixTimeMs = [currentDate timeIntervalSince1970] * 1000; // unix timestamp in milliseconds
267
- NSString *cursorType = getCursorType();
268
-
269
- // Cursor data oluştur
270
- NSDictionary *cursorInfo = @{
271
- @"x": @((int)location.x),
272
- @"y": @((int)location.y),
273
- @"timestamp": @(timestamp),
274
- @"unixTimeMs": @(unixTimeMs),
275
- @"cursorType": cursorType,
276
- @"type": @"move"
277
- };
278
-
279
- // Direkt dosyaya yaz
280
- writeToFile(cursorInfo);
281
- }
282
- }
283
-
284
- // Helper function to cleanup cursor tracking
285
- void cleanupCursorTracking() {
286
- g_isCursorTracking = false;
287
-
288
- // Timer temizle
289
- if (g_cursorTimer) {
290
- [g_cursorTimer invalidate];
291
- g_cursorTimer = nil;
292
- }
293
-
294
- if (g_timerTarget) {
295
- [g_timerTarget autorelease];
296
- g_timerTarget = nil;
297
- }
298
-
299
- // Dosyayı önce kapat (en önemli işlem)
300
- if (g_fileHandle) {
301
- @try {
302
- if (g_isFirstWrite) {
303
- // Hiç veri yazılmamışsa boş array
304
- [g_fileHandle writeData:[@"[]" dataUsingEncoding:NSUTF8StringEncoding]];
305
- } else {
306
- // JSON array'i kapat
307
- [g_fileHandle writeData:[@"]" dataUsingEncoding:NSUTF8StringEncoding]];
308
- }
309
- [g_fileHandle synchronizeFile];
310
- [g_fileHandle closeFile];
311
- } @catch (NSException *exception) {
312
- // Dosya işlemi hata verirse sessizce devam et
313
- }
314
- g_fileHandle = nil;
315
- }
316
-
317
- // Event tap'i durdur (non-blocking)
318
- if (g_eventTap) {
319
- CGEventTapEnable(g_eventTap, false);
320
- g_eventTap = NULL; // CFRelease işlemini yapmıyoruz - system handle etsin
321
- }
322
-
323
- // Run loop source'unu kaldır (non-blocking)
324
- if (g_runLoopSource) {
325
- g_runLoopSource = NULL; // CFRelease işlemini yapmıyoruz
326
- }
327
-
328
- // Global değişkenleri sıfırla
329
- g_trackingStartTime = nil;
330
- g_outputPath = nil;
331
- g_debugCallbackCount = 0;
332
- g_lastDetectedCursorType = nil;
333
- g_cursorTypeCounter = 0;
334
- g_isFirstWrite = true;
335
- }
336
-
337
- // NAPI Function: Start Cursor Tracking
338
- Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
339
- Napi::Env env = info.Env();
340
-
341
- if (info.Length() < 1) {
342
- Napi::TypeError::New(env, "Output path required").ThrowAsJavaScriptException();
343
- return env.Null();
344
- }
345
-
346
- if (g_isCursorTracking) {
347
- return Napi::Boolean::New(env, false);
348
- }
349
-
350
- std::string outputPath = info[0].As<Napi::String>().Utf8Value();
351
-
352
- @try {
353
- // Dosyayı oluştur ve aç
354
- g_outputPath = [NSString stringWithUTF8String:outputPath.c_str()];
355
- g_fileHandle = [[NSFileHandle fileHandleForWritingAtPath:g_outputPath] retain];
356
-
357
- if (!g_fileHandle) {
358
- // Dosya yoksa oluştur
359
- [[NSFileManager defaultManager] createFileAtPath:g_outputPath contents:nil attributes:nil];
360
- g_fileHandle = [[NSFileHandle fileHandleForWritingAtPath:g_outputPath] retain];
361
- }
362
-
363
- if (!g_fileHandle) {
364
- return Napi::Boolean::New(env, false);
365
- }
366
-
367
- // Dosyayı temizle (baştan başla)
368
- [g_fileHandle truncateFileAtOffset:0];
369
- g_isFirstWrite = true;
370
-
371
- g_trackingStartTime = [NSDate date];
372
-
373
- // Create event tap for mouse events
374
- CGEventMask eventMask = (CGEventMaskBit(kCGEventLeftMouseDown) |
375
- CGEventMaskBit(kCGEventLeftMouseUp) |
376
- CGEventMaskBit(kCGEventRightMouseDown) |
377
- CGEventMaskBit(kCGEventRightMouseUp) |
378
- CGEventMaskBit(kCGEventOtherMouseDown) |
379
- CGEventMaskBit(kCGEventOtherMouseUp) |
380
- CGEventMaskBit(kCGEventMouseMoved) |
381
- CGEventMaskBit(kCGEventLeftMouseDragged) |
382
- CGEventMaskBit(kCGEventRightMouseDragged) |
383
- CGEventMaskBit(kCGEventOtherMouseDragged));
384
-
385
- g_eventTap = CGEventTapCreate(kCGSessionEventTap,
386
- kCGHeadInsertEventTap,
387
- kCGEventTapOptionListenOnly,
388
- eventMask,
389
- eventCallback,
390
- NULL);
391
-
392
- if (g_eventTap) {
393
- // Event tap başarılı - detaylı event tracking aktif
394
- g_runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, g_eventTap, 0);
395
- CFRunLoopAddSource(CFRunLoopGetMain(), g_runLoopSource, kCFRunLoopCommonModes);
396
- CGEventTapEnable(g_eventTap, true);
397
- }
398
-
399
- // NSTimer kullan (main thread'de çalışır)
400
- g_timerTarget = [[CursorTimerTarget alloc] init];
401
-
402
- g_cursorTimer = [NSTimer timerWithTimeInterval:0.05 // 50ms (20 FPS)
403
- target:g_timerTarget
404
- selector:@selector(timerCallback:)
405
- userInfo:nil
406
- repeats:YES];
407
-
408
- // Main run loop'a ekle
409
- [[NSRunLoop mainRunLoop] addTimer:g_cursorTimer forMode:NSRunLoopCommonModes];
410
-
411
- g_isCursorTracking = true;
412
- return Napi::Boolean::New(env, true);
413
-
414
- } @catch (NSException *exception) {
415
- cleanupCursorTracking();
416
- return Napi::Boolean::New(env, false);
417
- }
418
- }
419
-
420
- // NAPI Function: Stop Cursor Tracking
421
- Napi::Value StopCursorTracking(const Napi::CallbackInfo& info) {
422
- Napi::Env env = info.Env();
423
-
424
- if (!g_isCursorTracking) {
425
- return Napi::Boolean::New(env, false);
426
- }
427
-
428
- @try {
429
- cleanupCursorTracking();
430
- return Napi::Boolean::New(env, true);
431
-
432
- } @catch (NSException *exception) {
433
- cleanupCursorTracking();
434
- return Napi::Boolean::New(env, false);
435
- }
436
- }
437
-
438
- // NAPI Function: Get Current Cursor Position
439
- Napi::Value GetCursorPosition(const Napi::CallbackInfo& info) {
440
- Napi::Env env = info.Env();
441
-
442
- @try {
443
- // NSEvent mouseLocation zaten global koordinatlarda (all displays combined)
444
- // CGEventGetLocation kullanarak direkt global koordinat al - daha doğru
445
- CGEventRef event = CGEventCreate(NULL);
446
- CGPoint location = CGEventGetLocation(event);
447
- if (event) {
448
- CFRelease(event);
449
- }
450
-
451
- NSString *cursorType = getCursorType();
452
-
453
- // Mouse button state'ini kontrol et
454
- bool currentLeftMouseDown = CGEventSourceButtonState(kCGEventSourceStateHIDSystemState, kCGMouseButtonLeft);
455
- bool currentRightMouseDown = CGEventSourceButtonState(kCGEventSourceStateHIDSystemState, kCGMouseButtonRight);
456
-
457
- NSString *eventType = @"move";
458
-
459
- // Mouse button state değişikliklerini tespit et
460
- if (currentLeftMouseDown && !g_leftMouseDown) {
461
- eventType = @"mousedown";
462
- g_lastEventType = @"mousedown";
463
- } else if (!currentLeftMouseDown && g_leftMouseDown) {
464
- eventType = @"mouseup";
465
- g_lastEventType = @"mouseup";
466
- } else if (currentRightMouseDown && !g_rightMouseDown) {
467
- eventType = @"rightmousedown";
468
- g_lastEventType = @"rightmousedown";
469
- } else if (!currentRightMouseDown && g_rightMouseDown) {
470
- eventType = @"rightmouseup";
471
- g_lastEventType = @"rightmouseup";
472
- } else {
473
- eventType = @"move";
474
- g_lastEventType = @"move";
475
- }
476
-
477
- // State'i güncelle
478
- g_leftMouseDown = currentLeftMouseDown;
479
- g_rightMouseDown = currentRightMouseDown;
480
-
481
- Napi::Object result = Napi::Object::New(env);
482
- result.Set("x", Napi::Number::New(env, (int)location.x));
483
- result.Set("y", Napi::Number::New(env, (int)location.y));
484
- result.Set("cursorType", Napi::String::New(env, [cursorType UTF8String]));
485
- result.Set("eventType", Napi::String::New(env, [eventType UTF8String]));
486
-
487
- return result;
488
-
489
- } @catch (NSException *exception) {
490
- return env.Null();
491
- }
492
- }
493
-
494
- // NAPI Function: Get Cursor Tracking Status
495
- Napi::Value GetCursorTrackingStatus(const Napi::CallbackInfo& info) {
496
- Napi::Env env = info.Env();
497
-
498
- Napi::Object result = Napi::Object::New(env);
499
- result.Set("isTracking", Napi::Boolean::New(env, g_isCursorTracking));
500
- result.Set("hasEventTap", Napi::Boolean::New(env, g_eventTap != NULL));
501
- result.Set("hasRunLoopSource", Napi::Boolean::New(env, g_runLoopSource != NULL));
502
- result.Set("hasFileHandle", Napi::Boolean::New(env, g_fileHandle != NULL));
503
- result.Set("hasTimer", Napi::Boolean::New(env, g_cursorTimer != NULL));
504
- result.Set("debugCallbackCount", Napi::Number::New(env, g_debugCallbackCount));
505
- result.Set("cursorTypeCounter", Napi::Number::New(env, g_cursorTypeCounter));
506
-
507
- return result;
508
- }
509
-
510
- // Export functions
511
- Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports) {
512
- exports.Set("startCursorTracking", Napi::Function::New(env, StartCursorTracking));
513
- exports.Set("stopCursorTracking", Napi::Function::New(env, StopCursorTracking));
514
- exports.Set("getCursorPosition", Napi::Function::New(env, GetCursorPosition));
515
- exports.Set("getCursorTrackingStatus", Napi::Function::New(env, GetCursorTrackingStatus));
516
-
517
- return exports;
518
- }