node-mac-recorder 1.0.5 → 1.2.0
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/README.md +246 -0
- package/binding.gyp +6 -2
- package/cursor-test.js +176 -0
- package/index.js +148 -0
- package/manual-cursor-data.json +352 -0
- package/package.json +1 -1
- package/src/cursor_tracker.mm +399 -0
- package/src/mac_recorder.mm +199 -0
|
@@ -0,0 +1,399 @@
|
|
|
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 NSMutableArray *g_cursorData = nil;
|
|
12
|
+
static CFMachPortRef g_eventTap = NULL;
|
|
13
|
+
static CFRunLoopSourceRef g_runLoopSource = NULL;
|
|
14
|
+
static NSDate *g_trackingStartTime = nil;
|
|
15
|
+
static NSString *g_outputPath = nil;
|
|
16
|
+
static NSTimer *g_cursorTimer = nil;
|
|
17
|
+
static int g_debugCallbackCount = 0;
|
|
18
|
+
|
|
19
|
+
// Forward declaration
|
|
20
|
+
void cursorTimerCallback(NSTimer *timer);
|
|
21
|
+
|
|
22
|
+
// Timer helper class
|
|
23
|
+
@interface CursorTimerTarget : NSObject
|
|
24
|
+
- (void)timerCallback:(NSTimer *)timer;
|
|
25
|
+
@end
|
|
26
|
+
|
|
27
|
+
@implementation CursorTimerTarget
|
|
28
|
+
- (void)timerCallback:(NSTimer *)timer {
|
|
29
|
+
cursorTimerCallback(timer);
|
|
30
|
+
}
|
|
31
|
+
@end
|
|
32
|
+
|
|
33
|
+
static CursorTimerTarget *g_timerTarget = nil;
|
|
34
|
+
|
|
35
|
+
// Global cursor state tracking
|
|
36
|
+
static NSString *g_lastDetectedCursorType = nil;
|
|
37
|
+
static int g_cursorTypeCounter = 0;
|
|
38
|
+
|
|
39
|
+
// Cursor type detection helper
|
|
40
|
+
NSString* getCursorType() {
|
|
41
|
+
@autoreleasepool {
|
|
42
|
+
g_cursorTypeCounter++;
|
|
43
|
+
|
|
44
|
+
// Simple simulation - cycle through cursor types for demo
|
|
45
|
+
// Bu gerçek uygulamada daha akıllı olacak
|
|
46
|
+
int typeIndex = (g_cursorTypeCounter / 6) % 4; // Her 6 call'da değiştir (daha hızlı demo)
|
|
47
|
+
|
|
48
|
+
switch (typeIndex) {
|
|
49
|
+
case 0:
|
|
50
|
+
g_lastDetectedCursorType = @"default";
|
|
51
|
+
return @"default";
|
|
52
|
+
case 1:
|
|
53
|
+
g_lastDetectedCursorType = @"pointer";
|
|
54
|
+
return @"pointer";
|
|
55
|
+
case 2:
|
|
56
|
+
g_lastDetectedCursorType = @"text";
|
|
57
|
+
return @"text";
|
|
58
|
+
case 3:
|
|
59
|
+
g_lastDetectedCursorType = @"grabbing";
|
|
60
|
+
return @"grabbing";
|
|
61
|
+
default:
|
|
62
|
+
g_lastDetectedCursorType = @"default";
|
|
63
|
+
return @"default";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Event callback for mouse events
|
|
69
|
+
CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
|
|
70
|
+
@autoreleasepool {
|
|
71
|
+
if (!g_isCursorTracking || !g_cursorData || !g_trackingStartTime) {
|
|
72
|
+
return event;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
CGPoint location = CGEventGetLocation(event);
|
|
76
|
+
NSTimeInterval timestamp = [[NSDate date] timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
|
|
77
|
+
NSString *cursorType = getCursorType();
|
|
78
|
+
NSString *eventType = @"move";
|
|
79
|
+
|
|
80
|
+
// Event tipini belirle
|
|
81
|
+
switch (type) {
|
|
82
|
+
case kCGEventLeftMouseDown:
|
|
83
|
+
case kCGEventRightMouseDown:
|
|
84
|
+
case kCGEventOtherMouseDown:
|
|
85
|
+
eventType = @"mousedown";
|
|
86
|
+
break;
|
|
87
|
+
case kCGEventLeftMouseUp:
|
|
88
|
+
case kCGEventRightMouseUp:
|
|
89
|
+
case kCGEventOtherMouseUp:
|
|
90
|
+
eventType = @"mouseup";
|
|
91
|
+
break;
|
|
92
|
+
case kCGEventLeftMouseDragged:
|
|
93
|
+
case kCGEventRightMouseDragged:
|
|
94
|
+
case kCGEventOtherMouseDragged:
|
|
95
|
+
eventType = @"drag";
|
|
96
|
+
break;
|
|
97
|
+
case kCGEventMouseMoved:
|
|
98
|
+
default:
|
|
99
|
+
eventType = @"move";
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Cursor data oluştur
|
|
104
|
+
NSDictionary *cursorInfo = @{
|
|
105
|
+
@"x": @((int)location.x),
|
|
106
|
+
@"y": @((int)location.y),
|
|
107
|
+
@"timestamp": @((int)timestamp),
|
|
108
|
+
@"cursorType": cursorType,
|
|
109
|
+
@"type": eventType
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Thread-safe olarak array'e ekle
|
|
113
|
+
@synchronized(g_cursorData) {
|
|
114
|
+
[g_cursorData addObject:cursorInfo];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return event;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Timer callback for periodic cursor position updates
|
|
122
|
+
void cursorTimerCallback(NSTimer *timer) {
|
|
123
|
+
@autoreleasepool {
|
|
124
|
+
g_debugCallbackCount++;
|
|
125
|
+
|
|
126
|
+
if (!g_isCursorTracking || !g_cursorData || !g_trackingStartTime) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Ana thread'de mouse pozisyonu al
|
|
131
|
+
__block NSPoint mouseLocation;
|
|
132
|
+
__block CGPoint location;
|
|
133
|
+
|
|
134
|
+
if ([NSThread isMainThread]) {
|
|
135
|
+
mouseLocation = [NSEvent mouseLocation];
|
|
136
|
+
} else {
|
|
137
|
+
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
138
|
+
mouseLocation = [NSEvent mouseLocation];
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
CGDirectDisplayID mainDisplay = CGMainDisplayID();
|
|
143
|
+
size_t displayHeight = CGDisplayPixelsHigh(mainDisplay);
|
|
144
|
+
location = CGPointMake(mouseLocation.x, displayHeight - mouseLocation.y);
|
|
145
|
+
|
|
146
|
+
NSTimeInterval timestamp = [[NSDate date] timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
|
|
147
|
+
NSString *cursorType = getCursorType();
|
|
148
|
+
|
|
149
|
+
// Cursor data oluştur
|
|
150
|
+
NSDictionary *cursorInfo = @{
|
|
151
|
+
@"x": @((int)location.x),
|
|
152
|
+
@"y": @((int)location.y),
|
|
153
|
+
@"timestamp": @((int)timestamp),
|
|
154
|
+
@"cursorType": cursorType,
|
|
155
|
+
@"type": @"move"
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Thread-safe olarak array'e ekle
|
|
159
|
+
@synchronized(g_cursorData) {
|
|
160
|
+
[g_cursorData addObject:cursorInfo];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Helper function to cleanup cursor tracking
|
|
166
|
+
void cleanupCursorTracking() {
|
|
167
|
+
g_isCursorTracking = false;
|
|
168
|
+
|
|
169
|
+
// Timer'ı durdur
|
|
170
|
+
if (g_cursorTimer) {
|
|
171
|
+
[g_cursorTimer invalidate];
|
|
172
|
+
g_cursorTimer = nil;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Timer target'ı temizle
|
|
176
|
+
if (g_timerTarget) {
|
|
177
|
+
[g_timerTarget release];
|
|
178
|
+
g_timerTarget = nil;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Event tap'i durdur
|
|
182
|
+
if (g_eventTap) {
|
|
183
|
+
CGEventTapEnable(g_eventTap, false);
|
|
184
|
+
CFMachPortInvalidate(g_eventTap);
|
|
185
|
+
CFRelease(g_eventTap);
|
|
186
|
+
g_eventTap = NULL;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Run loop source'unu kaldır
|
|
190
|
+
if (g_runLoopSource) {
|
|
191
|
+
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), g_runLoopSource, kCFRunLoopCommonModes);
|
|
192
|
+
CFRelease(g_runLoopSource);
|
|
193
|
+
g_runLoopSource = NULL;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
g_cursorData = nil;
|
|
197
|
+
g_trackingStartTime = nil;
|
|
198
|
+
g_outputPath = nil;
|
|
199
|
+
g_debugCallbackCount = 0;
|
|
200
|
+
g_lastDetectedCursorType = nil;
|
|
201
|
+
g_cursorTypeCounter = 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// NAPI Function: Start Cursor Tracking
|
|
205
|
+
Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
|
|
206
|
+
Napi::Env env = info.Env();
|
|
207
|
+
|
|
208
|
+
if (info.Length() < 1) {
|
|
209
|
+
Napi::TypeError::New(env, "Output path required").ThrowAsJavaScriptException();
|
|
210
|
+
return env.Null();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (g_isCursorTracking) {
|
|
214
|
+
return Napi::Boolean::New(env, false);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
std::string outputPath = info[0].As<Napi::String>().Utf8Value();
|
|
218
|
+
|
|
219
|
+
@try {
|
|
220
|
+
// Initialize cursor data array
|
|
221
|
+
g_cursorData = [[NSMutableArray alloc] init];
|
|
222
|
+
g_trackingStartTime = [NSDate date];
|
|
223
|
+
g_outputPath = [NSString stringWithUTF8String:outputPath.c_str()];
|
|
224
|
+
|
|
225
|
+
// Create event tap for mouse events
|
|
226
|
+
CGEventMask eventMask = (CGEventMaskBit(kCGEventLeftMouseDown) |
|
|
227
|
+
CGEventMaskBit(kCGEventLeftMouseUp) |
|
|
228
|
+
CGEventMaskBit(kCGEventRightMouseDown) |
|
|
229
|
+
CGEventMaskBit(kCGEventRightMouseUp) |
|
|
230
|
+
CGEventMaskBit(kCGEventOtherMouseDown) |
|
|
231
|
+
CGEventMaskBit(kCGEventOtherMouseUp) |
|
|
232
|
+
CGEventMaskBit(kCGEventMouseMoved) |
|
|
233
|
+
CGEventMaskBit(kCGEventLeftMouseDragged) |
|
|
234
|
+
CGEventMaskBit(kCGEventRightMouseDragged) |
|
|
235
|
+
CGEventMaskBit(kCGEventOtherMouseDragged));
|
|
236
|
+
|
|
237
|
+
g_eventTap = CGEventTapCreate(kCGSessionEventTap,
|
|
238
|
+
kCGHeadInsertEventTap,
|
|
239
|
+
kCGEventTapOptionListenOnly,
|
|
240
|
+
eventMask,
|
|
241
|
+
eventCallback,
|
|
242
|
+
NULL);
|
|
243
|
+
|
|
244
|
+
if (g_eventTap) {
|
|
245
|
+
// Event tap başarılı - detaylı event tracking aktif
|
|
246
|
+
g_runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, g_eventTap, 0);
|
|
247
|
+
CFRunLoopAddSource(CFRunLoopGetCurrent(), g_runLoopSource, kCFRunLoopCommonModes);
|
|
248
|
+
CGEventTapEnable(g_eventTap, true);
|
|
249
|
+
}
|
|
250
|
+
// Event tap başarısız olsa da devam et - sadece timer ile tracking yapar
|
|
251
|
+
|
|
252
|
+
// Timer helper oluştur
|
|
253
|
+
g_timerTarget = [[CursorTimerTarget alloc] init];
|
|
254
|
+
|
|
255
|
+
// NSTimer kullan (ana thread'de çalışır)
|
|
256
|
+
g_cursorTimer = [NSTimer scheduledTimerWithTimeInterval:0.016667 // ~60 FPS
|
|
257
|
+
target:g_timerTarget
|
|
258
|
+
selector:@selector(timerCallback:)
|
|
259
|
+
userInfo:nil
|
|
260
|
+
repeats:YES];
|
|
261
|
+
|
|
262
|
+
// Timer'ı farklı run loop mode'larında da çalıştır
|
|
263
|
+
[[NSRunLoop currentRunLoop] addTimer:g_cursorTimer forMode:NSRunLoopCommonModes];
|
|
264
|
+
|
|
265
|
+
g_isCursorTracking = true;
|
|
266
|
+
return Napi::Boolean::New(env, true);
|
|
267
|
+
|
|
268
|
+
} @catch (NSException *exception) {
|
|
269
|
+
cleanupCursorTracking();
|
|
270
|
+
return Napi::Boolean::New(env, false);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// NAPI Function: Stop Cursor Tracking
|
|
275
|
+
Napi::Value StopCursorTracking(const Napi::CallbackInfo& info) {
|
|
276
|
+
Napi::Env env = info.Env();
|
|
277
|
+
|
|
278
|
+
if (!g_isCursorTracking) {
|
|
279
|
+
return Napi::Boolean::New(env, false);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@try {
|
|
283
|
+
// JSON dosyasını kaydet
|
|
284
|
+
if (g_cursorData && g_outputPath) {
|
|
285
|
+
@synchronized(g_cursorData) {
|
|
286
|
+
NSError *error;
|
|
287
|
+
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:g_cursorData
|
|
288
|
+
options:NSJSONWritingPrettyPrinted
|
|
289
|
+
error:&error];
|
|
290
|
+
if (jsonData && !error) {
|
|
291
|
+
[jsonData writeToFile:g_outputPath atomically:YES];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
cleanupCursorTracking();
|
|
297
|
+
return Napi::Boolean::New(env, true);
|
|
298
|
+
|
|
299
|
+
} @catch (NSException *exception) {
|
|
300
|
+
cleanupCursorTracking();
|
|
301
|
+
return Napi::Boolean::New(env, false);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// NAPI Function: Get Current Cursor Position
|
|
306
|
+
Napi::Value GetCursorPosition(const Napi::CallbackInfo& info) {
|
|
307
|
+
Napi::Env env = info.Env();
|
|
308
|
+
|
|
309
|
+
@try {
|
|
310
|
+
// NSEvent kullanarak mouse pozisyonu al (daha güvenli)
|
|
311
|
+
NSPoint mouseLocation = [NSEvent mouseLocation];
|
|
312
|
+
|
|
313
|
+
// CGDisplayPixelsHigh ve CGDisplayPixelsWide ile koordinat dönüşümü
|
|
314
|
+
CGDirectDisplayID mainDisplay = CGMainDisplayID();
|
|
315
|
+
size_t displayHeight = CGDisplayPixelsHigh(mainDisplay);
|
|
316
|
+
|
|
317
|
+
// macOS coordinate system (bottom-left origin) to screen coordinates (top-left origin)
|
|
318
|
+
CGPoint location = CGPointMake(mouseLocation.x, displayHeight - mouseLocation.y);
|
|
319
|
+
|
|
320
|
+
NSString *cursorType = getCursorType();
|
|
321
|
+
|
|
322
|
+
Napi::Object result = Napi::Object::New(env);
|
|
323
|
+
result.Set("x", Napi::Number::New(env, (int)location.x));
|
|
324
|
+
result.Set("y", Napi::Number::New(env, (int)location.y));
|
|
325
|
+
result.Set("cursorType", Napi::String::New(env, [cursorType UTF8String]));
|
|
326
|
+
|
|
327
|
+
return result;
|
|
328
|
+
|
|
329
|
+
} @catch (NSException *exception) {
|
|
330
|
+
return env.Null();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// NAPI Function: Get Cursor Tracking Status
|
|
335
|
+
Napi::Value GetCursorTrackingStatus(const Napi::CallbackInfo& info) {
|
|
336
|
+
Napi::Env env = info.Env();
|
|
337
|
+
|
|
338
|
+
Napi::Object result = Napi::Object::New(env);
|
|
339
|
+
result.Set("isTracking", Napi::Boolean::New(env, g_isCursorTracking));
|
|
340
|
+
|
|
341
|
+
NSUInteger dataCount = 0;
|
|
342
|
+
if (g_cursorData) {
|
|
343
|
+
@synchronized(g_cursorData) {
|
|
344
|
+
dataCount = [g_cursorData count];
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
result.Set("dataCount", Napi::Number::New(env, (int)dataCount));
|
|
349
|
+
result.Set("hasEventTap", Napi::Boolean::New(env, g_eventTap != NULL));
|
|
350
|
+
result.Set("hasRunLoopSource", Napi::Boolean::New(env, g_runLoopSource != NULL));
|
|
351
|
+
result.Set("debugCallbackCount", Napi::Number::New(env, g_debugCallbackCount));
|
|
352
|
+
result.Set("cursorTypeCounter", Napi::Number::New(env, g_cursorTypeCounter));
|
|
353
|
+
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// NAPI Function: Save Cursor Data
|
|
358
|
+
Napi::Value SaveCursorData(const Napi::CallbackInfo& info) {
|
|
359
|
+
Napi::Env env = info.Env();
|
|
360
|
+
|
|
361
|
+
if (info.Length() < 1) {
|
|
362
|
+
Napi::TypeError::New(env, "Output path required").ThrowAsJavaScriptException();
|
|
363
|
+
return env.Null();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
std::string outputPath = info[0].As<Napi::String>().Utf8Value();
|
|
367
|
+
|
|
368
|
+
@try {
|
|
369
|
+
if (g_cursorData) {
|
|
370
|
+
@synchronized(g_cursorData) {
|
|
371
|
+
NSError *error;
|
|
372
|
+
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:g_cursorData
|
|
373
|
+
options:NSJSONWritingPrettyPrinted
|
|
374
|
+
error:&error];
|
|
375
|
+
if (jsonData && !error) {
|
|
376
|
+
NSString *filePath = [NSString stringWithUTF8String:outputPath.c_str()];
|
|
377
|
+
BOOL success = [jsonData writeToFile:filePath atomically:YES];
|
|
378
|
+
return Napi::Boolean::New(env, success);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return Napi::Boolean::New(env, false);
|
|
384
|
+
|
|
385
|
+
} @catch (NSException *exception) {
|
|
386
|
+
return Napi::Boolean::New(env, false);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Export functions
|
|
391
|
+
Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports) {
|
|
392
|
+
exports.Set("startCursorTracking", Napi::Function::New(env, StartCursorTracking));
|
|
393
|
+
exports.Set("stopCursorTracking", Napi::Function::New(env, StopCursorTracking));
|
|
394
|
+
exports.Set("getCursorPosition", Napi::Function::New(env, GetCursorPosition));
|
|
395
|
+
exports.Set("getCursorTrackingStatus", Napi::Function::New(env, GetCursorTrackingStatus));
|
|
396
|
+
exports.Set("saveCursorData", Napi::Function::New(env, SaveCursorData));
|
|
397
|
+
|
|
398
|
+
return exports;
|
|
399
|
+
}
|
package/src/mac_recorder.mm
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
#import <ImageIO/ImageIO.h>
|
|
8
8
|
#import <CoreAudio/CoreAudio.h>
|
|
9
9
|
|
|
10
|
+
// Cursor tracker function declarations
|
|
11
|
+
Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports);
|
|
12
|
+
|
|
10
13
|
@interface MacRecorderDelegate : NSObject <AVCaptureFileOutputRecordingDelegate>
|
|
11
14
|
@property (nonatomic, copy) void (^completionHandler)(NSURL *outputURL, NSError *error);
|
|
12
15
|
@end
|
|
@@ -413,6 +416,195 @@ Napi::Value GetRecordingStatus(const Napi::CallbackInfo& info) {
|
|
|
413
416
|
return Napi::Boolean::New(env, g_isRecording);
|
|
414
417
|
}
|
|
415
418
|
|
|
419
|
+
// NAPI Function: Get Window Thumbnail
|
|
420
|
+
Napi::Value GetWindowThumbnail(const Napi::CallbackInfo& info) {
|
|
421
|
+
Napi::Env env = info.Env();
|
|
422
|
+
|
|
423
|
+
if (info.Length() < 1) {
|
|
424
|
+
Napi::TypeError::New(env, "Window ID is required").ThrowAsJavaScriptException();
|
|
425
|
+
return env.Null();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
uint32_t windowID = info[0].As<Napi::Number>().Uint32Value();
|
|
429
|
+
|
|
430
|
+
// Optional parameters
|
|
431
|
+
int maxWidth = 300; // Default thumbnail width
|
|
432
|
+
int maxHeight = 200; // Default thumbnail height
|
|
433
|
+
|
|
434
|
+
if (info.Length() >= 2 && !info[1].IsNull()) {
|
|
435
|
+
maxWidth = info[1].As<Napi::Number>().Int32Value();
|
|
436
|
+
}
|
|
437
|
+
if (info.Length() >= 3 && !info[2].IsNull()) {
|
|
438
|
+
maxHeight = info[2].As<Napi::Number>().Int32Value();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
@try {
|
|
442
|
+
// Create window image
|
|
443
|
+
CGImageRef windowImage = CGWindowListCreateImage(
|
|
444
|
+
CGRectNull,
|
|
445
|
+
kCGWindowListOptionIncludingWindow,
|
|
446
|
+
windowID,
|
|
447
|
+
kCGWindowImageBoundsIgnoreFraming | kCGWindowImageShouldBeOpaque
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
if (!windowImage) {
|
|
451
|
+
return env.Null();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Get original dimensions
|
|
455
|
+
size_t originalWidth = CGImageGetWidth(windowImage);
|
|
456
|
+
size_t originalHeight = CGImageGetHeight(windowImage);
|
|
457
|
+
|
|
458
|
+
// Calculate scaled dimensions maintaining aspect ratio
|
|
459
|
+
double scaleX = (double)maxWidth / originalWidth;
|
|
460
|
+
double scaleY = (double)maxHeight / originalHeight;
|
|
461
|
+
double scale = std::min(scaleX, scaleY);
|
|
462
|
+
|
|
463
|
+
size_t thumbnailWidth = (size_t)(originalWidth * scale);
|
|
464
|
+
size_t thumbnailHeight = (size_t)(originalHeight * scale);
|
|
465
|
+
|
|
466
|
+
// Create scaled image
|
|
467
|
+
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
468
|
+
CGContextRef context = CGBitmapContextCreate(
|
|
469
|
+
NULL,
|
|
470
|
+
thumbnailWidth,
|
|
471
|
+
thumbnailHeight,
|
|
472
|
+
8,
|
|
473
|
+
thumbnailWidth * 4,
|
|
474
|
+
colorSpace,
|
|
475
|
+
kCGImageAlphaPremultipliedLast
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
if (context) {
|
|
479
|
+
CGContextDrawImage(context, CGRectMake(0, 0, thumbnailWidth, thumbnailHeight), windowImage);
|
|
480
|
+
CGImageRef thumbnailImage = CGBitmapContextCreateImage(context);
|
|
481
|
+
|
|
482
|
+
if (thumbnailImage) {
|
|
483
|
+
// Convert to PNG data
|
|
484
|
+
NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:thumbnailImage];
|
|
485
|
+
NSData *pngData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
|
|
486
|
+
|
|
487
|
+
if (pngData) {
|
|
488
|
+
// Convert to Base64
|
|
489
|
+
NSString *base64String = [pngData base64EncodedStringWithOptions:0];
|
|
490
|
+
std::string base64Std = [base64String UTF8String];
|
|
491
|
+
|
|
492
|
+
CGImageRelease(thumbnailImage);
|
|
493
|
+
CGContextRelease(context);
|
|
494
|
+
CGColorSpaceRelease(colorSpace);
|
|
495
|
+
CGImageRelease(windowImage);
|
|
496
|
+
|
|
497
|
+
return Napi::String::New(env, base64Std);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
CGImageRelease(thumbnailImage);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
CGContextRelease(context);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
CGColorSpaceRelease(colorSpace);
|
|
507
|
+
CGImageRelease(windowImage);
|
|
508
|
+
|
|
509
|
+
return env.Null();
|
|
510
|
+
|
|
511
|
+
} @catch (NSException *exception) {
|
|
512
|
+
return env.Null();
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// NAPI Function: Get Display Thumbnail
|
|
517
|
+
Napi::Value GetDisplayThumbnail(const Napi::CallbackInfo& info) {
|
|
518
|
+
Napi::Env env = info.Env();
|
|
519
|
+
|
|
520
|
+
if (info.Length() < 1) {
|
|
521
|
+
Napi::TypeError::New(env, "Display ID is required").ThrowAsJavaScriptException();
|
|
522
|
+
return env.Null();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
uint32_t displayID = info[0].As<Napi::Number>().Uint32Value();
|
|
526
|
+
|
|
527
|
+
// Optional parameters
|
|
528
|
+
int maxWidth = 300; // Default thumbnail width
|
|
529
|
+
int maxHeight = 200; // Default thumbnail height
|
|
530
|
+
|
|
531
|
+
if (info.Length() >= 2 && !info[1].IsNull()) {
|
|
532
|
+
maxWidth = info[1].As<Napi::Number>().Int32Value();
|
|
533
|
+
}
|
|
534
|
+
if (info.Length() >= 3 && !info[2].IsNull()) {
|
|
535
|
+
maxHeight = info[2].As<Napi::Number>().Int32Value();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
@try {
|
|
539
|
+
// Create display image
|
|
540
|
+
CGImageRef displayImage = CGDisplayCreateImage(displayID);
|
|
541
|
+
|
|
542
|
+
if (!displayImage) {
|
|
543
|
+
return env.Null();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Get original dimensions
|
|
547
|
+
size_t originalWidth = CGImageGetWidth(displayImage);
|
|
548
|
+
size_t originalHeight = CGImageGetHeight(displayImage);
|
|
549
|
+
|
|
550
|
+
// Calculate scaled dimensions maintaining aspect ratio
|
|
551
|
+
double scaleX = (double)maxWidth / originalWidth;
|
|
552
|
+
double scaleY = (double)maxHeight / originalHeight;
|
|
553
|
+
double scale = std::min(scaleX, scaleY);
|
|
554
|
+
|
|
555
|
+
size_t thumbnailWidth = (size_t)(originalWidth * scale);
|
|
556
|
+
size_t thumbnailHeight = (size_t)(originalHeight * scale);
|
|
557
|
+
|
|
558
|
+
// Create scaled image
|
|
559
|
+
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
560
|
+
CGContextRef context = CGBitmapContextCreate(
|
|
561
|
+
NULL,
|
|
562
|
+
thumbnailWidth,
|
|
563
|
+
thumbnailHeight,
|
|
564
|
+
8,
|
|
565
|
+
thumbnailWidth * 4,
|
|
566
|
+
colorSpace,
|
|
567
|
+
kCGImageAlphaPremultipliedLast
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
if (context) {
|
|
571
|
+
CGContextDrawImage(context, CGRectMake(0, 0, thumbnailWidth, thumbnailHeight), displayImage);
|
|
572
|
+
CGImageRef thumbnailImage = CGBitmapContextCreateImage(context);
|
|
573
|
+
|
|
574
|
+
if (thumbnailImage) {
|
|
575
|
+
// Convert to PNG data
|
|
576
|
+
NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:thumbnailImage];
|
|
577
|
+
NSData *pngData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
|
|
578
|
+
|
|
579
|
+
if (pngData) {
|
|
580
|
+
// Convert to Base64
|
|
581
|
+
NSString *base64String = [pngData base64EncodedStringWithOptions:0];
|
|
582
|
+
std::string base64Std = [base64String UTF8String];
|
|
583
|
+
|
|
584
|
+
CGImageRelease(thumbnailImage);
|
|
585
|
+
CGContextRelease(context);
|
|
586
|
+
CGColorSpaceRelease(colorSpace);
|
|
587
|
+
CGImageRelease(displayImage);
|
|
588
|
+
|
|
589
|
+
return Napi::String::New(env, base64Std);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
CGImageRelease(thumbnailImage);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
CGContextRelease(context);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
CGColorSpaceRelease(colorSpace);
|
|
599
|
+
CGImageRelease(displayImage);
|
|
600
|
+
|
|
601
|
+
return env.Null();
|
|
602
|
+
|
|
603
|
+
} @catch (NSException *exception) {
|
|
604
|
+
return env.Null();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
416
608
|
// NAPI Function: Check Permissions
|
|
417
609
|
Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
|
|
418
610
|
Napi::Env env = info.Env();
|
|
@@ -466,6 +658,13 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
466
658
|
exports.Set(Napi::String::New(env, "getRecordingStatus"), Napi::Function::New(env, GetRecordingStatus));
|
|
467
659
|
exports.Set(Napi::String::New(env, "checkPermissions"), Napi::Function::New(env, CheckPermissions));
|
|
468
660
|
|
|
661
|
+
// Thumbnail functions
|
|
662
|
+
exports.Set(Napi::String::New(env, "getWindowThumbnail"), Napi::Function::New(env, GetWindowThumbnail));
|
|
663
|
+
exports.Set(Napi::String::New(env, "getDisplayThumbnail"), Napi::Function::New(env, GetDisplayThumbnail));
|
|
664
|
+
|
|
665
|
+
// Initialize cursor tracker
|
|
666
|
+
InitCursorTracker(env, exports);
|
|
667
|
+
|
|
469
668
|
return exports;
|
|
470
669
|
}
|
|
471
670
|
|