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,829 +0,0 @@
1
- #import <napi.h>
2
- #import <AVFoundation/AVFoundation.h>
3
- #import <CoreMedia/CoreMedia.h>
4
- #import <AppKit/AppKit.h>
5
- #import <Foundation/Foundation.h>
6
- #import <CoreGraphics/CoreGraphics.h>
7
- #import <ImageIO/ImageIO.h>
8
- #import <CoreAudio/CoreAudio.h>
9
-
10
- // Import screen capture
11
- #import "screen_capture.h"
12
-
13
- // Cursor tracker function declarations
14
- Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports);
15
-
16
- // Window selector function declarations
17
- Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports);
18
-
19
- @interface MacRecorderDelegate : NSObject <AVCaptureFileOutputRecordingDelegate>
20
- @property (nonatomic, copy) void (^completionHandler)(NSURL *outputURL, NSError *error);
21
- @end
22
-
23
- @implementation MacRecorderDelegate
24
- - (void)captureOutput:(AVCaptureFileOutput *)output
25
- didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
26
- fromConnections:(NSArray<AVCaptureConnection *> *)connections
27
- error:(NSError *)error {
28
- if (self.completionHandler) {
29
- self.completionHandler(outputFileURL, error);
30
- }
31
- }
32
- @end
33
-
34
- // Global state for recording
35
- static AVCaptureSession *g_captureSession = nil;
36
- static AVCaptureMovieFileOutput *g_movieFileOutput = nil;
37
- static AVCaptureScreenInput *g_screenInput = nil;
38
- static AVCaptureDeviceInput *g_audioInput = nil;
39
- static MacRecorderDelegate *g_delegate = nil;
40
- static bool g_isRecording = false;
41
-
42
- // Helper function to cleanup recording resources
43
- void cleanupRecording() {
44
- if (g_captureSession) {
45
- [g_captureSession stopRunning];
46
- g_captureSession = nil;
47
- }
48
- g_movieFileOutput = nil;
49
- g_screenInput = nil;
50
- g_audioInput = nil;
51
- g_delegate = nil;
52
- g_isRecording = false;
53
- }
54
-
55
- // NAPI Function: Start Recording
56
- Napi::Value StartRecording(const Napi::CallbackInfo& info) {
57
- Napi::Env env = info.Env();
58
-
59
- if (info.Length() < 1) {
60
- Napi::TypeError::New(env, "Output path required").ThrowAsJavaScriptException();
61
- return env.Null();
62
- }
63
-
64
- if (g_isRecording) {
65
- return Napi::Boolean::New(env, false);
66
- }
67
-
68
- std::string outputPath = info[0].As<Napi::String>().Utf8Value();
69
-
70
- // Options parsing
71
- CGRect captureRect = CGRectNull;
72
- bool captureCursor = false; // Default olarak cursor gizli
73
- bool includeMicrophone = false; // Default olarak mikrofon kapalı
74
- bool includeSystemAudio = true; // Default olarak sistem sesi açık
75
- CGDirectDisplayID displayID = CGMainDisplayID(); // Default ana ekran
76
- NSString *audioDeviceId = nil; // Default audio device ID
77
- NSString *systemAudioDeviceId = nil; // System audio device ID
78
-
79
- if (info.Length() > 1 && info[1].IsObject()) {
80
- Napi::Object options = info[1].As<Napi::Object>();
81
-
82
- // Capture area
83
- if (options.Has("captureArea") && options.Get("captureArea").IsObject()) {
84
- Napi::Object rectObj = options.Get("captureArea").As<Napi::Object>();
85
- if (rectObj.Has("x") && rectObj.Has("y") && rectObj.Has("width") && rectObj.Has("height")) {
86
- captureRect = CGRectMake(
87
- rectObj.Get("x").As<Napi::Number>().DoubleValue(),
88
- rectObj.Get("y").As<Napi::Number>().DoubleValue(),
89
- rectObj.Get("width").As<Napi::Number>().DoubleValue(),
90
- rectObj.Get("height").As<Napi::Number>().DoubleValue()
91
- );
92
- }
93
- }
94
-
95
- // Capture cursor
96
- if (options.Has("captureCursor")) {
97
- captureCursor = options.Get("captureCursor").As<Napi::Boolean>();
98
- }
99
-
100
- // Microphone
101
- if (options.Has("includeMicrophone")) {
102
- includeMicrophone = options.Get("includeMicrophone").As<Napi::Boolean>();
103
- }
104
-
105
- // Audio device ID
106
- if (options.Has("audioDeviceId") && !options.Get("audioDeviceId").IsNull()) {
107
- std::string deviceId = options.Get("audioDeviceId").As<Napi::String>().Utf8Value();
108
- audioDeviceId = [NSString stringWithUTF8String:deviceId.c_str()];
109
- }
110
-
111
- // System audio
112
- if (options.Has("includeSystemAudio")) {
113
- includeSystemAudio = options.Get("includeSystemAudio").As<Napi::Boolean>();
114
- }
115
-
116
- // System audio device ID
117
- if (options.Has("systemAudioDeviceId") && !options.Get("systemAudioDeviceId").IsNull()) {
118
- std::string sysDeviceId = options.Get("systemAudioDeviceId").As<Napi::String>().Utf8Value();
119
- systemAudioDeviceId = [NSString stringWithUTF8String:sysDeviceId.c_str()];
120
- }
121
-
122
- // Display ID
123
- if (options.Has("displayId") && !options.Get("displayId").IsNull()) {
124
- double displayIdNum = options.Get("displayId").As<Napi::Number>().DoubleValue();
125
-
126
- // Use the display ID directly (not as an index)
127
- // The JavaScript layer passes the actual CGDirectDisplayID
128
- displayID = (CGDirectDisplayID)displayIdNum;
129
-
130
- // Verify that this display ID is valid
131
- uint32_t displayCount;
132
- CGGetActiveDisplayList(0, NULL, &displayCount);
133
- if (displayCount > 0) {
134
- CGDirectDisplayID *displays = (CGDirectDisplayID*)malloc(displayCount * sizeof(CGDirectDisplayID));
135
- CGGetActiveDisplayList(displayCount, displays, &displayCount);
136
-
137
- bool validDisplay = false;
138
- for (uint32_t i = 0; i < displayCount; i++) {
139
- if (displays[i] == displayID) {
140
- validDisplay = true;
141
- break;
142
- }
143
- }
144
-
145
- if (!validDisplay) {
146
- // Fallback to main display if invalid ID provided
147
- displayID = CGMainDisplayID();
148
- }
149
-
150
- free(displays);
151
- }
152
- }
153
-
154
- // Window ID için gelecekte kullanım (şimdilik captureArea ile hallediliyor)
155
- if (options.Has("windowId") && !options.Get("windowId").IsNull()) {
156
- // WindowId belirtilmiş ama captureArea JavaScript tarafında ayarlanıyor
157
- // Bu parametre gelecekte native level pencere seçimi için kullanılabilir
158
- }
159
- }
160
-
161
- @try {
162
- // Create capture session
163
- g_captureSession = [[AVCaptureSession alloc] init];
164
- [g_captureSession beginConfiguration];
165
-
166
- // Set session preset
167
- g_captureSession.sessionPreset = AVCaptureSessionPresetHigh;
168
-
169
- // Create screen input with selected display
170
- g_screenInput = [[AVCaptureScreenInput alloc] initWithDisplayID:displayID];
171
-
172
- if (!CGRectIsNull(captureRect)) {
173
- g_screenInput.cropRect = captureRect;
174
- }
175
-
176
- // Set cursor capture
177
- g_screenInput.capturesCursor = captureCursor;
178
-
179
- if ([g_captureSession canAddInput:g_screenInput]) {
180
- [g_captureSession addInput:g_screenInput];
181
- } else {
182
- cleanupRecording();
183
- return Napi::Boolean::New(env, false);
184
- }
185
-
186
- // Add microphone input if requested
187
- if (includeMicrophone) {
188
- AVCaptureDevice *audioDevice = nil;
189
-
190
- if (audioDeviceId) {
191
- // Try to find the specified device
192
- NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
193
- NSLog(@"[DEBUG] Looking for audio device with ID: %@", audioDeviceId);
194
- NSLog(@"[DEBUG] Available audio devices:");
195
- for (AVCaptureDevice *device in devices) {
196
- NSLog(@"[DEBUG] - Device: %@ (ID: %@)", device.localizedName, device.uniqueID);
197
- if ([device.uniqueID isEqualToString:audioDeviceId]) {
198
- NSLog(@"[DEBUG] Found matching device: %@", device.localizedName);
199
- audioDevice = device;
200
- break;
201
- }
202
- }
203
-
204
- if (!audioDevice) {
205
- NSLog(@"[DEBUG] Specified audio device not found, falling back to default");
206
- }
207
- }
208
-
209
- // Fallback to default device if specified device not found
210
- if (!audioDevice) {
211
- audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
212
- NSLog(@"[DEBUG] Using default audio device: %@ (ID: %@)", audioDevice.localizedName, audioDevice.uniqueID);
213
- }
214
-
215
- if (audioDevice) {
216
- NSError *error;
217
- g_audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:&error];
218
- if (g_audioInput && [g_captureSession canAddInput:g_audioInput]) {
219
- [g_captureSession addInput:g_audioInput];
220
- NSLog(@"[DEBUG] Successfully added audio input device");
221
- } else {
222
- NSLog(@"[DEBUG] Failed to add audio input device: %@", error);
223
- }
224
- }
225
- }
226
-
227
- // System audio configuration
228
- if (includeSystemAudio) {
229
- // Enable audio capture in screen input
230
- g_screenInput.capturesMouseClicks = YES;
231
-
232
- // Try to add system audio input using Core Audio
233
- // This approach captures system audio by creating a virtual audio device
234
- if (@available(macOS 10.15, *)) {
235
- // Configure screen input for better audio capture
236
- g_screenInput.capturesCursor = captureCursor;
237
- g_screenInput.capturesMouseClicks = YES;
238
-
239
- // Try to find and add system audio device (like Soundflower, BlackHole, etc.)
240
- NSArray *audioDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
241
- AVCaptureDevice *systemAudioDevice = nil;
242
-
243
- // If specific system audio device ID is provided, try to find it first
244
- if (systemAudioDeviceId) {
245
- for (AVCaptureDevice *device in audioDevices) {
246
- if ([device.uniqueID isEqualToString:systemAudioDeviceId]) {
247
- systemAudioDevice = device;
248
- NSLog(@"[DEBUG] Found specified system audio device: %@ (ID: %@)", device.localizedName, device.uniqueID);
249
- break;
250
- }
251
- }
252
- }
253
-
254
- // If no specific device found or specified, look for known system audio devices
255
- if (!systemAudioDevice) {
256
- for (AVCaptureDevice *device in audioDevices) {
257
- NSString *deviceName = [device.localizedName lowercaseString];
258
- // Check for common system audio capture devices
259
- if ([deviceName containsString:@"soundflower"] ||
260
- [deviceName containsString:@"blackhole"] ||
261
- [deviceName containsString:@"loopback"] ||
262
- [deviceName containsString:@"system audio"] ||
263
- [deviceName containsString:@"aggregate"]) {
264
- systemAudioDevice = device;
265
- NSLog(@"[DEBUG] Auto-detected system audio device: %@", device.localizedName);
266
- break;
267
- }
268
- }
269
- }
270
-
271
- // If we found a system audio device, add it as an additional input
272
- if (systemAudioDevice && !includeMicrophone) {
273
- // Only add system audio device if microphone is not already added
274
- NSError *error;
275
- AVCaptureDeviceInput *systemAudioInput = [[AVCaptureDeviceInput alloc] initWithDevice:systemAudioDevice error:&error];
276
- if (systemAudioInput && [g_captureSession canAddInput:systemAudioInput]) {
277
- [g_captureSession addInput:systemAudioInput];
278
- NSLog(@"[DEBUG] Successfully added system audio device: %@", systemAudioDevice.localizedName);
279
- } else if (error) {
280
- NSLog(@"[DEBUG] Failed to add system audio device: %@", error.localizedDescription);
281
- }
282
- } else if (includeSystemAudio && !systemAudioDevice) {
283
- NSLog(@"[DEBUG] System audio requested but no suitable device found. Available devices:");
284
- for (AVCaptureDevice *device in audioDevices) {
285
- NSLog(@"[DEBUG] - %@ (ID: %@)", device.localizedName, device.uniqueID);
286
- }
287
- }
288
- }
289
- } else {
290
- // Explicitly disable audio capture if not requested
291
- g_screenInput.capturesMouseClicks = NO;
292
- }
293
-
294
- // Create movie file output
295
- g_movieFileOutput = [[AVCaptureMovieFileOutput alloc] init];
296
- if ([g_captureSession canAddOutput:g_movieFileOutput]) {
297
- [g_captureSession addOutput:g_movieFileOutput];
298
- } else {
299
- cleanupRecording();
300
- return Napi::Boolean::New(env, false);
301
- }
302
-
303
- [g_captureSession commitConfiguration];
304
-
305
- // Start session
306
- [g_captureSession startRunning];
307
-
308
- // Create delegate
309
- g_delegate = [[MacRecorderDelegate alloc] init];
310
-
311
- // Start recording
312
- NSURL *outputURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:outputPath.c_str()]];
313
- [g_movieFileOutput startRecordingToOutputFileURL:outputURL recordingDelegate:g_delegate];
314
-
315
- g_isRecording = true;
316
- return Napi::Boolean::New(env, true);
317
-
318
- } @catch (NSException *exception) {
319
- cleanupRecording();
320
- return Napi::Boolean::New(env, false);
321
- }
322
- }
323
-
324
- // NAPI Function: Stop Recording
325
- Napi::Value StopRecording(const Napi::CallbackInfo& info) {
326
- Napi::Env env = info.Env();
327
-
328
- if (!g_isRecording || !g_movieFileOutput) {
329
- return Napi::Boolean::New(env, false);
330
- }
331
-
332
- @try {
333
- [g_movieFileOutput stopRecording];
334
- [g_captureSession stopRunning];
335
-
336
- g_isRecording = false;
337
- return Napi::Boolean::New(env, true);
338
-
339
- } @catch (NSException *exception) {
340
- cleanupRecording();
341
- return Napi::Boolean::New(env, false);
342
- }
343
- }
344
-
345
-
346
-
347
- // NAPI Function: Get Windows List
348
- Napi::Value GetWindows(const Napi::CallbackInfo& info) {
349
- Napi::Env env = info.Env();
350
- Napi::Array windowArray = Napi::Array::New(env);
351
-
352
- @try {
353
- // Get window list
354
- CFArrayRef windowList = CGWindowListCopyWindowInfo(
355
- kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
356
- kCGNullWindowID
357
- );
358
-
359
- if (!windowList) {
360
- return windowArray;
361
- }
362
-
363
- CFIndex windowCount = CFArrayGetCount(windowList);
364
- uint32_t arrayIndex = 0;
365
-
366
- for (CFIndex i = 0; i < windowCount; i++) {
367
- CFDictionaryRef window = (CFDictionaryRef)CFArrayGetValueAtIndex(windowList, i);
368
-
369
- // Get window ID
370
- CFNumberRef windowIDRef = (CFNumberRef)CFDictionaryGetValue(window, kCGWindowNumber);
371
- if (!windowIDRef) continue;
372
-
373
- uint32_t windowID;
374
- CFNumberGetValue(windowIDRef, kCFNumberSInt32Type, &windowID);
375
-
376
- // Get window name
377
- CFStringRef windowNameRef = (CFStringRef)CFDictionaryGetValue(window, kCGWindowName);
378
- std::string windowName = "";
379
- if (windowNameRef) {
380
- const char* windowNameCStr = CFStringGetCStringPtr(windowNameRef, kCFStringEncodingUTF8);
381
- if (windowNameCStr) {
382
- windowName = std::string(windowNameCStr);
383
- } else {
384
- // Fallback for non-ASCII characters
385
- CFIndex length = CFStringGetLength(windowNameRef);
386
- CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
387
- char* buffer = (char*)malloc(maxSize);
388
- if (CFStringGetCString(windowNameRef, buffer, maxSize, kCFStringEncodingUTF8)) {
389
- windowName = std::string(buffer);
390
- }
391
- free(buffer);
392
- }
393
- }
394
-
395
- // Get application name
396
- CFStringRef appNameRef = (CFStringRef)CFDictionaryGetValue(window, kCGWindowOwnerName);
397
- std::string appName = "";
398
- if (appNameRef) {
399
- const char* appNameCStr = CFStringGetCStringPtr(appNameRef, kCFStringEncodingUTF8);
400
- if (appNameCStr) {
401
- appName = std::string(appNameCStr);
402
- } else {
403
- CFIndex length = CFStringGetLength(appNameRef);
404
- CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
405
- char* buffer = (char*)malloc(maxSize);
406
- if (CFStringGetCString(appNameRef, buffer, maxSize, kCFStringEncodingUTF8)) {
407
- appName = std::string(buffer);
408
- }
409
- free(buffer);
410
- }
411
- }
412
-
413
- // Get window bounds
414
- CFDictionaryRef boundsRef = (CFDictionaryRef)CFDictionaryGetValue(window, kCGWindowBounds);
415
- CGRect bounds = CGRectZero;
416
- if (boundsRef) {
417
- CGRectMakeWithDictionaryRepresentation(boundsRef, &bounds);
418
- }
419
-
420
- // Skip windows without name or very small windows
421
- if (windowName.empty() || bounds.size.width < 50 || bounds.size.height < 50) {
422
- continue;
423
- }
424
-
425
- // Create window object
426
- Napi::Object windowObj = Napi::Object::New(env);
427
- windowObj.Set("id", Napi::Number::New(env, windowID));
428
- windowObj.Set("name", Napi::String::New(env, windowName));
429
- windowObj.Set("appName", Napi::String::New(env, appName));
430
- windowObj.Set("x", Napi::Number::New(env, bounds.origin.x));
431
- windowObj.Set("y", Napi::Number::New(env, bounds.origin.y));
432
- windowObj.Set("width", Napi::Number::New(env, bounds.size.width));
433
- windowObj.Set("height", Napi::Number::New(env, bounds.size.height));
434
-
435
- windowArray.Set(arrayIndex++, windowObj);
436
- }
437
-
438
- CFRelease(windowList);
439
- return windowArray;
440
-
441
- } @catch (NSException *exception) {
442
- return windowArray;
443
- }
444
- }
445
-
446
- // NAPI Function: Get Audio Devices
447
- Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
448
- Napi::Env env = info.Env();
449
-
450
- @try {
451
- NSMutableArray *devices = [NSMutableArray array];
452
-
453
- // Get all audio devices
454
- NSArray *audioDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
455
-
456
- for (AVCaptureDevice *device in audioDevices) {
457
- [devices addObject:@{
458
- @"id": device.uniqueID,
459
- @"name": device.localizedName,
460
- @"manufacturer": device.manufacturer ?: @"Unknown",
461
- @"isDefault": @([device isEqual:[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]])
462
- }];
463
- }
464
-
465
- // Convert to NAPI array
466
- Napi::Array result = Napi::Array::New(env, devices.count);
467
- for (NSUInteger i = 0; i < devices.count; i++) {
468
- NSDictionary *device = devices[i];
469
- Napi::Object deviceObj = Napi::Object::New(env);
470
- deviceObj.Set("id", Napi::String::New(env, [device[@"id"] UTF8String]));
471
- deviceObj.Set("name", Napi::String::New(env, [device[@"name"] UTF8String]));
472
- deviceObj.Set("manufacturer", Napi::String::New(env, [device[@"manufacturer"] UTF8String]));
473
- deviceObj.Set("isDefault", Napi::Boolean::New(env, [device[@"isDefault"] boolValue]));
474
- result[i] = deviceObj;
475
- }
476
-
477
- return result;
478
-
479
- } @catch (NSException *exception) {
480
- return Napi::Array::New(env, 0);
481
- }
482
- }
483
-
484
- // NAPI Function: Get Displays
485
- Napi::Value GetDisplays(const Napi::CallbackInfo& info) {
486
- Napi::Env env = info.Env();
487
-
488
- @try {
489
- NSArray *displays = [ScreenCapture getAvailableDisplays];
490
- Napi::Array result = Napi::Array::New(env, displays.count);
491
-
492
- NSLog(@"Found %lu displays", (unsigned long)displays.count);
493
-
494
- for (NSUInteger i = 0; i < displays.count; i++) {
495
- NSDictionary *display = displays[i];
496
- NSLog(@"Display %lu: ID=%u, Name=%@, Size=%@x%@",
497
- (unsigned long)i,
498
- [display[@"id"] unsignedIntValue],
499
- display[@"name"],
500
- display[@"width"],
501
- display[@"height"]);
502
-
503
- Napi::Object displayObj = Napi::Object::New(env);
504
- displayObj.Set("id", Napi::Number::New(env, [display[@"id"] unsignedIntValue]));
505
- displayObj.Set("name", Napi::String::New(env, [display[@"name"] UTF8String]));
506
- displayObj.Set("width", Napi::Number::New(env, [display[@"width"] doubleValue]));
507
- displayObj.Set("height", Napi::Number::New(env, [display[@"height"] doubleValue]));
508
- displayObj.Set("x", Napi::Number::New(env, [display[@"x"] doubleValue]));
509
- displayObj.Set("y", Napi::Number::New(env, [display[@"y"] doubleValue]));
510
- displayObj.Set("isPrimary", Napi::Boolean::New(env, [display[@"isPrimary"] boolValue]));
511
- result[i] = displayObj;
512
- }
513
-
514
- return result;
515
-
516
- } @catch (NSException *exception) {
517
- NSLog(@"Exception in GetDisplays: %@", exception);
518
- return Napi::Array::New(env, 0);
519
- }
520
- }
521
-
522
- // NAPI Function: Get Recording Status
523
- Napi::Value GetRecordingStatus(const Napi::CallbackInfo& info) {
524
- Napi::Env env = info.Env();
525
- return Napi::Boolean::New(env, g_isRecording);
526
- }
527
-
528
- // NAPI Function: Get Window Thumbnail
529
- Napi::Value GetWindowThumbnail(const Napi::CallbackInfo& info) {
530
- Napi::Env env = info.Env();
531
-
532
- if (info.Length() < 1) {
533
- Napi::TypeError::New(env, "Window ID is required").ThrowAsJavaScriptException();
534
- return env.Null();
535
- }
536
-
537
- uint32_t windowID = info[0].As<Napi::Number>().Uint32Value();
538
-
539
- // Optional parameters
540
- int maxWidth = 300; // Default thumbnail width
541
- int maxHeight = 200; // Default thumbnail height
542
-
543
- if (info.Length() >= 2 && !info[1].IsNull()) {
544
- maxWidth = info[1].As<Napi::Number>().Int32Value();
545
- }
546
- if (info.Length() >= 3 && !info[2].IsNull()) {
547
- maxHeight = info[2].As<Napi::Number>().Int32Value();
548
- }
549
-
550
- @try {
551
- // Create window image
552
- CGImageRef windowImage = CGWindowListCreateImage(
553
- CGRectNull,
554
- kCGWindowListOptionIncludingWindow,
555
- windowID,
556
- kCGWindowImageBoundsIgnoreFraming | kCGWindowImageShouldBeOpaque
557
- );
558
-
559
- if (!windowImage) {
560
- return env.Null();
561
- }
562
-
563
- // Get original dimensions
564
- size_t originalWidth = CGImageGetWidth(windowImage);
565
- size_t originalHeight = CGImageGetHeight(windowImage);
566
-
567
- // Calculate scaled dimensions maintaining aspect ratio
568
- double scaleX = (double)maxWidth / originalWidth;
569
- double scaleY = (double)maxHeight / originalHeight;
570
- double scale = std::min(scaleX, scaleY);
571
-
572
- size_t thumbnailWidth = (size_t)(originalWidth * scale);
573
- size_t thumbnailHeight = (size_t)(originalHeight * scale);
574
-
575
- // Create scaled image
576
- CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
577
- CGContextRef context = CGBitmapContextCreate(
578
- NULL,
579
- thumbnailWidth,
580
- thumbnailHeight,
581
- 8,
582
- thumbnailWidth * 4,
583
- colorSpace,
584
- kCGImageAlphaPremultipliedLast
585
- );
586
-
587
- if (context) {
588
- CGContextDrawImage(context, CGRectMake(0, 0, thumbnailWidth, thumbnailHeight), windowImage);
589
- CGImageRef thumbnailImage = CGBitmapContextCreateImage(context);
590
-
591
- if (thumbnailImage) {
592
- // Convert to PNG data
593
- NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:thumbnailImage];
594
- NSData *pngData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
595
-
596
- if (pngData) {
597
- // Convert to Base64
598
- NSString *base64String = [pngData base64EncodedStringWithOptions:0];
599
- std::string base64Std = [base64String UTF8String];
600
-
601
- CGImageRelease(thumbnailImage);
602
- CGContextRelease(context);
603
- CGColorSpaceRelease(colorSpace);
604
- CGImageRelease(windowImage);
605
-
606
- return Napi::String::New(env, base64Std);
607
- }
608
-
609
- CGImageRelease(thumbnailImage);
610
- }
611
-
612
- CGContextRelease(context);
613
- }
614
-
615
- CGColorSpaceRelease(colorSpace);
616
- CGImageRelease(windowImage);
617
-
618
- return env.Null();
619
-
620
- } @catch (NSException *exception) {
621
- return env.Null();
622
- }
623
- }
624
-
625
- // NAPI Function: Get Display Thumbnail
626
- Napi::Value GetDisplayThumbnail(const Napi::CallbackInfo& info) {
627
- Napi::Env env = info.Env();
628
-
629
- if (info.Length() < 1) {
630
- Napi::TypeError::New(env, "Display ID is required").ThrowAsJavaScriptException();
631
- return env.Null();
632
- }
633
-
634
- uint32_t displayID = info[0].As<Napi::Number>().Uint32Value();
635
-
636
- // Optional parameters
637
- int maxWidth = 300; // Default thumbnail width
638
- int maxHeight = 200; // Default thumbnail height
639
-
640
- if (info.Length() >= 2 && !info[1].IsNull()) {
641
- maxWidth = info[1].As<Napi::Number>().Int32Value();
642
- }
643
- if (info.Length() >= 3 && !info[2].IsNull()) {
644
- maxHeight = info[2].As<Napi::Number>().Int32Value();
645
- }
646
-
647
- @try {
648
- // Verify display exists
649
- CGDirectDisplayID activeDisplays[32];
650
- uint32_t displayCount;
651
- CGError err = CGGetActiveDisplayList(32, activeDisplays, &displayCount);
652
-
653
- if (err != kCGErrorSuccess) {
654
- NSLog(@"Failed to get active display list: %d", err);
655
- return env.Null();
656
- }
657
-
658
- bool displayFound = false;
659
- for (uint32_t i = 0; i < displayCount; i++) {
660
- if (activeDisplays[i] == displayID) {
661
- displayFound = true;
662
- break;
663
- }
664
- }
665
-
666
- if (!displayFound) {
667
- NSLog(@"Display ID %u not found in active displays", displayID);
668
- return env.Null();
669
- }
670
-
671
- // Create display image
672
- CGImageRef displayImage = CGDisplayCreateImage(displayID);
673
-
674
- if (!displayImage) {
675
- NSLog(@"CGDisplayCreateImage failed for display ID: %u", displayID);
676
- return env.Null();
677
- }
678
-
679
- // Get original dimensions
680
- size_t originalWidth = CGImageGetWidth(displayImage);
681
- size_t originalHeight = CGImageGetHeight(displayImage);
682
-
683
- NSLog(@"Original dimensions: %zux%zu", originalWidth, originalHeight);
684
-
685
- // Calculate scaled dimensions maintaining aspect ratio
686
- double scaleX = (double)maxWidth / originalWidth;
687
- double scaleY = (double)maxHeight / originalHeight;
688
- double scale = std::min(scaleX, scaleY);
689
-
690
- size_t thumbnailWidth = (size_t)(originalWidth * scale);
691
- size_t thumbnailHeight = (size_t)(originalHeight * scale);
692
-
693
- NSLog(@"Thumbnail dimensions: %zux%zu (scale: %f)", thumbnailWidth, thumbnailHeight, scale);
694
-
695
- // Create scaled image
696
- CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
697
- CGContextRef context = CGBitmapContextCreate(
698
- NULL,
699
- thumbnailWidth,
700
- thumbnailHeight,
701
- 8,
702
- thumbnailWidth * 4,
703
- colorSpace,
704
- kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big
705
- );
706
-
707
- if (!context) {
708
- NSLog(@"Failed to create bitmap context");
709
- CGImageRelease(displayImage);
710
- CGColorSpaceRelease(colorSpace);
711
- return env.Null();
712
- }
713
-
714
- // Set interpolation quality for better scaling
715
- CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
716
-
717
- // Draw the image
718
- CGContextDrawImage(context, CGRectMake(0, 0, thumbnailWidth, thumbnailHeight), displayImage);
719
- CGImageRef thumbnailImage = CGBitmapContextCreateImage(context);
720
-
721
- if (!thumbnailImage) {
722
- NSLog(@"Failed to create thumbnail image");
723
- CGContextRelease(context);
724
- CGImageRelease(displayImage);
725
- CGColorSpaceRelease(colorSpace);
726
- return env.Null();
727
- }
728
-
729
- // Convert to PNG data
730
- NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:thumbnailImage];
731
- NSDictionary *properties = @{NSImageCompressionFactor: @0.8};
732
- NSData *pngData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:properties];
733
-
734
- if (!pngData) {
735
- NSLog(@"Failed to convert image to PNG data");
736
- CGImageRelease(thumbnailImage);
737
- CGContextRelease(context);
738
- CGImageRelease(displayImage);
739
- CGColorSpaceRelease(colorSpace);
740
- return env.Null();
741
- }
742
-
743
- // Convert to Base64
744
- NSString *base64String = [pngData base64EncodedStringWithOptions:0];
745
- std::string base64Std = [base64String UTF8String];
746
-
747
- NSLog(@"Successfully created thumbnail with base64 length: %lu", (unsigned long)base64Std.length());
748
-
749
- // Cleanup
750
- CGImageRelease(thumbnailImage);
751
- CGContextRelease(context);
752
- CGColorSpaceRelease(colorSpace);
753
- CGImageRelease(displayImage);
754
-
755
- return Napi::String::New(env, base64Std);
756
-
757
- } @catch (NSException *exception) {
758
- NSLog(@"Exception in GetDisplayThumbnail: %@", exception);
759
- return env.Null();
760
- }
761
- }
762
-
763
- // NAPI Function: Check Permissions
764
- Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
765
- Napi::Env env = info.Env();
766
-
767
- @try {
768
- // Check screen recording permission
769
- bool hasScreenPermission = true;
770
-
771
- if (@available(macOS 10.15, *)) {
772
- // Try to create a display stream to test permissions
773
- CGDisplayStreamRef stream = CGDisplayStreamCreate(
774
- CGMainDisplayID(),
775
- 1, 1,
776
- kCVPixelFormatType_32BGRA,
777
- nil,
778
- ^(CGDisplayStreamFrameStatus status, uint64_t displayTime, IOSurfaceRef frameSurface, CGDisplayStreamUpdateRef updateRef) {
779
- // Empty handler
780
- }
781
- );
782
-
783
- if (stream) {
784
- CFRelease(stream);
785
- hasScreenPermission = true;
786
- } else {
787
- hasScreenPermission = false;
788
- }
789
- }
790
-
791
- // Check audio permission
792
- bool hasAudioPermission = true;
793
- if (@available(macOS 10.14, *)) {
794
- AVAuthorizationStatus audioStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
795
- hasAudioPermission = (audioStatus == AVAuthorizationStatusAuthorized);
796
- }
797
-
798
- return Napi::Boolean::New(env, hasScreenPermission && hasAudioPermission);
799
-
800
- } @catch (NSException *exception) {
801
- return Napi::Boolean::New(env, false);
802
- }
803
- }
804
-
805
- // Initialize NAPI Module
806
- Napi::Object Init(Napi::Env env, Napi::Object exports) {
807
- exports.Set(Napi::String::New(env, "startRecording"), Napi::Function::New(env, StartRecording));
808
- exports.Set(Napi::String::New(env, "stopRecording"), Napi::Function::New(env, StopRecording));
809
-
810
- exports.Set(Napi::String::New(env, "getAudioDevices"), Napi::Function::New(env, GetAudioDevices));
811
- exports.Set(Napi::String::New(env, "getDisplays"), Napi::Function::New(env, GetDisplays));
812
- exports.Set(Napi::String::New(env, "getWindows"), Napi::Function::New(env, GetWindows));
813
- exports.Set(Napi::String::New(env, "getRecordingStatus"), Napi::Function::New(env, GetRecordingStatus));
814
- exports.Set(Napi::String::New(env, "checkPermissions"), Napi::Function::New(env, CheckPermissions));
815
-
816
- // Thumbnail functions
817
- exports.Set(Napi::String::New(env, "getWindowThumbnail"), Napi::Function::New(env, GetWindowThumbnail));
818
- exports.Set(Napi::String::New(env, "getDisplayThumbnail"), Napi::Function::New(env, GetDisplayThumbnail));
819
-
820
- // Initialize cursor tracker
821
- InitCursorTracker(env, exports);
822
-
823
- // Initialize window selector
824
- InitWindowSelector(env, exports);
825
-
826
- return exports;
827
- }
828
-
829
- NODE_API_MODULE(mac_recorder, Init)