node-mac-recorder 2.1.2 → 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 +7 -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 +23 -6
- package/index.js +14 -0
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/node.napi.node +0 -0
- package/prebuilds/darwin-x64/node.napi.node +0 -0
- package/scripts/test-exclude.js +72 -0
- package/src/audio_capture.mm +96 -40
- package/src/cursor_tracker.mm +3 -4
- package/src/mac_recorder.mm +807 -609
- package/src/screen_capture.h +5 -0
- package/src/screen_capture.mm +150 -55
- package/src/screen_capture_kit.mm +41 -66
- 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 +109 -0
- package/test-sync.js +52 -0
- package/test-windows.js +57 -0
package/src/mac_recorder.mm
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#import <napi.h>
|
|
2
|
+
#import <ScreenCaptureKit/ScreenCaptureKit.h>
|
|
3
|
+
<<<<<<< HEAD
|
|
4
|
+
=======
|
|
2
5
|
#import <AVFoundation/AVFoundation.h>
|
|
3
6
|
#import <CoreMedia/CoreMedia.h>
|
|
7
|
+
>>>>>>> screencapture
|
|
4
8
|
#import <AppKit/AppKit.h>
|
|
5
9
|
#import <Foundation/Foundation.h>
|
|
6
10
|
#import <CoreGraphics/CoreGraphics.h>
|
|
@@ -17,57 +21,234 @@ Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports);
|
|
|
17
21
|
// Window selector function declarations
|
|
18
22
|
Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports);
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
<<<<<<< HEAD
|
|
25
|
+
@interface MacRecorderDelegate : NSObject
|
|
26
|
+
=======
|
|
27
|
+
// ScreenCaptureKit Recording Delegate
|
|
28
|
+
API_AVAILABLE(macos(12.3))
|
|
29
|
+
@interface SCKRecorderDelegate : NSObject <SCStreamDelegate, SCStreamOutput>
|
|
30
|
+
>>>>>>> screencapture
|
|
21
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;
|
|
22
42
|
@end
|
|
23
43
|
|
|
44
|
+
<<<<<<< HEAD
|
|
24
45
|
@implementation MacRecorderDelegate
|
|
25
|
-
- (void)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
@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");
|
|
29
68
|
if (self.completionHandler) {
|
|
30
|
-
self.completionHandler(
|
|
69
|
+
self.completionHandler(self.outputURL, error);
|
|
70
|
+
>>>>>>> screencapture
|
|
71
|
+
}
|
|
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
|
+
}
|
|
31
140
|
}
|
|
32
141
|
}
|
|
142
|
+
|
|
33
143
|
@end
|
|
34
144
|
|
|
145
|
+
<<<<<<< HEAD
|
|
35
146
|
// Global state for recording
|
|
36
|
-
static AVCaptureSession *g_captureSession = nil;
|
|
37
|
-
static AVCaptureMovieFileOutput *g_movieFileOutput = nil;
|
|
38
|
-
static AVCaptureScreenInput *g_screenInput = nil;
|
|
39
|
-
static AVCaptureDeviceInput *g_audioInput = nil;
|
|
40
147
|
static MacRecorderDelegate *g_delegate = nil;
|
|
41
148
|
static bool g_isRecording = false;
|
|
42
149
|
|
|
43
150
|
// Helper function to cleanup recording resources
|
|
44
151
|
void cleanupRecording() {
|
|
45
|
-
if (g_captureSession) {
|
|
46
|
-
[g_captureSession stopRunning];
|
|
47
|
-
g_captureSession = nil;
|
|
48
|
-
}
|
|
49
|
-
g_movieFileOutput = nil;
|
|
50
|
-
g_screenInput = nil;
|
|
51
|
-
g_audioInput = nil;
|
|
52
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
|
|
53
205
|
g_isRecording = false;
|
|
54
206
|
}
|
|
55
207
|
|
|
56
|
-
//
|
|
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
|
|
57
217
|
Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
58
218
|
Napi::Env env = info.Env();
|
|
59
219
|
|
|
220
|
+
if (!isScreenCaptureKitAvailable()) {
|
|
221
|
+
NSLog(@"ScreenCaptureKit requires macOS 12.3 or later");
|
|
222
|
+
return Napi::Boolean::New(env, false);
|
|
223
|
+
}
|
|
224
|
+
|
|
60
225
|
if (info.Length() < 1) {
|
|
61
|
-
|
|
62
|
-
return env
|
|
226
|
+
NSLog(@"Output path required");
|
|
227
|
+
return Napi::Boolean::New(env, false);
|
|
63
228
|
}
|
|
64
229
|
|
|
65
230
|
if (g_isRecording) {
|
|
231
|
+
NSLog(@"⚠️ Already recording");
|
|
66
232
|
return Napi::Boolean::New(env, false);
|
|
67
233
|
}
|
|
68
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
|
+
|
|
69
248
|
std::string outputPath = info[0].As<Napi::String>().Utf8Value();
|
|
249
|
+
NSLog(@"[mac_recorder] StartRecording: output=%@", [NSString stringWithUTF8String:outputPath.c_str()]);
|
|
70
250
|
|
|
251
|
+
<<<<<<< HEAD
|
|
71
252
|
// Options parsing (shared)
|
|
72
253
|
CGRect captureRect = CGRectNull;
|
|
73
254
|
bool captureCursor = false; // Default olarak cursor gizli
|
|
@@ -82,44 +263,29 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
82
263
|
NSMutableArray<NSNumber*> *excludedPIDs = [NSMutableArray array];
|
|
83
264
|
NSMutableArray<NSNumber*> *excludedWindowIds = [NSMutableArray array];
|
|
84
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
|
|
85
274
|
|
|
275
|
+
// Parse options
|
|
86
276
|
if (info.Length() > 1 && info[1].IsObject()) {
|
|
87
277
|
Napi::Object options = info[1].As<Napi::Object>();
|
|
88
278
|
|
|
89
|
-
// Capture area
|
|
90
|
-
if (options.Has("captureArea") && options.Get("captureArea").IsObject()) {
|
|
91
|
-
Napi::Object rectObj = options.Get("captureArea").As<Napi::Object>();
|
|
92
|
-
if (rectObj.Has("x") && rectObj.Has("y") && rectObj.Has("width") && rectObj.Has("height")) {
|
|
93
|
-
captureRect = CGRectMake(
|
|
94
|
-
rectObj.Get("x").As<Napi::Number>().DoubleValue(),
|
|
95
|
-
rectObj.Get("y").As<Napi::Number>().DoubleValue(),
|
|
96
|
-
rectObj.Get("width").As<Napi::Number>().DoubleValue(),
|
|
97
|
-
rectObj.Get("height").As<Napi::Number>().DoubleValue()
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Capture cursor
|
|
103
279
|
if (options.Has("captureCursor")) {
|
|
104
280
|
captureCursor = options.Get("captureCursor").As<Napi::Boolean>();
|
|
105
281
|
}
|
|
106
282
|
|
|
107
|
-
// Microphone
|
|
108
|
-
if (options.Has("includeMicrophone")) {
|
|
109
|
-
includeMicrophone = options.Get("includeMicrophone").As<Napi::Boolean>();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Audio device ID
|
|
113
|
-
if (options.Has("audioDeviceId") && !options.Get("audioDeviceId").IsNull()) {
|
|
114
|
-
std::string deviceId = options.Get("audioDeviceId").As<Napi::String>().Utf8Value();
|
|
115
|
-
audioDeviceId = [NSString stringWithUTF8String:deviceId.c_str()];
|
|
116
|
-
}
|
|
117
283
|
|
|
118
|
-
// System audio
|
|
119
284
|
if (options.Has("includeSystemAudio")) {
|
|
120
285
|
includeSystemAudio = options.Get("includeSystemAudio").As<Napi::Boolean>();
|
|
121
286
|
}
|
|
122
287
|
|
|
288
|
+
<<<<<<< HEAD
|
|
123
289
|
// System audio device ID
|
|
124
290
|
if (options.Has("systemAudioDeviceId") && !options.Get("systemAudioDeviceId").IsNull()) {
|
|
125
291
|
std::string sysDeviceId = options.Get("systemAudioDeviceId").As<Napi::String>().Utf8Value();
|
|
@@ -164,48 +330,38 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
164
330
|
}
|
|
165
331
|
|
|
166
332
|
// Display ID
|
|
333
|
+
=======
|
|
334
|
+
>>>>>>> screencapture
|
|
167
335
|
if (options.Has("displayId") && !options.Get("displayId").IsNull()) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
// The JavaScript layer passes the actual CGDirectDisplayID
|
|
172
|
-
displayID = (CGDirectDisplayID)displayIdNum;
|
|
173
|
-
|
|
174
|
-
// Verify that this display ID is valid
|
|
175
|
-
uint32_t displayCount;
|
|
176
|
-
CGGetActiveDisplayList(0, NULL, &displayCount);
|
|
177
|
-
if (displayCount > 0) {
|
|
178
|
-
CGDirectDisplayID *displays = (CGDirectDisplayID*)malloc(displayCount * sizeof(CGDirectDisplayID));
|
|
179
|
-
CGGetActiveDisplayList(displayCount, displays, &displayCount);
|
|
180
|
-
|
|
181
|
-
bool validDisplay = false;
|
|
182
|
-
for (uint32_t i = 0; i < displayCount; i++) {
|
|
183
|
-
if (displays[i] == displayID) {
|
|
184
|
-
validDisplay = true;
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (!validDisplay) {
|
|
190
|
-
// Fallback to main display if invalid ID provided
|
|
191
|
-
displayID = CGMainDisplayID();
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
free(displays);
|
|
336
|
+
uint32_t tempDisplayID = options.Get("displayId").As<Napi::Number>().Uint32Value();
|
|
337
|
+
if (tempDisplayID != 0) {
|
|
338
|
+
displayID = tempDisplayID;
|
|
195
339
|
}
|
|
196
340
|
}
|
|
197
341
|
|
|
198
|
-
// Window ID için gelecekte kullanım (şimdilik captureArea ile hallediliyor)
|
|
199
342
|
if (options.Has("windowId") && !options.Get("windowId").IsNull()) {
|
|
200
|
-
|
|
201
|
-
|
|
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
|
+
}
|
|
202
356
|
}
|
|
203
357
|
}
|
|
204
358
|
|
|
359
|
+
<<<<<<< HEAD
|
|
205
360
|
@try {
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
if (
|
|
361
|
+
// Always prefer ScreenCaptureKit if available
|
|
362
|
+
NSLog(@"[mac_recorder] Checking ScreenCaptureKit availability");
|
|
363
|
+
if (@available(macOS 12.3, *)) {
|
|
364
|
+
if ([ScreenCaptureKitRecorder isScreenCaptureKitAvailable]) {
|
|
209
365
|
NSMutableDictionary *scConfig = [@{} mutableCopy];
|
|
210
366
|
scConfig[@"displayId"] = @(displayID);
|
|
211
367
|
if (!CGRectIsNull(captureRect)) {
|
|
@@ -233,321 +389,352 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
233
389
|
scConfig[@"outputPath"] = [NSString stringWithUTF8String:outputPathStr.c_str()];
|
|
234
390
|
|
|
235
391
|
NSError *scErr = nil;
|
|
236
|
-
|
|
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];
|
|
237
398
|
if (ok) {
|
|
238
399
|
g_isRecording = true;
|
|
400
|
+
NSLog(@"[mac_recorder] ScreenCaptureKit startRecording → OK");
|
|
239
401
|
return Napi::Boolean::New(env, true);
|
|
240
402
|
}
|
|
241
|
-
|
|
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);
|
|
242
411
|
}
|
|
243
|
-
// Create capture session
|
|
244
|
-
g_captureSession = [[AVCaptureSession alloc] init];
|
|
245
|
-
[g_captureSession beginConfiguration];
|
|
246
|
-
|
|
247
|
-
// Set session preset
|
|
248
|
-
g_captureSession.sessionPreset = AVCaptureSessionPresetHigh;
|
|
249
412
|
|
|
250
|
-
|
|
251
|
-
|
|
413
|
+
} @catch (NSException *exception) {
|
|
414
|
+
cleanupRecording();
|
|
415
|
+
return Napi::Boolean::New(env, false);
|
|
416
|
+
=======
|
|
417
|
+
// Create output URL
|
|
418
|
+
NSURL *outputURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:outputPath.c_str()]];
|
|
419
|
+
NSLog(@"📁 Output URL: %@", outputURL.absoluteString);
|
|
420
|
+
|
|
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
|
+
}
|
|
252
462
|
|
|
253
|
-
if (!
|
|
254
|
-
|
|
463
|
+
if (!targetWindow) {
|
|
464
|
+
NSLog(@"Window not found with ID: %u", windowID);
|
|
465
|
+
return Napi::Boolean::New(env, false);
|
|
255
466
|
}
|
|
256
467
|
|
|
257
|
-
|
|
258
|
-
|
|
468
|
+
contentFilter = [[SCContentFilter alloc] initWithDesktopIndependentWindow:targetWindow];
|
|
469
|
+
} else {
|
|
470
|
+
// Display recording
|
|
471
|
+
NSLog(@"🔍 Selecting display among %lu available displays", (unsigned long)shareableContent.displays.count);
|
|
259
472
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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);
|
|
265
478
|
}
|
|
266
479
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
|
|
274
|
-
NSLog(@"[DEBUG] Looking for audio device with ID: %@", audioDeviceId);
|
|
275
|
-
NSLog(@"[DEBUG] Available audio devices:");
|
|
276
|
-
for (AVCaptureDevice *device in devices) {
|
|
277
|
-
NSLog(@"[DEBUG] - Device: %@ (ID: %@)", device.localizedName, device.uniqueID);
|
|
278
|
-
if ([device.uniqueID isEqualToString:audioDeviceId]) {
|
|
279
|
-
NSLog(@"[DEBUG] Found matching device: %@", device.localizedName);
|
|
280
|
-
audioDevice = device;
|
|
281
|
-
break;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (!audioDevice) {
|
|
286
|
-
NSLog(@"[DEBUG] Specified audio device not found, falling back to default");
|
|
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;
|
|
287
486
|
}
|
|
288
487
|
}
|
|
289
488
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
|
|
293
|
-
NSLog(@"[DEBUG] Using default audio device: %@ (ID: %@)", audioDevice.localizedName, audioDevice.uniqueID);
|
|
489
|
+
if (!targetDisplay) {
|
|
490
|
+
NSLog(@"❌ Display not found with ID: %u", displayID);
|
|
294
491
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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);
|
|
305
502
|
}
|
|
503
|
+
} else {
|
|
504
|
+
NSLog(@"✅ Using specified display: ID=%u, %dx%d", targetDisplay.displayID, (int)targetDisplay.width, (int)targetDisplay.height);
|
|
306
505
|
}
|
|
307
506
|
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
break;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// If no specific device found or specified, look for known system audio devices
|
|
336
|
-
if (!systemAudioDevice) {
|
|
337
|
-
for (AVCaptureDevice *device in audioDevices) {
|
|
338
|
-
NSString *deviceName = [device.localizedName lowercaseString];
|
|
339
|
-
// Check for common system audio capture devices
|
|
340
|
-
if ([deviceName containsString:@"soundflower"] ||
|
|
341
|
-
[deviceName containsString:@"blackhole"] ||
|
|
342
|
-
[deviceName containsString:@"loopback"] ||
|
|
343
|
-
[deviceName containsString:@"system audio"] ||
|
|
344
|
-
[deviceName containsString:@"aggregate"]) {
|
|
345
|
-
systemAudioDevice = device;
|
|
346
|
-
NSLog(@"[DEBUG] Auto-detected system audio device: %@", device.localizedName);
|
|
347
|
-
break;
|
|
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
|
+
}
|
|
348
529
|
}
|
|
349
530
|
}
|
|
350
531
|
}
|
|
351
|
-
|
|
352
|
-
// If we found a system audio device, add it as an additional input
|
|
353
|
-
if (systemAudioDevice && !includeMicrophone) {
|
|
354
|
-
// Only add system audio device if microphone is not already added
|
|
355
|
-
NSError *error;
|
|
356
|
-
AVCaptureDeviceInput *systemAudioInput = [[AVCaptureDeviceInput alloc] initWithDevice:systemAudioDevice error:&error];
|
|
357
|
-
if (systemAudioInput && [g_captureSession canAddInput:systemAudioInput]) {
|
|
358
|
-
[g_captureSession addInput:systemAudioInput];
|
|
359
|
-
NSLog(@"[DEBUG] Successfully added system audio device: %@", systemAudioDevice.localizedName);
|
|
360
|
-
} else if (error) {
|
|
361
|
-
NSLog(@"[DEBUG] Failed to add system audio device: %@", error.localizedDescription);
|
|
362
|
-
}
|
|
363
|
-
} else if (includeSystemAudio && !systemAudioDevice) {
|
|
364
|
-
NSLog(@"[DEBUG] System audio requested but no suitable device found. Available devices:");
|
|
365
|
-
for (AVCaptureDevice *device in audioDevices) {
|
|
366
|
-
NSLog(@"[DEBUG] - %@ (ID: %@)", device.localizedName, device.uniqueID);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
532
|
}
|
|
370
|
-
} else {
|
|
371
|
-
// Explicitly disable audio capture if not requested
|
|
372
|
-
g_screenInput.capturesMouseClicks = NO;
|
|
373
533
|
}
|
|
374
534
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
+
}
|
|
382
542
|
}
|
|
383
543
|
|
|
384
|
-
[
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
[g_captureSession startRunning];
|
|
388
|
-
|
|
389
|
-
// Create delegate
|
|
390
|
-
g_delegate = [[MacRecorderDelegate alloc] init];
|
|
391
|
-
|
|
392
|
-
// Start recording
|
|
393
|
-
NSURL *outputURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:outputPath.c_str()]];
|
|
394
|
-
[g_movieFileOutput startRecordingToOutputFileURL:outputURL recordingDelegate:g_delegate];
|
|
395
|
-
|
|
396
|
-
g_isRecording = true;
|
|
397
|
-
return Napi::Boolean::New(env, true);
|
|
398
|
-
|
|
399
|
-
} @catch (NSException *exception) {
|
|
400
|
-
cleanupRecording();
|
|
401
|
-
return Napi::Boolean::New(env, false);
|
|
544
|
+
contentFilter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:excluded];
|
|
545
|
+
NSLog(@"✅ Content filter created for display recording");
|
|
546
|
+
>>>>>>> screencapture
|
|
402
547
|
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// NAPI Function: Stop Recording
|
|
406
|
-
Napi::Value StopRecording(const Napi::CallbackInfo& info) {
|
|
407
|
-
Napi::Env env = info.Env();
|
|
408
548
|
|
|
549
|
+
<<<<<<< HEAD
|
|
409
550
|
if (!g_isRecording) {
|
|
410
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
|
|
411
575
|
}
|
|
576
|
+
config.showsCursor = captureCursor;
|
|
412
577
|
|
|
578
|
+
<<<<<<< HEAD
|
|
413
579
|
@try {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
[ScreenCaptureKitRecorder stopRecording];
|
|
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
|
+
}
|
|
422
587
|
g_isRecording = false;
|
|
588
|
+
cleanupRecording();
|
|
589
|
+
NSLog(@"[mac_recorder] ScreenCaptureKit stopped");
|
|
423
590
|
return Napi::Boolean::New(env, true);
|
|
424
591
|
|
|
425
592
|
} @catch (NSException *exception) {
|
|
426
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
|
|
427
615
|
return Napi::Boolean::New(env, false);
|
|
428
616
|
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
// NAPI Function: Get Windows List
|
|
434
|
-
Napi::Value GetWindows(const Napi::CallbackInfo& info) {
|
|
435
|
-
Napi::Env env = info.Env();
|
|
436
|
-
Napi::Array windowArray = Napi::Array::New(env);
|
|
437
617
|
|
|
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");
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
<<<<<<< HEAD
|
|
438
639
|
@try {
|
|
439
|
-
|
|
440
|
-
CFArrayRef windowList = CGWindowListCopyWindowInfo(
|
|
441
|
-
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
|
|
442
|
-
kCGNullWindowID
|
|
443
|
-
);
|
|
640
|
+
NSMutableArray *devices = [NSMutableArray array];
|
|
444
641
|
|
|
445
|
-
|
|
446
|
-
|
|
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);
|
|
447
653
|
}
|
|
448
654
|
|
|
449
|
-
|
|
450
|
-
|
|
655
|
+
UInt32 deviceCount = dataSize / sizeof(AudioDeviceID);
|
|
656
|
+
AudioDeviceID *audioDevices = (AudioDeviceID *)malloc(dataSize);
|
|
451
657
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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];
|
|
458
666
|
|
|
459
|
-
|
|
460
|
-
|
|
667
|
+
// Check if device has input streams
|
|
668
|
+
AudioObjectPropertyAddress streamsAddress = {
|
|
669
|
+
kAudioDevicePropertyStreams,
|
|
670
|
+
kAudioDevicePropertyScopeInput,
|
|
671
|
+
kAudioObjectPropertyElementMain
|
|
672
|
+
};
|
|
461
673
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const char* windowNameCStr = CFStringGetCStringPtr(windowNameRef, kCFStringEncodingUTF8);
|
|
467
|
-
if (windowNameCStr) {
|
|
468
|
-
windowName = std::string(windowNameCStr);
|
|
469
|
-
} else {
|
|
470
|
-
// Fallback for non-ASCII characters
|
|
471
|
-
CFIndex length = CFStringGetLength(windowNameRef);
|
|
472
|
-
CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
|
|
473
|
-
char* buffer = (char*)malloc(maxSize);
|
|
474
|
-
if (CFStringGetCString(windowNameRef, buffer, maxSize, kCFStringEncodingUTF8)) {
|
|
475
|
-
windowName = std::string(buffer);
|
|
476
|
-
}
|
|
477
|
-
free(buffer);
|
|
478
|
-
}
|
|
674
|
+
UInt32 streamsSize = 0;
|
|
675
|
+
status = AudioObjectGetPropertyDataSize(deviceID, &streamsAddress, 0, NULL, &streamsSize);
|
|
676
|
+
if (status != noErr || streamsSize == 0) {
|
|
677
|
+
continue; // Skip output-only devices
|
|
479
678
|
}
|
|
480
679
|
|
|
481
|
-
// Get
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
appName = std::string(appNameCStr);
|
|
488
|
-
} else {
|
|
489
|
-
CFIndex length = CFStringGetLength(appNameRef);
|
|
490
|
-
CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
|
|
491
|
-
char* buffer = (char*)malloc(maxSize);
|
|
492
|
-
if (CFStringGetCString(appNameRef, buffer, maxSize, kCFStringEncodingUTF8)) {
|
|
493
|
-
appName = std::string(buffer);
|
|
494
|
-
}
|
|
495
|
-
free(buffer);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
680
|
+
// Get device name
|
|
681
|
+
AudioObjectPropertyAddress nameAddress = {
|
|
682
|
+
kAudioDevicePropertyDeviceNameCFString,
|
|
683
|
+
kAudioObjectPropertyScopeGlobal,
|
|
684
|
+
kAudioObjectPropertyElementMain
|
|
685
|
+
};
|
|
498
686
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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;
|
|
504
694
|
}
|
|
505
695
|
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
|
|
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;
|
|
509
710
|
}
|
|
510
711
|
|
|
511
|
-
//
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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);
|
|
520
724
|
|
|
521
|
-
windowArray.Set(arrayIndex++, windowObj);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
CFRelease(windowList);
|
|
525
|
-
return windowArray;
|
|
526
|
-
|
|
527
|
-
} @catch (NSException *exception) {
|
|
528
|
-
return windowArray;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// NAPI Function: Get Audio Devices
|
|
533
|
-
Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
|
|
534
|
-
Napi::Env env = info.Env();
|
|
535
|
-
|
|
536
|
-
@try {
|
|
537
|
-
NSMutableArray *devices = [NSMutableArray array];
|
|
538
|
-
|
|
539
|
-
// Get all audio devices
|
|
540
|
-
NSArray *audioDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
|
|
541
|
-
|
|
542
|
-
for (AVCaptureDevice *device in audioDevices) {
|
|
543
725
|
[devices addObject:@{
|
|
544
|
-
@"id":
|
|
545
|
-
@"name":
|
|
546
|
-
@"manufacturer":
|
|
547
|
-
@"isDefault": @(
|
|
726
|
+
@"id": deviceUID,
|
|
727
|
+
@"name": deviceName,
|
|
728
|
+
@"manufacturer": @"Unknown",
|
|
729
|
+
@"isDefault": @(isDefault)
|
|
548
730
|
}];
|
|
731
|
+
|
|
732
|
+
if (deviceNameRef) CFRelease(deviceNameRef);
|
|
733
|
+
if (deviceUIDRef) CFRelease(deviceUIDRef);
|
|
549
734
|
}
|
|
550
735
|
|
|
736
|
+
free(audioDevices);
|
|
737
|
+
|
|
551
738
|
// Convert to NAPI array
|
|
552
739
|
Napi::Array result = Napi::Array::New(env, devices.count);
|
|
553
740
|
for (NSUInteger i = 0; i < devices.count; i++) {
|
|
@@ -558,338 +745,337 @@ Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
|
|
|
558
745
|
deviceObj.Set("manufacturer", Napi::String::New(env, [device[@"manufacturer"] UTF8String]));
|
|
559
746
|
deviceObj.Set("isDefault", Napi::Boolean::New(env, [device[@"isDefault"] boolValue]));
|
|
560
747
|
result[i] = deviceObj;
|
|
748
|
+
=======
|
|
749
|
+
// Audio input settings (if needed)
|
|
750
|
+
if (includeSystemAudio) {
|
|
751
|
+
NSDictionary *audioSettings = @{
|
|
752
|
+
AVFormatIDKey: @(kAudioFormatMPEG4AAC),
|
|
753
|
+
AVSampleRateKey: @44100,
|
|
754
|
+
AVNumberOfChannelsKey: @2
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
g_scDelegate.audioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:audioSettings];
|
|
758
|
+
g_scDelegate.audioInput.expectsMediaDataInRealTime = YES;
|
|
759
|
+
|
|
760
|
+
if ([g_scDelegate.assetWriter canAddInput:g_scDelegate.audioInput]) {
|
|
761
|
+
[g_scDelegate.assetWriter addInput:g_scDelegate.audioInput];
|
|
561
762
|
}
|
|
562
|
-
|
|
563
|
-
return result;
|
|
564
|
-
|
|
565
|
-
} @catch (NSException *exception) {
|
|
566
|
-
return Napi::Array::New(env, 0);
|
|
567
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);
|
|
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);
|
|
568
840
|
}
|
|
569
841
|
|
|
570
|
-
// NAPI Function:
|
|
571
|
-
Napi::Value
|
|
842
|
+
// NAPI Function: Stop Recording
|
|
843
|
+
Napi::Value StopRecording(const Napi::CallbackInfo& info) {
|
|
572
844
|
Napi::Env env = info.Env();
|
|
573
845
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
Napi::Array result = Napi::Array::New(env, displays.count);
|
|
577
|
-
|
|
578
|
-
NSLog(@"Found %lu displays", (unsigned long)displays.count);
|
|
579
|
-
|
|
580
|
-
for (NSUInteger i = 0; i < displays.count; i++) {
|
|
581
|
-
NSDictionary *display = displays[i];
|
|
582
|
-
NSLog(@"Display %lu: ID=%u, Name=%@, Size=%@x%@",
|
|
583
|
-
(unsigned long)i,
|
|
584
|
-
[display[@"id"] unsignedIntValue],
|
|
585
|
-
display[@"name"],
|
|
586
|
-
display[@"width"],
|
|
587
|
-
display[@"height"]);
|
|
588
|
-
|
|
589
|
-
Napi::Object displayObj = Napi::Object::New(env);
|
|
590
|
-
displayObj.Set("id", Napi::Number::New(env, [display[@"id"] unsignedIntValue]));
|
|
591
|
-
displayObj.Set("name", Napi::String::New(env, [display[@"name"] UTF8String]));
|
|
592
|
-
displayObj.Set("width", Napi::Number::New(env, [display[@"width"] doubleValue]));
|
|
593
|
-
displayObj.Set("height", Napi::Number::New(env, [display[@"height"] doubleValue]));
|
|
594
|
-
displayObj.Set("x", Napi::Number::New(env, [display[@"x"] doubleValue]));
|
|
595
|
-
displayObj.Set("y", Napi::Number::New(env, [display[@"y"] doubleValue]));
|
|
596
|
-
displayObj.Set("isPrimary", Napi::Boolean::New(env, [display[@"isPrimary"] boolValue]));
|
|
597
|
-
result[i] = displayObj;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
return result;
|
|
601
|
-
|
|
602
|
-
} @catch (NSException *exception) {
|
|
603
|
-
NSLog(@"Exception in GetDisplays: %@", exception);
|
|
604
|
-
return Napi::Array::New(env, 0);
|
|
846
|
+
if (!g_isRecording) {
|
|
847
|
+
return Napi::Boolean::New(env, false);
|
|
605
848
|
}
|
|
849
|
+
|
|
850
|
+
cleanupSCKRecording();
|
|
851
|
+
return Napi::Boolean::New(env, true);
|
|
606
852
|
}
|
|
607
853
|
|
|
608
854
|
// NAPI Function: Get Recording Status
|
|
609
|
-
Napi::Value
|
|
855
|
+
Napi::Value IsRecording(const Napi::CallbackInfo& info) {
|
|
610
856
|
Napi::Env env = info.Env();
|
|
611
857
|
return Napi::Boolean::New(env, g_isRecording);
|
|
612
858
|
}
|
|
613
859
|
|
|
614
|
-
// NAPI Function: Get
|
|
615
|
-
Napi::Value
|
|
860
|
+
// NAPI Function: Get Displays
|
|
861
|
+
Napi::Value GetDisplays(const Napi::CallbackInfo& info) {
|
|
616
862
|
Napi::Env env = info.Env();
|
|
617
863
|
|
|
618
|
-
if (
|
|
619
|
-
|
|
620
|
-
return
|
|
864
|
+
if (!isScreenCaptureKitAvailable()) {
|
|
865
|
+
// Fallback to legacy method
|
|
866
|
+
return GetAvailableDisplays(info);
|
|
621
867
|
}
|
|
622
868
|
|
|
623
|
-
|
|
869
|
+
// Use ScreenCaptureKit
|
|
870
|
+
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
|
871
|
+
__block SCShareableContent *shareableContent = nil;
|
|
872
|
+
__block NSError *error = nil;
|
|
624
873
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
874
|
+
[SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent * _Nullable content, NSError * _Nullable err) {
|
|
875
|
+
shareableContent = content;
|
|
876
|
+
error = err;
|
|
877
|
+
dispatch_semaphore_signal(semaphore);
|
|
878
|
+
}];
|
|
628
879
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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);
|
|
634
885
|
}
|
|
635
886
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
);
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// Get original dimensions
|
|
650
|
-
size_t originalWidth = CGImageGetWidth(windowImage);
|
|
651
|
-
size_t originalHeight = CGImageGetHeight(windowImage);
|
|
652
|
-
|
|
653
|
-
// Calculate scaled dimensions maintaining aspect ratio
|
|
654
|
-
double scaleX = (double)maxWidth / originalWidth;
|
|
655
|
-
double scaleY = (double)maxHeight / originalHeight;
|
|
656
|
-
double scale = std::min(scaleX, scaleY);
|
|
657
|
-
|
|
658
|
-
size_t thumbnailWidth = (size_t)(originalWidth * scale);
|
|
659
|
-
size_t thumbnailHeight = (size_t)(originalHeight * scale);
|
|
660
|
-
|
|
661
|
-
// Create scaled image
|
|
662
|
-
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
663
|
-
CGContextRef context = CGBitmapContextCreate(
|
|
664
|
-
NULL,
|
|
665
|
-
thumbnailWidth,
|
|
666
|
-
thumbnailHeight,
|
|
667
|
-
8,
|
|
668
|
-
thumbnailWidth * 4,
|
|
669
|
-
colorSpace,
|
|
670
|
-
kCGImageAlphaPremultipliedLast
|
|
671
|
-
);
|
|
672
|
-
|
|
673
|
-
if (context) {
|
|
674
|
-
CGContextDrawImage(context, CGRectMake(0, 0, thumbnailWidth, thumbnailHeight), windowImage);
|
|
675
|
-
CGImageRef thumbnailImage = CGBitmapContextCreateImage(context);
|
|
676
|
-
|
|
677
|
-
if (thumbnailImage) {
|
|
678
|
-
// Convert to PNG data
|
|
679
|
-
NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:thumbnailImage];
|
|
680
|
-
NSData *pngData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
|
|
681
|
-
|
|
682
|
-
if (pngData) {
|
|
683
|
-
// Convert to Base64
|
|
684
|
-
NSString *base64String = [pngData base64EncodedStringWithOptions:0];
|
|
685
|
-
std::string base64Std = [base64String UTF8String];
|
|
686
|
-
|
|
687
|
-
CGImageRelease(thumbnailImage);
|
|
688
|
-
CGContextRelease(context);
|
|
689
|
-
CGColorSpaceRelease(colorSpace);
|
|
690
|
-
CGImageRelease(windowImage);
|
|
691
|
-
|
|
692
|
-
return Napi::String::New(env, base64Std);
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
CGImageRelease(thumbnailImage);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
CGContextRelease(context);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
CGColorSpaceRelease(colorSpace);
|
|
702
|
-
CGImageRelease(windowImage);
|
|
703
|
-
|
|
704
|
-
return env.Null();
|
|
705
|
-
|
|
706
|
-
} @catch (NSException *exception) {
|
|
707
|
-
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);
|
|
708
898
|
}
|
|
899
|
+
|
|
900
|
+
return displaysArray;
|
|
709
901
|
}
|
|
710
902
|
|
|
711
|
-
|
|
712
|
-
|
|
903
|
+
|
|
904
|
+
// NAPI Function: Get Windows
|
|
905
|
+
Napi::Value GetWindows(const Napi::CallbackInfo& info) {
|
|
713
906
|
Napi::Env env = info.Env();
|
|
714
907
|
|
|
715
|
-
if (
|
|
716
|
-
|
|
717
|
-
return
|
|
908
|
+
if (!isScreenCaptureKitAvailable()) {
|
|
909
|
+
// Use legacy CGWindowList method
|
|
910
|
+
return GetWindowList(info);
|
|
718
911
|
}
|
|
719
912
|
|
|
720
|
-
|
|
913
|
+
// Use ScreenCaptureKit
|
|
914
|
+
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
|
915
|
+
__block SCShareableContent *shareableContent = nil;
|
|
916
|
+
__block NSError *error = nil;
|
|
721
917
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
918
|
+
[SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent * _Nullable content, NSError * _Nullable err) {
|
|
919
|
+
shareableContent = content;
|
|
920
|
+
error = err;
|
|
921
|
+
dispatch_semaphore_signal(semaphore);
|
|
922
|
+
}];
|
|
725
923
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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);
|
|
731
929
|
}
|
|
732
930
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
for (uint32_t i = 0; i < displayCount; i++) {
|
|
746
|
-
if (activeDisplays[i] == displayID) {
|
|
747
|
-
displayFound = true;
|
|
748
|
-
break;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
if (!displayFound) {
|
|
753
|
-
NSLog(@"Display ID %u not found in active displays", displayID);
|
|
754
|
-
return env.Null();
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Create display image
|
|
758
|
-
CGImageRef displayImage = CGDisplayCreateImage(displayID);
|
|
759
|
-
|
|
760
|
-
if (!displayImage) {
|
|
761
|
-
NSLog(@"CGDisplayCreateImage failed for display ID: %u", displayID);
|
|
762
|
-
return env.Null();
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
// Get original dimensions
|
|
766
|
-
size_t originalWidth = CGImageGetWidth(displayImage);
|
|
767
|
-
size_t originalHeight = CGImageGetHeight(displayImage);
|
|
768
|
-
|
|
769
|
-
NSLog(@"Original dimensions: %zux%zu", originalWidth, originalHeight);
|
|
770
|
-
|
|
771
|
-
// Calculate scaled dimensions maintaining aspect ratio
|
|
772
|
-
double scaleX = (double)maxWidth / originalWidth;
|
|
773
|
-
double scaleY = (double)maxHeight / originalHeight;
|
|
774
|
-
double scale = std::min(scaleX, scaleY);
|
|
775
|
-
|
|
776
|
-
size_t thumbnailWidth = (size_t)(originalWidth * scale);
|
|
777
|
-
size_t thumbnailHeight = (size_t)(originalHeight * scale);
|
|
778
|
-
|
|
779
|
-
NSLog(@"Thumbnail dimensions: %zux%zu (scale: %f)", thumbnailWidth, thumbnailHeight, scale);
|
|
780
|
-
|
|
781
|
-
// Create scaled image
|
|
782
|
-
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
783
|
-
CGContextRef context = CGBitmapContextCreate(
|
|
784
|
-
NULL,
|
|
785
|
-
thumbnailWidth,
|
|
786
|
-
thumbnailHeight,
|
|
787
|
-
8,
|
|
788
|
-
thumbnailWidth * 4,
|
|
789
|
-
colorSpace,
|
|
790
|
-
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big
|
|
791
|
-
);
|
|
792
|
-
|
|
793
|
-
if (!context) {
|
|
794
|
-
NSLog(@"Failed to create bitmap context");
|
|
795
|
-
CGImageRelease(displayImage);
|
|
796
|
-
CGColorSpaceRelease(colorSpace);
|
|
797
|
-
return env.Null();
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// Set interpolation quality for better scaling
|
|
801
|
-
CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
|
|
802
|
-
|
|
803
|
-
// Draw the image
|
|
804
|
-
CGContextDrawImage(context, CGRectMake(0, 0, thumbnailWidth, thumbnailHeight), displayImage);
|
|
805
|
-
CGImageRef thumbnailImage = CGBitmapContextCreateImage(context);
|
|
806
|
-
|
|
807
|
-
if (!thumbnailImage) {
|
|
808
|
-
NSLog(@"Failed to create thumbnail image");
|
|
809
|
-
CGContextRelease(context);
|
|
810
|
-
CGImageRelease(displayImage);
|
|
811
|
-
CGColorSpaceRelease(colorSpace);
|
|
812
|
-
return env.Null();
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
// Convert to PNG data
|
|
816
|
-
NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:thumbnailImage];
|
|
817
|
-
NSDictionary *properties = @{NSImageCompressionFactor: @0.8};
|
|
818
|
-
NSData *pngData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:properties];
|
|
819
|
-
|
|
820
|
-
if (!pngData) {
|
|
821
|
-
NSLog(@"Failed to convert image to PNG data");
|
|
822
|
-
CGImageRelease(thumbnailImage);
|
|
823
|
-
CGContextRelease(context);
|
|
824
|
-
CGImageRelease(displayImage);
|
|
825
|
-
CGColorSpaceRelease(colorSpace);
|
|
826
|
-
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);
|
|
827
943
|
}
|
|
828
|
-
|
|
829
|
-
// Convert to Base64
|
|
830
|
-
NSString *base64String = [pngData base64EncodedStringWithOptions:0];
|
|
831
|
-
std::string base64Std = [base64String UTF8String];
|
|
832
|
-
|
|
833
|
-
NSLog(@"Successfully created thumbnail with base64 length: %lu", (unsigned long)base64Std.length());
|
|
834
|
-
|
|
835
|
-
// Cleanup
|
|
836
|
-
CGImageRelease(thumbnailImage);
|
|
837
|
-
CGContextRelease(context);
|
|
838
|
-
CGColorSpaceRelease(colorSpace);
|
|
839
|
-
CGImageRelease(displayImage);
|
|
840
|
-
|
|
841
|
-
return Napi::String::New(env, base64Std);
|
|
842
|
-
|
|
843
|
-
} @catch (NSException *exception) {
|
|
844
|
-
NSLog(@"Exception in GetDisplayThumbnail: %@", exception);
|
|
845
|
-
return env.Null();
|
|
846
944
|
}
|
|
945
|
+
|
|
946
|
+
return windowsArray;
|
|
847
947
|
}
|
|
848
948
|
|
|
849
949
|
// NAPI Function: Check Permissions
|
|
850
950
|
Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
|
|
851
951
|
Napi::Env env = info.Env();
|
|
852
952
|
|
|
953
|
+
<<<<<<< HEAD
|
|
853
954
|
@try {
|
|
854
|
-
// Check screen recording permission
|
|
955
|
+
// Check screen recording permission using ScreenCaptureKit
|
|
855
956
|
bool hasScreenPermission = true;
|
|
856
957
|
|
|
857
|
-
if (@available(macOS
|
|
858
|
-
// Try to
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
nil,
|
|
864
|
-
^(CGDisplayStreamFrameStatus status, uint64_t displayTime, IOSurfaceRef frameSurface, CGDisplayStreamUpdateRef updateRef) {
|
|
865
|
-
// Empty handler
|
|
866
|
-
}
|
|
867
|
-
);
|
|
868
|
-
|
|
869
|
-
if (stream) {
|
|
870
|
-
CFRelease(stream);
|
|
871
|
-
hasScreenPermission = true;
|
|
872
|
-
} else {
|
|
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) {
|
|
873
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
|
|
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
|
+
}
|
|
874
1057
|
}
|
|
875
1058
|
}
|
|
1059
|
+
<<<<<<< HEAD
|
|
876
1060
|
|
|
877
|
-
//
|
|
1061
|
+
// For audio permission, we'll use a simpler check since we're using CoreAudio
|
|
878
1062
|
bool hasAudioPermission = true;
|
|
879
|
-
if (@available(macOS 10.14, *)) {
|
|
880
|
-
AVAuthorizationStatus audioStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
|
|
881
|
-
hasAudioPermission = (audioStatus == AVAuthorizationStatusAuthorized);
|
|
882
|
-
}
|
|
883
1063
|
|
|
884
1064
|
return Napi::Boolean::New(env, hasScreenPermission && hasAudioPermission);
|
|
885
1065
|
|
|
886
1066
|
} @catch (NSException *exception) {
|
|
887
1067
|
return Napi::Boolean::New(env, false);
|
|
1068
|
+
=======
|
|
1069
|
+
>>>>>>> screencapture
|
|
888
1070
|
}
|
|
1071
|
+
|
|
1072
|
+
free(audioDevices);
|
|
1073
|
+
return devices;
|
|
889
1074
|
}
|
|
890
1075
|
|
|
891
|
-
// Initialize
|
|
1076
|
+
// Initialize the addon
|
|
892
1077
|
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
1078
|
+
<<<<<<< HEAD
|
|
893
1079
|
exports.Set(Napi::String::New(env, "startRecording"), Napi::Function::New(env, StartRecording));
|
|
894
1080
|
exports.Set(Napi::String::New(env, "stopRecording"), Napi::Function::New(env, StopRecording));
|
|
895
1081
|
|
|
@@ -901,13 +1087,25 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
901
1087
|
// ScreenCaptureKit availability (optional for clients)
|
|
902
1088
|
exports.Set(Napi::String::New(env, "isScreenCaptureKitAvailable"), Napi::Function::New(env, [](const Napi::CallbackInfo& info){
|
|
903
1089
|
Napi::Env env = info.Env();
|
|
904
|
-
|
|
905
|
-
|
|
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);
|
|
906
1095
|
}));
|
|
907
1096
|
|
|
908
1097
|
// Thumbnail functions
|
|
909
1098
|
exports.Set(Napi::String::New(env, "getWindowThumbnail"), Napi::Function::New(env, GetWindowThumbnail));
|
|
910
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
|
|
911
1109
|
|
|
912
1110
|
// Initialize cursor tracker
|
|
913
1111
|
InitCursorTracker(env, exports);
|
|
@@ -918,4 +1116,4 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
918
1116
|
return exports;
|
|
919
1117
|
}
|
|
920
1118
|
|
|
921
|
-
NODE_API_MODULE(mac_recorder, Init)
|
|
1119
|
+
NODE_API_MODULE(mac_recorder, Init)
|