node-mac-recorder 2.1.1 → 2.1.3
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 +2 -1
- package/README.md +79 -17
- package/binding.gyp +6 -5
- 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/mac_recorder.mm +160 -212
- package/src/screen_capture.mm +1 -3
- package/src/screen_capture_kit.mm +41 -66
- package/test-sck.js +50 -0
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 for optimal performance.
|
|
3
|
+
A powerful native macOS screen recording Node.js package with advanced window selection, multi-display support, and granular audio controls. Built with AVFoundation and ScreenCaptureKit for optimal performance.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
@@ -12,6 +12,7 @@ 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
|
|
15
16
|
|
|
16
17
|
🎵 **Granular Audio Controls**
|
|
17
18
|
|
|
@@ -49,6 +50,11 @@ npm install node-mac-recorder
|
|
|
49
50
|
- **Screen Recording Permission** (automatically requested)
|
|
50
51
|
- **CPU Architecture**: Intel (x64) and Apple Silicon (ARM64) supported
|
|
51
52
|
|
|
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
|
+
|
|
52
58
|
### Build Requirements
|
|
53
59
|
|
|
54
60
|
```bash
|
|
@@ -110,9 +116,22 @@ await recorder.startRecording("./recording.mov", {
|
|
|
110
116
|
quality: "high", // 'low', 'medium', 'high'
|
|
111
117
|
frameRate: 30, // FPS (15, 30, 60)
|
|
112
118
|
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,
|
|
113
127
|
});
|
|
114
128
|
```
|
|
115
129
|
|
|
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
|
+
|
|
116
135
|
#### `stopRecording()`
|
|
117
136
|
|
|
118
137
|
Stops the current recording.
|
|
@@ -305,6 +324,29 @@ await new Promise((resolve) => setTimeout(resolve, 10000)); // 10 seconds
|
|
|
305
324
|
await recorder.stopRecording();
|
|
306
325
|
```
|
|
307
326
|
|
|
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
|
+
|
|
308
350
|
### Multi-Display Recording
|
|
309
351
|
|
|
310
352
|
```javascript
|
|
@@ -362,16 +404,17 @@ audioDevices.forEach((device, i) => {
|
|
|
362
404
|
});
|
|
363
405
|
|
|
364
406
|
// Find system audio device (like BlackHole, Soundflower, etc.)
|
|
365
|
-
const systemAudioDevice = audioDevices.find(
|
|
366
|
-
device
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
407
|
+
const systemAudioDevice = audioDevices.find(
|
|
408
|
+
(device) =>
|
|
409
|
+
device.name.toLowerCase().includes("blackhole") ||
|
|
410
|
+
device.name.toLowerCase().includes("soundflower") ||
|
|
411
|
+
device.name.toLowerCase().includes("loopback") ||
|
|
412
|
+
device.name.toLowerCase().includes("aggregate")
|
|
370
413
|
);
|
|
371
414
|
|
|
372
415
|
if (systemAudioDevice) {
|
|
373
416
|
console.log(`Using system audio device: ${systemAudioDevice.name}`);
|
|
374
|
-
|
|
417
|
+
|
|
375
418
|
// Record with specific system audio device
|
|
376
419
|
await recorder.startRecording("./system-audio-specific.mov", {
|
|
377
420
|
includeMicrophone: false,
|
|
@@ -380,8 +423,10 @@ if (systemAudioDevice) {
|
|
|
380
423
|
captureArea: { x: 0, y: 0, width: 1, height: 1 }, // Minimal video
|
|
381
424
|
});
|
|
382
425
|
} else {
|
|
383
|
-
console.log(
|
|
384
|
-
|
|
426
|
+
console.log(
|
|
427
|
+
"No system audio device found. Installing BlackHole or Soundflower recommended."
|
|
428
|
+
);
|
|
429
|
+
|
|
385
430
|
// Record with default system audio capture (may not work without virtual audio device)
|
|
386
431
|
await recorder.startRecording("./system-audio-default.mov", {
|
|
387
432
|
includeMicrophone: false,
|
|
@@ -391,7 +436,7 @@ if (systemAudioDevice) {
|
|
|
391
436
|
}
|
|
392
437
|
|
|
393
438
|
// Record for 10 seconds
|
|
394
|
-
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
439
|
+
await new Promise((resolve) => setTimeout(resolve, 10000));
|
|
395
440
|
await recorder.stopRecording();
|
|
396
441
|
```
|
|
397
442
|
|
|
@@ -400,7 +445,7 @@ await recorder.stopRecording();
|
|
|
400
445
|
For reliable system audio capture, install a virtual audio device:
|
|
401
446
|
|
|
402
447
|
1. **BlackHole** (Free): https://github.com/ExistentialAudio/BlackHole
|
|
403
|
-
2. **Soundflower** (Free): https://github.com/mattingalls/Soundflower
|
|
448
|
+
2. **Soundflower** (Free): https://github.com/mattingalls/Soundflower
|
|
404
449
|
3. **Loopback** (Paid): https://rogueamoeba.com/loopback/
|
|
405
450
|
|
|
406
451
|
These create aggregate audio devices that the package can detect and use for system audio capture.
|
|
@@ -720,6 +765,11 @@ npm cache clean --force
|
|
|
720
765
|
xcode-select --install
|
|
721
766
|
```
|
|
722
767
|
|
|
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
|
+
|
|
723
773
|
### Recording Issues
|
|
724
774
|
|
|
725
775
|
1. **Empty/Black Video**: Check screen recording permissions
|
|
@@ -774,12 +824,24 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
774
824
|
|
|
775
825
|
### Latest Updates
|
|
776
826
|
|
|
777
|
-
- ✅
|
|
778
|
-
- ✅
|
|
779
|
-
- ✅
|
|
780
|
-
- ✅
|
|
781
|
-
- ✅
|
|
782
|
-
- ✅
|
|
827
|
+
- ✅ ScreenCaptureKit path with exclusions (apps by bundle id/PID and window IDs) on macOS 15+
|
|
828
|
+
- ✅ Electron apps auto-excluded by default (can be disabled with `autoExcludeSelf: false`)
|
|
829
|
+
- ✅ Prebuilt binaries for darwin-arm64 and darwin-x64; automatic loading via `node-gyp-build`
|
|
830
|
+
- ✅ Cursor Tracking: Track mouse position, cursor types, and click events with JSON export
|
|
831
|
+
- ✅ Window Recording: Automatic coordinate conversion for multi-display setups
|
|
832
|
+
- ✅ Audio Controls: Separate microphone and system audio controls
|
|
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.
|
|
783
845
|
|
|
784
846
|
---
|
|
785
847
|
|
package/binding.gyp
CHANGED
|
@@ -21,22 +21,23 @@
|
|
|
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
25
|
"OTHER_CFLAGS": [
|
|
26
26
|
"-ObjC++"
|
|
27
27
|
]
|
|
28
28
|
},
|
|
29
29
|
"link_settings": {
|
|
30
30
|
"libraries": [
|
|
31
|
-
"-framework AVFoundation",
|
|
32
|
-
"-framework CoreMedia",
|
|
33
|
-
"-framework CoreVideo",
|
|
34
31
|
"-framework Foundation",
|
|
35
32
|
"-framework AppKit",
|
|
36
33
|
"-framework ScreenCaptureKit",
|
|
37
34
|
"-framework ApplicationServices",
|
|
38
35
|
"-framework Carbon",
|
|
39
|
-
"-framework Accessibility"
|
|
36
|
+
"-framework Accessibility",
|
|
37
|
+
"-framework CoreAudio",
|
|
38
|
+
"-framework AVFoundation",
|
|
39
|
+
"-framework CoreMedia",
|
|
40
|
+
"-framework CoreVideo"
|
|
40
41
|
]
|
|
41
42
|
},
|
|
42
43
|
"defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ]
|
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/mac_recorder.mm
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#import <napi.h>
|
|
2
|
-
#import <
|
|
3
|
-
#import <CoreMedia/CoreMedia.h>
|
|
2
|
+
#import <ScreenCaptureKit/ScreenCaptureKit.h>
|
|
4
3
|
#import <AppKit/AppKit.h>
|
|
5
4
|
#import <Foundation/Foundation.h>
|
|
6
5
|
#import <CoreGraphics/CoreGraphics.h>
|
|
@@ -17,38 +16,32 @@ Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports);
|
|
|
17
16
|
// Window selector function declarations
|
|
18
17
|
Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports);
|
|
19
18
|
|
|
20
|
-
@interface MacRecorderDelegate : NSObject
|
|
19
|
+
@interface MacRecorderDelegate : NSObject
|
|
21
20
|
@property (nonatomic, copy) void (^completionHandler)(NSURL *outputURL, NSError *error);
|
|
22
21
|
@end
|
|
23
22
|
|
|
24
23
|
@implementation MacRecorderDelegate
|
|
25
|
-
- (void)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
- (void)recordingDidStart {
|
|
25
|
+
NSLog(@"[mac_recorder] ScreenCaptureKit recording started");
|
|
26
|
+
}
|
|
27
|
+
- (void)recordingDidFinish:(NSURL *)outputURL error:(NSError *)error {
|
|
28
|
+
if (error) {
|
|
29
|
+
NSLog(@"[mac_recorder] ScreenCaptureKit recording finished with error: %@", error.localizedDescription);
|
|
30
|
+
} else {
|
|
31
|
+
NSLog(@"[mac_recorder] ScreenCaptureKit recording finished OK → %@", outputURL.path);
|
|
32
|
+
}
|
|
29
33
|
if (self.completionHandler) {
|
|
30
|
-
self.completionHandler(
|
|
34
|
+
self.completionHandler(outputURL, error);
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
37
|
@end
|
|
34
38
|
|
|
35
39
|
// 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
40
|
static MacRecorderDelegate *g_delegate = nil;
|
|
41
41
|
static bool g_isRecording = false;
|
|
42
42
|
|
|
43
43
|
// Helper function to cleanup recording resources
|
|
44
44
|
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
45
|
g_delegate = nil;
|
|
53
46
|
g_isRecording = false;
|
|
54
47
|
}
|
|
@@ -67,6 +60,7 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
67
60
|
}
|
|
68
61
|
|
|
69
62
|
std::string outputPath = info[0].As<Napi::String>().Utf8Value();
|
|
63
|
+
NSLog(@"[mac_recorder] StartRecording: output=%@", [NSString stringWithUTF8String:outputPath.c_str()]);
|
|
70
64
|
|
|
71
65
|
// Options parsing (shared)
|
|
72
66
|
CGRect captureRect = CGRectNull;
|
|
@@ -203,9 +197,10 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
203
197
|
}
|
|
204
198
|
|
|
205
199
|
@try {
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
if (
|
|
200
|
+
// Always prefer ScreenCaptureKit if available
|
|
201
|
+
NSLog(@"[mac_recorder] Checking ScreenCaptureKit availability");
|
|
202
|
+
if (@available(macOS 12.3, *)) {
|
|
203
|
+
if ([ScreenCaptureKitRecorder isScreenCaptureKitAvailable]) {
|
|
209
204
|
NSMutableDictionary *scConfig = [@{} mutableCopy];
|
|
210
205
|
scConfig[@"displayId"] = @(displayID);
|
|
211
206
|
if (!CGRectIsNull(captureRect)) {
|
|
@@ -233,169 +228,27 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
233
228
|
scConfig[@"outputPath"] = [NSString stringWithUTF8String:outputPathStr.c_str()];
|
|
234
229
|
|
|
235
230
|
NSError *scErr = nil;
|
|
236
|
-
|
|
231
|
+
NSLog(@"[mac_recorder] Using ScreenCaptureKit path (displayId=%u)", displayID);
|
|
232
|
+
|
|
233
|
+
// Create and set up delegate
|
|
234
|
+
g_delegate = [[MacRecorderDelegate alloc] init];
|
|
235
|
+
|
|
236
|
+
BOOL ok = [ScreenCaptureKitRecorder startRecordingWithConfiguration:scConfig delegate:g_delegate error:&scErr];
|
|
237
237
|
if (ok) {
|
|
238
238
|
g_isRecording = true;
|
|
239
|
+
NSLog(@"[mac_recorder] ScreenCaptureKit startRecording → OK");
|
|
239
240
|
return Napi::Boolean::New(env, true);
|
|
240
241
|
}
|
|
241
|
-
|
|
242
|
-
}
|
|
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
|
-
|
|
250
|
-
// Create screen input with selected display
|
|
251
|
-
g_screenInput = [[AVCaptureScreenInput alloc] initWithDisplayID:displayID];
|
|
252
|
-
|
|
253
|
-
if (!CGRectIsNull(captureRect)) {
|
|
254
|
-
g_screenInput.cropRect = captureRect;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Set cursor capture
|
|
258
|
-
g_screenInput.capturesCursor = captureCursor;
|
|
259
|
-
|
|
260
|
-
if ([g_captureSession canAddInput:g_screenInput]) {
|
|
261
|
-
[g_captureSession addInput:g_screenInput];
|
|
262
|
-
} else {
|
|
242
|
+
NSLog(@"[mac_recorder] ScreenCaptureKit startRecording → FAIL: %@", scErr.localizedDescription);
|
|
263
243
|
cleanupRecording();
|
|
264
244
|
return Napi::Boolean::New(env, false);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Add microphone input if requested
|
|
268
|
-
if (includeMicrophone) {
|
|
269
|
-
AVCaptureDevice *audioDevice = nil;
|
|
270
|
-
|
|
271
|
-
if (audioDeviceId) {
|
|
272
|
-
// Try to find the specified device
|
|
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");
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Fallback to default device if specified device not found
|
|
291
|
-
if (!audioDevice) {
|
|
292
|
-
audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
|
|
293
|
-
NSLog(@"[DEBUG] Using default audio device: %@ (ID: %@)", audioDevice.localizedName, audioDevice.uniqueID);
|
|
294
245
|
}
|
|
295
|
-
|
|
296
|
-
if (audioDevice) {
|
|
297
|
-
NSError *error;
|
|
298
|
-
g_audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:&error];
|
|
299
|
-
if (g_audioInput && [g_captureSession canAddInput:g_audioInput]) {
|
|
300
|
-
[g_captureSession addInput:g_audioInput];
|
|
301
|
-
NSLog(@"[DEBUG] Successfully added audio input device");
|
|
302
|
-
} else {
|
|
303
|
-
NSLog(@"[DEBUG] Failed to add audio input device: %@", error);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// System audio configuration
|
|
309
|
-
if (includeSystemAudio) {
|
|
310
|
-
// Enable audio capture in screen input
|
|
311
|
-
g_screenInput.capturesMouseClicks = YES;
|
|
312
|
-
|
|
313
|
-
// Try to add system audio input using Core Audio
|
|
314
|
-
// This approach captures system audio by creating a virtual audio device
|
|
315
|
-
if (@available(macOS 10.15, *)) {
|
|
316
|
-
// Configure screen input for better audio capture
|
|
317
|
-
g_screenInput.capturesCursor = captureCursor;
|
|
318
|
-
g_screenInput.capturesMouseClicks = YES;
|
|
319
|
-
|
|
320
|
-
// Try to find and add system audio device (like Soundflower, BlackHole, etc.)
|
|
321
|
-
NSArray *audioDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
|
|
322
|
-
AVCaptureDevice *systemAudioDevice = nil;
|
|
323
|
-
|
|
324
|
-
// If specific system audio device ID is provided, try to find it first
|
|
325
|
-
if (systemAudioDeviceId) {
|
|
326
|
-
for (AVCaptureDevice *device in audioDevices) {
|
|
327
|
-
if ([device.uniqueID isEqualToString:systemAudioDeviceId]) {
|
|
328
|
-
systemAudioDevice = device;
|
|
329
|
-
NSLog(@"[DEBUG] Found specified system audio device: %@ (ID: %@)", device.localizedName, device.uniqueID);
|
|
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;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
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
|
-
}
|
|
370
|
-
} else {
|
|
371
|
-
// Explicitly disable audio capture if not requested
|
|
372
|
-
g_screenInput.capturesMouseClicks = NO;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Create movie file output
|
|
376
|
-
g_movieFileOutput = [[AVCaptureMovieFileOutput alloc] init];
|
|
377
|
-
if ([g_captureSession canAddOutput:g_movieFileOutput]) {
|
|
378
|
-
[g_captureSession addOutput:g_movieFileOutput];
|
|
379
246
|
} else {
|
|
247
|
+
NSLog(@"[mac_recorder] ScreenCaptureKit not available");
|
|
380
248
|
cleanupRecording();
|
|
381
249
|
return Napi::Boolean::New(env, false);
|
|
382
250
|
}
|
|
383
251
|
|
|
384
|
-
[g_captureSession commitConfiguration];
|
|
385
|
-
|
|
386
|
-
// Start session
|
|
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
252
|
} @catch (NSException *exception) {
|
|
400
253
|
cleanupRecording();
|
|
401
254
|
return Napi::Boolean::New(env, false);
|
|
@@ -411,15 +264,16 @@ Napi::Value StopRecording(const Napi::CallbackInfo& info) {
|
|
|
411
264
|
}
|
|
412
265
|
|
|
413
266
|
@try {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
[ScreenCaptureKitRecorder stopRecording];
|
|
267
|
+
NSLog(@"[mac_recorder] StopRecording called");
|
|
268
|
+
|
|
269
|
+
// Stop ScreenCaptureKit recording
|
|
270
|
+
NSLog(@"[mac_recorder] Stopping ScreenCaptureKit stream");
|
|
271
|
+
if (@available(macOS 12.3, *)) {
|
|
272
|
+
[ScreenCaptureKitRecorder stopRecording];
|
|
273
|
+
}
|
|
422
274
|
g_isRecording = false;
|
|
275
|
+
cleanupRecording();
|
|
276
|
+
NSLog(@"[mac_recorder] ScreenCaptureKit stopped");
|
|
423
277
|
return Napi::Boolean::New(env, true);
|
|
424
278
|
|
|
425
279
|
} @catch (NSException *exception) {
|
|
@@ -536,18 +390,102 @@ Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
|
|
|
536
390
|
@try {
|
|
537
391
|
NSMutableArray *devices = [NSMutableArray array];
|
|
538
392
|
|
|
539
|
-
//
|
|
540
|
-
|
|
393
|
+
// Use CoreAudio to get audio devices since we're removing AVFoundation
|
|
394
|
+
AudioObjectPropertyAddress propertyAddress = {
|
|
395
|
+
kAudioHardwarePropertyDevices,
|
|
396
|
+
kAudioObjectPropertyScopeGlobal,
|
|
397
|
+
kAudioObjectPropertyElementMain
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
UInt32 dataSize = 0;
|
|
401
|
+
OSStatus status = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize);
|
|
402
|
+
if (status != noErr) {
|
|
403
|
+
return Napi::Array::New(env, 0);
|
|
404
|
+
}
|
|
541
405
|
|
|
542
|
-
|
|
406
|
+
UInt32 deviceCount = dataSize / sizeof(AudioDeviceID);
|
|
407
|
+
AudioDeviceID *audioDevices = (AudioDeviceID *)malloc(dataSize);
|
|
408
|
+
|
|
409
|
+
status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize, audioDevices);
|
|
410
|
+
if (status != noErr) {
|
|
411
|
+
free(audioDevices);
|
|
412
|
+
return Napi::Array::New(env, 0);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
for (UInt32 i = 0; i < deviceCount; i++) {
|
|
416
|
+
AudioDeviceID deviceID = audioDevices[i];
|
|
417
|
+
|
|
418
|
+
// Check if device has input streams
|
|
419
|
+
AudioObjectPropertyAddress streamsAddress = {
|
|
420
|
+
kAudioDevicePropertyStreams,
|
|
421
|
+
kAudioDevicePropertyScopeInput,
|
|
422
|
+
kAudioObjectPropertyElementMain
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
UInt32 streamsSize = 0;
|
|
426
|
+
status = AudioObjectGetPropertyDataSize(deviceID, &streamsAddress, 0, NULL, &streamsSize);
|
|
427
|
+
if (status != noErr || streamsSize == 0) {
|
|
428
|
+
continue; // Skip output-only devices
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Get device name
|
|
432
|
+
AudioObjectPropertyAddress nameAddress = {
|
|
433
|
+
kAudioDevicePropertyDeviceNameCFString,
|
|
434
|
+
kAudioObjectPropertyScopeGlobal,
|
|
435
|
+
kAudioObjectPropertyElementMain
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
CFStringRef deviceNameRef = NULL;
|
|
439
|
+
UInt32 nameSize = sizeof(CFStringRef);
|
|
440
|
+
status = AudioObjectGetPropertyData(deviceID, &nameAddress, 0, NULL, &nameSize, &deviceNameRef);
|
|
441
|
+
|
|
442
|
+
NSString *deviceName = @"Unknown Device";
|
|
443
|
+
if (status == noErr && deviceNameRef) {
|
|
444
|
+
deviceName = (__bridge NSString *)deviceNameRef;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Get device UID
|
|
448
|
+
AudioObjectPropertyAddress uidAddress = {
|
|
449
|
+
kAudioDevicePropertyDeviceUID,
|
|
450
|
+
kAudioObjectPropertyScopeGlobal,
|
|
451
|
+
kAudioObjectPropertyElementMain
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
CFStringRef deviceUIDRef = NULL;
|
|
455
|
+
UInt32 uidSize = sizeof(CFStringRef);
|
|
456
|
+
status = AudioObjectGetPropertyData(deviceID, &uidAddress, 0, NULL, &uidSize, &deviceUIDRef);
|
|
457
|
+
|
|
458
|
+
NSString *deviceUID = [NSString stringWithFormat:@"%u", deviceID];
|
|
459
|
+
if (status == noErr && deviceUIDRef) {
|
|
460
|
+
deviceUID = (__bridge NSString *)deviceUIDRef;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Check if this is the default input device
|
|
464
|
+
AudioObjectPropertyAddress defaultAddress = {
|
|
465
|
+
kAudioHardwarePropertyDefaultInputDevice,
|
|
466
|
+
kAudioObjectPropertyScopeGlobal,
|
|
467
|
+
kAudioObjectPropertyElementMain
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
AudioDeviceID defaultDeviceID = kAudioDeviceUnknown;
|
|
471
|
+
UInt32 defaultSize = sizeof(AudioDeviceID);
|
|
472
|
+
AudioObjectGetPropertyData(kAudioObjectSystemObject, &defaultAddress, 0, NULL, &defaultSize, &defaultDeviceID);
|
|
473
|
+
|
|
474
|
+
BOOL isDefault = (deviceID == defaultDeviceID);
|
|
475
|
+
|
|
543
476
|
[devices addObject:@{
|
|
544
|
-
@"id":
|
|
545
|
-
@"name":
|
|
546
|
-
@"manufacturer":
|
|
547
|
-
@"isDefault": @(
|
|
477
|
+
@"id": deviceUID,
|
|
478
|
+
@"name": deviceName,
|
|
479
|
+
@"manufacturer": @"Unknown",
|
|
480
|
+
@"isDefault": @(isDefault)
|
|
548
481
|
}];
|
|
482
|
+
|
|
483
|
+
if (deviceNameRef) CFRelease(deviceNameRef);
|
|
484
|
+
if (deviceUIDRef) CFRelease(deviceUIDRef);
|
|
549
485
|
}
|
|
550
486
|
|
|
487
|
+
free(audioDevices);
|
|
488
|
+
|
|
551
489
|
// Convert to NAPI array
|
|
552
490
|
Napi::Array result = Napi::Array::New(env, devices.count);
|
|
553
491
|
for (NSUInteger i = 0; i < devices.count; i++) {
|
|
@@ -851,35 +789,42 @@ Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
|
|
|
851
789
|
Napi::Env env = info.Env();
|
|
852
790
|
|
|
853
791
|
@try {
|
|
854
|
-
// Check screen recording permission
|
|
792
|
+
// Check screen recording permission using ScreenCaptureKit
|
|
855
793
|
bool hasScreenPermission = true;
|
|
856
794
|
|
|
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 {
|
|
795
|
+
if (@available(macOS 12.3, *)) {
|
|
796
|
+
// Try to get shareable content to test ScreenCaptureKit permissions
|
|
797
|
+
@try {
|
|
798
|
+
SCShareableContent *content = [SCShareableContent currentShareableContent];
|
|
799
|
+
hasScreenPermission = (content != nil && content.displays.count > 0);
|
|
800
|
+
} @catch (NSException *exception) {
|
|
873
801
|
hasScreenPermission = false;
|
|
874
802
|
}
|
|
803
|
+
} else {
|
|
804
|
+
// Fallback for older macOS versions
|
|
805
|
+
if (@available(macOS 10.15, *)) {
|
|
806
|
+
// Try to create a display stream to test permissions
|
|
807
|
+
CGDisplayStreamRef stream = CGDisplayStreamCreate(
|
|
808
|
+
CGMainDisplayID(),
|
|
809
|
+
1, 1,
|
|
810
|
+
kCVPixelFormatType_32BGRA,
|
|
811
|
+
nil,
|
|
812
|
+
^(CGDisplayStreamFrameStatus status, uint64_t displayTime, IOSurfaceRef frameSurface, CGDisplayStreamUpdateRef updateRef) {
|
|
813
|
+
// Empty handler
|
|
814
|
+
}
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
if (stream) {
|
|
818
|
+
CFRelease(stream);
|
|
819
|
+
hasScreenPermission = true;
|
|
820
|
+
} else {
|
|
821
|
+
hasScreenPermission = false;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
875
824
|
}
|
|
876
825
|
|
|
877
|
-
//
|
|
826
|
+
// For audio permission, we'll use a simpler check since we're using CoreAudio
|
|
878
827
|
bool hasAudioPermission = true;
|
|
879
|
-
if (@available(macOS 10.14, *)) {
|
|
880
|
-
AVAuthorizationStatus audioStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
|
|
881
|
-
hasAudioPermission = (audioStatus == AVAuthorizationStatusAuthorized);
|
|
882
|
-
}
|
|
883
828
|
|
|
884
829
|
return Napi::Boolean::New(env, hasScreenPermission && hasAudioPermission);
|
|
885
830
|
|
|
@@ -901,8 +846,11 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
901
846
|
// ScreenCaptureKit availability (optional for clients)
|
|
902
847
|
exports.Set(Napi::String::New(env, "isScreenCaptureKitAvailable"), Napi::Function::New(env, [](const Napi::CallbackInfo& info){
|
|
903
848
|
Napi::Env env = info.Env();
|
|
904
|
-
|
|
905
|
-
|
|
849
|
+
if (@available(macOS 12.3, *)) {
|
|
850
|
+
bool available = [ScreenCaptureKitRecorder isScreenCaptureKitAvailable];
|
|
851
|
+
return Napi::Boolean::New(env, available);
|
|
852
|
+
}
|
|
853
|
+
return Napi::Boolean::New(env, false);
|
|
906
854
|
}));
|
|
907
855
|
|
|
908
856
|
// Thumbnail functions
|
package/src/screen_capture.mm
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#import <AVFoundation/AVFoundation.h>
|
|
2
1
|
#import <CoreGraphics/CoreGraphics.h>
|
|
3
2
|
#import <AppKit/AppKit.h>
|
|
4
3
|
|
|
@@ -84,7 +83,7 @@
|
|
|
84
83
|
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
|
|
85
84
|
CGImageDestinationRef destination = CGImageDestinationCreateWithURL(
|
|
86
85
|
(__bridge CFURLRef)fileURL,
|
|
87
|
-
|
|
86
|
+
CFSTR("public.png"),
|
|
88
87
|
1,
|
|
89
88
|
NULL
|
|
90
89
|
);
|
|
@@ -149,7 +148,6 @@
|
|
|
149
148
|
|
|
150
149
|
+ (CGImageRef)createScreenshotFromDisplay:(CGDirectDisplayID)displayID
|
|
151
150
|
rect:(CGRect)rect {
|
|
152
|
-
|
|
153
151
|
if (CGRectIsNull(rect)) {
|
|
154
152
|
// Capture entire display
|
|
155
153
|
return CGDisplayCreateImage(displayID);
|
|
@@ -44,25 +44,14 @@ static NSArray<SCWindow *> *g_windowsToExclude = nil;
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
@try {
|
|
47
|
-
|
|
48
|
-
__block NSError *contentErr = nil;
|
|
49
|
-
|
|
50
|
-
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
|
47
|
+
SCShareableContent *content = nil;
|
|
51
48
|
if (@available(macOS 13.0, *)) {
|
|
52
|
-
[
|
|
53
|
-
|
|
54
|
-
completionHandler:^(SCShareableContent * _Nullable shareableContent, NSError * _Nullable err) {
|
|
55
|
-
content = shareableContent;
|
|
56
|
-
contentErr = err;
|
|
57
|
-
dispatch_semaphore_signal(sem);
|
|
58
|
-
}];
|
|
59
|
-
} else {
|
|
60
|
-
dispatch_semaphore_signal(sem);
|
|
49
|
+
NSLog(@"[SCK] Fetching shareable content...");
|
|
50
|
+
content = [SCShareableContent currentShareableContent];
|
|
61
51
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (error) { *error = contentErr ?: [NSError errorWithDomain:@"ScreenCaptureKitRecorder" code:-2 userInfo:nil]; }
|
|
52
|
+
if (!content) {
|
|
53
|
+
if (error) { *error = [NSError errorWithDomain:@"ScreenCaptureKitRecorder" code:-2 userInfo:@{NSLocalizedDescriptionKey:@"Failed to get shareable content"}]; }
|
|
54
|
+
NSLog(@"[SCK] Failed to get shareable content");
|
|
66
55
|
return NO;
|
|
67
56
|
}
|
|
68
57
|
|
|
@@ -85,8 +74,9 @@ static NSArray<SCWindow *> *g_windowsToExclude = nil;
|
|
|
85
74
|
}
|
|
86
75
|
if (!targetDisplay) {
|
|
87
76
|
targetDisplay = content.displays.firstObject;
|
|
88
|
-
if (!targetDisplay) { return NO; }
|
|
77
|
+
if (!targetDisplay) { NSLog(@"[SCK] No displays found"); return NO; }
|
|
89
78
|
}
|
|
79
|
+
NSLog(@"[SCK] Using displayID=%u", targetDisplay.displayID);
|
|
90
80
|
|
|
91
81
|
NSMutableArray<SCRunningApplication*> *appsToExclude = [NSMutableArray array];
|
|
92
82
|
if (excludedBundleIds.count > 0) {
|
|
@@ -128,7 +118,7 @@ static NSArray<SCWindow *> *g_windowsToExclude = nil;
|
|
|
128
118
|
filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:(g_windowsToExclude ?: @[])];
|
|
129
119
|
}
|
|
130
120
|
}
|
|
131
|
-
if (!filter) { return NO; }
|
|
121
|
+
if (!filter) { NSLog(@"[SCK] Failed to create filter"); return NO; }
|
|
132
122
|
|
|
133
123
|
SCStreamConfiguration *cfg = [[SCStreamConfiguration alloc] init];
|
|
134
124
|
if (captureArea && captureArea[@"width"] && captureArea[@"height"]) {
|
|
@@ -163,58 +153,43 @@ static NSArray<SCWindow *> *g_windowsToExclude = nil;
|
|
|
163
153
|
}
|
|
164
154
|
}
|
|
165
155
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
dispatch_async(dispatch_get_main_queue(), ^{
|
|
170
|
-
if (@available(macOS 15.0, *)) {
|
|
171
|
-
g_scStream = [[SCStream alloc] initWithFilter:filter configuration:cfg delegate:nil];
|
|
172
|
-
|
|
173
|
-
SCRecordingOutputConfiguration *recCfg = [[SCRecordingOutputConfiguration alloc] init];
|
|
174
|
-
g_outputURL = [NSURL fileURLWithPath:outputPath];
|
|
175
|
-
recCfg.outputURL = g_outputURL;
|
|
176
|
-
recCfg.outputFileType = AVFileTypeQuickTimeMovie;
|
|
177
|
-
|
|
178
|
-
id<SCRecordingOutputDelegate> delegateObject = (id<SCRecordingOutputDelegate>)delegate;
|
|
179
|
-
if (!delegateObject) {
|
|
180
|
-
if (!g_scDelegate) {
|
|
181
|
-
g_scDelegate = [[ScreenCaptureKitRecorder alloc] init];
|
|
182
|
-
}
|
|
183
|
-
delegateObject = (id<SCRecordingOutputDelegate>)g_scDelegate;
|
|
184
|
-
}
|
|
185
|
-
g_scRecordingOutput = [[SCRecordingOutput alloc] initWithConfiguration:recCfg delegate:delegateObject];
|
|
156
|
+
if (@available(macOS 15.0, *)) {
|
|
157
|
+
g_scStream = [[SCStream alloc] initWithFilter:filter configuration:cfg delegate:nil];
|
|
158
|
+
NSLog(@"[SCK] Stream created. w=%d h=%d cursor=%@ audio=%@", cfg.width, cfg.height, cfg.showsCursor?@"YES":@"NO", (@available(macOS 13.0, *) ? (cfg.capturesAudio?@"YES":@"NO") : @"N/A"));
|
|
186
159
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
g_scRecordingOutput = nil; g_scStream = nil; g_outputURL = nil;
|
|
192
|
-
dispatch_semaphore_signal(startSem);
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
160
|
+
SCRecordingOutputConfiguration *recCfg = [[SCRecordingOutputConfiguration alloc] init];
|
|
161
|
+
g_outputURL = [NSURL fileURLWithPath:outputPath];
|
|
162
|
+
recCfg.outputURL = g_outputURL;
|
|
163
|
+
recCfg.outputFileType = AVFileTypeQuickTimeMovie;
|
|
195
164
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
165
|
+
id<SCRecordingOutputDelegate> delegateObject = (id<SCRecordingOutputDelegate>)delegate;
|
|
166
|
+
if (!delegateObject) {
|
|
167
|
+
if (!g_scDelegate) {
|
|
168
|
+
g_scDelegate = [[ScreenCaptureKitRecorder alloc] init];
|
|
169
|
+
}
|
|
170
|
+
delegateObject = (id<SCRecordingOutputDelegate>)g_scDelegate;
|
|
171
|
+
}
|
|
172
|
+
g_scRecordingOutput = [[SCRecordingOutput alloc] initWithConfiguration:recCfg delegate:delegateObject];
|
|
173
|
+
|
|
174
|
+
NSError *addErr = nil;
|
|
175
|
+
BOOL added = [g_scStream addRecordingOutput:g_scRecordingOutput error:&addErr];
|
|
176
|
+
if (!added) {
|
|
177
|
+
NSLog(@"[SCK] addRecordingOutput failed: %@", addErr.localizedDescription);
|
|
178
|
+
if (error) { *error = addErr ?: [NSError errorWithDomain:@"ScreenCaptureKitRecorder" code:-3 userInfo:nil]; }
|
|
179
|
+
g_scRecordingOutput = nil; g_scStream = nil; g_outputURL = nil;
|
|
180
|
+
return NO;
|
|
204
181
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (g_scRecordingOutput && g_scStream) {
|
|
210
|
-
if (@available(macOS 15.0, *)) {
|
|
182
|
+
|
|
183
|
+
[g_scStream startCaptureWithCompletionHandler:^(NSError * _Nullable err) {
|
|
184
|
+
if (err) {
|
|
185
|
+
NSLog(@"[SCK] startCapture error: %@", err.localizedDescription);
|
|
211
186
|
[g_scStream removeRecordingOutput:g_scRecordingOutput error:nil];
|
|
187
|
+
g_scRecordingOutput = nil; g_scStream = nil; g_outputURL = nil;
|
|
188
|
+
} else {
|
|
189
|
+
NSLog(@"[SCK] startCapture OK");
|
|
212
190
|
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return NO;
|
|
216
|
-
}
|
|
217
|
-
if (startedOK) {
|
|
191
|
+
}];
|
|
192
|
+
// Return immediately; capture will start asynchronously
|
|
218
193
|
return YES;
|
|
219
194
|
}
|
|
220
195
|
|
package/test-sck.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const nativeBinding = require('./build/Release/mac_recorder.node');
|
|
2
|
+
|
|
3
|
+
console.log('=== ScreenCaptureKit Migration Test ===');
|
|
4
|
+
console.log('ScreenCaptureKit available:', nativeBinding.isScreenCaptureKitAvailable());
|
|
5
|
+
console.log('Displays:', nativeBinding.getDisplays().length);
|
|
6
|
+
console.log('Audio devices:', nativeBinding.getAudioDevices().length);
|
|
7
|
+
console.log('Permissions OK:', nativeBinding.checkPermissions());
|
|
8
|
+
|
|
9
|
+
const displays = nativeBinding.getDisplays();
|
|
10
|
+
console.log('\nDisplay info:', displays[0]);
|
|
11
|
+
|
|
12
|
+
const audioDevices = nativeBinding.getAudioDevices();
|
|
13
|
+
console.log('\nFirst audio device:', audioDevices[0]);
|
|
14
|
+
|
|
15
|
+
// Test starting and stopping recording
|
|
16
|
+
console.log('\n=== Recording Test ===');
|
|
17
|
+
const outputPath = '/tmp/test-recording-sck.mov';
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
console.log('Starting recording...');
|
|
21
|
+
const success = nativeBinding.startRecording(outputPath, {
|
|
22
|
+
displayId: displays[0].id,
|
|
23
|
+
captureCursor: true,
|
|
24
|
+
includeMicrophone: false,
|
|
25
|
+
includeSystemAudio: false
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
console.log('Recording started:', success);
|
|
29
|
+
|
|
30
|
+
if (success) {
|
|
31
|
+
setTimeout(() => {
|
|
32
|
+
console.log('Stopping recording...');
|
|
33
|
+
const stopped = nativeBinding.stopRecording();
|
|
34
|
+
console.log('Recording stopped:', stopped);
|
|
35
|
+
|
|
36
|
+
// Check if file was created
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
setTimeout(() => {
|
|
39
|
+
if (fs.existsSync(outputPath)) {
|
|
40
|
+
const stats = fs.statSync(outputPath);
|
|
41
|
+
console.log(`Recording file created: ${outputPath} (${stats.size} bytes)`);
|
|
42
|
+
} else {
|
|
43
|
+
console.log('Recording file not found');
|
|
44
|
+
}
|
|
45
|
+
}, 1000);
|
|
46
|
+
}, 3000); // Record for 3 seconds
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('Recording test failed:', error.message);
|
|
50
|
+
}
|