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/.claude/settings.local.json +6 -1
- package/README.md +17 -79
- 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 +15 -10
- package/index.js +22 -62
- package/package.json +3 -8
- package/src/audio_capture.mm +96 -40
- package/src/cursor_tracker.mm +3 -4
- package/src/mac_recorder.mm +604 -715
- package/src/screen_capture.h +5 -0
- package/src/screen_capture.mm +141 -60
- 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 +50 -44
- package/test-sync.js +52 -0
- package/test-windows.js +57 -0
- package/prebuilds/darwin-arm64/node.napi.node +0 -0
- package/prebuilds/darwin-x64/node.napi.node +0 -0
- package/scripts/test-exclude.js +0 -72
- package/src/screen_capture_kit.mm +0 -222
|
@@ -17,7 +17,12 @@
|
|
|
17
17
|
"Bash(touch:*)",
|
|
18
18
|
"Bash(git add:*)",
|
|
19
19
|
"Bash(git commit:*)",
|
|
20
|
-
"Bash(find:*)"
|
|
20
|
+
"Bash(find:*)",
|
|
21
|
+
"WebFetch(domain:developer.apple.com)",
|
|
22
|
+
"WebFetch(domain:github.com)",
|
|
23
|
+
"WebFetch(domain:nonstrict.eu)",
|
|
24
|
+
"Bash(cp:*)",
|
|
25
|
+
"Bash(git checkout:*)"
|
|
21
26
|
],
|
|
22
27
|
"deny": []
|
|
23
28
|
}
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# node-mac-recorder
|
|
2
2
|
|
|
3
|
-
A powerful native macOS screen recording Node.js package with advanced window selection, multi-display support, and granular audio controls. Built with AVFoundation
|
|
3
|
+
A powerful native macOS screen recording Node.js package with advanced window selection, multi-display support, and granular audio controls. Built with AVFoundation for optimal performance.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
@@ -12,7 +12,6 @@ A powerful native macOS screen recording Node.js package with advanced window se
|
|
|
12
12
|
- 🖱️ **Multi-Display Support** - Automatic display detection and selection
|
|
13
13
|
- 🎨 **Cursor Control** - Toggle cursor visibility in recordings
|
|
14
14
|
- 🖱️ **Cursor Tracking** - Track mouse position, cursor types, and click events
|
|
15
|
-
- 🚫 **Exclude Apps/Windows (macOS 15+)** - Use ScreenCaptureKit to exclude specific apps (bundle id/PID) and windows
|
|
16
15
|
|
|
17
16
|
🎵 **Granular Audio Controls**
|
|
18
17
|
|
|
@@ -50,11 +49,6 @@ npm install node-mac-recorder
|
|
|
50
49
|
- **Screen Recording Permission** (automatically requested)
|
|
51
50
|
- **CPU Architecture**: Intel (x64) and Apple Silicon (ARM64) supported
|
|
52
51
|
|
|
53
|
-
ScreenCaptureKit path and exclusion support:
|
|
54
|
-
|
|
55
|
-
- Requires macOS 15.0+ for on-disk recording via `SCRecordingOutput`
|
|
56
|
-
- On older systems (<=14), the library falls back to AVFoundation automatically (exclusions not available)
|
|
57
|
-
|
|
58
52
|
### Build Requirements
|
|
59
53
|
|
|
60
54
|
```bash
|
|
@@ -116,22 +110,9 @@ await recorder.startRecording("./recording.mov", {
|
|
|
116
110
|
quality: "high", // 'low', 'medium', 'high'
|
|
117
111
|
frameRate: 30, // FPS (15, 30, 60)
|
|
118
112
|
captureCursor: false, // Show cursor (default: false)
|
|
119
|
-
|
|
120
|
-
// ScreenCaptureKit (macOS 15+) - optional, backward compatible
|
|
121
|
-
useScreenCaptureKit: false, // If true and available, prefers ScreenCaptureKit
|
|
122
|
-
excludedAppBundleIds: ["com.apple.Safari"], // Exclude by bundle id
|
|
123
|
-
excludedPIDs: [process.pid], // Exclude by PID
|
|
124
|
-
excludedWindowIds: [12345, 67890], // Exclude specific window IDs
|
|
125
|
-
// When running under Electron, autoExcludeSelf defaults to true
|
|
126
|
-
autoExcludeSelf: true,
|
|
127
113
|
});
|
|
128
114
|
```
|
|
129
115
|
|
|
130
|
-
Notes
|
|
131
|
-
|
|
132
|
-
- If any of `excludedAppBundleIds`, `excludedPIDs`, `excludedWindowIds` are provided, the library automatically switches to ScreenCaptureKit on supported macOS versions.
|
|
133
|
-
- When running inside Electron, the current app PID is excluded by default (`autoExcludeSelf: true`).
|
|
134
|
-
|
|
135
116
|
#### `stopRecording()`
|
|
136
117
|
|
|
137
118
|
Stops the current recording.
|
|
@@ -324,29 +305,6 @@ await new Promise((resolve) => setTimeout(resolve, 10000)); // 10 seconds
|
|
|
324
305
|
await recorder.stopRecording();
|
|
325
306
|
```
|
|
326
307
|
|
|
327
|
-
### Exclude Electron app and other windows (ScreenCaptureKit)
|
|
328
|
-
|
|
329
|
-
```javascript
|
|
330
|
-
const recorder = new MacRecorder();
|
|
331
|
-
|
|
332
|
-
// By default, when running inside Electron, your app is auto-excluded.
|
|
333
|
-
// You can also exclude other apps/windows explicitly:
|
|
334
|
-
await recorder.startRecording("./excluded.mov", {
|
|
335
|
-
captureCursor: true,
|
|
336
|
-
// Prefer SC explicitly (optional — auto-enabled when exclusions are set)
|
|
337
|
-
useScreenCaptureKit: true,
|
|
338
|
-
excludedAppBundleIds: ["com.apple.Safari"],
|
|
339
|
-
excludedWindowIds: [
|
|
340
|
-
/* CGWindowID list */
|
|
341
|
-
],
|
|
342
|
-
// autoExcludeSelf is true by default on Electron; set false to include your app
|
|
343
|
-
// autoExcludeSelf: false,
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
// ... later
|
|
347
|
-
await recorder.stopRecording();
|
|
348
|
-
```
|
|
349
|
-
|
|
350
308
|
### Multi-Display Recording
|
|
351
309
|
|
|
352
310
|
```javascript
|
|
@@ -404,17 +362,16 @@ audioDevices.forEach((device, i) => {
|
|
|
404
362
|
});
|
|
405
363
|
|
|
406
364
|
// Find system audio device (like BlackHole, Soundflower, etc.)
|
|
407
|
-
const systemAudioDevice = audioDevices.find(
|
|
408
|
-
(
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
device.name.toLowerCase().includes("aggregate")
|
|
365
|
+
const systemAudioDevice = audioDevices.find(device =>
|
|
366
|
+
device.name.toLowerCase().includes('blackhole') ||
|
|
367
|
+
device.name.toLowerCase().includes('soundflower') ||
|
|
368
|
+
device.name.toLowerCase().includes('loopback') ||
|
|
369
|
+
device.name.toLowerCase().includes('aggregate')
|
|
413
370
|
);
|
|
414
371
|
|
|
415
372
|
if (systemAudioDevice) {
|
|
416
373
|
console.log(`Using system audio device: ${systemAudioDevice.name}`);
|
|
417
|
-
|
|
374
|
+
|
|
418
375
|
// Record with specific system audio device
|
|
419
376
|
await recorder.startRecording("./system-audio-specific.mov", {
|
|
420
377
|
includeMicrophone: false,
|
|
@@ -423,10 +380,8 @@ if (systemAudioDevice) {
|
|
|
423
380
|
captureArea: { x: 0, y: 0, width: 1, height: 1 }, // Minimal video
|
|
424
381
|
});
|
|
425
382
|
} else {
|
|
426
|
-
console.log(
|
|
427
|
-
|
|
428
|
-
);
|
|
429
|
-
|
|
383
|
+
console.log("No system audio device found. Installing BlackHole or Soundflower recommended.");
|
|
384
|
+
|
|
430
385
|
// Record with default system audio capture (may not work without virtual audio device)
|
|
431
386
|
await recorder.startRecording("./system-audio-default.mov", {
|
|
432
387
|
includeMicrophone: false,
|
|
@@ -436,7 +391,7 @@ if (systemAudioDevice) {
|
|
|
436
391
|
}
|
|
437
392
|
|
|
438
393
|
// Record for 10 seconds
|
|
439
|
-
await new Promise(
|
|
394
|
+
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
440
395
|
await recorder.stopRecording();
|
|
441
396
|
```
|
|
442
397
|
|
|
@@ -445,7 +400,7 @@ await recorder.stopRecording();
|
|
|
445
400
|
For reliable system audio capture, install a virtual audio device:
|
|
446
401
|
|
|
447
402
|
1. **BlackHole** (Free): https://github.com/ExistentialAudio/BlackHole
|
|
448
|
-
2. **Soundflower** (Free): https://github.com/mattingalls/Soundflower
|
|
403
|
+
2. **Soundflower** (Free): https://github.com/mattingalls/Soundflower
|
|
449
404
|
3. **Loopback** (Paid): https://rogueamoeba.com/loopback/
|
|
450
405
|
|
|
451
406
|
These create aggregate audio devices that the package can detect and use for system audio capture.
|
|
@@ -765,11 +720,6 @@ npm cache clean --force
|
|
|
765
720
|
xcode-select --install
|
|
766
721
|
```
|
|
767
722
|
|
|
768
|
-
### ScreenCaptureKit availability
|
|
769
|
-
|
|
770
|
-
- Exclusions require macOS 15+ (uses `SCRecordingOutput`).
|
|
771
|
-
- On macOS 12.3–14, the module will fall back to AVFoundation (no exclusions). This is automatic and backward-compatible.
|
|
772
|
-
|
|
773
723
|
### Recording Issues
|
|
774
724
|
|
|
775
725
|
1. **Empty/Black Video**: Check screen recording permissions
|
|
@@ -824,24 +774,12 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
824
774
|
|
|
825
775
|
### Latest Updates
|
|
826
776
|
|
|
827
|
-
- ✅
|
|
828
|
-
- ✅
|
|
829
|
-
- ✅
|
|
830
|
-
- ✅
|
|
831
|
-
- ✅
|
|
832
|
-
- ✅
|
|
833
|
-
- ✅ Display Selection: Multi-monitor support with automatic detection
|
|
834
|
-
- ✅ Smart Filtering: Improved window detection and filtering
|
|
835
|
-
- ✅ Performance: Optimized native implementation
|
|
836
|
-
|
|
837
|
-
## Prebuilt binaries
|
|
838
|
-
|
|
839
|
-
This package ships prebuilt native binaries for macOS:
|
|
840
|
-
|
|
841
|
-
- darwin-arm64 (Apple Silicon)
|
|
842
|
-
- darwin-x64 (Intel)
|
|
843
|
-
|
|
844
|
-
At runtime, the correct binary is loaded automatically via `node-gyp-build`. If a prebuilt is not available for your environment, the module falls back to a local build.
|
|
777
|
+
- ✅ **Cursor Tracking**: Track mouse position, cursor types, and click events with JSON export
|
|
778
|
+
- ✅ **Window Recording**: Automatic coordinate conversion for multi-display setups
|
|
779
|
+
- ✅ **Audio Controls**: Separate microphone and system audio controls
|
|
780
|
+
- ✅ **Display Selection**: Multi-monitor support with automatic detection
|
|
781
|
+
- ✅ **Smart Filtering**: Improved window detection and filtering
|
|
782
|
+
- ✅ **Performance**: Optimized native implementation
|
|
845
783
|
|
|
846
784
|
---
|
|
847
785
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"targets": [
|
|
3
|
+
{
|
|
4
|
+
"target_name": "mac_recorder",
|
|
5
|
+
"sources": [
|
|
6
|
+
"src/mac_recorder.mm",
|
|
7
|
+
"src/screen_capture.mm",
|
|
8
|
+
"src/audio_capture.mm",
|
|
9
|
+
"src/cursor_tracker.mm",
|
|
10
|
+
"src/window_selector.mm"
|
|
11
|
+
],
|
|
12
|
+
"include_dirs": [
|
|
13
|
+
"<!@(node -p \"require('node-addon-api').include\")"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": [
|
|
16
|
+
"<!(node -p \"require('node-addon-api').gyp\")"
|
|
17
|
+
],
|
|
18
|
+
"cflags!": [ "-fno-exceptions" ],
|
|
19
|
+
"cflags_cc!": [ "-fno-exceptions" ],
|
|
20
|
+
"xcode_settings": {
|
|
21
|
+
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
|
|
22
|
+
"CLANG_CXX_LIBRARY": "libc++",
|
|
23
|
+
"MACOSX_DEPLOYMENT_TARGET": "10.15",
|
|
24
|
+
"OTHER_CFLAGS": [
|
|
25
|
+
"-ObjC++"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"link_settings": {
|
|
29
|
+
"libraries": [
|
|
30
|
+
"-framework AVFoundation",
|
|
31
|
+
"-framework CoreMedia",
|
|
32
|
+
"-framework CoreVideo",
|
|
33
|
+
"-framework Foundation",
|
|
34
|
+
"-framework AppKit",
|
|
35
|
+
"-framework ScreenCaptureKit",
|
|
36
|
+
"-framework ApplicationServices",
|
|
37
|
+
"-framework Carbon",
|
|
38
|
+
"-framework Accessibility"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ]
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#import <AVFoundation/AVFoundation.h>
|
|
2
|
+
#import <CoreAudio/CoreAudio.h>
|
|
3
|
+
|
|
4
|
+
@interface AudioCapture : NSObject
|
|
5
|
+
|
|
6
|
+
+ (NSArray *)getAudioDevices;
|
|
7
|
+
+ (BOOL)hasAudioPermission;
|
|
8
|
+
+ (void)requestAudioPermission:(void(^)(BOOL granted))completion;
|
|
9
|
+
|
|
10
|
+
@end
|
|
11
|
+
|
|
12
|
+
@implementation AudioCapture
|
|
13
|
+
|
|
14
|
+
+ (NSArray *)getAudioDevices {
|
|
15
|
+
NSMutableArray *devices = [NSMutableArray array];
|
|
16
|
+
|
|
17
|
+
// Get all audio devices
|
|
18
|
+
NSArray *audioDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
|
|
19
|
+
|
|
20
|
+
for (AVCaptureDevice *device in audioDevices) {
|
|
21
|
+
NSDictionary *deviceInfo = @{
|
|
22
|
+
@"id": device.uniqueID,
|
|
23
|
+
@"name": device.localizedName,
|
|
24
|
+
@"manufacturer": device.manufacturer ?: @"Unknown",
|
|
25
|
+
@"isDefault": @([device isEqual:[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]])
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
[devices addObject:deviceInfo];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Also get system audio devices using Core Audio
|
|
32
|
+
AudioObjectPropertyAddress propertyAddress = {
|
|
33
|
+
kAudioHardwarePropertyDevices,
|
|
34
|
+
kAudioObjectPropertyScopeGlobal,
|
|
35
|
+
kAudioObjectPropertyElementMaster
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
UInt32 dataSize = 0;
|
|
39
|
+
OSStatus status = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize);
|
|
40
|
+
|
|
41
|
+
if (status == kAudioHardwareNoError) {
|
|
42
|
+
UInt32 deviceCount = dataSize / sizeof(AudioDeviceID);
|
|
43
|
+
AudioDeviceID *audioDeviceIDs = (AudioDeviceID *)malloc(dataSize);
|
|
44
|
+
|
|
45
|
+
status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize, audioDeviceIDs);
|
|
46
|
+
|
|
47
|
+
if (status == kAudioHardwareNoError) {
|
|
48
|
+
for (UInt32 i = 0; i < deviceCount; i++) {
|
|
49
|
+
AudioDeviceID deviceID = audioDeviceIDs[i];
|
|
50
|
+
|
|
51
|
+
// Get device name
|
|
52
|
+
propertyAddress.mSelector = kAudioDevicePropertyDeviceNameCFString;
|
|
53
|
+
propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
|
|
54
|
+
|
|
55
|
+
CFStringRef deviceName = NULL;
|
|
56
|
+
dataSize = sizeof(deviceName);
|
|
57
|
+
|
|
58
|
+
status = AudioObjectGetPropertyData(deviceID, &propertyAddress, 0, NULL, &dataSize, &deviceName);
|
|
59
|
+
|
|
60
|
+
if (status == kAudioHardwareNoError && deviceName) {
|
|
61
|
+
// Check if it's an input device
|
|
62
|
+
propertyAddress.mSelector = kAudioDevicePropertyStreamConfiguration;
|
|
63
|
+
propertyAddress.mScope = kAudioDevicePropertyScopeInput;
|
|
64
|
+
|
|
65
|
+
AudioObjectGetPropertyDataSize(deviceID, &propertyAddress, 0, NULL, &dataSize);
|
|
66
|
+
|
|
67
|
+
if (dataSize > 0) {
|
|
68
|
+
AudioBufferList *bufferList = (AudioBufferList *)malloc(dataSize);
|
|
69
|
+
AudioObjectGetPropertyData(deviceID, &propertyAddress, 0, NULL, &dataSize, bufferList);
|
|
70
|
+
|
|
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
|
+
}
|
|
81
|
+
|
|
82
|
+
free(bufferList);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
CFRelease(deviceName);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
free(audioDeviceIDs);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return [devices copy];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
+ (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
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
+ (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(), ^{
|
|
108
|
+
completion(granted);
|
|
109
|
+
});
|
|
110
|
+
}];
|
|
111
|
+
} else {
|
|
112
|
+
completion(YES);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@end
|