node-mac-recorder 2.15.0 → 2.15.2
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/.claude/settings.local.json +3 -1
- package/ELECTRON_SAFE_README.md +372 -0
- package/build-electron-safe.js +135 -0
- package/electron-safe-binding.gyp +67 -0
- package/electron-safe-index.js +399 -0
- package/examples/electron-integration-example.js +230 -0
- package/examples/electron-preload.js +46 -0
- package/examples/electron-renderer.html +634 -0
- package/package.json +5 -2
- package/src/electron_safe/audio_capture_electron.mm +137 -0
- package/src/electron_safe/cursor_tracker_electron.mm +90 -0
- package/src/electron_safe/mac_recorder_electron.mm +337 -0
- package/src/electron_safe/screen_capture_electron.h +30 -0
- package/src/electron_safe/screen_capture_electron.mm +558 -0
- package/src/electron_safe/window_selector_electron.mm +279 -0
- package/src/screen_capture_kit.mm +77 -23
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#import <napi.h>
|
|
2
|
+
#import <AVFoundation/AVFoundation.h>
|
|
3
|
+
#import <CoreAudio/CoreAudio.h>
|
|
4
|
+
|
|
5
|
+
// Thread-safe audio device management for Electron
|
|
6
|
+
static dispatch_queue_t g_audioQueue = nil;
|
|
7
|
+
|
|
8
|
+
static void initializeAudioQueue() {
|
|
9
|
+
static dispatch_once_t onceToken;
|
|
10
|
+
dispatch_once(&onceToken, ^{
|
|
11
|
+
g_audioQueue = dispatch_queue_create("com.macrecorder.audio.electron", DISPATCH_QUEUE_SERIAL);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// NAPI Function: Get Audio Devices (Electron-safe)
|
|
16
|
+
Napi::Value GetAudioDevicesElectronSafe(const Napi::CallbackInfo& info) {
|
|
17
|
+
Napi::Env env = info.Env();
|
|
18
|
+
|
|
19
|
+
@try {
|
|
20
|
+
initializeAudioQueue();
|
|
21
|
+
|
|
22
|
+
__block NSArray *devices = nil;
|
|
23
|
+
|
|
24
|
+
dispatch_sync(g_audioQueue, ^{
|
|
25
|
+
@try {
|
|
26
|
+
NSMutableArray *audioDevices = [NSMutableArray array];
|
|
27
|
+
|
|
28
|
+
// Get all audio devices
|
|
29
|
+
AudioObjectPropertyAddress propertyAddress = {
|
|
30
|
+
kAudioHardwarePropertyDevices,
|
|
31
|
+
kAudioObjectPropertyScopeGlobal,
|
|
32
|
+
kAudioObjectPropertyElementMaster
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
UInt32 dataSize = 0;
|
|
36
|
+
OSStatus status = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject,
|
|
37
|
+
&propertyAddress,
|
|
38
|
+
0,
|
|
39
|
+
NULL,
|
|
40
|
+
&dataSize);
|
|
41
|
+
|
|
42
|
+
if (status == noErr && dataSize > 0) {
|
|
43
|
+
UInt32 deviceCount = dataSize / sizeof(AudioDeviceID);
|
|
44
|
+
AudioDeviceID *audioDeviceIDs = (AudioDeviceID*)malloc(dataSize);
|
|
45
|
+
|
|
46
|
+
status = AudioObjectGetPropertyData(kAudioObjectSystemObject,
|
|
47
|
+
&propertyAddress,
|
|
48
|
+
0,
|
|
49
|
+
NULL,
|
|
50
|
+
&dataSize,
|
|
51
|
+
audioDeviceIDs);
|
|
52
|
+
|
|
53
|
+
if (status == noErr) {
|
|
54
|
+
for (UInt32 i = 0; i < deviceCount; i++) {
|
|
55
|
+
AudioDeviceID deviceID = audioDeviceIDs[i];
|
|
56
|
+
|
|
57
|
+
// Get device name
|
|
58
|
+
CFStringRef deviceName = NULL;
|
|
59
|
+
UInt32 nameSize = sizeof(CFStringRef);
|
|
60
|
+
AudioObjectPropertyAddress nameAddress = {
|
|
61
|
+
kAudioDevicePropertyDeviceNameCFString,
|
|
62
|
+
kAudioObjectPropertyScopeGlobal,
|
|
63
|
+
kAudioObjectPropertyElementMaster
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
status = AudioObjectGetPropertyData(deviceID,
|
|
67
|
+
&nameAddress,
|
|
68
|
+
0,
|
|
69
|
+
NULL,
|
|
70
|
+
&nameSize,
|
|
71
|
+
&deviceName);
|
|
72
|
+
|
|
73
|
+
if (status == noErr && deviceName) {
|
|
74
|
+
NSString *name = (__bridge NSString*)deviceName;
|
|
75
|
+
|
|
76
|
+
NSDictionary *deviceInfo = @{
|
|
77
|
+
@"id": @(deviceID),
|
|
78
|
+
@"name": name,
|
|
79
|
+
@"type": @"Audio Device"
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
[audioDevices addObject:deviceInfo];
|
|
83
|
+
CFRelease(deviceName);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
free(audioDeviceIDs);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
devices = [audioDevices copy];
|
|
92
|
+
|
|
93
|
+
} @catch (NSException *e) {
|
|
94
|
+
NSLog(@"❌ Exception getting audio devices: %@", e.reason);
|
|
95
|
+
devices = @[];
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
Napi::Array result = Napi::Array::New(env, devices ? devices.count : 0);
|
|
100
|
+
|
|
101
|
+
if (devices) {
|
|
102
|
+
for (NSUInteger i = 0; i < devices.count; i++) {
|
|
103
|
+
NSDictionary *device = devices[i];
|
|
104
|
+
Napi::Object deviceObj = Napi::Object::New(env);
|
|
105
|
+
|
|
106
|
+
deviceObj.Set("id", Napi::Number::New(env, [device[@"id"] unsignedIntValue]));
|
|
107
|
+
deviceObj.Set("name", Napi::String::New(env, [device[@"name"] UTF8String]));
|
|
108
|
+
deviceObj.Set("type", Napi::String::New(env, [device[@"type"] UTF8String]));
|
|
109
|
+
|
|
110
|
+
result.Set(i, deviceObj);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return result;
|
|
115
|
+
|
|
116
|
+
} @catch (NSException *e) {
|
|
117
|
+
NSLog(@"❌ Fatal exception in GetAudioDevicesElectronSafe: %@", e.reason);
|
|
118
|
+
Napi::Error::New(env, "Failed to get audio devices").ThrowAsJavaScriptException();
|
|
119
|
+
return env.Null();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Initialize audio capture module
|
|
124
|
+
Napi::Object InitAudioCaptureElectron(Napi::Env env, Napi::Object exports) {
|
|
125
|
+
@try {
|
|
126
|
+
initializeAudioQueue();
|
|
127
|
+
|
|
128
|
+
exports.Set("getAudioDevices", Napi::Function::New(env, GetAudioDevicesElectronSafe));
|
|
129
|
+
|
|
130
|
+
NSLog(@"✅ Electron-safe audio capture initialized");
|
|
131
|
+
return exports;
|
|
132
|
+
|
|
133
|
+
} @catch (NSException *e) {
|
|
134
|
+
NSLog(@"❌ Exception initializing audio capture: %@", e.reason);
|
|
135
|
+
return exports;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#import <napi.h>
|
|
2
|
+
#import <CoreGraphics/CoreGraphics.h>
|
|
3
|
+
#import <AppKit/AppKit.h>
|
|
4
|
+
|
|
5
|
+
// Thread-safe cursor tracking for Electron
|
|
6
|
+
static dispatch_queue_t g_cursorQueue = nil;
|
|
7
|
+
|
|
8
|
+
static void initializeCursorQueue() {
|
|
9
|
+
static dispatch_once_t onceToken;
|
|
10
|
+
dispatch_once(&onceToken, ^{
|
|
11
|
+
g_cursorQueue = dispatch_queue_create("com.macrecorder.cursor.electron", DISPATCH_QUEUE_SERIAL);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// NAPI Function: Get Cursor Position (Electron-safe)
|
|
16
|
+
Napi::Value GetCursorPositionElectronSafe(const Napi::CallbackInfo& info) {
|
|
17
|
+
Napi::Env env = info.Env();
|
|
18
|
+
|
|
19
|
+
@try {
|
|
20
|
+
initializeCursorQueue();
|
|
21
|
+
|
|
22
|
+
__block CGPoint mouseLocation = CGPointZero;
|
|
23
|
+
__block NSString *cursorType = @"arrow";
|
|
24
|
+
|
|
25
|
+
dispatch_sync(g_cursorQueue, ^{
|
|
26
|
+
@try {
|
|
27
|
+
// Get mouse location
|
|
28
|
+
mouseLocation = [NSEvent mouseLocation];
|
|
29
|
+
|
|
30
|
+
// Convert to screen coordinates (flip Y)
|
|
31
|
+
NSArray *screens = [NSScreen screens];
|
|
32
|
+
if (screens.count > 0) {
|
|
33
|
+
NSScreen *mainScreen = screens[0];
|
|
34
|
+
CGFloat screenHeight = mainScreen.frame.size.height;
|
|
35
|
+
mouseLocation.y = screenHeight - mouseLocation.y;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Get cursor type safely
|
|
39
|
+
NSCursor *currentCursor = [NSCursor currentCursor];
|
|
40
|
+
if (currentCursor) {
|
|
41
|
+
if (currentCursor == [NSCursor arrowCursor]) {
|
|
42
|
+
cursorType = @"arrow";
|
|
43
|
+
} else if (currentCursor == [NSCursor IBeamCursor]) {
|
|
44
|
+
cursorType = @"ibeam";
|
|
45
|
+
} else if (currentCursor == [NSCursor pointingHandCursor]) {
|
|
46
|
+
cursorType = @"hand";
|
|
47
|
+
} else if (currentCursor == [NSCursor resizeLeftRightCursor]) {
|
|
48
|
+
cursorType = @"resize-horizontal";
|
|
49
|
+
} else if (currentCursor == [NSCursor resizeUpDownCursor]) {
|
|
50
|
+
cursorType = @"resize-vertical";
|
|
51
|
+
} else {
|
|
52
|
+
cursorType = @"default";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
} @catch (NSException *e) {
|
|
57
|
+
NSLog(@"❌ Exception getting cursor position: %@", e.reason);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
Napi::Object result = Napi::Object::New(env);
|
|
62
|
+
result.Set("x", Napi::Number::New(env, mouseLocation.x));
|
|
63
|
+
result.Set("y", Napi::Number::New(env, mouseLocation.y));
|
|
64
|
+
result.Set("cursorType", Napi::String::New(env, [cursorType UTF8String]));
|
|
65
|
+
result.Set("eventType", Napi::String::New(env, "move"));
|
|
66
|
+
|
|
67
|
+
return result;
|
|
68
|
+
|
|
69
|
+
} @catch (NSException *e) {
|
|
70
|
+
NSLog(@"❌ Fatal exception in GetCursorPositionElectronSafe: %@", e.reason);
|
|
71
|
+
Napi::Error::New(env, "Failed to get cursor position").ThrowAsJavaScriptException();
|
|
72
|
+
return env.Null();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Initialize cursor tracker module
|
|
77
|
+
Napi::Object InitCursorTrackerElectron(Napi::Env env, Napi::Object exports) {
|
|
78
|
+
@try {
|
|
79
|
+
initializeCursorQueue();
|
|
80
|
+
|
|
81
|
+
exports.Set("getCursorPosition", Napi::Function::New(env, GetCursorPositionElectronSafe));
|
|
82
|
+
|
|
83
|
+
NSLog(@"✅ Electron-safe cursor tracker initialized");
|
|
84
|
+
return exports;
|
|
85
|
+
|
|
86
|
+
} @catch (NSException *e) {
|
|
87
|
+
NSLog(@"❌ Exception initializing cursor tracker: %@", e.reason);
|
|
88
|
+
return exports;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
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
|
+
// Electron-safe screen capture headers
|
|
11
|
+
#import "screen_capture_electron.h"
|
|
12
|
+
|
|
13
|
+
// Forward declarations for other modules
|
|
14
|
+
Napi::Object InitCursorTrackerElectron(Napi::Env env, Napi::Object exports);
|
|
15
|
+
Napi::Object InitWindowSelectorElectron(Napi::Env env, Napi::Object exports);
|
|
16
|
+
|
|
17
|
+
// Thread-safe recording state with proper synchronization
|
|
18
|
+
@interface ElectronSafeRecordingState : NSObject
|
|
19
|
+
@property (atomic) BOOL isRecording;
|
|
20
|
+
@property (atomic, strong) NSString *outputPath;
|
|
21
|
+
@property (atomic, strong) NSDate *startTime;
|
|
22
|
+
+ (instancetype)sharedState;
|
|
23
|
+
- (void)resetState;
|
|
24
|
+
@end
|
|
25
|
+
|
|
26
|
+
@implementation ElectronSafeRecordingState
|
|
27
|
+
+ (instancetype)sharedState {
|
|
28
|
+
static ElectronSafeRecordingState *shared = nil;
|
|
29
|
+
static dispatch_once_t onceToken;
|
|
30
|
+
dispatch_once(&onceToken, ^{
|
|
31
|
+
shared = [[ElectronSafeRecordingState alloc] init];
|
|
32
|
+
});
|
|
33
|
+
return shared;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
- (instancetype)init {
|
|
37
|
+
if (self = [super init]) {
|
|
38
|
+
[self resetState];
|
|
39
|
+
}
|
|
40
|
+
return self;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
- (void)resetState {
|
|
44
|
+
self.isRecording = NO;
|
|
45
|
+
self.outputPath = nil;
|
|
46
|
+
self.startTime = nil;
|
|
47
|
+
}
|
|
48
|
+
@end
|
|
49
|
+
|
|
50
|
+
// Electron-safe cleanup function
|
|
51
|
+
void electronSafeCleanup() {
|
|
52
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
53
|
+
@try {
|
|
54
|
+
[[ElectronSafeRecordingState sharedState] resetState];
|
|
55
|
+
|
|
56
|
+
// Stop any active recording safely
|
|
57
|
+
if (@available(macOS 12.3, *)) {
|
|
58
|
+
[ElectronSafeScreenCapture stopRecordingSafely];
|
|
59
|
+
}
|
|
60
|
+
} @catch (NSException *e) {
|
|
61
|
+
NSLog(@"⚠️ Safe cleanup exception: %@", e.reason);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// NAPI Function: Electron-safe Start Recording
|
|
67
|
+
Napi::Value StartRecordingElectronSafe(const Napi::CallbackInfo& info) {
|
|
68
|
+
Napi::Env env = info.Env();
|
|
69
|
+
|
|
70
|
+
@try {
|
|
71
|
+
if (info.Length() < 1) {
|
|
72
|
+
Napi::TypeError::New(env, "Output path required").ThrowAsJavaScriptException();
|
|
73
|
+
return env.Null();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
ElectronSafeRecordingState *state = [ElectronSafeRecordingState sharedState];
|
|
77
|
+
|
|
78
|
+
if (state.isRecording) {
|
|
79
|
+
NSLog(@"⚠️ Recording already in progress");
|
|
80
|
+
return Napi::Boolean::New(env, false);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
std::string outputPath = info[0].As<Napi::String>().Utf8Value();
|
|
84
|
+
|
|
85
|
+
// Parse options safely
|
|
86
|
+
NSDictionary *options = @{};
|
|
87
|
+
if (info.Length() > 1 && info[1].IsObject()) {
|
|
88
|
+
Napi::Object optionsObj = info[1].As<Napi::Object>();
|
|
89
|
+
NSMutableDictionary *mutableOptions = [NSMutableDictionary dictionary];
|
|
90
|
+
|
|
91
|
+
// Extract options with proper type checking
|
|
92
|
+
if (optionsObj.Has("captureCursor")) {
|
|
93
|
+
mutableOptions[@"captureCursor"] = @(optionsObj.Get("captureCursor").As<Napi::Boolean>().Value());
|
|
94
|
+
}
|
|
95
|
+
if (optionsObj.Has("includeMicrophone")) {
|
|
96
|
+
mutableOptions[@"includeMicrophone"] = @(optionsObj.Get("includeMicrophone").As<Napi::Boolean>().Value());
|
|
97
|
+
}
|
|
98
|
+
if (optionsObj.Has("includeSystemAudio")) {
|
|
99
|
+
mutableOptions[@"includeSystemAudio"] = @(optionsObj.Get("includeSystemAudio").As<Napi::Boolean>().Value());
|
|
100
|
+
}
|
|
101
|
+
if (optionsObj.Has("displayId")) {
|
|
102
|
+
mutableOptions[@"displayId"] = @(optionsObj.Get("displayId").As<Napi::Number>().Uint32Value());
|
|
103
|
+
}
|
|
104
|
+
if (optionsObj.Has("windowId")) {
|
|
105
|
+
mutableOptions[@"windowId"] = @(optionsObj.Get("windowId").As<Napi::Number>().Uint32Value());
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Capture area with bounds checking
|
|
109
|
+
if (optionsObj.Has("captureArea") && optionsObj.Get("captureArea").IsObject()) {
|
|
110
|
+
Napi::Object areaObj = optionsObj.Get("captureArea").As<Napi::Object>();
|
|
111
|
+
if (areaObj.Has("x") && areaObj.Has("y") && areaObj.Has("width") && areaObj.Has("height")) {
|
|
112
|
+
double x = areaObj.Get("x").As<Napi::Number>().DoubleValue();
|
|
113
|
+
double y = areaObj.Get("y").As<Napi::Number>().DoubleValue();
|
|
114
|
+
double width = areaObj.Get("width").As<Napi::Number>().DoubleValue();
|
|
115
|
+
double height = areaObj.Get("height").As<Napi::Number>().DoubleValue();
|
|
116
|
+
|
|
117
|
+
// Validate bounds
|
|
118
|
+
if (width > 0 && height > 0 && x >= 0 && y >= 0) {
|
|
119
|
+
mutableOptions[@"captureArea"] = @{
|
|
120
|
+
@"x": @(x),
|
|
121
|
+
@"y": @(y),
|
|
122
|
+
@"width": @(width),
|
|
123
|
+
@"height": @(height)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
options = [mutableOptions copy];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Start recording on main queue for Electron safety
|
|
133
|
+
__block BOOL success = NO;
|
|
134
|
+
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
135
|
+
@try {
|
|
136
|
+
NSString *nsOutputPath = [NSString stringWithUTF8String:outputPath.c_str()];
|
|
137
|
+
|
|
138
|
+
if (@available(macOS 12.3, *)) {
|
|
139
|
+
success = [ElectronSafeScreenCapture startRecordingWithPath:nsOutputPath options:options];
|
|
140
|
+
} else {
|
|
141
|
+
NSLog(@"❌ ScreenCaptureKit not available on this macOS version");
|
|
142
|
+
success = NO;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (success) {
|
|
146
|
+
state.isRecording = YES;
|
|
147
|
+
state.outputPath = nsOutputPath;
|
|
148
|
+
state.startTime = [NSDate date];
|
|
149
|
+
NSLog(@"✅ Electron-safe recording started: %@", nsOutputPath);
|
|
150
|
+
}
|
|
151
|
+
} @catch (NSException *e) {
|
|
152
|
+
NSLog(@"❌ Recording start exception: %@", e.reason);
|
|
153
|
+
success = NO;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return Napi::Boolean::New(env, success);
|
|
158
|
+
|
|
159
|
+
} @catch (NSException *e) {
|
|
160
|
+
NSLog(@"❌ Fatal exception in StartRecordingElectronSafe: %@", e.reason);
|
|
161
|
+
electronSafeCleanup();
|
|
162
|
+
Napi::Error::New(env, "Native recording failed").ThrowAsJavaScriptException();
|
|
163
|
+
return env.Null();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// NAPI Function: Electron-safe Stop Recording
|
|
168
|
+
Napi::Value StopRecordingElectronSafe(const Napi::CallbackInfo& info) {
|
|
169
|
+
Napi::Env env = info.Env();
|
|
170
|
+
|
|
171
|
+
@try {
|
|
172
|
+
ElectronSafeRecordingState *state = [ElectronSafeRecordingState sharedState];
|
|
173
|
+
|
|
174
|
+
if (!state.isRecording) {
|
|
175
|
+
NSLog(@"⚠️ No recording in progress");
|
|
176
|
+
return Napi::Boolean::New(env, false);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
__block BOOL success = NO;
|
|
180
|
+
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
181
|
+
@try {
|
|
182
|
+
if (@available(macOS 12.3, *)) {
|
|
183
|
+
success = [ElectronSafeScreenCapture stopRecordingSafely];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Always reset state
|
|
187
|
+
[state resetState];
|
|
188
|
+
|
|
189
|
+
NSLog(@"✅ Electron-safe recording stopped");
|
|
190
|
+
} @catch (NSException *e) {
|
|
191
|
+
NSLog(@"❌ Recording stop exception: %@", e.reason);
|
|
192
|
+
[state resetState]; // Reset state even on error
|
|
193
|
+
success = NO;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return Napi::Boolean::New(env, success);
|
|
198
|
+
|
|
199
|
+
} @catch (NSException *e) {
|
|
200
|
+
NSLog(@"❌ Fatal exception in StopRecordingElectronSafe: %@", e.reason);
|
|
201
|
+
electronSafeCleanup();
|
|
202
|
+
Napi::Error::New(env, "Native stop recording failed").ThrowAsJavaScriptException();
|
|
203
|
+
return env.Null();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// NAPI Function: Get Recording Status (Electron-safe)
|
|
208
|
+
Napi::Value GetRecordingStatusElectronSafe(const Napi::CallbackInfo& info) {
|
|
209
|
+
Napi::Env env = info.Env();
|
|
210
|
+
|
|
211
|
+
@try {
|
|
212
|
+
ElectronSafeRecordingState *state = [ElectronSafeRecordingState sharedState];
|
|
213
|
+
|
|
214
|
+
Napi::Object status = Napi::Object::New(env);
|
|
215
|
+
status.Set("isRecording", Napi::Boolean::New(env, state.isRecording));
|
|
216
|
+
|
|
217
|
+
if (state.outputPath) {
|
|
218
|
+
status.Set("outputPath", Napi::String::New(env, [state.outputPath UTF8String]));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (state.startTime) {
|
|
222
|
+
NSTimeInterval elapsed = [[NSDate date] timeIntervalSinceDate:state.startTime];
|
|
223
|
+
status.Set("elapsedTime", Napi::Number::New(env, elapsed));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return status;
|
|
227
|
+
|
|
228
|
+
} @catch (NSException *e) {
|
|
229
|
+
NSLog(@"❌ Fatal exception in GetRecordingStatusElectronSafe: %@", e.reason);
|
|
230
|
+
Napi::Error::New(env, "Failed to get status").ThrowAsJavaScriptException();
|
|
231
|
+
return env.Null();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// NAPI Function: Get Displays (Electron-safe)
|
|
236
|
+
Napi::Value GetDisplaysElectronSafe(const Napi::CallbackInfo& info) {
|
|
237
|
+
Napi::Env env = info.Env();
|
|
238
|
+
|
|
239
|
+
@try {
|
|
240
|
+
__block NSArray *displays = nil;
|
|
241
|
+
|
|
242
|
+
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
243
|
+
@try {
|
|
244
|
+
displays = [ElectronSafeScreenCapture getAvailableDisplays];
|
|
245
|
+
} @catch (NSException *e) {
|
|
246
|
+
NSLog(@"❌ Exception getting displays: %@", e.reason);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if (!displays) {
|
|
251
|
+
return Napi::Array::New(env, 0);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
Napi::Array result = Napi::Array::New(env, displays.count);
|
|
255
|
+
for (NSUInteger i = 0; i < displays.count; i++) {
|
|
256
|
+
NSDictionary *display = displays[i];
|
|
257
|
+
Napi::Object displayObj = Napi::Object::New(env);
|
|
258
|
+
|
|
259
|
+
displayObj.Set("id", Napi::Number::New(env, [display[@"id"] unsignedIntValue]));
|
|
260
|
+
displayObj.Set("name", Napi::String::New(env, [display[@"name"] UTF8String]));
|
|
261
|
+
displayObj.Set("width", Napi::Number::New(env, [display[@"width"] intValue]));
|
|
262
|
+
displayObj.Set("height", Napi::Number::New(env, [display[@"height"] intValue]));
|
|
263
|
+
displayObj.Set("x", Napi::Number::New(env, [display[@"x"] intValue]));
|
|
264
|
+
displayObj.Set("y", Napi::Number::New(env, [display[@"y"] intValue]));
|
|
265
|
+
displayObj.Set("isPrimary", Napi::Boolean::New(env, [display[@"isPrimary"] boolValue]));
|
|
266
|
+
|
|
267
|
+
result.Set(i, displayObj);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return result;
|
|
271
|
+
|
|
272
|
+
} @catch (NSException *e) {
|
|
273
|
+
NSLog(@"❌ Fatal exception in GetDisplaysElectronSafe: %@", e.reason);
|
|
274
|
+
Napi::Error::New(env, "Failed to get displays").ThrowAsJavaScriptException();
|
|
275
|
+
return env.Null();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// NAPI Function: Check Permissions (Electron-safe)
|
|
280
|
+
Napi::Value CheckPermissionsElectronSafe(const Napi::CallbackInfo& info) {
|
|
281
|
+
Napi::Env env = info.Env();
|
|
282
|
+
|
|
283
|
+
@try {
|
|
284
|
+
__block BOOL hasPermission = NO;
|
|
285
|
+
|
|
286
|
+
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
287
|
+
@try {
|
|
288
|
+
hasPermission = [ElectronSafeScreenCapture checkPermissions];
|
|
289
|
+
} @catch (NSException *e) {
|
|
290
|
+
NSLog(@"❌ Exception checking permissions: %@", e.reason);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
return Napi::Boolean::New(env, hasPermission);
|
|
295
|
+
|
|
296
|
+
} @catch (NSException *e) {
|
|
297
|
+
NSLog(@"❌ Fatal exception in CheckPermissionsElectronSafe: %@", e.reason);
|
|
298
|
+
return Napi::Boolean::New(env, false);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Module initialization for Electron
|
|
303
|
+
Napi::Object InitElectronSafe(Napi::Env env, Napi::Object exports) {
|
|
304
|
+
@try {
|
|
305
|
+
NSLog(@"🔌 Initializing Electron-safe mac-recorder module");
|
|
306
|
+
|
|
307
|
+
// Export Electron-safe functions
|
|
308
|
+
exports.Set("startRecording", Napi::Function::New(env, StartRecordingElectronSafe));
|
|
309
|
+
exports.Set("stopRecording", Napi::Function::New(env, StopRecordingElectronSafe));
|
|
310
|
+
exports.Set("getRecordingStatus", Napi::Function::New(env, GetRecordingStatusElectronSafe));
|
|
311
|
+
exports.Set("getDisplays", Napi::Function::New(env, GetDisplaysElectronSafe));
|
|
312
|
+
exports.Set("checkPermissions", Napi::Function::New(env, CheckPermissionsElectronSafe));
|
|
313
|
+
|
|
314
|
+
// Initialize sub-modules safely
|
|
315
|
+
@try {
|
|
316
|
+
InitCursorTrackerElectron(env, exports);
|
|
317
|
+
} @catch (NSException *e) {
|
|
318
|
+
NSLog(@"⚠️ Cursor tracker initialization failed: %@", e.reason);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
@try {
|
|
322
|
+
InitWindowSelectorElectron(env, exports);
|
|
323
|
+
} @catch (NSException *e) {
|
|
324
|
+
NSLog(@"⚠️ Window selector initialization failed: %@", e.reason);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
NSLog(@"✅ Electron-safe module initialized successfully");
|
|
328
|
+
return exports;
|
|
329
|
+
|
|
330
|
+
} @catch (NSException *e) {
|
|
331
|
+
NSLog(@"❌ Fatal exception during module initialization: %@", e.reason);
|
|
332
|
+
return exports;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Register the module
|
|
337
|
+
NODE_API_MODULE(mac_recorder_electron, InitElectronSafe)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#ifndef SCREEN_CAPTURE_ELECTRON_H
|
|
2
|
+
#define SCREEN_CAPTURE_ELECTRON_H
|
|
3
|
+
|
|
4
|
+
#import <Foundation/Foundation.h>
|
|
5
|
+
#import <CoreGraphics/CoreGraphics.h>
|
|
6
|
+
#import <ScreenCaptureKit/ScreenCaptureKit.h>
|
|
7
|
+
|
|
8
|
+
@interface ElectronSafeScreenCapture : NSObject
|
|
9
|
+
|
|
10
|
+
// Core recording functions
|
|
11
|
+
+ (BOOL)startRecordingWithPath:(NSString *)outputPath options:(NSDictionary *)options;
|
|
12
|
+
+ (BOOL)stopRecordingSafely;
|
|
13
|
+
+ (BOOL)isRecording;
|
|
14
|
+
|
|
15
|
+
// Information functions
|
|
16
|
+
+ (NSArray *)getAvailableDisplays;
|
|
17
|
+
+ (NSArray *)getAvailableWindows;
|
|
18
|
+
+ (BOOL)checkPermissions;
|
|
19
|
+
|
|
20
|
+
// Thumbnail functions
|
|
21
|
+
+ (NSString *)getDisplayThumbnailBase64:(CGDirectDisplayID)displayID
|
|
22
|
+
maxWidth:(NSInteger)maxWidth
|
|
23
|
+
maxHeight:(NSInteger)maxHeight;
|
|
24
|
+
+ (NSString *)getWindowThumbnailBase64:(uint32_t)windowID
|
|
25
|
+
maxWidth:(NSInteger)maxWidth
|
|
26
|
+
maxHeight:(NSInteger)maxHeight;
|
|
27
|
+
|
|
28
|
+
@end
|
|
29
|
+
|
|
30
|
+
#endif // SCREEN_CAPTURE_ELECTRON_H
|