node-mac-recorder 2.1.3 → 2.4.0

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
@@ -5,7 +5,6 @@
5
5
  "sources": [
6
6
  "src/mac_recorder.mm",
7
7
  "src/screen_capture.mm",
8
- "src/screen_capture_kit.mm",
9
8
  "src/audio_capture.mm",
10
9
  "src/cursor_tracker.mm",
11
10
  "src/window_selector.mm"
@@ -22,25 +21,31 @@
22
21
  "GCC_ENABLE_CPP_EXCEPTIONS": "YES",
23
22
  "CLANG_CXX_LIBRARY": "libc++",
24
23
  "MACOSX_DEPLOYMENT_TARGET": "12.3",
24
+ "ARCHS": ["arm64"],
25
+ "VALID_ARCHS": ["arm64"],
25
26
  "OTHER_CFLAGS": [
26
- "-ObjC++"
27
- ]
27
+ "-ObjC++",
28
+ "-fmodules"
29
+ ],
30
+ "CLANG_ENABLE_OBJC_ARC": "YES"
28
31
  },
29
32
  "link_settings": {
30
33
  "libraries": [
34
+ "-framework ScreenCaptureKit",
35
+ "-framework AVFoundation",
36
+ "-framework CoreMedia",
37
+ "-framework CoreVideo",
31
38
  "-framework Foundation",
32
39
  "-framework AppKit",
33
- "-framework ScreenCaptureKit",
34
40
  "-framework ApplicationServices",
35
41
  "-framework Carbon",
36
- "-framework Accessibility",
37
- "-framework CoreAudio",
38
- "-framework AVFoundation",
39
- "-framework CoreMedia",
40
- "-framework CoreVideo"
42
+ "-framework Accessibility"
41
43
  ]
42
44
  },
43
- "defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ]
45
+ "defines": [
46
+ "NAPI_DISABLE_CPP_EXCEPTIONS",
47
+ "USE_SCREENCAPTUREKIT=1"
48
+ ]
44
49
  }
45
50
  ]
46
51
  }
package/index.js CHANGED
@@ -2,24 +2,19 @@ const { EventEmitter } = require("events");
2
2
  const path = require("path");
3
3
  const fs = require("fs");
4
4
 
5
- // Native modülü yükle (prebuilt > local build fallback)
5
+ // Native modülü yükle
6
6
  let nativeBinding;
7
7
  try {
8
- // Prebuilt
9
- nativeBinding = require("node-gyp-build")(__dirname);
10
- } catch (e) {
8
+ nativeBinding = require("./build/Release/mac_recorder.node");
9
+ } catch (error) {
11
10
  try {
12
- nativeBinding = require("./build/Release/mac_recorder.node");
13
- } catch (error) {
14
- try {
15
- nativeBinding = require("./build/Debug/mac_recorder.node");
16
- } catch (debugError) {
17
- throw new Error(
18
- 'Native module not found. Please run "npm run build" to compile the native module.\n' +
19
- "Original error: " +
20
- (error?.message || e?.message)
21
- );
22
- }
11
+ nativeBinding = require("./build/Debug/mac_recorder.node");
12
+ } catch (debugError) {
13
+ throw new Error(
14
+ 'Native module not found. Please run "npm run build" to compile the native module.\n' +
15
+ "Original error: " +
16
+ error.message
17
+ );
23
18
  }
24
19
  }
25
20
 
@@ -50,12 +45,6 @@ class MacRecorder extends EventEmitter {
50
45
  showClicks: false,
51
46
  displayId: null, // Hangi ekranı kaydedeceği (null = ana ekran)
52
47
  windowId: null, // Hangi pencereyi kaydedeceği (null = tam ekran)
53
- // SC (gizli, opsiyonel) - mevcut kullanıcıları bozmaz
54
- useScreenCaptureKit: false,
55
- excludedAppBundleIds: [],
56
- excludedPIDs: [],
57
- excludedWindowIds: [],
58
- autoExcludeSelf: !!(process.versions && process.versions.electron),
59
48
  };
60
49
 
61
50
  // Display cache için async initialization
@@ -129,14 +118,11 @@ class MacRecorder extends EventEmitter {
129
118
  audioDeviceId: options.audioDeviceId || null, // null = default device
130
119
  systemAudioDeviceId: options.systemAudioDeviceId || null, // null = auto-detect system audio device
131
120
  captureArea: options.captureArea || null,
132
- useScreenCaptureKit: options.useScreenCaptureKit || false,
133
- excludedAppBundleIds: options.excludedAppBundleIds || [],
134
- excludedPIDs: options.excludedPIDs || [],
135
- excludedWindowIds: options.excludedWindowIds || [],
136
- autoExcludeSelf:
137
- typeof options.autoExcludeSelf === "boolean"
138
- ? options.autoExcludeSelf
139
- : !!(process.versions && process.versions.electron),
121
+ // Exclusion options
122
+ excludeCurrentApp: options.excludeCurrentApp || false,
123
+ excludeWindowIds: Array.isArray(options.excludeWindowIds)
124
+ ? options.excludeWindowIds
125
+ : [],
140
126
  };
141
127
  }
142
128
 
@@ -297,11 +283,9 @@ class MacRecorder extends EventEmitter {
297
283
  windowId: this.options.windowId || null, // null = tam ekran
298
284
  audioDeviceId: this.options.audioDeviceId || null, // null = default device
299
285
  systemAudioDeviceId: this.options.systemAudioDeviceId || null, // null = auto-detect system audio device
300
- useScreenCaptureKit: this.options.useScreenCaptureKit || false,
301
- excludedAppBundleIds: this.options.excludedAppBundleIds || [],
302
- excludedPIDs: this.options.excludedPIDs || [],
303
- excludedWindowIds: this.options.excludedWindowIds || [],
304
- autoExcludeSelf: this.options.autoExcludeSelf === true,
286
+ // Exclusion options passthrough
287
+ excludeCurrentApp: this.options.excludeCurrentApp || false,
288
+ excludeWindowIds: this.options.excludeWindowIds || [],
305
289
  };
306
290
 
307
291
  // Manuel captureArea varsa onu kullan
@@ -314,34 +298,10 @@ class MacRecorder extends EventEmitter {
314
298
  };
315
299
  }
316
300
 
317
- // SC yolu: kullanıcıdan SC talebi varsa veya exclude listeleri doluysa ve SC mevcutsa otomatik kullan
318
- let success;
319
- try {
320
- const wantsSC = !!(
321
- this.options.useScreenCaptureKit ||
322
- this.options.excludedAppBundleIds?.length ||
323
- this.options.excludedPIDs?.length ||
324
- this.options.excludedWindowIds?.length
325
- );
326
- const scAvailable =
327
- typeof nativeBinding.isScreenCaptureKitAvailable === "function" &&
328
- nativeBinding.isScreenCaptureKitAvailable();
329
- if (wantsSC && scAvailable) {
330
- const scOptions = {
331
- ...recordingOptions,
332
- useScreenCaptureKit: true,
333
- };
334
- success = nativeBinding.startRecording(outputPath, scOptions);
335
- } else {
336
- success = nativeBinding.startRecording(
337
- outputPath,
338
- recordingOptions
339
- );
340
- }
341
- } catch (e) {
342
- // Fallback AVFoundation
343
- success = nativeBinding.startRecording(outputPath, recordingOptions);
344
- }
301
+ const success = nativeBinding.startRecording(
302
+ outputPath,
303
+ recordingOptions
304
+ );
345
305
 
346
306
  if (success) {
347
307
  this.isRecording = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.1.3",
3
+ "version": "2.4.0",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -38,19 +38,14 @@
38
38
  "build": "node-gyp build",
39
39
  "rebuild": "node-gyp rebuild",
40
40
  "clean": "node-gyp clean",
41
- "prebuild:darwin:arm64": "prebuildify --napi --strip --platform darwin --arch arm64",
42
- "prebuild:darwin:x64": "prebuildify --napi --strip --platform darwin --arch x64",
43
- "prebuild": "npm run prebuild:darwin:arm64 && npm run prebuild:darwin:x64",
44
41
  "test:window-selector": "node window-selector-test.js",
45
42
  "example:window-selector": "node examples/window-selector-example.js"
46
43
  },
47
44
  "dependencies": {
48
- "node-addon-api": "^7.0.0",
49
- "node-gyp-build": "^4.6.0"
45
+ "node-addon-api": "^7.0.0"
50
46
  },
51
47
  "devDependencies": {
52
- "node-gyp": "^10.0.0",
53
- "prebuildify": "^5.0.0"
48
+ "node-gyp": "^10.0.0"
54
49
  },
55
50
  "gypfile": true
56
51
  }
@@ -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) {