node-mac-recorder 2.1.3 → 2.2.1
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 +6 -1
- package/backup/binding.gyp +44 -0
- package/backup/src/audio_capture.mm +116 -0
- package/backup/src/cursor_tracker.mm +518 -0
- package/backup/src/mac_recorder.mm +829 -0
- package/backup/src/screen_capture.h +19 -0
- package/backup/src/screen_capture.mm +162 -0
- package/backup/src/screen_capture_kit.h +15 -0
- package/backup/src/window_selector.mm +1457 -0
- package/binding.gyp +20 -4
- package/index.js +14 -0
- package/package.json +1 -1
- package/src/audio_capture.mm +96 -40
- package/src/cursor_tracker.mm +3 -4
- package/src/mac_recorder.mm +669 -419
- package/src/screen_capture.h +5 -0
- package/src/screen_capture.mm +150 -53
- package/src/window_selector.mm +12 -22
- package/test-api-compatibility.js +92 -0
- package/test-audio.js +94 -0
- package/test-comprehensive.js +164 -0
- package/test-recording.js +142 -0
- package/test-sck-simple.js +37 -0
- package/test-sck.js +60 -1
- package/test-sync.js +52 -0
- package/test-windows.js +57 -0
package/src/mac_recorder.mm
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
#import <napi.h>
|
|
2
2
|
#import <ScreenCaptureKit/ScreenCaptureKit.h>
|
|
3
|
+
<<<<<<< HEAD
|
|
4
|
+
=======
|
|
5
|
+
#import <AVFoundation/AVFoundation.h>
|
|
6
|
+
#import <CoreMedia/CoreMedia.h>
|
|
7
|
+
>>>>>>> screencapture
|
|
3
8
|
#import <AppKit/AppKit.h>
|
|
4
9
|
#import <Foundation/Foundation.h>
|
|
5
10
|
#import <CoreGraphics/CoreGraphics.h>
|
|
@@ -16,10 +21,27 @@ Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports);
|
|
|
16
21
|
// Window selector function declarations
|
|
17
22
|
Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports);
|
|
18
23
|
|
|
24
|
+
<<<<<<< HEAD
|
|
19
25
|
@interface MacRecorderDelegate : NSObject
|
|
26
|
+
=======
|
|
27
|
+
// ScreenCaptureKit Recording Delegate
|
|
28
|
+
API_AVAILABLE(macos(12.3))
|
|
29
|
+
@interface SCKRecorderDelegate : NSObject <SCStreamDelegate, SCStreamOutput>
|
|
30
|
+
>>>>>>> screencapture
|
|
20
31
|
@property (nonatomic, copy) void (^completionHandler)(NSURL *outputURL, NSError *error);
|
|
32
|
+
@property (nonatomic, copy) void (^startedHandler)(void);
|
|
33
|
+
@property (nonatomic, strong) AVAssetWriter *assetWriter;
|
|
34
|
+
@property (nonatomic, strong) AVAssetWriterInput *videoInput;
|
|
35
|
+
@property (nonatomic, strong) AVAssetWriterInput *audioInput;
|
|
36
|
+
@property (nonatomic, strong) NSURL *outputURL;
|
|
37
|
+
@property (nonatomic, assign) BOOL isWriting;
|
|
38
|
+
@property (nonatomic, assign) CMTime startTime;
|
|
39
|
+
@property (nonatomic, assign) BOOL hasStartTime;
|
|
40
|
+
@property (nonatomic, assign) BOOL startAttempted;
|
|
41
|
+
@property (nonatomic, assign) BOOL startFailed;
|
|
21
42
|
@end
|
|
22
43
|
|
|
44
|
+
<<<<<<< HEAD
|
|
23
45
|
@implementation MacRecorderDelegate
|
|
24
46
|
- (void)recordingDidStart {
|
|
25
47
|
NSLog(@"[mac_recorder] ScreenCaptureKit recording started");
|
|
@@ -32,10 +54,95 @@ Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports);
|
|
|
32
54
|
}
|
|
33
55
|
if (self.completionHandler) {
|
|
34
56
|
self.completionHandler(outputURL, error);
|
|
57
|
+
=======
|
|
58
|
+
@implementation SCKRecorderDelegate
|
|
59
|
+
|
|
60
|
+
// Standard SCStreamDelegate method - should be called automatically
|
|
61
|
+
- (void)stream:(SCStream *)stream didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type {
|
|
62
|
+
NSLog(@"📹 SCStreamDelegate received sample buffer of type: %ld", (long)type);
|
|
63
|
+
[self handleSampleBuffer:sampleBuffer ofType:type fromStream:stream];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
- (void)stream:(SCStream *)stream didStopWithError:(NSError *)error {
|
|
67
|
+
NSLog(@"🛑 Stream stopped with error: %@", error ? error.localizedDescription : @"none");
|
|
68
|
+
if (self.completionHandler) {
|
|
69
|
+
self.completionHandler(self.outputURL, error);
|
|
70
|
+
>>>>>>> screencapture
|
|
35
71
|
}
|
|
36
72
|
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
// Main sample buffer handler (renamed to avoid conflicts)
|
|
76
|
+
- (void)handleSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type fromStream:(SCStream *)stream {
|
|
77
|
+
NSLog(@"📹 Handling sample buffer of type: %ld", (long)type);
|
|
78
|
+
|
|
79
|
+
if (!self.isWriting || !self.assetWriter) {
|
|
80
|
+
NSLog(@"⚠️ Not writing or no asset writer available");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (self.startFailed) {
|
|
84
|
+
NSLog(@"⚠️ Asset writer start previously failed; ignoring buffers");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Start asset writer on first sample buffer
|
|
89
|
+
if (!self.hasStartTime) {
|
|
90
|
+
NSLog(@"🚀 Starting asset writer with first sample buffer");
|
|
91
|
+
if (self.startAttempted) {
|
|
92
|
+
// Another thread already attempted start; wait for success/fail flag to flip
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
self.startAttempted = YES;
|
|
96
|
+
if (![self.assetWriter startWriting]) {
|
|
97
|
+
NSLog(@"❌ Failed to start asset writer: %@", self.assetWriter.error.localizedDescription);
|
|
98
|
+
self.startFailed = YES;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
NSLog(@"✅ Asset writer started successfully");
|
|
103
|
+
self.startTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
|
|
104
|
+
self.hasStartTime = YES;
|
|
105
|
+
[self.assetWriter startSessionAtSourceTime:self.startTime];
|
|
106
|
+
NSLog(@"✅ Asset writer session started at time: %lld", self.startTime.value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
switch (type) {
|
|
110
|
+
case SCStreamOutputTypeScreen: {
|
|
111
|
+
NSLog(@"📺 Processing screen sample buffer");
|
|
112
|
+
if (self.videoInput && self.videoInput.isReadyForMoreMediaData) {
|
|
113
|
+
BOOL success = [self.videoInput appendSampleBuffer:sampleBuffer];
|
|
114
|
+
NSLog(@"📺 Video sample buffer appended: %@", success ? @"SUCCESS" : @"FAILED");
|
|
115
|
+
} else {
|
|
116
|
+
NSLog(@"⚠️ Video input not ready for more data");
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case SCStreamOutputTypeAudio: {
|
|
121
|
+
NSLog(@"🔊 Processing audio sample buffer");
|
|
122
|
+
if (self.audioInput && self.audioInput.isReadyForMoreMediaData) {
|
|
123
|
+
BOOL success = [self.audioInput appendSampleBuffer:sampleBuffer];
|
|
124
|
+
NSLog(@"🔊 Audio sample buffer appended: %@", success ? @"SUCCESS" : @"FAILED");
|
|
125
|
+
} else {
|
|
126
|
+
NSLog(@"⚠️ Audio input not ready for more data (or no audio input)");
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
case SCStreamOutputTypeMicrophone: {
|
|
131
|
+
NSLog(@"🎤 Processing microphone sample buffer");
|
|
132
|
+
if (self.audioInput && self.audioInput.isReadyForMoreMediaData) {
|
|
133
|
+
BOOL success = [self.audioInput appendSampleBuffer:sampleBuffer];
|
|
134
|
+
NSLog(@"🎤 Microphone sample buffer appended: %@", success ? @"SUCCESS" : @"FAILED");
|
|
135
|
+
} else {
|
|
136
|
+
NSLog(@"⚠️ Microphone input not ready for more data (or no audio input)");
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
37
143
|
@end
|
|
38
144
|
|
|
145
|
+
<<<<<<< HEAD
|
|
39
146
|
// Global state for recording
|
|
40
147
|
static MacRecorderDelegate *g_delegate = nil;
|
|
41
148
|
static bool g_isRecording = false;
|
|
@@ -43,25 +150,105 @@ static bool g_isRecording = false;
|
|
|
43
150
|
// Helper function to cleanup recording resources
|
|
44
151
|
void cleanupRecording() {
|
|
45
152
|
g_delegate = nil;
|
|
153
|
+
=======
|
|
154
|
+
// Global state for ScreenCaptureKit recording
|
|
155
|
+
static SCStream *g_scStream = nil;
|
|
156
|
+
static SCKRecorderDelegate *g_scDelegate = nil;
|
|
157
|
+
static bool g_isRecording = false;
|
|
158
|
+
|
|
159
|
+
// Helper function to cleanup ScreenCaptureKit recording resources
|
|
160
|
+
void cleanupSCKRecording() {
|
|
161
|
+
NSLog(@"🛑 Cleaning up ScreenCaptureKit recording");
|
|
162
|
+
|
|
163
|
+
if (g_scStream) {
|
|
164
|
+
NSLog(@"🛑 Stopping SCStream");
|
|
165
|
+
[g_scStream stopCaptureWithCompletionHandler:^(NSError * _Nullable error) {
|
|
166
|
+
if (error) {
|
|
167
|
+
NSLog(@"❌ Error stopping SCStream: %@", error.localizedDescription);
|
|
168
|
+
} else {
|
|
169
|
+
NSLog(@"✅ SCStream stopped successfully");
|
|
170
|
+
}
|
|
171
|
+
}];
|
|
172
|
+
g_scStream = nil;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (g_scDelegate) {
|
|
176
|
+
if (g_scDelegate.assetWriter && g_scDelegate.isWriting) {
|
|
177
|
+
NSLog(@"🛑 Finishing asset writer (status: %ld)", (long)g_scDelegate.assetWriter.status);
|
|
178
|
+
g_scDelegate.isWriting = NO;
|
|
179
|
+
|
|
180
|
+
// Only mark inputs as finished if asset writer is actually writing
|
|
181
|
+
if (g_scDelegate.assetWriter.status == AVAssetWriterStatusWriting) {
|
|
182
|
+
if (g_scDelegate.videoInput) {
|
|
183
|
+
[g_scDelegate.videoInput markAsFinished];
|
|
184
|
+
}
|
|
185
|
+
if (g_scDelegate.audioInput) {
|
|
186
|
+
[g_scDelegate.audioInput markAsFinished];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
[g_scDelegate.assetWriter finishWritingWithCompletionHandler:^{
|
|
190
|
+
NSLog(@"✅ Asset writer finished. Status: %ld", (long)g_scDelegate.assetWriter.status);
|
|
191
|
+
if (g_scDelegate.assetWriter.error) {
|
|
192
|
+
NSLog(@"❌ Asset writer error: %@", g_scDelegate.assetWriter.error.localizedDescription);
|
|
193
|
+
}
|
|
194
|
+
}];
|
|
195
|
+
} else {
|
|
196
|
+
NSLog(@"⚠️ Asset writer not in writing status, cannot finish normally");
|
|
197
|
+
if (g_scDelegate.assetWriter.status == AVAssetWriterStatusFailed) {
|
|
198
|
+
NSLog(@"❌ Asset writer failed: %@", g_scDelegate.assetWriter.error.localizedDescription);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
g_scDelegate = nil;
|
|
203
|
+
}
|
|
204
|
+
>>>>>>> screencapture
|
|
46
205
|
g_isRecording = false;
|
|
47
206
|
}
|
|
48
207
|
|
|
49
|
-
//
|
|
208
|
+
// Check if ScreenCaptureKit is available
|
|
209
|
+
bool isScreenCaptureKitAvailable() {
|
|
210
|
+
if (@available(macOS 12.3, *)) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// NAPI Function: Start Recording with ScreenCaptureKit
|
|
50
217
|
Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
51
218
|
Napi::Env env = info.Env();
|
|
52
219
|
|
|
220
|
+
if (!isScreenCaptureKitAvailable()) {
|
|
221
|
+
NSLog(@"ScreenCaptureKit requires macOS 12.3 or later");
|
|
222
|
+
return Napi::Boolean::New(env, false);
|
|
223
|
+
}
|
|
224
|
+
|
|
53
225
|
if (info.Length() < 1) {
|
|
54
|
-
|
|
55
|
-
return env
|
|
226
|
+
NSLog(@"Output path required");
|
|
227
|
+
return Napi::Boolean::New(env, false);
|
|
56
228
|
}
|
|
57
229
|
|
|
58
230
|
if (g_isRecording) {
|
|
231
|
+
NSLog(@"⚠️ Already recording");
|
|
59
232
|
return Napi::Boolean::New(env, false);
|
|
60
233
|
}
|
|
61
234
|
|
|
235
|
+
// Verify permissions before starting
|
|
236
|
+
if (!CGPreflightScreenCaptureAccess()) {
|
|
237
|
+
NSLog(@"❌ Screen recording permission not granted - requesting access");
|
|
238
|
+
bool requestResult = CGRequestScreenCaptureAccess();
|
|
239
|
+
NSLog(@"📋 Permission request result: %@", requestResult ? @"SUCCESS" : @"FAILED");
|
|
240
|
+
|
|
241
|
+
if (!CGPreflightScreenCaptureAccess()) {
|
|
242
|
+
NSLog(@"❌ Screen recording permission still not available");
|
|
243
|
+
return Napi::Boolean::New(env, false);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
NSLog(@"✅ Screen recording permission verified");
|
|
247
|
+
|
|
62
248
|
std::string outputPath = info[0].As<Napi::String>().Utf8Value();
|
|
63
249
|
NSLog(@"[mac_recorder] StartRecording: output=%@", [NSString stringWithUTF8String:outputPath.c_str()]);
|
|
64
250
|
|
|
251
|
+
<<<<<<< HEAD
|
|
65
252
|
// Options parsing (shared)
|
|
66
253
|
CGRect captureRect = CGRectNull;
|
|
67
254
|
bool captureCursor = false; // Default olarak cursor gizli
|
|
@@ -76,44 +263,29 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
76
263
|
NSMutableArray<NSNumber*> *excludedPIDs = [NSMutableArray array];
|
|
77
264
|
NSMutableArray<NSNumber*> *excludedWindowIds = [NSMutableArray array];
|
|
78
265
|
bool autoExcludeSelf = false;
|
|
266
|
+
=======
|
|
267
|
+
// Default options
|
|
268
|
+
bool captureCursor = false;
|
|
269
|
+
bool includeSystemAudio = true;
|
|
270
|
+
CGDirectDisplayID displayID = 0; // Will be set to first available display
|
|
271
|
+
uint32_t windowID = 0;
|
|
272
|
+
CGRect captureRect = CGRectNull;
|
|
273
|
+
>>>>>>> screencapture
|
|
79
274
|
|
|
275
|
+
// Parse options
|
|
80
276
|
if (info.Length() > 1 && info[1].IsObject()) {
|
|
81
277
|
Napi::Object options = info[1].As<Napi::Object>();
|
|
82
278
|
|
|
83
|
-
// Capture area
|
|
84
|
-
if (options.Has("captureArea") && options.Get("captureArea").IsObject()) {
|
|
85
|
-
Napi::Object rectObj = options.Get("captureArea").As<Napi::Object>();
|
|
86
|
-
if (rectObj.Has("x") && rectObj.Has("y") && rectObj.Has("width") && rectObj.Has("height")) {
|
|
87
|
-
captureRect = CGRectMake(
|
|
88
|
-
rectObj.Get("x").As<Napi::Number>().DoubleValue(),
|
|
89
|
-
rectObj.Get("y").As<Napi::Number>().DoubleValue(),
|
|
90
|
-
rectObj.Get("width").As<Napi::Number>().DoubleValue(),
|
|
91
|
-
rectObj.Get("height").As<Napi::Number>().DoubleValue()
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Capture cursor
|
|
97
279
|
if (options.Has("captureCursor")) {
|
|
98
280
|
captureCursor = options.Get("captureCursor").As<Napi::Boolean>();
|
|
99
281
|
}
|
|
100
282
|
|
|
101
|
-
// Microphone
|
|
102
|
-
if (options.Has("includeMicrophone")) {
|
|
103
|
-
includeMicrophone = options.Get("includeMicrophone").As<Napi::Boolean>();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Audio device ID
|
|
107
|
-
if (options.Has("audioDeviceId") && !options.Get("audioDeviceId").IsNull()) {
|
|
108
|
-
std::string deviceId = options.Get("audioDeviceId").As<Napi::String>().Utf8Value();
|
|
109
|
-
audioDeviceId = [NSString stringWithUTF8String:deviceId.c_str()];
|
|
110
|
-
}
|
|
111
283
|
|
|
112
|
-
// System audio
|
|
113
284
|
if (options.Has("includeSystemAudio")) {
|
|
114
285
|
includeSystemAudio = options.Get("includeSystemAudio").As<Napi::Boolean>();
|
|
115
286
|
}
|
|
116
287
|
|
|
288
|
+
<<<<<<< HEAD
|
|
117
289
|
// System audio device ID
|
|
118
290
|
if (options.Has("systemAudioDeviceId") && !options.Get("systemAudioDeviceId").IsNull()) {
|
|
119
291
|
std::string sysDeviceId = options.Get("systemAudioDeviceId").As<Napi::String>().Utf8Value();
|
|
@@ -158,44 +330,33 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
158
330
|
}
|
|
159
331
|
|
|
160
332
|
// Display ID
|
|
333
|
+
=======
|
|
334
|
+
>>>>>>> screencapture
|
|
161
335
|
if (options.Has("displayId") && !options.Get("displayId").IsNull()) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
// The JavaScript layer passes the actual CGDirectDisplayID
|
|
166
|
-
displayID = (CGDirectDisplayID)displayIdNum;
|
|
167
|
-
|
|
168
|
-
// Verify that this display ID is valid
|
|
169
|
-
uint32_t displayCount;
|
|
170
|
-
CGGetActiveDisplayList(0, NULL, &displayCount);
|
|
171
|
-
if (displayCount > 0) {
|
|
172
|
-
CGDirectDisplayID *displays = (CGDirectDisplayID*)malloc(displayCount * sizeof(CGDirectDisplayID));
|
|
173
|
-
CGGetActiveDisplayList(displayCount, displays, &displayCount);
|
|
174
|
-
|
|
175
|
-
bool validDisplay = false;
|
|
176
|
-
for (uint32_t i = 0; i < displayCount; i++) {
|
|
177
|
-
if (displays[i] == displayID) {
|
|
178
|
-
validDisplay = true;
|
|
179
|
-
break;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (!validDisplay) {
|
|
184
|
-
// Fallback to main display if invalid ID provided
|
|
185
|
-
displayID = CGMainDisplayID();
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
free(displays);
|
|
336
|
+
uint32_t tempDisplayID = options.Get("displayId").As<Napi::Number>().Uint32Value();
|
|
337
|
+
if (tempDisplayID != 0) {
|
|
338
|
+
displayID = tempDisplayID;
|
|
189
339
|
}
|
|
190
340
|
}
|
|
191
341
|
|
|
192
|
-
// Window ID için gelecekte kullanım (şimdilik captureArea ile hallediliyor)
|
|
193
342
|
if (options.Has("windowId") && !options.Get("windowId").IsNull()) {
|
|
194
|
-
|
|
195
|
-
|
|
343
|
+
windowID = options.Get("windowId").As<Napi::Number>().Uint32Value();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (options.Has("captureArea") && options.Get("captureArea").IsObject()) {
|
|
347
|
+
Napi::Object rectObj = options.Get("captureArea").As<Napi::Object>();
|
|
348
|
+
if (rectObj.Has("x") && rectObj.Has("y") && rectObj.Has("width") && rectObj.Has("height")) {
|
|
349
|
+
captureRect = CGRectMake(
|
|
350
|
+
rectObj.Get("x").As<Napi::Number>().DoubleValue(),
|
|
351
|
+
rectObj.Get("y").As<Napi::Number>().DoubleValue(),
|
|
352
|
+
rectObj.Get("width").As<Napi::Number>().DoubleValue(),
|
|
353
|
+
rectObj.Get("height").As<Napi::Number>().DoubleValue()
|
|
354
|
+
);
|
|
355
|
+
}
|
|
196
356
|
}
|
|
197
357
|
}
|
|
198
358
|
|
|
359
|
+
<<<<<<< HEAD
|
|
199
360
|
@try {
|
|
200
361
|
// Always prefer ScreenCaptureKit if available
|
|
201
362
|
NSLog(@"[mac_recorder] Checking ScreenCaptureKit availability");
|
|
@@ -252,17 +413,169 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
252
413
|
} @catch (NSException *exception) {
|
|
253
414
|
cleanupRecording();
|
|
254
415
|
return Napi::Boolean::New(env, false);
|
|
255
|
-
|
|
256
|
-
|
|
416
|
+
=======
|
|
417
|
+
// Create output URL
|
|
418
|
+
NSURL *outputURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:outputPath.c_str()]];
|
|
419
|
+
NSLog(@"📁 Output URL: %@", outputURL.absoluteString);
|
|
257
420
|
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
421
|
+
// Remove existing file if present to avoid AVAssetWriter "Cannot Save" error
|
|
422
|
+
NSFileManager *fm = [NSFileManager defaultManager];
|
|
423
|
+
if ([fm fileExistsAtPath:outputURL.path]) {
|
|
424
|
+
NSError *rmErr = nil;
|
|
425
|
+
[fm removeItemAtURL:outputURL error:&rmErr];
|
|
426
|
+
if (rmErr) {
|
|
427
|
+
NSLog(@"⚠️ Failed to remove existing output file (%@): %@", outputURL.path, rmErr.localizedDescription);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Get shareable content
|
|
432
|
+
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
|
433
|
+
__block NSError *contentError = nil;
|
|
434
|
+
__block SCShareableContent *shareableContent = nil;
|
|
435
|
+
|
|
436
|
+
[SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent * _Nullable content, NSError * _Nullable error) {
|
|
437
|
+
shareableContent = content;
|
|
438
|
+
contentError = error;
|
|
439
|
+
dispatch_semaphore_signal(semaphore);
|
|
440
|
+
}];
|
|
441
|
+
|
|
442
|
+
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
|
|
443
|
+
|
|
444
|
+
if (contentError) {
|
|
445
|
+
NSLog(@"ScreenCaptureKit error: %@", contentError.localizedDescription);
|
|
446
|
+
NSLog(@"This is likely due to missing screen recording permissions");
|
|
447
|
+
return Napi::Boolean::New(env, false);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Find target display or window
|
|
451
|
+
SCContentFilter *contentFilter = nil;
|
|
452
|
+
|
|
453
|
+
if (windowID > 0) {
|
|
454
|
+
// Window recording
|
|
455
|
+
SCWindow *targetWindow = nil;
|
|
456
|
+
for (SCWindow *window in shareableContent.windows) {
|
|
457
|
+
if (window.windowID == windowID) {
|
|
458
|
+
targetWindow = window;
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (!targetWindow) {
|
|
464
|
+
NSLog(@"Window not found with ID: %u", windowID);
|
|
465
|
+
return Napi::Boolean::New(env, false);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
contentFilter = [[SCContentFilter alloc] initWithDesktopIndependentWindow:targetWindow];
|
|
469
|
+
} else {
|
|
470
|
+
// Display recording
|
|
471
|
+
NSLog(@"🔍 Selecting display among %lu available displays", (unsigned long)shareableContent.displays.count);
|
|
472
|
+
|
|
473
|
+
SCDisplay *targetDisplay = nil;
|
|
474
|
+
|
|
475
|
+
// Log all available displays first
|
|
476
|
+
for (SCDisplay *display in shareableContent.displays) {
|
|
477
|
+
NSLog(@"📺 Available display: ID=%u, width=%d, height=%d", display.displayID, (int)display.width, (int)display.height);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (displayID != 0) {
|
|
481
|
+
// Look for specific display ID
|
|
482
|
+
for (SCDisplay *display in shareableContent.displays) {
|
|
483
|
+
if (display.displayID == displayID) {
|
|
484
|
+
targetDisplay = display;
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (!targetDisplay) {
|
|
490
|
+
NSLog(@"❌ Display not found with ID: %u", displayID);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// If no specific display was requested or found, use the first available
|
|
495
|
+
if (!targetDisplay) {
|
|
496
|
+
if (shareableContent.displays.count > 0) {
|
|
497
|
+
targetDisplay = shareableContent.displays.firstObject;
|
|
498
|
+
NSLog(@"✅ Using first available display: ID=%u, %dx%d", targetDisplay.displayID, (int)targetDisplay.width, (int)targetDisplay.height);
|
|
499
|
+
} else {
|
|
500
|
+
NSLog(@"❌ No displays available at all");
|
|
501
|
+
return Napi::Boolean::New(env, false);
|
|
502
|
+
}
|
|
503
|
+
} else {
|
|
504
|
+
NSLog(@"✅ Using specified display: ID=%u, %dx%d", targetDisplay.displayID, (int)targetDisplay.width, (int)targetDisplay.height);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Update displayID for subsequent use
|
|
508
|
+
displayID = targetDisplay.displayID;
|
|
509
|
+
|
|
510
|
+
// Build exclusion windows array if provided
|
|
511
|
+
NSMutableArray<SCWindow *> *excluded = [NSMutableArray array];
|
|
512
|
+
BOOL excludeCurrentApp = NO;
|
|
513
|
+
if (info.Length() > 1 && info[1].IsObject()) {
|
|
514
|
+
Napi::Object options = info[1].As<Napi::Object>();
|
|
515
|
+
if (options.Has("excludeCurrentApp")) {
|
|
516
|
+
excludeCurrentApp = options.Get("excludeCurrentApp").As<Napi::Boolean>();
|
|
517
|
+
}
|
|
518
|
+
if (options.Has("excludeWindowIds") && options.Get("excludeWindowIds").IsArray()) {
|
|
519
|
+
Napi::Array arr = options.Get("excludeWindowIds").As<Napi::Array>();
|
|
520
|
+
for (uint32_t i = 0; i < arr.Length(); i++) {
|
|
521
|
+
Napi::Value v = arr.Get(i);
|
|
522
|
+
if (v.IsNumber()) {
|
|
523
|
+
uint32_t wid = v.As<Napi::Number>().Uint32Value();
|
|
524
|
+
for (SCWindow *w in shareableContent.windows) {
|
|
525
|
+
if (w.windowID == wid) {
|
|
526
|
+
[excluded addObject:w];
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (excludeCurrentApp) {
|
|
536
|
+
pid_t pid = [[NSProcessInfo processInfo] processIdentifier];
|
|
537
|
+
for (SCWindow *w in shareableContent.windows) {
|
|
538
|
+
if (w.owningApplication && w.owningApplication.processID == pid) {
|
|
539
|
+
[excluded addObject:w];
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
contentFilter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:excluded];
|
|
545
|
+
NSLog(@"✅ Content filter created for display recording");
|
|
546
|
+
>>>>>>> screencapture
|
|
547
|
+
}
|
|
261
548
|
|
|
549
|
+
<<<<<<< HEAD
|
|
262
550
|
if (!g_isRecording) {
|
|
263
551
|
return Napi::Boolean::New(env, false);
|
|
552
|
+
=======
|
|
553
|
+
// Get actual display dimensions for proper video configuration
|
|
554
|
+
CGRect displayBounds = CGDisplayBounds(displayID);
|
|
555
|
+
NSSize videoSize = NSMakeSize(displayBounds.size.width, displayBounds.size.height);
|
|
556
|
+
|
|
557
|
+
// Create stream configuration
|
|
558
|
+
SCStreamConfiguration *config = [[SCStreamConfiguration alloc] init];
|
|
559
|
+
config.width = videoSize.width;
|
|
560
|
+
config.height = videoSize.height;
|
|
561
|
+
config.minimumFrameInterval = CMTimeMake(1, 30); // 30 FPS
|
|
562
|
+
|
|
563
|
+
// Try a more compatible pixel format
|
|
564
|
+
config.pixelFormat = kCVPixelFormatType_32BGRA;
|
|
565
|
+
|
|
566
|
+
NSLog(@"📐 Stream configuration: %dx%d, FPS=30, cursor=%@", (int)config.width, (int)config.height, captureCursor ? @"YES" : @"NO");
|
|
567
|
+
|
|
568
|
+
if (@available(macOS 13.0, *)) {
|
|
569
|
+
config.capturesAudio = includeSystemAudio;
|
|
570
|
+
config.excludesCurrentProcessAudio = YES;
|
|
571
|
+
NSLog(@"🔊 Audio configuration: capture=%@, excludeProcess=%@", includeSystemAudio ? @"YES" : @"NO", @"YES");
|
|
572
|
+
} else {
|
|
573
|
+
NSLog(@"⚠️ macOS 13.0+ features not available");
|
|
574
|
+
>>>>>>> screencapture
|
|
264
575
|
}
|
|
576
|
+
config.showsCursor = captureCursor;
|
|
265
577
|
|
|
578
|
+
<<<<<<< HEAD
|
|
266
579
|
@try {
|
|
267
580
|
NSLog(@"[mac_recorder] StopRecording called");
|
|
268
581
|
|
|
@@ -278,115 +591,51 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
|
|
|
278
591
|
|
|
279
592
|
} @catch (NSException *exception) {
|
|
280
593
|
cleanupRecording();
|
|
594
|
+
=======
|
|
595
|
+
if (!CGRectIsNull(captureRect)) {
|
|
596
|
+
config.sourceRect = captureRect;
|
|
597
|
+
// Update video size if capture rect is specified
|
|
598
|
+
videoSize = NSMakeSize(captureRect.size.width, captureRect.size.height);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Create delegate
|
|
602
|
+
g_scDelegate = [[SCKRecorderDelegate alloc] init];
|
|
603
|
+
g_scDelegate.outputURL = outputURL;
|
|
604
|
+
g_scDelegate.hasStartTime = NO;
|
|
605
|
+
g_scDelegate.startAttempted = NO;
|
|
606
|
+
g_scDelegate.startFailed = NO;
|
|
607
|
+
|
|
608
|
+
// Setup AVAssetWriter
|
|
609
|
+
NSError *writerError = nil;
|
|
610
|
+
g_scDelegate.assetWriter = [[AVAssetWriter alloc] initWithURL:outputURL fileType:AVFileTypeQuickTimeMovie error:&writerError];
|
|
611
|
+
|
|
612
|
+
if (writerError) {
|
|
613
|
+
NSLog(@"❌ Failed to create asset writer: %@", writerError.localizedDescription);
|
|
614
|
+
>>>>>>> screencapture
|
|
281
615
|
return Napi::Boolean::New(env, false);
|
|
282
616
|
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
// NAPI Function: Get Windows List
|
|
288
|
-
Napi::Value GetWindows(const Napi::CallbackInfo& info) {
|
|
289
|
-
Napi::Env env = info.Env();
|
|
290
|
-
Napi::Array windowArray = Napi::Array::New(env);
|
|
291
617
|
|
|
292
|
-
@
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
CFNumberRef windowIDRef = (CFNumberRef)CFDictionaryGetValue(window, kCGWindowNumber);
|
|
311
|
-
if (!windowIDRef) continue;
|
|
312
|
-
|
|
313
|
-
uint32_t windowID;
|
|
314
|
-
CFNumberGetValue(windowIDRef, kCFNumberSInt32Type, &windowID);
|
|
315
|
-
|
|
316
|
-
// Get window name
|
|
317
|
-
CFStringRef windowNameRef = (CFStringRef)CFDictionaryGetValue(window, kCGWindowName);
|
|
318
|
-
std::string windowName = "";
|
|
319
|
-
if (windowNameRef) {
|
|
320
|
-
const char* windowNameCStr = CFStringGetCStringPtr(windowNameRef, kCFStringEncodingUTF8);
|
|
321
|
-
if (windowNameCStr) {
|
|
322
|
-
windowName = std::string(windowNameCStr);
|
|
323
|
-
} else {
|
|
324
|
-
// Fallback for non-ASCII characters
|
|
325
|
-
CFIndex length = CFStringGetLength(windowNameRef);
|
|
326
|
-
CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
|
|
327
|
-
char* buffer = (char*)malloc(maxSize);
|
|
328
|
-
if (CFStringGetCString(windowNameRef, buffer, maxSize, kCFStringEncodingUTF8)) {
|
|
329
|
-
windowName = std::string(buffer);
|
|
330
|
-
}
|
|
331
|
-
free(buffer);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Get application name
|
|
336
|
-
CFStringRef appNameRef = (CFStringRef)CFDictionaryGetValue(window, kCGWindowOwnerName);
|
|
337
|
-
std::string appName = "";
|
|
338
|
-
if (appNameRef) {
|
|
339
|
-
const char* appNameCStr = CFStringGetCStringPtr(appNameRef, kCFStringEncodingUTF8);
|
|
340
|
-
if (appNameCStr) {
|
|
341
|
-
appName = std::string(appNameCStr);
|
|
342
|
-
} else {
|
|
343
|
-
CFIndex length = CFStringGetLength(appNameRef);
|
|
344
|
-
CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
|
|
345
|
-
char* buffer = (char*)malloc(maxSize);
|
|
346
|
-
if (CFStringGetCString(appNameRef, buffer, maxSize, kCFStringEncodingUTF8)) {
|
|
347
|
-
appName = std::string(buffer);
|
|
348
|
-
}
|
|
349
|
-
free(buffer);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Get window bounds
|
|
354
|
-
CFDictionaryRef boundsRef = (CFDictionaryRef)CFDictionaryGetValue(window, kCGWindowBounds);
|
|
355
|
-
CGRect bounds = CGRectZero;
|
|
356
|
-
if (boundsRef) {
|
|
357
|
-
CGRectMakeWithDictionaryRepresentation(boundsRef, &bounds);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Skip windows without name or very small windows
|
|
361
|
-
if (windowName.empty() || bounds.size.width < 50 || bounds.size.height < 50) {
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Create window object
|
|
366
|
-
Napi::Object windowObj = Napi::Object::New(env);
|
|
367
|
-
windowObj.Set("id", Napi::Number::New(env, windowID));
|
|
368
|
-
windowObj.Set("name", Napi::String::New(env, windowName));
|
|
369
|
-
windowObj.Set("appName", Napi::String::New(env, appName));
|
|
370
|
-
windowObj.Set("x", Napi::Number::New(env, bounds.origin.x));
|
|
371
|
-
windowObj.Set("y", Napi::Number::New(env, bounds.origin.y));
|
|
372
|
-
windowObj.Set("width", Napi::Number::New(env, bounds.size.width));
|
|
373
|
-
windowObj.Set("height", Napi::Number::New(env, bounds.size.height));
|
|
374
|
-
|
|
375
|
-
windowArray.Set(arrayIndex++, windowObj);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
CFRelease(windowList);
|
|
379
|
-
return windowArray;
|
|
380
|
-
|
|
381
|
-
} @catch (NSException *exception) {
|
|
382
|
-
return windowArray;
|
|
618
|
+
NSLog(@"✅ Asset writer created successfully");
|
|
619
|
+
|
|
620
|
+
// Video input settings using actual dimensions
|
|
621
|
+
NSLog(@"📺 Setting up video input: %dx%d", (int)videoSize.width, (int)videoSize.height);
|
|
622
|
+
NSDictionary *videoSettings = @{
|
|
623
|
+
AVVideoCodecKey: AVVideoCodecTypeH264,
|
|
624
|
+
AVVideoWidthKey: @((NSInteger)videoSize.width),
|
|
625
|
+
AVVideoHeightKey: @((NSInteger)videoSize.height)
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
g_scDelegate.videoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
|
|
629
|
+
g_scDelegate.videoInput.expectsMediaDataInRealTime = YES;
|
|
630
|
+
|
|
631
|
+
if ([g_scDelegate.assetWriter canAddInput:g_scDelegate.videoInput]) {
|
|
632
|
+
[g_scDelegate.assetWriter addInput:g_scDelegate.videoInput];
|
|
633
|
+
NSLog(@"✅ Video input added to asset writer");
|
|
634
|
+
} else {
|
|
635
|
+
NSLog(@"❌ Cannot add video input to asset writer");
|
|
383
636
|
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// NAPI Function: Get Audio Devices
|
|
387
|
-
Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
|
|
388
|
-
Napi::Env env = info.Env();
|
|
389
637
|
|
|
638
|
+
<<<<<<< HEAD
|
|
390
639
|
@try {
|
|
391
640
|
NSMutableArray *devices = [NSMutableArray array];
|
|
392
641
|
|
|
@@ -496,298 +745,212 @@ Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
|
|
|
496
745
|
deviceObj.Set("manufacturer", Napi::String::New(env, [device[@"manufacturer"] UTF8String]));
|
|
497
746
|
deviceObj.Set("isDefault", Napi::Boolean::New(env, [device[@"isDefault"] boolValue]));
|
|
498
747
|
result[i] = deviceObj;
|
|
499
|
-
|
|
748
|
+
=======
|
|
749
|
+
// Audio input settings (if needed)
|
|
750
|
+
if (includeSystemAudio) {
|
|
751
|
+
NSDictionary *audioSettings = @{
|
|
752
|
+
AVFormatIDKey: @(kAudioFormatMPEG4AAC),
|
|
753
|
+
AVSampleRateKey: @44100,
|
|
754
|
+
AVNumberOfChannelsKey: @2
|
|
755
|
+
};
|
|
500
756
|
|
|
501
|
-
|
|
757
|
+
g_scDelegate.audioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:audioSettings];
|
|
758
|
+
g_scDelegate.audioInput.expectsMediaDataInRealTime = YES;
|
|
502
759
|
|
|
503
|
-
|
|
504
|
-
|
|
760
|
+
if ([g_scDelegate.assetWriter canAddInput:g_scDelegate.audioInput]) {
|
|
761
|
+
[g_scDelegate.assetWriter addInput:g_scDelegate.audioInput];
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Create callback queue for the delegate
|
|
766
|
+
dispatch_queue_t delegateQueue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);
|
|
767
|
+
|
|
768
|
+
// Create and start stream first
|
|
769
|
+
g_scStream = [[SCStream alloc] initWithFilter:contentFilter configuration:config delegate:g_scDelegate];
|
|
770
|
+
|
|
771
|
+
// Attach outputs to actually receive sample buffers
|
|
772
|
+
NSLog(@"✅ Setting up stream output callback for sample buffers");
|
|
773
|
+
dispatch_queue_t outputQueue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);
|
|
774
|
+
NSError *outputError = nil;
|
|
775
|
+
BOOL addedScreenOutput = [g_scStream addStreamOutput:g_scDelegate type:SCStreamOutputTypeScreen sampleHandlerQueue:outputQueue error:&outputError];
|
|
776
|
+
if (addedScreenOutput) {
|
|
777
|
+
NSLog(@"✅ Screen output attached to SCStream");
|
|
778
|
+
} else {
|
|
779
|
+
NSLog(@"❌ Failed to attach screen output to SCStream: %@", outputError.localizedDescription);
|
|
780
|
+
}
|
|
781
|
+
if (includeSystemAudio) {
|
|
782
|
+
outputError = nil;
|
|
783
|
+
BOOL addedAudioOutput = [g_scStream addStreamOutput:g_scDelegate type:SCStreamOutputTypeAudio sampleHandlerQueue:outputQueue error:&outputError];
|
|
784
|
+
if (addedAudioOutput) {
|
|
785
|
+
NSLog(@"✅ Audio output attached to SCStream");
|
|
786
|
+
} else {
|
|
787
|
+
NSLog(@"⚠️ Failed to attach audio output to SCStream (audio may be disabled): %@", outputError.localizedDescription);
|
|
788
|
+
>>>>>>> screencapture
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (!g_scStream) {
|
|
793
|
+
NSLog(@"❌ Failed to create SCStream");
|
|
794
|
+
return Napi::Boolean::New(env, false);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
NSLog(@"✅ SCStream created successfully");
|
|
798
|
+
|
|
799
|
+
// Add callback queue for sample buffers (this might be important)
|
|
800
|
+
if (@available(macOS 14.0, *)) {
|
|
801
|
+
// In macOS 14+, we can set a specific queue
|
|
802
|
+
// For now, we'll rely on the default behavior
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Start capture and wait for it to begin
|
|
806
|
+
dispatch_semaphore_t startSemaphore = dispatch_semaphore_create(0);
|
|
807
|
+
__block NSError *startError = nil;
|
|
808
|
+
|
|
809
|
+
NSLog(@"🚀 Starting ScreenCaptureKit capture");
|
|
810
|
+
[g_scStream startCaptureWithCompletionHandler:^(NSError * _Nullable error) {
|
|
811
|
+
startError = error;
|
|
812
|
+
dispatch_semaphore_signal(startSemaphore);
|
|
813
|
+
}];
|
|
814
|
+
|
|
815
|
+
dispatch_semaphore_wait(startSemaphore, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC));
|
|
816
|
+
|
|
817
|
+
if (startError) {
|
|
818
|
+
NSLog(@"❌ Failed to start capture: %@", startError.localizedDescription);
|
|
819
|
+
return Napi::Boolean::New(env, false);
|
|
505
820
|
}
|
|
821
|
+
|
|
822
|
+
NSLog(@"✅ ScreenCaptureKit capture started successfully");
|
|
823
|
+
|
|
824
|
+
// Mark that we're ready to write (asset writer will be started in first sample buffer)
|
|
825
|
+
g_scDelegate.isWriting = YES;
|
|
826
|
+
g_isRecording = true;
|
|
827
|
+
|
|
828
|
+
// Wait a moment to see if we get any sample buffers
|
|
829
|
+
NSLog(@"⏱️ Waiting 1 second for sample buffers to arrive...");
|
|
830
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
831
|
+
if (g_scDelegate && !g_scDelegate.hasStartTime) {
|
|
832
|
+
NSLog(@"⚠️ No sample buffers received after 1 second - this might indicate a permission or configuration issue");
|
|
833
|
+
} else if (g_scDelegate && g_scDelegate.hasStartTime) {
|
|
834
|
+
NSLog(@"✅ Sample buffers are being received successfully");
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
NSLog(@"🎬 Recording initialized successfully");
|
|
839
|
+
return Napi::Boolean::New(env, true);
|
|
506
840
|
}
|
|
507
841
|
|
|
508
|
-
// NAPI Function:
|
|
509
|
-
Napi::Value
|
|
842
|
+
// NAPI Function: Stop Recording
|
|
843
|
+
Napi::Value StopRecording(const Napi::CallbackInfo& info) {
|
|
510
844
|
Napi::Env env = info.Env();
|
|
511
845
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
Napi::Array result = Napi::Array::New(env, displays.count);
|
|
515
|
-
|
|
516
|
-
NSLog(@"Found %lu displays", (unsigned long)displays.count);
|
|
517
|
-
|
|
518
|
-
for (NSUInteger i = 0; i < displays.count; i++) {
|
|
519
|
-
NSDictionary *display = displays[i];
|
|
520
|
-
NSLog(@"Display %lu: ID=%u, Name=%@, Size=%@x%@",
|
|
521
|
-
(unsigned long)i,
|
|
522
|
-
[display[@"id"] unsignedIntValue],
|
|
523
|
-
display[@"name"],
|
|
524
|
-
display[@"width"],
|
|
525
|
-
display[@"height"]);
|
|
526
|
-
|
|
527
|
-
Napi::Object displayObj = Napi::Object::New(env);
|
|
528
|
-
displayObj.Set("id", Napi::Number::New(env, [display[@"id"] unsignedIntValue]));
|
|
529
|
-
displayObj.Set("name", Napi::String::New(env, [display[@"name"] UTF8String]));
|
|
530
|
-
displayObj.Set("width", Napi::Number::New(env, [display[@"width"] doubleValue]));
|
|
531
|
-
displayObj.Set("height", Napi::Number::New(env, [display[@"height"] doubleValue]));
|
|
532
|
-
displayObj.Set("x", Napi::Number::New(env, [display[@"x"] doubleValue]));
|
|
533
|
-
displayObj.Set("y", Napi::Number::New(env, [display[@"y"] doubleValue]));
|
|
534
|
-
displayObj.Set("isPrimary", Napi::Boolean::New(env, [display[@"isPrimary"] boolValue]));
|
|
535
|
-
result[i] = displayObj;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
return result;
|
|
539
|
-
|
|
540
|
-
} @catch (NSException *exception) {
|
|
541
|
-
NSLog(@"Exception in GetDisplays: %@", exception);
|
|
542
|
-
return Napi::Array::New(env, 0);
|
|
846
|
+
if (!g_isRecording) {
|
|
847
|
+
return Napi::Boolean::New(env, false);
|
|
543
848
|
}
|
|
849
|
+
|
|
850
|
+
cleanupSCKRecording();
|
|
851
|
+
return Napi::Boolean::New(env, true);
|
|
544
852
|
}
|
|
545
853
|
|
|
546
854
|
// NAPI Function: Get Recording Status
|
|
547
|
-
Napi::Value
|
|
855
|
+
Napi::Value IsRecording(const Napi::CallbackInfo& info) {
|
|
548
856
|
Napi::Env env = info.Env();
|
|
549
857
|
return Napi::Boolean::New(env, g_isRecording);
|
|
550
858
|
}
|
|
551
859
|
|
|
552
|
-
// NAPI Function: Get
|
|
553
|
-
Napi::Value
|
|
860
|
+
// NAPI Function: Get Displays
|
|
861
|
+
Napi::Value GetDisplays(const Napi::CallbackInfo& info) {
|
|
554
862
|
Napi::Env env = info.Env();
|
|
555
863
|
|
|
556
|
-
if (
|
|
557
|
-
|
|
558
|
-
return
|
|
864
|
+
if (!isScreenCaptureKitAvailable()) {
|
|
865
|
+
// Fallback to legacy method
|
|
866
|
+
return GetAvailableDisplays(info);
|
|
559
867
|
}
|
|
560
868
|
|
|
561
|
-
|
|
869
|
+
// Use ScreenCaptureKit
|
|
870
|
+
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
|
871
|
+
__block SCShareableContent *shareableContent = nil;
|
|
872
|
+
__block NSError *error = nil;
|
|
562
873
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
874
|
+
[SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent * _Nullable content, NSError * _Nullable err) {
|
|
875
|
+
shareableContent = content;
|
|
876
|
+
error = err;
|
|
877
|
+
dispatch_semaphore_signal(semaphore);
|
|
878
|
+
}];
|
|
566
879
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
880
|
+
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
|
|
881
|
+
|
|
882
|
+
if (error) {
|
|
883
|
+
NSLog(@"Failed to get displays: %@", error.localizedDescription);
|
|
884
|
+
return Napi::Array::New(env, 0);
|
|
572
885
|
}
|
|
573
886
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
);
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// Get original dimensions
|
|
588
|
-
size_t originalWidth = CGImageGetWidth(windowImage);
|
|
589
|
-
size_t originalHeight = CGImageGetHeight(windowImage);
|
|
590
|
-
|
|
591
|
-
// Calculate scaled dimensions maintaining aspect ratio
|
|
592
|
-
double scaleX = (double)maxWidth / originalWidth;
|
|
593
|
-
double scaleY = (double)maxHeight / originalHeight;
|
|
594
|
-
double scale = std::min(scaleX, scaleY);
|
|
595
|
-
|
|
596
|
-
size_t thumbnailWidth = (size_t)(originalWidth * scale);
|
|
597
|
-
size_t thumbnailHeight = (size_t)(originalHeight * scale);
|
|
598
|
-
|
|
599
|
-
// Create scaled image
|
|
600
|
-
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
601
|
-
CGContextRef context = CGBitmapContextCreate(
|
|
602
|
-
NULL,
|
|
603
|
-
thumbnailWidth,
|
|
604
|
-
thumbnailHeight,
|
|
605
|
-
8,
|
|
606
|
-
thumbnailWidth * 4,
|
|
607
|
-
colorSpace,
|
|
608
|
-
kCGImageAlphaPremultipliedLast
|
|
609
|
-
);
|
|
610
|
-
|
|
611
|
-
if (context) {
|
|
612
|
-
CGContextDrawImage(context, CGRectMake(0, 0, thumbnailWidth, thumbnailHeight), windowImage);
|
|
613
|
-
CGImageRef thumbnailImage = CGBitmapContextCreateImage(context);
|
|
614
|
-
|
|
615
|
-
if (thumbnailImage) {
|
|
616
|
-
// Convert to PNG data
|
|
617
|
-
NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:thumbnailImage];
|
|
618
|
-
NSData *pngData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
|
|
619
|
-
|
|
620
|
-
if (pngData) {
|
|
621
|
-
// Convert to Base64
|
|
622
|
-
NSString *base64String = [pngData base64EncodedStringWithOptions:0];
|
|
623
|
-
std::string base64Std = [base64String UTF8String];
|
|
624
|
-
|
|
625
|
-
CGImageRelease(thumbnailImage);
|
|
626
|
-
CGContextRelease(context);
|
|
627
|
-
CGColorSpaceRelease(colorSpace);
|
|
628
|
-
CGImageRelease(windowImage);
|
|
629
|
-
|
|
630
|
-
return Napi::String::New(env, base64Std);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
CGImageRelease(thumbnailImage);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
CGContextRelease(context);
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
CGColorSpaceRelease(colorSpace);
|
|
640
|
-
CGImageRelease(windowImage);
|
|
641
|
-
|
|
642
|
-
return env.Null();
|
|
643
|
-
|
|
644
|
-
} @catch (NSException *exception) {
|
|
645
|
-
return env.Null();
|
|
887
|
+
Napi::Array displaysArray = Napi::Array::New(env);
|
|
888
|
+
uint32_t index = 0;
|
|
889
|
+
|
|
890
|
+
for (SCDisplay *display in shareableContent.displays) {
|
|
891
|
+
Napi::Object displayObj = Napi::Object::New(env);
|
|
892
|
+
displayObj.Set("id", Napi::Number::New(env, display.displayID));
|
|
893
|
+
displayObj.Set("width", Napi::Number::New(env, display.width));
|
|
894
|
+
displayObj.Set("height", Napi::Number::New(env, display.height));
|
|
895
|
+
displayObj.Set("frame", Napi::Object::New(env)); // TODO: Add frame details
|
|
896
|
+
|
|
897
|
+
displaysArray.Set(index++, displayObj);
|
|
646
898
|
}
|
|
899
|
+
|
|
900
|
+
return displaysArray;
|
|
647
901
|
}
|
|
648
902
|
|
|
649
|
-
|
|
650
|
-
|
|
903
|
+
|
|
904
|
+
// NAPI Function: Get Windows
|
|
905
|
+
Napi::Value GetWindows(const Napi::CallbackInfo& info) {
|
|
651
906
|
Napi::Env env = info.Env();
|
|
652
907
|
|
|
653
|
-
if (
|
|
654
|
-
|
|
655
|
-
return
|
|
908
|
+
if (!isScreenCaptureKitAvailable()) {
|
|
909
|
+
// Use legacy CGWindowList method
|
|
910
|
+
return GetWindowList(info);
|
|
656
911
|
}
|
|
657
912
|
|
|
658
|
-
|
|
913
|
+
// Use ScreenCaptureKit
|
|
914
|
+
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
|
915
|
+
__block SCShareableContent *shareableContent = nil;
|
|
916
|
+
__block NSError *error = nil;
|
|
659
917
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
918
|
+
[SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent * _Nullable content, NSError * _Nullable err) {
|
|
919
|
+
shareableContent = content;
|
|
920
|
+
error = err;
|
|
921
|
+
dispatch_semaphore_signal(semaphore);
|
|
922
|
+
}];
|
|
663
923
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
924
|
+
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
|
|
925
|
+
|
|
926
|
+
if (error) {
|
|
927
|
+
NSLog(@"Failed to get windows: %@", error.localizedDescription);
|
|
928
|
+
return Napi::Array::New(env, 0);
|
|
669
929
|
}
|
|
670
930
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
for (uint32_t i = 0; i < displayCount; i++) {
|
|
684
|
-
if (activeDisplays[i] == displayID) {
|
|
685
|
-
displayFound = true;
|
|
686
|
-
break;
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
if (!displayFound) {
|
|
691
|
-
NSLog(@"Display ID %u not found in active displays", displayID);
|
|
692
|
-
return env.Null();
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
// Create display image
|
|
696
|
-
CGImageRef displayImage = CGDisplayCreateImage(displayID);
|
|
697
|
-
|
|
698
|
-
if (!displayImage) {
|
|
699
|
-
NSLog(@"CGDisplayCreateImage failed for display ID: %u", displayID);
|
|
700
|
-
return env.Null();
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
// Get original dimensions
|
|
704
|
-
size_t originalWidth = CGImageGetWidth(displayImage);
|
|
705
|
-
size_t originalHeight = CGImageGetHeight(displayImage);
|
|
706
|
-
|
|
707
|
-
NSLog(@"Original dimensions: %zux%zu", originalWidth, originalHeight);
|
|
708
|
-
|
|
709
|
-
// Calculate scaled dimensions maintaining aspect ratio
|
|
710
|
-
double scaleX = (double)maxWidth / originalWidth;
|
|
711
|
-
double scaleY = (double)maxHeight / originalHeight;
|
|
712
|
-
double scale = std::min(scaleX, scaleY);
|
|
713
|
-
|
|
714
|
-
size_t thumbnailWidth = (size_t)(originalWidth * scale);
|
|
715
|
-
size_t thumbnailHeight = (size_t)(originalHeight * scale);
|
|
716
|
-
|
|
717
|
-
NSLog(@"Thumbnail dimensions: %zux%zu (scale: %f)", thumbnailWidth, thumbnailHeight, scale);
|
|
718
|
-
|
|
719
|
-
// Create scaled image
|
|
720
|
-
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
721
|
-
CGContextRef context = CGBitmapContextCreate(
|
|
722
|
-
NULL,
|
|
723
|
-
thumbnailWidth,
|
|
724
|
-
thumbnailHeight,
|
|
725
|
-
8,
|
|
726
|
-
thumbnailWidth * 4,
|
|
727
|
-
colorSpace,
|
|
728
|
-
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big
|
|
729
|
-
);
|
|
730
|
-
|
|
731
|
-
if (!context) {
|
|
732
|
-
NSLog(@"Failed to create bitmap context");
|
|
733
|
-
CGImageRelease(displayImage);
|
|
734
|
-
CGColorSpaceRelease(colorSpace);
|
|
735
|
-
return env.Null();
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// Set interpolation quality for better scaling
|
|
739
|
-
CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
|
|
740
|
-
|
|
741
|
-
// Draw the image
|
|
742
|
-
CGContextDrawImage(context, CGRectMake(0, 0, thumbnailWidth, thumbnailHeight), displayImage);
|
|
743
|
-
CGImageRef thumbnailImage = CGBitmapContextCreateImage(context);
|
|
744
|
-
|
|
745
|
-
if (!thumbnailImage) {
|
|
746
|
-
NSLog(@"Failed to create thumbnail image");
|
|
747
|
-
CGContextRelease(context);
|
|
748
|
-
CGImageRelease(displayImage);
|
|
749
|
-
CGColorSpaceRelease(colorSpace);
|
|
750
|
-
return env.Null();
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
// Convert to PNG data
|
|
754
|
-
NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:thumbnailImage];
|
|
755
|
-
NSDictionary *properties = @{NSImageCompressionFactor: @0.8};
|
|
756
|
-
NSData *pngData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:properties];
|
|
757
|
-
|
|
758
|
-
if (!pngData) {
|
|
759
|
-
NSLog(@"Failed to convert image to PNG data");
|
|
760
|
-
CGImageRelease(thumbnailImage);
|
|
761
|
-
CGContextRelease(context);
|
|
762
|
-
CGImageRelease(displayImage);
|
|
763
|
-
CGColorSpaceRelease(colorSpace);
|
|
764
|
-
return env.Null();
|
|
931
|
+
Napi::Array windowsArray = Napi::Array::New(env);
|
|
932
|
+
uint32_t index = 0;
|
|
933
|
+
|
|
934
|
+
for (SCWindow *window in shareableContent.windows) {
|
|
935
|
+
if (window.isOnScreen && window.frame.size.width > 50 && window.frame.size.height > 50) {
|
|
936
|
+
Napi::Object windowObj = Napi::Object::New(env);
|
|
937
|
+
windowObj.Set("id", Napi::Number::New(env, window.windowID));
|
|
938
|
+
windowObj.Set("title", Napi::String::New(env, window.title ? [window.title UTF8String] : ""));
|
|
939
|
+
windowObj.Set("ownerName", Napi::String::New(env, window.owningApplication.applicationName ? [window.owningApplication.applicationName UTF8String] : ""));
|
|
940
|
+
windowObj.Set("bounds", Napi::Object::New(env)); // TODO: Add bounds details
|
|
941
|
+
|
|
942
|
+
windowsArray.Set(index++, windowObj);
|
|
765
943
|
}
|
|
766
|
-
|
|
767
|
-
// Convert to Base64
|
|
768
|
-
NSString *base64String = [pngData base64EncodedStringWithOptions:0];
|
|
769
|
-
std::string base64Std = [base64String UTF8String];
|
|
770
|
-
|
|
771
|
-
NSLog(@"Successfully created thumbnail with base64 length: %lu", (unsigned long)base64Std.length());
|
|
772
|
-
|
|
773
|
-
// Cleanup
|
|
774
|
-
CGImageRelease(thumbnailImage);
|
|
775
|
-
CGContextRelease(context);
|
|
776
|
-
CGColorSpaceRelease(colorSpace);
|
|
777
|
-
CGImageRelease(displayImage);
|
|
778
|
-
|
|
779
|
-
return Napi::String::New(env, base64Std);
|
|
780
|
-
|
|
781
|
-
} @catch (NSException *exception) {
|
|
782
|
-
NSLog(@"Exception in GetDisplayThumbnail: %@", exception);
|
|
783
|
-
return env.Null();
|
|
784
944
|
}
|
|
945
|
+
|
|
946
|
+
return windowsArray;
|
|
785
947
|
}
|
|
786
948
|
|
|
787
949
|
// NAPI Function: Check Permissions
|
|
788
950
|
Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
|
|
789
951
|
Napi::Env env = info.Env();
|
|
790
952
|
|
|
953
|
+
<<<<<<< HEAD
|
|
791
954
|
@try {
|
|
792
955
|
// Check screen recording permission using ScreenCaptureKit
|
|
793
956
|
bool hasScreenPermission = true;
|
|
@@ -799,6 +962,77 @@ Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
|
|
|
799
962
|
hasScreenPermission = (content != nil && content.displays.count > 0);
|
|
800
963
|
} @catch (NSException *exception) {
|
|
801
964
|
hasScreenPermission = false;
|
|
965
|
+
=======
|
|
966
|
+
// Check screen recording permission
|
|
967
|
+
bool hasPermission = CGPreflightScreenCaptureAccess();
|
|
968
|
+
|
|
969
|
+
// If we don't have permission, try to request it
|
|
970
|
+
if (!hasPermission) {
|
|
971
|
+
NSLog(@"⚠️ Screen recording permission not granted, requesting access");
|
|
972
|
+
bool requestResult = CGRequestScreenCaptureAccess();
|
|
973
|
+
NSLog(@"📋 Permission request result: %@", requestResult ? @"SUCCESS" : @"FAILED");
|
|
974
|
+
|
|
975
|
+
// Check again after request
|
|
976
|
+
hasPermission = CGPreflightScreenCaptureAccess();
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return Napi::Boolean::New(env, hasPermission);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// NAPI Function: Get Audio Devices
|
|
983
|
+
Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
|
|
984
|
+
Napi::Env env = info.Env();
|
|
985
|
+
|
|
986
|
+
Napi::Array devices = Napi::Array::New(env);
|
|
987
|
+
uint32_t index = 0;
|
|
988
|
+
|
|
989
|
+
AudioObjectPropertyAddress propertyAddress = {
|
|
990
|
+
kAudioHardwarePropertyDevices,
|
|
991
|
+
kAudioObjectPropertyScopeGlobal,
|
|
992
|
+
kAudioObjectPropertyElementMain
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
UInt32 dataSize = 0;
|
|
996
|
+
OSStatus status = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize);
|
|
997
|
+
|
|
998
|
+
if (status != noErr) {
|
|
999
|
+
return devices;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
UInt32 deviceCount = dataSize / sizeof(AudioDeviceID);
|
|
1003
|
+
AudioDeviceID *audioDevices = (AudioDeviceID *)malloc(dataSize);
|
|
1004
|
+
|
|
1005
|
+
status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize, audioDevices);
|
|
1006
|
+
|
|
1007
|
+
if (status == noErr) {
|
|
1008
|
+
for (UInt32 i = 0; i < deviceCount; ++i) {
|
|
1009
|
+
AudioDeviceID deviceID = audioDevices[i];
|
|
1010
|
+
|
|
1011
|
+
// Get device name
|
|
1012
|
+
CFStringRef deviceName = NULL;
|
|
1013
|
+
UInt32 size = sizeof(deviceName);
|
|
1014
|
+
AudioObjectPropertyAddress nameAddress = {
|
|
1015
|
+
kAudioDevicePropertyDeviceNameCFString,
|
|
1016
|
+
kAudioDevicePropertyScopeInput,
|
|
1017
|
+
kAudioObjectPropertyElementMain
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
status = AudioObjectGetPropertyData(deviceID, &nameAddress, 0, NULL, &size, &deviceName);
|
|
1021
|
+
|
|
1022
|
+
if (status == noErr && deviceName) {
|
|
1023
|
+
Napi::Object deviceObj = Napi::Object::New(env);
|
|
1024
|
+
deviceObj.Set("id", Napi::String::New(env, std::to_string(deviceID)));
|
|
1025
|
+
|
|
1026
|
+
const char *name = CFStringGetCStringPtr(deviceName, kCFStringEncodingUTF8);
|
|
1027
|
+
if (name) {
|
|
1028
|
+
deviceObj.Set("name", Napi::String::New(env, name));
|
|
1029
|
+
} else {
|
|
1030
|
+
deviceObj.Set("name", Napi::String::New(env, "Unknown Device"));
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
devices.Set(index++, deviceObj);
|
|
1034
|
+
CFRelease(deviceName);
|
|
1035
|
+
>>>>>>> screencapture
|
|
802
1036
|
}
|
|
803
1037
|
} else {
|
|
804
1038
|
// Fallback for older macOS versions
|
|
@@ -822,6 +1056,7 @@ Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
|
|
|
822
1056
|
}
|
|
823
1057
|
}
|
|
824
1058
|
}
|
|
1059
|
+
<<<<<<< HEAD
|
|
825
1060
|
|
|
826
1061
|
// For audio permission, we'll use a simpler check since we're using CoreAudio
|
|
827
1062
|
bool hasAudioPermission = true;
|
|
@@ -830,11 +1065,17 @@ Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
|
|
|
830
1065
|
|
|
831
1066
|
} @catch (NSException *exception) {
|
|
832
1067
|
return Napi::Boolean::New(env, false);
|
|
1068
|
+
=======
|
|
1069
|
+
>>>>>>> screencapture
|
|
833
1070
|
}
|
|
1071
|
+
|
|
1072
|
+
free(audioDevices);
|
|
1073
|
+
return devices;
|
|
834
1074
|
}
|
|
835
1075
|
|
|
836
|
-
// Initialize
|
|
1076
|
+
// Initialize the addon
|
|
837
1077
|
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
1078
|
+
<<<<<<< HEAD
|
|
838
1079
|
exports.Set(Napi::String::New(env, "startRecording"), Napi::Function::New(env, StartRecording));
|
|
839
1080
|
exports.Set(Napi::String::New(env, "stopRecording"), Napi::Function::New(env, StopRecording));
|
|
840
1081
|
|
|
@@ -856,6 +1097,15 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
856
1097
|
// Thumbnail functions
|
|
857
1098
|
exports.Set(Napi::String::New(env, "getWindowThumbnail"), Napi::Function::New(env, GetWindowThumbnail));
|
|
858
1099
|
exports.Set(Napi::String::New(env, "getDisplayThumbnail"), Napi::Function::New(env, GetDisplayThumbnail));
|
|
1100
|
+
=======
|
|
1101
|
+
exports.Set("startRecording", Napi::Function::New(env, StartRecording));
|
|
1102
|
+
exports.Set("stopRecording", Napi::Function::New(env, StopRecording));
|
|
1103
|
+
exports.Set("isRecording", Napi::Function::New(env, IsRecording));
|
|
1104
|
+
exports.Set("getDisplays", Napi::Function::New(env, GetDisplays));
|
|
1105
|
+
exports.Set("getWindows", Napi::Function::New(env, GetWindows));
|
|
1106
|
+
exports.Set("checkPermissions", Napi::Function::New(env, CheckPermissions));
|
|
1107
|
+
exports.Set("getAudioDevices", Napi::Function::New(env, GetAudioDevices));
|
|
1108
|
+
>>>>>>> screencapture
|
|
859
1109
|
|
|
860
1110
|
// Initialize cursor tracker
|
|
861
1111
|
InitCursorTracker(env, exports);
|
|
@@ -866,4 +1116,4 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
866
1116
|
return exports;
|
|
867
1117
|
}
|
|
868
1118
|
|
|
869
|
-
NODE_API_MODULE(mac_recorder, Init)
|
|
1119
|
+
NODE_API_MODULE(mac_recorder, Init)
|