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.
- package/binding.gyp +5 -12
- package/index.js +25 -104
- package/install.js +2 -19
- package/package.json +2 -6
- package/src/audio_capture.mm +40 -96
- package/src/cursor_tracker.mm +4 -3
- package/src/mac_recorder.mm +673 -753
- package/src/screen_capture.h +0 -5
- package/src/screen_capture.mm +60 -139
- package/src/window_selector.mm +113 -399
- package/window-selector.js +34 -112
- package/ELECTRON-INTEGRATION.md +0 -710
- package/WINDOW_SELECTOR_USAGE.md +0 -447
- package/backup/binding.gyp +0 -44
- package/backup/src/audio_capture.mm +0 -116
- package/backup/src/cursor_tracker.mm +0 -518
- package/backup/src/mac_recorder.mm +0 -829
- package/backup/src/screen_capture.h +0 -19
- package/backup/src/screen_capture.mm +0 -162
- package/backup/src/screen_capture_kit.h +0 -15
- package/backup/src/window_selector.mm +0 -1457
- package/electron-window-selector.js +0 -698
- package/node-mac-recorder-2.4.2.tgz +0 -0
- package/prebuilds/darwin-arm64/node.napi.node +0 -0
- package/test-api-compatibility.js +0 -92
- package/test-audio.js +0 -94
- package/test-comprehensive.js +0 -164
- package/test-electron-window-selector.js +0 -119
- package/test-overlay-fix.js +0 -72
- package/test-recording.js +0 -142
- package/test-sck-availability.js +0 -26
- package/test-sck-simple.js +0 -37
- package/test-sck.js +0 -56
- package/test-screencapture-overlay.js +0 -54
- package/test-simple-windows.js +0 -29
- package/test-sync.js +0 -52
- package/test-window-details.js +0 -34
- package/test-windows.js +0 -57
|
@@ -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)
|