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/binding.gyp
CHANGED
|
@@ -21,25 +21,42 @@
|
|
|
21
21
|
"xcode_settings": {
|
|
22
22
|
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
|
|
23
23
|
"CLANG_CXX_LIBRARY": "libc++",
|
|
24
|
-
"MACOSX_DEPLOYMENT_TARGET": "
|
|
24
|
+
"MACOSX_DEPLOYMENT_TARGET": "12.3",
|
|
25
|
+
<<<<<<< HEAD
|
|
26
|
+
=======
|
|
27
|
+
"ARCHS": ["arm64"],
|
|
28
|
+
"VALID_ARCHS": ["arm64"],
|
|
29
|
+
>>>>>>> screencapture
|
|
25
30
|
"OTHER_CFLAGS": [
|
|
26
|
-
"-ObjC++"
|
|
27
|
-
|
|
31
|
+
"-ObjC++",
|
|
32
|
+
"-fmodules"
|
|
33
|
+
],
|
|
34
|
+
"CLANG_ENABLE_OBJC_ARC": "YES"
|
|
28
35
|
},
|
|
29
36
|
"link_settings": {
|
|
30
37
|
"libraries": [
|
|
38
|
+
<<<<<<< HEAD
|
|
39
|
+
=======
|
|
40
|
+
"-framework ScreenCaptureKit",
|
|
31
41
|
"-framework AVFoundation",
|
|
32
42
|
"-framework CoreMedia",
|
|
33
43
|
"-framework CoreVideo",
|
|
44
|
+
>>>>>>> screencapture
|
|
34
45
|
"-framework Foundation",
|
|
35
46
|
"-framework AppKit",
|
|
36
|
-
"-framework ScreenCaptureKit",
|
|
37
47
|
"-framework ApplicationServices",
|
|
38
48
|
"-framework Carbon",
|
|
39
|
-
"-framework Accessibility"
|
|
49
|
+
"-framework Accessibility",
|
|
50
|
+
"-framework CoreAudio",
|
|
51
|
+
"-framework AVFoundation",
|
|
52
|
+
"-framework CoreMedia",
|
|
53
|
+
"-framework CoreVideo"
|
|
40
54
|
]
|
|
41
55
|
},
|
|
42
|
-
"defines": [
|
|
56
|
+
"defines": [
|
|
57
|
+
"NAPI_DISABLE_CPP_EXCEPTIONS",
|
|
58
|
+
"USE_SCREENCAPTUREKIT=1"
|
|
59
|
+
]
|
|
43
60
|
}
|
|
44
61
|
]
|
|
45
62
|
}
|
package/index.js
CHANGED
|
@@ -129,6 +129,7 @@ class MacRecorder extends EventEmitter {
|
|
|
129
129
|
audioDeviceId: options.audioDeviceId || null, // null = default device
|
|
130
130
|
systemAudioDeviceId: options.systemAudioDeviceId || null, // null = auto-detect system audio device
|
|
131
131
|
captureArea: options.captureArea || null,
|
|
132
|
+
<<<<<<< HEAD
|
|
132
133
|
useScreenCaptureKit: options.useScreenCaptureKit || false,
|
|
133
134
|
excludedAppBundleIds: options.excludedAppBundleIds || [],
|
|
134
135
|
excludedPIDs: options.excludedPIDs || [],
|
|
@@ -137,6 +138,13 @@ class MacRecorder extends EventEmitter {
|
|
|
137
138
|
typeof options.autoExcludeSelf === "boolean"
|
|
138
139
|
? options.autoExcludeSelf
|
|
139
140
|
: !!(process.versions && process.versions.electron),
|
|
141
|
+
=======
|
|
142
|
+
// Exclusion options
|
|
143
|
+
excludeCurrentApp: options.excludeCurrentApp || false,
|
|
144
|
+
excludeWindowIds: Array.isArray(options.excludeWindowIds)
|
|
145
|
+
? options.excludeWindowIds
|
|
146
|
+
: [],
|
|
147
|
+
>>>>>>> screencapture
|
|
140
148
|
};
|
|
141
149
|
}
|
|
142
150
|
|
|
@@ -297,11 +305,17 @@ class MacRecorder extends EventEmitter {
|
|
|
297
305
|
windowId: this.options.windowId || null, // null = tam ekran
|
|
298
306
|
audioDeviceId: this.options.audioDeviceId || null, // null = default device
|
|
299
307
|
systemAudioDeviceId: this.options.systemAudioDeviceId || null, // null = auto-detect system audio device
|
|
308
|
+
<<<<<<< HEAD
|
|
300
309
|
useScreenCaptureKit: this.options.useScreenCaptureKit || false,
|
|
301
310
|
excludedAppBundleIds: this.options.excludedAppBundleIds || [],
|
|
302
311
|
excludedPIDs: this.options.excludedPIDs || [],
|
|
303
312
|
excludedWindowIds: this.options.excludedWindowIds || [],
|
|
304
313
|
autoExcludeSelf: this.options.autoExcludeSelf === true,
|
|
314
|
+
=======
|
|
315
|
+
// Exclusion options passthrough
|
|
316
|
+
excludeCurrentApp: this.options.excludeCurrentApp || false,
|
|
317
|
+
excludeWindowIds: this.options.excludeWindowIds || [],
|
|
318
|
+
>>>>>>> screencapture
|
|
305
319
|
};
|
|
306
320
|
|
|
307
321
|
// Manuel captureArea varsa onu kullan
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Simple test runner: starts a 2s recording with ScreenCaptureKit and exclusions.
|
|
3
|
+
*/
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const MacRecorder = require("..");
|
|
7
|
+
|
|
8
|
+
async function sleep(ms) {
|
|
9
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function main() {
|
|
13
|
+
const recorder = new MacRecorder();
|
|
14
|
+
const outDir = path.resolve(process.cwd(), "test-output");
|
|
15
|
+
const outPath = path.join(outDir, `sc-exclude-${Date.now()}.mov`);
|
|
16
|
+
await fs.promises.mkdir(outDir, { recursive: true });
|
|
17
|
+
|
|
18
|
+
console.log("[TEST] Starting 2s recording with SC exclusions...");
|
|
19
|
+
// Try to ensure overlays are not active in this process
|
|
20
|
+
|
|
21
|
+
const perms = await recorder.checkPermissions();
|
|
22
|
+
if (!perms?.screenRecording) {
|
|
23
|
+
console.error(
|
|
24
|
+
"[TEST] Screen Recording permission is not granted. Enable it in System Settings → Privacy & Security → Screen Recording for Terminal/Node, then re-run."
|
|
25
|
+
);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await recorder.startRecording(outPath, {
|
|
31
|
+
useScreenCaptureKit: true,
|
|
32
|
+
captureCursor: false,
|
|
33
|
+
excludedAppBundleIds: ["com.apple.Safari"],
|
|
34
|
+
});
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.error("[TEST] Failed to start recording:", e.message);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await sleep(2000);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const result = await recorder.stopRecording();
|
|
44
|
+
console.log("[TEST] Stopped. Result:", result);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.error("[TEST] Failed to stop recording:", e.message);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// SCRecordingOutput write may be async; wait up to 10s for the file
|
|
51
|
+
const deadline = Date.now() + 10000;
|
|
52
|
+
let stats = null;
|
|
53
|
+
while (Date.now() < deadline) {
|
|
54
|
+
if (fs.existsSync(outPath)) {
|
|
55
|
+
stats = fs.statSync(outPath);
|
|
56
|
+
if (stats.size > 0) break;
|
|
57
|
+
}
|
|
58
|
+
await sleep(200);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (stats && fs.existsSync(outPath)) {
|
|
62
|
+
console.log(`[TEST] Output saved: ${outPath} (${stats.size} bytes)`);
|
|
63
|
+
} else {
|
|
64
|
+
console.error("[TEST] Output file not found or empty:", outPath);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
main().catch((e) => {
|
|
70
|
+
console.error("[TEST] Unhandled error:", e);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
});
|
package/src/audio_capture.mm
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
#import <ScreenCaptureKit/ScreenCaptureKit.h>
|
|
1
2
|
#import <AVFoundation/AVFoundation.h>
|
|
2
3
|
#import <CoreAudio/CoreAudio.h>
|
|
3
4
|
|
|
4
5
|
@interface AudioCapture : NSObject
|
|
5
6
|
|
|
6
7
|
+ (NSArray *)getAudioDevices;
|
|
8
|
+
+ (NSArray *)getSystemAudioDevices;
|
|
7
9
|
+ (BOOL)hasAudioPermission;
|
|
8
10
|
+ (void)requestAudioPermission:(void(^)(BOOL granted))completion;
|
|
9
11
|
|
|
@@ -14,25 +16,39 @@
|
|
|
14
16
|
+ (NSArray *)getAudioDevices {
|
|
15
17
|
NSMutableArray *devices = [NSMutableArray array];
|
|
16
18
|
|
|
17
|
-
// Get
|
|
18
|
-
|
|
19
|
+
// Get microphone devices using AVFoundation
|
|
20
|
+
AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession
|
|
21
|
+
discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInMicrophone, AVCaptureDeviceTypeExternalUnknown]
|
|
22
|
+
mediaType:AVMediaTypeAudio
|
|
23
|
+
position:AVCaptureDevicePositionUnspecified];
|
|
24
|
+
NSArray *audioDevices = discoverySession.devices;
|
|
19
25
|
|
|
20
26
|
for (AVCaptureDevice *device in audioDevices) {
|
|
21
27
|
NSDictionary *deviceInfo = @{
|
|
22
28
|
@"id": device.uniqueID,
|
|
23
29
|
@"name": device.localizedName,
|
|
24
30
|
@"manufacturer": device.manufacturer ?: @"Unknown",
|
|
31
|
+
@"type": @"microphone",
|
|
25
32
|
@"isDefault": @([device isEqual:[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]])
|
|
26
33
|
};
|
|
27
34
|
|
|
28
35
|
[devices addObject:deviceInfo];
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
//
|
|
38
|
+
// Add system audio devices using Core Audio API
|
|
39
|
+
NSArray *systemDevices = [self getSystemAudioDevices];
|
|
40
|
+
[devices addObjectsFromArray:systemDevices];
|
|
41
|
+
|
|
42
|
+
return [devices copy];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
+ (NSArray *)getSystemAudioDevices {
|
|
46
|
+
NSMutableArray *devices = [NSMutableArray array];
|
|
47
|
+
|
|
32
48
|
AudioObjectPropertyAddress propertyAddress = {
|
|
33
49
|
kAudioHardwarePropertyDevices,
|
|
34
50
|
kAudioObjectPropertyScopeGlobal,
|
|
35
|
-
kAudioObjectPropertyElementMaster
|
|
51
|
+
kAudioObjectPropertyElementMain // Changed from kAudioObjectPropertyElementMaster (deprecated)
|
|
36
52
|
};
|
|
37
53
|
|
|
38
54
|
UInt32 dataSize = 0;
|
|
@@ -49,37 +65,41 @@
|
|
|
49
65
|
AudioDeviceID deviceID = audioDeviceIDs[i];
|
|
50
66
|
|
|
51
67
|
// Get device name
|
|
52
|
-
propertyAddress.mSelector = kAudioDevicePropertyDeviceNameCFString;
|
|
53
|
-
propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
|
|
54
|
-
|
|
55
68
|
CFStringRef deviceName = NULL;
|
|
56
|
-
|
|
69
|
+
UInt32 size = sizeof(deviceName);
|
|
70
|
+
AudioObjectPropertyAddress nameAddress = {
|
|
71
|
+
kAudioDevicePropertyDeviceNameCFString,
|
|
72
|
+
kAudioDevicePropertyScopeOutput, // Focus on output devices for system audio
|
|
73
|
+
kAudioObjectPropertyElementMain
|
|
74
|
+
};
|
|
57
75
|
|
|
58
|
-
status = AudioObjectGetPropertyData(deviceID, &
|
|
76
|
+
status = AudioObjectGetPropertyData(deviceID, &nameAddress, 0, NULL, &size, &deviceName);
|
|
59
77
|
|
|
60
78
|
if (status == kAudioHardwareNoError && deviceName) {
|
|
61
|
-
// Check if
|
|
62
|
-
|
|
63
|
-
|
|
79
|
+
// Check if this is an output device
|
|
80
|
+
AudioObjectPropertyAddress streamAddress = {
|
|
81
|
+
kAudioDevicePropertyStreams,
|
|
82
|
+
kAudioDevicePropertyScopeOutput,
|
|
83
|
+
kAudioObjectPropertyElementMain
|
|
84
|
+
};
|
|
64
85
|
|
|
65
|
-
|
|
86
|
+
UInt32 streamSize = 0;
|
|
87
|
+
status = AudioObjectGetPropertyDataSize(deviceID, &streamAddress, 0, NULL, &streamSize);
|
|
66
88
|
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
89
|
+
if (status == kAudioHardwareNoError && streamSize > 0) {
|
|
90
|
+
// This is an output device - can be used for system audio capture
|
|
91
|
+
const char *name = CFStringGetCStringPtr(deviceName, kCFStringEncodingUTF8);
|
|
92
|
+
NSString *deviceNameStr = name ? [NSString stringWithUTF8String:name] : @"Unknown Device";
|
|
70
93
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
[devices addObject:deviceInfo];
|
|
80
|
-
}
|
|
94
|
+
NSDictionary *deviceInfo = @{
|
|
95
|
+
@"id": [NSString stringWithFormat:@"%u", deviceID],
|
|
96
|
+
@"name": deviceNameStr,
|
|
97
|
+
@"manufacturer": @"System",
|
|
98
|
+
@"type": @"system_audio",
|
|
99
|
+
@"isDefault": @(NO) // We'll determine default separately if needed
|
|
100
|
+
};
|
|
81
101
|
|
|
82
|
-
|
|
102
|
+
[devices addObject:deviceInfo];
|
|
83
103
|
}
|
|
84
104
|
|
|
85
105
|
CFRelease(deviceName);
|
|
@@ -94,23 +114,59 @@
|
|
|
94
114
|
}
|
|
95
115
|
|
|
96
116
|
+ (BOOL)hasAudioPermission {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
return YES; // Older versions don't require explicit permission
|
|
117
|
+
// Check microphone permission using AVFoundation
|
|
118
|
+
AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
|
|
119
|
+
return authStatus == AVAuthorizationStatusAuthorized;
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
+ (void)requestAudioPermission:(void(^)(BOOL granted))completion {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
123
|
+
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {
|
|
124
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
125
|
+
if (completion) {
|
|
108
126
|
completion(granted);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@end
|
|
133
|
+
|
|
134
|
+
// ScreenCaptureKit Audio Configuration Helper
|
|
135
|
+
API_AVAILABLE(macos(12.3))
|
|
136
|
+
@interface SCKAudioConfiguration : NSObject
|
|
137
|
+
|
|
138
|
+
+ (BOOL)configureAudioForStream:(SCStreamConfiguration *)config
|
|
139
|
+
includeMicrophone:(BOOL)includeMicrophone
|
|
140
|
+
includeSystemAudio:(BOOL)includeSystemAudio
|
|
141
|
+
microphoneDevice:(NSString *)micDeviceID
|
|
142
|
+
systemAudioDevice:(NSString *)sysDeviceID;
|
|
143
|
+
|
|
144
|
+
@end
|
|
145
|
+
|
|
146
|
+
@implementation SCKAudioConfiguration
|
|
147
|
+
|
|
148
|
+
+ (BOOL)configureAudioForStream:(SCStreamConfiguration *)config
|
|
149
|
+
includeMicrophone:(BOOL)includeMicrophone
|
|
150
|
+
includeSystemAudio:(BOOL)includeSystemAudio
|
|
151
|
+
microphoneDevice:(NSString *)micDeviceID
|
|
152
|
+
systemAudioDevice:(NSString *)sysDeviceID {
|
|
153
|
+
|
|
154
|
+
// Configure system audio capture (requires macOS 13.0+)
|
|
155
|
+
if (@available(macOS 13.0, *)) {
|
|
156
|
+
config.capturesAudio = includeSystemAudio;
|
|
157
|
+
config.excludesCurrentProcessAudio = YES;
|
|
158
|
+
|
|
159
|
+
if (includeSystemAudio) {
|
|
160
|
+
// ScreenCaptureKit will capture system audio from the selected content
|
|
161
|
+
// Quality settings
|
|
162
|
+
config.channelCount = 2; // Stereo
|
|
163
|
+
config.sampleRate = 48000; // 48kHz
|
|
164
|
+
} else {
|
|
165
|
+
config.capturesAudio = NO;
|
|
166
|
+
}
|
|
113
167
|
}
|
|
168
|
+
|
|
169
|
+
return YES;
|
|
114
170
|
}
|
|
115
171
|
|
|
116
|
-
@end
|
|
172
|
+
@end
|
package/src/cursor_tracker.mm
CHANGED
|
@@ -168,7 +168,7 @@ void writeToFile(NSDictionary *cursorData) {
|
|
|
168
168
|
options:0
|
|
169
169
|
error:&error];
|
|
170
170
|
if (jsonData && !error) {
|
|
171
|
-
NSString *jsonString = [[
|
|
171
|
+
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
172
172
|
|
|
173
173
|
if (g_isFirstWrite) {
|
|
174
174
|
// İlk yazma - array başlat
|
|
@@ -292,7 +292,6 @@ void cleanupCursorTracking() {
|
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
if (g_timerTarget) {
|
|
295
|
-
[g_timerTarget autorelease];
|
|
296
295
|
g_timerTarget = nil;
|
|
297
296
|
}
|
|
298
297
|
|
|
@@ -352,12 +351,12 @@ Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
|
|
|
352
351
|
@try {
|
|
353
352
|
// Dosyayı oluştur ve aç
|
|
354
353
|
g_outputPath = [NSString stringWithUTF8String:outputPath.c_str()];
|
|
355
|
-
g_fileHandle = [
|
|
354
|
+
g_fileHandle = [NSFileHandle fileHandleForWritingAtPath:g_outputPath];
|
|
356
355
|
|
|
357
356
|
if (!g_fileHandle) {
|
|
358
357
|
// Dosya yoksa oluştur
|
|
359
358
|
[[NSFileManager defaultManager] createFileAtPath:g_outputPath contents:nil attributes:nil];
|
|
360
|
-
g_fileHandle = [
|
|
359
|
+
g_fileHandle = [NSFileHandle fileHandleForWritingAtPath:g_outputPath];
|
|
361
360
|
}
|
|
362
361
|
|
|
363
362
|
if (!g_fileHandle) {
|