node-mac-recorder 2.2.1 → 2.4.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/README.md +17 -79
- package/binding.gyp +1 -12
- package/index.js +14 -59
- package/install.js +19 -2
- package/package.json +6 -7
- package/prebuilds/darwin-arm64/node.napi.node +0 -0
- package/src/mac_recorder.mm +70 -388
- package/src/screen_capture.mm +0 -16
- package/test-sck.js +1 -54
- package/window-selector.js +50 -34
- package/prebuilds/darwin-x64/node.napi.node +0 -0
- package/scripts/test-exclude.js +0 -72
- package/src/screen_capture_kit.mm +0 -222
package/src/mac_recorder.mm
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
#import <napi.h>
|
|
2
2
|
#import <ScreenCaptureKit/ScreenCaptureKit.h>
|
|
3
|
-
<<<<<<< HEAD
|
|
4
|
-
=======
|
|
5
3
|
#import <AVFoundation/AVFoundation.h>
|
|
6
4
|
#import <CoreMedia/CoreMedia.h>
|
|
7
|
-
>>>>>>> screencapture
|
|
8
5
|
#import <AppKit/AppKit.h>
|
|
9
6
|
#import <Foundation/Foundation.h>
|
|
10
7
|
#import <CoreGraphics/CoreGraphics.h>
|
|
@@ -21,13 +18,9 @@ Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports);
|
|
|
21
18
|
// Window selector function declarations
|
|
22
19
|
Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports);
|
|
23
20
|
|
|
24
|
-
<<<<<<< HEAD
|
|
25
|
-
@interface MacRecorderDelegate : NSObject
|
|
26
|
-
=======
|
|
27
21
|
// ScreenCaptureKit Recording Delegate
|
|
28
22
|
API_AVAILABLE(macos(12.3))
|
|
29
23
|
@interface SCKRecorderDelegate : NSObject <SCStreamDelegate, SCStreamOutput>
|
|
30
|
-
>>>>>>> screencapture
|
|
31
24
|
@property (nonatomic, copy) void (^completionHandler)(NSURL *outputURL, NSError *error);
|
|
32
25
|
@property (nonatomic, copy) void (^startedHandler)(void);
|
|
33
26
|
@property (nonatomic, strong) AVAssetWriter *assetWriter;
|
|
@@ -41,20 +34,6 @@ API_AVAILABLE(macos(12.3))
|
|
|
41
34
|
@property (nonatomic, assign) BOOL startFailed;
|
|
42
35
|
@end
|
|
43
36
|
|
|
44
|
-
<<<<<<< HEAD
|
|
45
|
-
@implementation MacRecorderDelegate
|
|
46
|
-
- (void)recordingDidStart {
|
|
47
|
-
NSLog(@"[mac_recorder] ScreenCaptureKit recording started");
|
|
48
|
-
}
|
|
49
|
-
- (void)recordingDidFinish:(NSURL *)outputURL error:(NSError *)error {
|
|
50
|
-
if (error) {
|
|
51
|
-
NSLog(@"[mac_recorder] ScreenCaptureKit recording finished with error: %@", error.localizedDescription);
|
|
52
|
-
} else {
|
|
53
|
-
NSLog(@"[mac_recorder] ScreenCaptureKit recording finished OK → %@", outputURL.path);
|
|
54
|
-
}
|
|
55
|
-
if (self.completionHandler) {
|
|
56
|
-
self.completionHandler(outputURL, error);
|
|
57
|
-
=======
|
|
58
37
|
@implementation SCKRecorderDelegate
|
|
59
38
|
|
|
60
39
|
// Standard SCStreamDelegate method - should be called automatically
|
|
@@ -67,7 +46,6 @@ API_AVAILABLE(macos(12.3))
|
|
|
67
46
|
NSLog(@"🛑 Stream stopped with error: %@", error ? error.localizedDescription : @"none");
|
|
68
47
|
if (self.completionHandler) {
|
|
69
48
|
self.completionHandler(self.outputURL, error);
|
|
70
|
-
>>>>>>> screencapture
|
|
71
49
|
}
|
|
72
50
|
}
|
|
73
51
|
|
|
@@ -142,42 +120,75 @@ API_AVAILABLE(macos(12.3))
|
|
|
142
120
|
|
|
143
121
|
@end
|
|
144
122
|
|
|
145
|
-
<<<<<<< HEAD
|
|
146
|
-
// Global state for recording
|
|
147
|
-
static MacRecorderDelegate *g_delegate = nil;
|
|
148
|
-
static bool g_isRecording = false;
|
|
149
|
-
|
|
150
|
-
// Helper function to cleanup recording resources
|
|
151
|
-
void cleanupRecording() {
|
|
152
|
-
g_delegate = nil;
|
|
153
|
-
=======
|
|
154
123
|
// Global state for ScreenCaptureKit recording
|
|
155
124
|
static SCStream *g_scStream = nil;
|
|
156
125
|
static SCKRecorderDelegate *g_scDelegate = nil;
|
|
157
126
|
static bool g_isRecording = false;
|
|
127
|
+
static BOOL g_screenOutputAttached = NO;
|
|
128
|
+
static BOOL g_audioOutputAttached = NO;
|
|
129
|
+
static dispatch_queue_t g_outputQueue = NULL; // use a dedicated serial queue for sample handling
|
|
158
130
|
|
|
159
131
|
// Helper function to cleanup ScreenCaptureKit recording resources
|
|
160
132
|
void cleanupSCKRecording() {
|
|
161
133
|
NSLog(@"🛑 Cleaning up ScreenCaptureKit recording");
|
|
162
|
-
|
|
134
|
+
|
|
135
|
+
// Detach outputs first to prevent further callbacks into the delegate
|
|
136
|
+
if (g_scStream && g_scDelegate) {
|
|
137
|
+
NSError *rmError = nil;
|
|
138
|
+
if (g_screenOutputAttached) {
|
|
139
|
+
[g_scStream removeStreamOutput:g_scDelegate type:SCStreamOutputTypeScreen error:&rmError];
|
|
140
|
+
g_screenOutputAttached = NO;
|
|
141
|
+
}
|
|
142
|
+
if (g_audioOutputAttached) {
|
|
143
|
+
rmError = nil;
|
|
144
|
+
[g_scStream removeStreamOutput:g_scDelegate type:SCStreamOutputTypeAudio error:&rmError];
|
|
145
|
+
g_audioOutputAttached = NO;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
163
149
|
if (g_scStream) {
|
|
164
150
|
NSLog(@"🛑 Stopping SCStream");
|
|
165
|
-
|
|
151
|
+
SCStream *streamToStop = g_scStream; // keep local until stop completes
|
|
152
|
+
[streamToStop stopCaptureWithCompletionHandler:^(NSError * _Nullable error) {
|
|
166
153
|
if (error) {
|
|
167
154
|
NSLog(@"❌ Error stopping SCStream: %@", error.localizedDescription);
|
|
168
155
|
} else {
|
|
169
156
|
NSLog(@"✅ SCStream stopped successfully");
|
|
170
157
|
}
|
|
158
|
+
|
|
159
|
+
// Finish writer after stream has stopped to ensure no further buffers arrive
|
|
160
|
+
if (g_scDelegate && g_scDelegate.assetWriter && g_scDelegate.isWriting) {
|
|
161
|
+
NSLog(@"🛑 Finishing asset writer (status: %ld)", (long)g_scDelegate.assetWriter.status);
|
|
162
|
+
g_scDelegate.isWriting = NO;
|
|
163
|
+
|
|
164
|
+
if (g_scDelegate.assetWriter.status == AVAssetWriterStatusWriting) {
|
|
165
|
+
if (g_scDelegate.videoInput) {
|
|
166
|
+
[g_scDelegate.videoInput markAsFinished];
|
|
167
|
+
}
|
|
168
|
+
if (g_scDelegate.audioInput) {
|
|
169
|
+
[g_scDelegate.audioInput markAsFinished];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
[g_scDelegate.assetWriter finishWritingWithCompletionHandler:^{
|
|
173
|
+
NSLog(@"✅ Asset writer finished. Status: %ld", (long)g_scDelegate.assetWriter.status);
|
|
174
|
+
if (g_scDelegate.assetWriter.error) {
|
|
175
|
+
NSLog(@"❌ Asset writer error: %@", g_scDelegate.assetWriter.error.localizedDescription);
|
|
176
|
+
}
|
|
177
|
+
}];
|
|
178
|
+
} else if (g_scDelegate.assetWriter.status == AVAssetWriterStatusFailed) {
|
|
179
|
+
NSLog(@"❌ Asset writer failed: %@", g_scDelegate.assetWriter.error.localizedDescription);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
g_isRecording = false;
|
|
184
|
+
g_scStream = nil;
|
|
185
|
+
g_scDelegate = nil;
|
|
171
186
|
}];
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (g_scDelegate) {
|
|
176
|
-
if (g_scDelegate.assetWriter && g_scDelegate.isWriting) {
|
|
187
|
+
} else {
|
|
188
|
+
// No stream, just finalize writer if needed
|
|
189
|
+
if (g_scDelegate && g_scDelegate.assetWriter && g_scDelegate.isWriting) {
|
|
177
190
|
NSLog(@"🛑 Finishing asset writer (status: %ld)", (long)g_scDelegate.assetWriter.status);
|
|
178
191
|
g_scDelegate.isWriting = NO;
|
|
179
|
-
|
|
180
|
-
// Only mark inputs as finished if asset writer is actually writing
|
|
181
192
|
if (g_scDelegate.assetWriter.status == AVAssetWriterStatusWriting) {
|
|
182
193
|
if (g_scDelegate.videoInput) {
|
|
183
194
|
[g_scDelegate.videoInput markAsFinished];
|
|
@@ -185,24 +196,12 @@ void cleanupSCKRecording() {
|
|
|
185
196
|
if (g_scDelegate.audioInput) {
|
|
186
197
|
[g_scDelegate.audioInput markAsFinished];
|
|
187
198
|
}
|
|
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
|
-
}
|
|
199
|
+
[g_scDelegate.assetWriter finishWritingWithCompletionHandler:^{}];
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
|
+
g_isRecording = false;
|
|
202
203
|
g_scDelegate = nil;
|
|
203
204
|
}
|
|
204
|
-
>>>>>>> screencapture
|
|
205
|
-
g_isRecording = false;
|
|
206
205
|
}
|
|
207
206
|
|
|
208
207
|
// Check if ScreenCaptureKit is available
|
|
@@ -216,7 +215,7 @@ bool isScreenCaptureKitAvailable() {
|
|
|
216
215
|
// NAPI Function: Start Recording with ScreenCaptureKit
|
|
217
216
|
Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
218
217
|
Napi::Env env = info.Env();
|
|
219
|
-
|
|
218
|
+
@autoreleasepool {
|
|
220
219
|
if (!isScreenCaptureKitAvailable()) {
|
|
221
220
|
NSLog(@"ScreenCaptureKit requires macOS 12.3 or later");
|
|
222
221
|
return Napi::Boolean::New(env, false);
|
|
@@ -246,31 +245,13 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
246
245
|
NSLog(@"✅ Screen recording permission verified");
|
|
247
246
|
|
|
248
247
|
std::string outputPath = info[0].As<Napi::String>().Utf8Value();
|
|
249
|
-
NSLog(@"[mac_recorder] StartRecording: output=%@", [NSString stringWithUTF8String:outputPath.c_str()]);
|
|
250
248
|
|
|
251
|
-
<<<<<<< HEAD
|
|
252
|
-
// Options parsing (shared)
|
|
253
|
-
CGRect captureRect = CGRectNull;
|
|
254
|
-
bool captureCursor = false; // Default olarak cursor gizli
|
|
255
|
-
bool includeMicrophone = false; // Default olarak mikrofon kapalı
|
|
256
|
-
bool includeSystemAudio = true; // Default olarak sistem sesi açık
|
|
257
|
-
CGDirectDisplayID displayID = CGMainDisplayID(); // Default ana ekran
|
|
258
|
-
NSString *audioDeviceId = nil; // Default audio device ID
|
|
259
|
-
NSString *systemAudioDeviceId = nil; // System audio device ID
|
|
260
|
-
bool forceUseSC = false;
|
|
261
|
-
// Exclude options for ScreenCaptureKit (optional, backward compatible)
|
|
262
|
-
NSMutableArray<NSString*> *excludedAppBundleIds = [NSMutableArray array];
|
|
263
|
-
NSMutableArray<NSNumber*> *excludedPIDs = [NSMutableArray array];
|
|
264
|
-
NSMutableArray<NSNumber*> *excludedWindowIds = [NSMutableArray array];
|
|
265
|
-
bool autoExcludeSelf = false;
|
|
266
|
-
=======
|
|
267
249
|
// Default options
|
|
268
250
|
bool captureCursor = false;
|
|
269
251
|
bool includeSystemAudio = true;
|
|
270
252
|
CGDirectDisplayID displayID = 0; // Will be set to first available display
|
|
271
253
|
uint32_t windowID = 0;
|
|
272
254
|
CGRect captureRect = CGRectNull;
|
|
273
|
-
>>>>>>> screencapture
|
|
274
255
|
|
|
275
256
|
// Parse options
|
|
276
257
|
if (info.Length() > 1 && info[1].IsObject()) {
|
|
@@ -285,53 +266,6 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
285
266
|
includeSystemAudio = options.Get("includeSystemAudio").As<Napi::Boolean>();
|
|
286
267
|
}
|
|
287
268
|
|
|
288
|
-
<<<<<<< HEAD
|
|
289
|
-
// System audio device ID
|
|
290
|
-
if (options.Has("systemAudioDeviceId") && !options.Get("systemAudioDeviceId").IsNull()) {
|
|
291
|
-
std::string sysDeviceId = options.Get("systemAudioDeviceId").As<Napi::String>().Utf8Value();
|
|
292
|
-
systemAudioDeviceId = [NSString stringWithUTF8String:sysDeviceId.c_str()];
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// ScreenCaptureKit toggle (optional)
|
|
296
|
-
if (options.Has("useScreenCaptureKit")) {
|
|
297
|
-
forceUseSC = options.Get("useScreenCaptureKit").As<Napi::Boolean>();
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Exclusion lists (optional)
|
|
301
|
-
if (options.Has("excludedAppBundleIds") && options.Get("excludedAppBundleIds").IsArray()) {
|
|
302
|
-
Napi::Array arr = options.Get("excludedAppBundleIds").As<Napi::Array>();
|
|
303
|
-
for (uint32_t i = 0; i < arr.Length(); i++) {
|
|
304
|
-
if (!arr.Get(i).IsUndefined() && !arr.Get(i).IsNull()) {
|
|
305
|
-
std::string s = arr.Get(i).As<Napi::String>().Utf8Value();
|
|
306
|
-
[excludedAppBundleIds addObject:[NSString stringWithUTF8String:s.c_str()]];
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
if (options.Has("excludedPIDs") && options.Get("excludedPIDs").IsArray()) {
|
|
311
|
-
Napi::Array arr = options.Get("excludedPIDs").As<Napi::Array>();
|
|
312
|
-
for (uint32_t i = 0; i < arr.Length(); i++) {
|
|
313
|
-
if (!arr.Get(i).IsUndefined() && !arr.Get(i).IsNull()) {
|
|
314
|
-
double v = arr.Get(i).As<Napi::Number>().DoubleValue();
|
|
315
|
-
[excludedPIDs addObject:@( (pid_t)v )];
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
if (options.Has("excludedWindowIds") && options.Get("excludedWindowIds").IsArray()) {
|
|
320
|
-
Napi::Array arr = options.Get("excludedWindowIds").As<Napi::Array>();
|
|
321
|
-
for (uint32_t i = 0; i < arr.Length(); i++) {
|
|
322
|
-
if (!arr.Get(i).IsUndefined() && !arr.Get(i).IsNull()) {
|
|
323
|
-
double v = arr.Get(i).As<Napi::Number>().DoubleValue();
|
|
324
|
-
[excludedWindowIds addObject:@( (uint32_t)v )];
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
if (options.Has("autoExcludeSelf")) {
|
|
329
|
-
autoExcludeSelf = options.Get("autoExcludeSelf").As<Napi::Boolean>();
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Display ID
|
|
333
|
-
=======
|
|
334
|
-
>>>>>>> screencapture
|
|
335
269
|
if (options.Has("displayId") && !options.Get("displayId").IsNull()) {
|
|
336
270
|
uint32_t tempDisplayID = options.Get("displayId").As<Napi::Number>().Uint32Value();
|
|
337
271
|
if (tempDisplayID != 0) {
|
|
@@ -356,64 +290,6 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
356
290
|
}
|
|
357
291
|
}
|
|
358
292
|
|
|
359
|
-
<<<<<<< HEAD
|
|
360
|
-
@try {
|
|
361
|
-
// Always prefer ScreenCaptureKit if available
|
|
362
|
-
NSLog(@"[mac_recorder] Checking ScreenCaptureKit availability");
|
|
363
|
-
if (@available(macOS 12.3, *)) {
|
|
364
|
-
if ([ScreenCaptureKitRecorder isScreenCaptureKitAvailable]) {
|
|
365
|
-
NSMutableDictionary *scConfig = [@{} mutableCopy];
|
|
366
|
-
scConfig[@"displayId"] = @(displayID);
|
|
367
|
-
if (!CGRectIsNull(captureRect)) {
|
|
368
|
-
scConfig[@"captureArea"] = @{ @"x": @(captureRect.origin.x),
|
|
369
|
-
@"y": @(captureRect.origin.y),
|
|
370
|
-
@"width": @(captureRect.size.width),
|
|
371
|
-
@"height": @(captureRect.size.height) };
|
|
372
|
-
}
|
|
373
|
-
scConfig[@"captureCursor"] = @(captureCursor);
|
|
374
|
-
scConfig[@"includeMicrophone"] = @(includeMicrophone);
|
|
375
|
-
scConfig[@"includeSystemAudio"] = @(includeSystemAudio);
|
|
376
|
-
if (excludedAppBundleIds.count) scConfig[@"excludedAppBundleIds"] = excludedAppBundleIds;
|
|
377
|
-
if (excludedPIDs.count) scConfig[@"excludedPIDs"] = excludedPIDs;
|
|
378
|
-
if (excludedWindowIds.count) scConfig[@"excludedWindowIds"] = excludedWindowIds;
|
|
379
|
-
// Auto exclude current app by PID if requested
|
|
380
|
-
if (autoExcludeSelf) {
|
|
381
|
-
pid_t pid = getpid();
|
|
382
|
-
NSMutableArray *arr = [NSMutableArray arrayWithArray:scConfig[@"excludedPIDs"] ?: @[]];
|
|
383
|
-
[arr addObject:@(pid)];
|
|
384
|
-
scConfig[@"excludedPIDs"] = arr;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// Output path for SC
|
|
388
|
-
std::string outputPathStr = info[0].As<Napi::String>().Utf8Value();
|
|
389
|
-
scConfig[@"outputPath"] = [NSString stringWithUTF8String:outputPathStr.c_str()];
|
|
390
|
-
|
|
391
|
-
NSError *scErr = nil;
|
|
392
|
-
NSLog(@"[mac_recorder] Using ScreenCaptureKit path (displayId=%u)", displayID);
|
|
393
|
-
|
|
394
|
-
// Create and set up delegate
|
|
395
|
-
g_delegate = [[MacRecorderDelegate alloc] init];
|
|
396
|
-
|
|
397
|
-
BOOL ok = [ScreenCaptureKitRecorder startRecordingWithConfiguration:scConfig delegate:g_delegate error:&scErr];
|
|
398
|
-
if (ok) {
|
|
399
|
-
g_isRecording = true;
|
|
400
|
-
NSLog(@"[mac_recorder] ScreenCaptureKit startRecording → OK");
|
|
401
|
-
return Napi::Boolean::New(env, true);
|
|
402
|
-
}
|
|
403
|
-
NSLog(@"[mac_recorder] ScreenCaptureKit startRecording → FAIL: %@", scErr.localizedDescription);
|
|
404
|
-
cleanupRecording();
|
|
405
|
-
return Napi::Boolean::New(env, false);
|
|
406
|
-
}
|
|
407
|
-
} else {
|
|
408
|
-
NSLog(@"[mac_recorder] ScreenCaptureKit not available");
|
|
409
|
-
cleanupRecording();
|
|
410
|
-
return Napi::Boolean::New(env, false);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
} @catch (NSException *exception) {
|
|
414
|
-
cleanupRecording();
|
|
415
|
-
return Napi::Boolean::New(env, false);
|
|
416
|
-
=======
|
|
417
293
|
// Create output URL
|
|
418
294
|
NSURL *outputURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:outputPath.c_str()]];
|
|
419
295
|
NSLog(@"📁 Output URL: %@", outputURL.absoluteString);
|
|
@@ -543,13 +419,8 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
543
419
|
|
|
544
420
|
contentFilter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:excluded];
|
|
545
421
|
NSLog(@"✅ Content filter created for display recording");
|
|
546
|
-
>>>>>>> screencapture
|
|
547
422
|
}
|
|
548
423
|
|
|
549
|
-
<<<<<<< HEAD
|
|
550
|
-
if (!g_isRecording) {
|
|
551
|
-
return Napi::Boolean::New(env, false);
|
|
552
|
-
=======
|
|
553
424
|
// Get actual display dimensions for proper video configuration
|
|
554
425
|
CGRect displayBounds = CGDisplayBounds(displayID);
|
|
555
426
|
NSSize videoSize = NSMakeSize(displayBounds.size.width, displayBounds.size.height);
|
|
@@ -571,27 +442,9 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
571
442
|
NSLog(@"🔊 Audio configuration: capture=%@, excludeProcess=%@", includeSystemAudio ? @"YES" : @"NO", @"YES");
|
|
572
443
|
} else {
|
|
573
444
|
NSLog(@"⚠️ macOS 13.0+ features not available");
|
|
574
|
-
>>>>>>> screencapture
|
|
575
445
|
}
|
|
576
446
|
config.showsCursor = captureCursor;
|
|
577
447
|
|
|
578
|
-
<<<<<<< HEAD
|
|
579
|
-
@try {
|
|
580
|
-
NSLog(@"[mac_recorder] StopRecording called");
|
|
581
|
-
|
|
582
|
-
// Stop ScreenCaptureKit recording
|
|
583
|
-
NSLog(@"[mac_recorder] Stopping ScreenCaptureKit stream");
|
|
584
|
-
if (@available(macOS 12.3, *)) {
|
|
585
|
-
[ScreenCaptureKitRecorder stopRecording];
|
|
586
|
-
}
|
|
587
|
-
g_isRecording = false;
|
|
588
|
-
cleanupRecording();
|
|
589
|
-
NSLog(@"[mac_recorder] ScreenCaptureKit stopped");
|
|
590
|
-
return Napi::Boolean::New(env, true);
|
|
591
|
-
|
|
592
|
-
} @catch (NSException *exception) {
|
|
593
|
-
cleanupRecording();
|
|
594
|
-
=======
|
|
595
448
|
if (!CGRectIsNull(captureRect)) {
|
|
596
449
|
config.sourceRect = captureRect;
|
|
597
450
|
// Update video size if capture rect is specified
|
|
@@ -611,7 +464,6 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
611
464
|
|
|
612
465
|
if (writerError) {
|
|
613
466
|
NSLog(@"❌ Failed to create asset writer: %@", writerError.localizedDescription);
|
|
614
|
-
>>>>>>> screencapture
|
|
615
467
|
return Napi::Boolean::New(env, false);
|
|
616
468
|
}
|
|
617
469
|
|
|
@@ -635,117 +487,6 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
635
487
|
NSLog(@"❌ Cannot add video input to asset writer");
|
|
636
488
|
}
|
|
637
489
|
|
|
638
|
-
<<<<<<< HEAD
|
|
639
|
-
@try {
|
|
640
|
-
NSMutableArray *devices = [NSMutableArray array];
|
|
641
|
-
|
|
642
|
-
// Use CoreAudio to get audio devices since we're removing AVFoundation
|
|
643
|
-
AudioObjectPropertyAddress propertyAddress = {
|
|
644
|
-
kAudioHardwarePropertyDevices,
|
|
645
|
-
kAudioObjectPropertyScopeGlobal,
|
|
646
|
-
kAudioObjectPropertyElementMain
|
|
647
|
-
};
|
|
648
|
-
|
|
649
|
-
UInt32 dataSize = 0;
|
|
650
|
-
OSStatus status = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize);
|
|
651
|
-
if (status != noErr) {
|
|
652
|
-
return Napi::Array::New(env, 0);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
UInt32 deviceCount = dataSize / sizeof(AudioDeviceID);
|
|
656
|
-
AudioDeviceID *audioDevices = (AudioDeviceID *)malloc(dataSize);
|
|
657
|
-
|
|
658
|
-
status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize, audioDevices);
|
|
659
|
-
if (status != noErr) {
|
|
660
|
-
free(audioDevices);
|
|
661
|
-
return Napi::Array::New(env, 0);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
for (UInt32 i = 0; i < deviceCount; i++) {
|
|
665
|
-
AudioDeviceID deviceID = audioDevices[i];
|
|
666
|
-
|
|
667
|
-
// Check if device has input streams
|
|
668
|
-
AudioObjectPropertyAddress streamsAddress = {
|
|
669
|
-
kAudioDevicePropertyStreams,
|
|
670
|
-
kAudioDevicePropertyScopeInput,
|
|
671
|
-
kAudioObjectPropertyElementMain
|
|
672
|
-
};
|
|
673
|
-
|
|
674
|
-
UInt32 streamsSize = 0;
|
|
675
|
-
status = AudioObjectGetPropertyDataSize(deviceID, &streamsAddress, 0, NULL, &streamsSize);
|
|
676
|
-
if (status != noErr || streamsSize == 0) {
|
|
677
|
-
continue; // Skip output-only devices
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// Get device name
|
|
681
|
-
AudioObjectPropertyAddress nameAddress = {
|
|
682
|
-
kAudioDevicePropertyDeviceNameCFString,
|
|
683
|
-
kAudioObjectPropertyScopeGlobal,
|
|
684
|
-
kAudioObjectPropertyElementMain
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
CFStringRef deviceNameRef = NULL;
|
|
688
|
-
UInt32 nameSize = sizeof(CFStringRef);
|
|
689
|
-
status = AudioObjectGetPropertyData(deviceID, &nameAddress, 0, NULL, &nameSize, &deviceNameRef);
|
|
690
|
-
|
|
691
|
-
NSString *deviceName = @"Unknown Device";
|
|
692
|
-
if (status == noErr && deviceNameRef) {
|
|
693
|
-
deviceName = (__bridge NSString *)deviceNameRef;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// Get device UID
|
|
697
|
-
AudioObjectPropertyAddress uidAddress = {
|
|
698
|
-
kAudioDevicePropertyDeviceUID,
|
|
699
|
-
kAudioObjectPropertyScopeGlobal,
|
|
700
|
-
kAudioObjectPropertyElementMain
|
|
701
|
-
};
|
|
702
|
-
|
|
703
|
-
CFStringRef deviceUIDRef = NULL;
|
|
704
|
-
UInt32 uidSize = sizeof(CFStringRef);
|
|
705
|
-
status = AudioObjectGetPropertyData(deviceID, &uidAddress, 0, NULL, &uidSize, &deviceUIDRef);
|
|
706
|
-
|
|
707
|
-
NSString *deviceUID = [NSString stringWithFormat:@"%u", deviceID];
|
|
708
|
-
if (status == noErr && deviceUIDRef) {
|
|
709
|
-
deviceUID = (__bridge NSString *)deviceUIDRef;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// Check if this is the default input device
|
|
713
|
-
AudioObjectPropertyAddress defaultAddress = {
|
|
714
|
-
kAudioHardwarePropertyDefaultInputDevice,
|
|
715
|
-
kAudioObjectPropertyScopeGlobal,
|
|
716
|
-
kAudioObjectPropertyElementMain
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
AudioDeviceID defaultDeviceID = kAudioDeviceUnknown;
|
|
720
|
-
UInt32 defaultSize = sizeof(AudioDeviceID);
|
|
721
|
-
AudioObjectGetPropertyData(kAudioObjectSystemObject, &defaultAddress, 0, NULL, &defaultSize, &defaultDeviceID);
|
|
722
|
-
|
|
723
|
-
BOOL isDefault = (deviceID == defaultDeviceID);
|
|
724
|
-
|
|
725
|
-
[devices addObject:@{
|
|
726
|
-
@"id": deviceUID,
|
|
727
|
-
@"name": deviceName,
|
|
728
|
-
@"manufacturer": @"Unknown",
|
|
729
|
-
@"isDefault": @(isDefault)
|
|
730
|
-
}];
|
|
731
|
-
|
|
732
|
-
if (deviceNameRef) CFRelease(deviceNameRef);
|
|
733
|
-
if (deviceUIDRef) CFRelease(deviceUIDRef);
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
free(audioDevices);
|
|
737
|
-
|
|
738
|
-
// Convert to NAPI array
|
|
739
|
-
Napi::Array result = Napi::Array::New(env, devices.count);
|
|
740
|
-
for (NSUInteger i = 0; i < devices.count; i++) {
|
|
741
|
-
NSDictionary *device = devices[i];
|
|
742
|
-
Napi::Object deviceObj = Napi::Object::New(env);
|
|
743
|
-
deviceObj.Set("id", Napi::String::New(env, [device[@"id"] UTF8String]));
|
|
744
|
-
deviceObj.Set("name", Napi::String::New(env, [device[@"name"] UTF8String]));
|
|
745
|
-
deviceObj.Set("manufacturer", Napi::String::New(env, [device[@"manufacturer"] UTF8String]));
|
|
746
|
-
deviceObj.Set("isDefault", Napi::Boolean::New(env, [device[@"isDefault"] boolValue]));
|
|
747
|
-
result[i] = deviceObj;
|
|
748
|
-
=======
|
|
749
490
|
// Audio input settings (if needed)
|
|
750
491
|
if (includeSystemAudio) {
|
|
751
492
|
NSDictionary *audioSettings = @{
|
|
@@ -762,19 +503,22 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
762
503
|
}
|
|
763
504
|
}
|
|
764
505
|
|
|
765
|
-
// Create
|
|
766
|
-
|
|
767
|
-
|
|
506
|
+
// Create a dedicated serial queue for output callbacks
|
|
507
|
+
if (g_outputQueue == NULL) {
|
|
508
|
+
g_outputQueue = dispatch_queue_create("com.node-mac-recorder.stream-output", DISPATCH_QUEUE_SERIAL);
|
|
509
|
+
}
|
|
510
|
+
|
|
768
511
|
// Create and start stream first
|
|
769
512
|
g_scStream = [[SCStream alloc] initWithFilter:contentFilter configuration:config delegate:g_scDelegate];
|
|
770
513
|
|
|
771
514
|
// Attach outputs to actually receive sample buffers
|
|
772
515
|
NSLog(@"✅ Setting up stream output callback for sample buffers");
|
|
773
|
-
dispatch_queue_t outputQueue =
|
|
516
|
+
dispatch_queue_t outputQueue = g_outputQueue;
|
|
774
517
|
NSError *outputError = nil;
|
|
775
518
|
BOOL addedScreenOutput = [g_scStream addStreamOutput:g_scDelegate type:SCStreamOutputTypeScreen sampleHandlerQueue:outputQueue error:&outputError];
|
|
776
519
|
if (addedScreenOutput) {
|
|
777
520
|
NSLog(@"✅ Screen output attached to SCStream");
|
|
521
|
+
g_screenOutputAttached = YES;
|
|
778
522
|
} else {
|
|
779
523
|
NSLog(@"❌ Failed to attach screen output to SCStream: %@", outputError.localizedDescription);
|
|
780
524
|
}
|
|
@@ -783,9 +527,9 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
783
527
|
BOOL addedAudioOutput = [g_scStream addStreamOutput:g_scDelegate type:SCStreamOutputTypeAudio sampleHandlerQueue:outputQueue error:&outputError];
|
|
784
528
|
if (addedAudioOutput) {
|
|
785
529
|
NSLog(@"✅ Audio output attached to SCStream");
|
|
530
|
+
g_audioOutputAttached = YES;
|
|
786
531
|
} else {
|
|
787
532
|
NSLog(@"⚠️ Failed to attach audio output to SCStream (audio may be disabled): %@", outputError.localizedDescription);
|
|
788
|
-
>>>>>>> screencapture
|
|
789
533
|
}
|
|
790
534
|
}
|
|
791
535
|
|
|
@@ -837,6 +581,7 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
837
581
|
|
|
838
582
|
NSLog(@"🎬 Recording initialized successfully");
|
|
839
583
|
return Napi::Boolean::New(env, true);
|
|
584
|
+
}
|
|
840
585
|
}
|
|
841
586
|
|
|
842
587
|
// NAPI Function: Stop Recording
|
|
@@ -851,6 +596,12 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
|
|
|
851
596
|
return Napi::Boolean::New(env, true);
|
|
852
597
|
}
|
|
853
598
|
|
|
599
|
+
// NAPI Function: Get Recording Status (for JS compatibility)
|
|
600
|
+
Napi::Value GetRecordingStatus(const Napi::CallbackInfo& info) {
|
|
601
|
+
Napi::Env env = info.Env();
|
|
602
|
+
return Napi::Boolean::New(env, g_isRecording);
|
|
603
|
+
}
|
|
604
|
+
|
|
854
605
|
// NAPI Function: Get Recording Status
|
|
855
606
|
Napi::Value IsRecording(const Napi::CallbackInfo& info) {
|
|
856
607
|
Napi::Env env = info.Env();
|
|
@@ -950,19 +701,6 @@ Napi::Value GetWindows(const Napi::CallbackInfo& info) {
|
|
|
950
701
|
Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
|
|
951
702
|
Napi::Env env = info.Env();
|
|
952
703
|
|
|
953
|
-
<<<<<<< HEAD
|
|
954
|
-
@try {
|
|
955
|
-
// Check screen recording permission using ScreenCaptureKit
|
|
956
|
-
bool hasScreenPermission = true;
|
|
957
|
-
|
|
958
|
-
if (@available(macOS 12.3, *)) {
|
|
959
|
-
// Try to get shareable content to test ScreenCaptureKit permissions
|
|
960
|
-
@try {
|
|
961
|
-
SCShareableContent *content = [SCShareableContent currentShareableContent];
|
|
962
|
-
hasScreenPermission = (content != nil && content.displays.count > 0);
|
|
963
|
-
} @catch (NSException *exception) {
|
|
964
|
-
hasScreenPermission = false;
|
|
965
|
-
=======
|
|
966
704
|
// Check screen recording permission
|
|
967
705
|
bool hasPermission = CGPreflightScreenCaptureAccess();
|
|
968
706
|
|
|
@@ -1032,41 +770,8 @@ Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
|
|
|
1032
770
|
|
|
1033
771
|
devices.Set(index++, deviceObj);
|
|
1034
772
|
CFRelease(deviceName);
|
|
1035
|
-
>>>>>>> screencapture
|
|
1036
|
-
}
|
|
1037
|
-
} else {
|
|
1038
|
-
// Fallback for older macOS versions
|
|
1039
|
-
if (@available(macOS 10.15, *)) {
|
|
1040
|
-
// Try to create a display stream to test permissions
|
|
1041
|
-
CGDisplayStreamRef stream = CGDisplayStreamCreate(
|
|
1042
|
-
CGMainDisplayID(),
|
|
1043
|
-
1, 1,
|
|
1044
|
-
kCVPixelFormatType_32BGRA,
|
|
1045
|
-
nil,
|
|
1046
|
-
^(CGDisplayStreamFrameStatus status, uint64_t displayTime, IOSurfaceRef frameSurface, CGDisplayStreamUpdateRef updateRef) {
|
|
1047
|
-
// Empty handler
|
|
1048
|
-
}
|
|
1049
|
-
);
|
|
1050
|
-
|
|
1051
|
-
if (stream) {
|
|
1052
|
-
CFRelease(stream);
|
|
1053
|
-
hasScreenPermission = true;
|
|
1054
|
-
} else {
|
|
1055
|
-
hasScreenPermission = false;
|
|
1056
|
-
}
|
|
1057
773
|
}
|
|
1058
774
|
}
|
|
1059
|
-
<<<<<<< HEAD
|
|
1060
|
-
|
|
1061
|
-
// For audio permission, we'll use a simpler check since we're using CoreAudio
|
|
1062
|
-
bool hasAudioPermission = true;
|
|
1063
|
-
|
|
1064
|
-
return Napi::Boolean::New(env, hasScreenPermission && hasAudioPermission);
|
|
1065
|
-
|
|
1066
|
-
} @catch (NSException *exception) {
|
|
1067
|
-
return Napi::Boolean::New(env, false);
|
|
1068
|
-
=======
|
|
1069
|
-
>>>>>>> screencapture
|
|
1070
775
|
}
|
|
1071
776
|
|
|
1072
777
|
free(audioDevices);
|
|
@@ -1075,37 +780,14 @@ Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
|
|
|
1075
780
|
|
|
1076
781
|
// Initialize the addon
|
|
1077
782
|
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
1078
|
-
<<<<<<< HEAD
|
|
1079
|
-
exports.Set(Napi::String::New(env, "startRecording"), Napi::Function::New(env, StartRecording));
|
|
1080
|
-
exports.Set(Napi::String::New(env, "stopRecording"), Napi::Function::New(env, StopRecording));
|
|
1081
|
-
|
|
1082
|
-
exports.Set(Napi::String::New(env, "getAudioDevices"), Napi::Function::New(env, GetAudioDevices));
|
|
1083
|
-
exports.Set(Napi::String::New(env, "getDisplays"), Napi::Function::New(env, GetDisplays));
|
|
1084
|
-
exports.Set(Napi::String::New(env, "getWindows"), Napi::Function::New(env, GetWindows));
|
|
1085
|
-
exports.Set(Napi::String::New(env, "getRecordingStatus"), Napi::Function::New(env, GetRecordingStatus));
|
|
1086
|
-
exports.Set(Napi::String::New(env, "checkPermissions"), Napi::Function::New(env, CheckPermissions));
|
|
1087
|
-
// ScreenCaptureKit availability (optional for clients)
|
|
1088
|
-
exports.Set(Napi::String::New(env, "isScreenCaptureKitAvailable"), Napi::Function::New(env, [](const Napi::CallbackInfo& info){
|
|
1089
|
-
Napi::Env env = info.Env();
|
|
1090
|
-
if (@available(macOS 12.3, *)) {
|
|
1091
|
-
bool available = [ScreenCaptureKitRecorder isScreenCaptureKitAvailable];
|
|
1092
|
-
return Napi::Boolean::New(env, available);
|
|
1093
|
-
}
|
|
1094
|
-
return Napi::Boolean::New(env, false);
|
|
1095
|
-
}));
|
|
1096
|
-
|
|
1097
|
-
// Thumbnail functions
|
|
1098
|
-
exports.Set(Napi::String::New(env, "getWindowThumbnail"), Napi::Function::New(env, GetWindowThumbnail));
|
|
1099
|
-
exports.Set(Napi::String::New(env, "getDisplayThumbnail"), Napi::Function::New(env, GetDisplayThumbnail));
|
|
1100
|
-
=======
|
|
1101
783
|
exports.Set("startRecording", Napi::Function::New(env, StartRecording));
|
|
1102
784
|
exports.Set("stopRecording", Napi::Function::New(env, StopRecording));
|
|
1103
785
|
exports.Set("isRecording", Napi::Function::New(env, IsRecording));
|
|
786
|
+
exports.Set("getRecordingStatus", Napi::Function::New(env, GetRecordingStatus));
|
|
1104
787
|
exports.Set("getDisplays", Napi::Function::New(env, GetDisplays));
|
|
1105
788
|
exports.Set("getWindows", Napi::Function::New(env, GetWindows));
|
|
1106
789
|
exports.Set("checkPermissions", Napi::Function::New(env, CheckPermissions));
|
|
1107
790
|
exports.Set("getAudioDevices", Napi::Function::New(env, GetAudioDevices));
|
|
1108
|
-
>>>>>>> screencapture
|
|
1109
791
|
|
|
1110
792
|
// Initialize cursor tracker
|
|
1111
793
|
InitCursorTracker(env, exports);
|