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/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": "10.15",
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": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ]
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.1.2",
3
+ "version": "2.2.1",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
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
+ });
@@ -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 all audio devices
18
- NSArray *audioDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
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
- // Also get system audio devices using Core Audio
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
- dataSize = sizeof(deviceName);
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, &propertyAddress, 0, NULL, &dataSize, &deviceName);
76
+ status = AudioObjectGetPropertyData(deviceID, &nameAddress, 0, NULL, &size, &deviceName);
59
77
 
60
78
  if (status == kAudioHardwareNoError && deviceName) {
61
- // Check if it's an input device
62
- propertyAddress.mSelector = kAudioDevicePropertyStreamConfiguration;
63
- propertyAddress.mScope = kAudioDevicePropertyScopeInput;
79
+ // Check if this is an output device
80
+ AudioObjectPropertyAddress streamAddress = {
81
+ kAudioDevicePropertyStreams,
82
+ kAudioDevicePropertyScopeOutput,
83
+ kAudioObjectPropertyElementMain
84
+ };
64
85
 
65
- AudioObjectGetPropertyDataSize(deviceID, &propertyAddress, 0, NULL, &dataSize);
86
+ UInt32 streamSize = 0;
87
+ status = AudioObjectGetPropertyDataSize(deviceID, &streamAddress, 0, NULL, &streamSize);
66
88
 
67
- if (dataSize > 0) {
68
- AudioBufferList *bufferList = (AudioBufferList *)malloc(dataSize);
69
- AudioObjectGetPropertyData(deviceID, &propertyAddress, 0, NULL, &dataSize, bufferList);
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
- if (bufferList->mNumberBuffers > 0) {
72
- NSDictionary *deviceInfo = @{
73
- @"id": @(deviceID),
74
- @"name": (__bridge NSString *)deviceName,
75
- @"type": @"System Audio Input",
76
- @"isSystemDevice": @YES
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
- free(bufferList);
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
- if (@available(macOS 10.14, *)) {
98
- AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
99
- return status == AVAuthorizationStatusAuthorized;
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
- if (@available(macOS 10.14, *)) {
106
- [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {
107
- dispatch_async(dispatch_get_main_queue(), ^{
123
+ [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {
124
+ dispatch_async(dispatch_get_main_queue(), ^{
125
+ if (completion) {
108
126
  completion(granted);
109
- });
110
- }];
111
- } else {
112
- completion(YES);
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
@@ -168,7 +168,7 @@ void writeToFile(NSDictionary *cursorData) {
168
168
  options:0
169
169
  error:&error];
170
170
  if (jsonData && !error) {
171
- NSString *jsonString = [[[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] autorelease];
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 = [[NSFileHandle fileHandleForWritingAtPath:g_outputPath] retain];
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 = [[NSFileHandle fileHandleForWritingAtPath:g_outputPath] retain];
359
+ g_fileHandle = [NSFileHandle fileHandleForWritingAtPath:g_outputPath];
361
360
  }
362
361
 
363
362
  if (!g_fileHandle) {