node-mac-recorder 2.2.1 → 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/README.md +17 -79
- package/binding.gyp +1 -12
- package/index.js +14 -68
- package/package.json +3 -8
- package/src/mac_recorder.mm +0 -361
- package/src/screen_capture.mm +0 -16
- package/test-sck.js +1 -54
- 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
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
|
|
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,11 +21,8 @@
|
|
|
22
21
|
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
|
|
23
22
|
"CLANG_CXX_LIBRARY": "libc++",
|
|
24
23
|
"MACOSX_DEPLOYMENT_TARGET": "12.3",
|
|
25
|
-
<<<<<<< HEAD
|
|
26
|
-
=======
|
|
27
24
|
"ARCHS": ["arm64"],
|
|
28
25
|
"VALID_ARCHS": ["arm64"],
|
|
29
|
-
>>>>>>> screencapture
|
|
30
26
|
"OTHER_CFLAGS": [
|
|
31
27
|
"-ObjC++",
|
|
32
28
|
"-fmodules"
|
|
@@ -35,22 +31,15 @@
|
|
|
35
31
|
},
|
|
36
32
|
"link_settings": {
|
|
37
33
|
"libraries": [
|
|
38
|
-
<<<<<<< HEAD
|
|
39
|
-
=======
|
|
40
34
|
"-framework ScreenCaptureKit",
|
|
41
35
|
"-framework AVFoundation",
|
|
42
36
|
"-framework CoreMedia",
|
|
43
37
|
"-framework CoreVideo",
|
|
44
|
-
>>>>>>> screencapture
|
|
45
38
|
"-framework Foundation",
|
|
46
39
|
"-framework AppKit",
|
|
47
40
|
"-framework ApplicationServices",
|
|
48
41
|
"-framework Carbon",
|
|
49
|
-
"-framework Accessibility"
|
|
50
|
-
"-framework CoreAudio",
|
|
51
|
-
"-framework AVFoundation",
|
|
52
|
-
"-framework CoreMedia",
|
|
53
|
-
"-framework CoreVideo"
|
|
42
|
+
"-framework Accessibility"
|
|
54
43
|
]
|
|
55
44
|
},
|
|
56
45
|
"defines": [
|
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
|
|
5
|
+
// Native modülü yükle
|
|
6
6
|
let nativeBinding;
|
|
7
7
|
try {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} catch (e) {
|
|
8
|
+
nativeBinding = require("./build/Release/mac_recorder.node");
|
|
9
|
+
} catch (error) {
|
|
11
10
|
try {
|
|
12
|
-
nativeBinding = require("./build/
|
|
13
|
-
} catch (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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,22 +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
|
-
<<<<<<< HEAD
|
|
133
|
-
useScreenCaptureKit: options.useScreenCaptureKit || false,
|
|
134
|
-
excludedAppBundleIds: options.excludedAppBundleIds || [],
|
|
135
|
-
excludedPIDs: options.excludedPIDs || [],
|
|
136
|
-
excludedWindowIds: options.excludedWindowIds || [],
|
|
137
|
-
autoExcludeSelf:
|
|
138
|
-
typeof options.autoExcludeSelf === "boolean"
|
|
139
|
-
? options.autoExcludeSelf
|
|
140
|
-
: !!(process.versions && process.versions.electron),
|
|
141
|
-
=======
|
|
142
121
|
// Exclusion options
|
|
143
122
|
excludeCurrentApp: options.excludeCurrentApp || false,
|
|
144
123
|
excludeWindowIds: Array.isArray(options.excludeWindowIds)
|
|
145
124
|
? options.excludeWindowIds
|
|
146
125
|
: [],
|
|
147
|
-
>>>>>>> screencapture
|
|
148
126
|
};
|
|
149
127
|
}
|
|
150
128
|
|
|
@@ -305,17 +283,9 @@ class MacRecorder extends EventEmitter {
|
|
|
305
283
|
windowId: this.options.windowId || null, // null = tam ekran
|
|
306
284
|
audioDeviceId: this.options.audioDeviceId || null, // null = default device
|
|
307
285
|
systemAudioDeviceId: this.options.systemAudioDeviceId || null, // null = auto-detect system audio device
|
|
308
|
-
<<<<<<< HEAD
|
|
309
|
-
useScreenCaptureKit: this.options.useScreenCaptureKit || false,
|
|
310
|
-
excludedAppBundleIds: this.options.excludedAppBundleIds || [],
|
|
311
|
-
excludedPIDs: this.options.excludedPIDs || [],
|
|
312
|
-
excludedWindowIds: this.options.excludedWindowIds || [],
|
|
313
|
-
autoExcludeSelf: this.options.autoExcludeSelf === true,
|
|
314
|
-
=======
|
|
315
286
|
// Exclusion options passthrough
|
|
316
287
|
excludeCurrentApp: this.options.excludeCurrentApp || false,
|
|
317
288
|
excludeWindowIds: this.options.excludeWindowIds || [],
|
|
318
|
-
>>>>>>> screencapture
|
|
319
289
|
};
|
|
320
290
|
|
|
321
291
|
// Manuel captureArea varsa onu kullan
|
|
@@ -328,34 +298,10 @@ class MacRecorder extends EventEmitter {
|
|
|
328
298
|
};
|
|
329
299
|
}
|
|
330
300
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
this.options.useScreenCaptureKit ||
|
|
336
|
-
this.options.excludedAppBundleIds?.length ||
|
|
337
|
-
this.options.excludedPIDs?.length ||
|
|
338
|
-
this.options.excludedWindowIds?.length
|
|
339
|
-
);
|
|
340
|
-
const scAvailable =
|
|
341
|
-
typeof nativeBinding.isScreenCaptureKitAvailable === "function" &&
|
|
342
|
-
nativeBinding.isScreenCaptureKitAvailable();
|
|
343
|
-
if (wantsSC && scAvailable) {
|
|
344
|
-
const scOptions = {
|
|
345
|
-
...recordingOptions,
|
|
346
|
-
useScreenCaptureKit: true,
|
|
347
|
-
};
|
|
348
|
-
success = nativeBinding.startRecording(outputPath, scOptions);
|
|
349
|
-
} else {
|
|
350
|
-
success = nativeBinding.startRecording(
|
|
351
|
-
outputPath,
|
|
352
|
-
recordingOptions
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
} catch (e) {
|
|
356
|
-
// Fallback AVFoundation
|
|
357
|
-
success = nativeBinding.startRecording(outputPath, recordingOptions);
|
|
358
|
-
}
|
|
301
|
+
const success = nativeBinding.startRecording(
|
|
302
|
+
outputPath,
|
|
303
|
+
recordingOptions
|
|
304
|
+
);
|
|
359
305
|
|
|
360
306
|
if (success) {
|
|
361
307
|
this.isRecording = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-mac-recorder",
|
|
3
|
-
"version": "2.
|
|
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
|
}
|
package/src/mac_recorder.mm
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
#import <napi.h>
|
|
2
2
|
#import <ScreenCaptureKit/ScreenCaptureKit.h>
|
|
3
|
-
<<<<<<< HEAD
|
|
4
|
-
=======
|
|
5
3
|
#import <AVFoundation/AVFoundation.h>
|
|
6
4
|
#import <CoreMedia/CoreMedia.h>
|
|
7
|
-
>>>>>>> screencapture
|
|
8
5
|
#import <AppKit/AppKit.h>
|
|
9
6
|
#import <Foundation/Foundation.h>
|
|
10
7
|
#import <CoreGraphics/CoreGraphics.h>
|
|
@@ -21,13 +18,9 @@ Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports);
|
|
|
21
18
|
// Window selector function declarations
|
|
22
19
|
Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports);
|
|
23
20
|
|
|
24
|
-
<<<<<<< HEAD
|
|
25
|
-
@interface MacRecorderDelegate : NSObject
|
|
26
|
-
=======
|
|
27
21
|
// ScreenCaptureKit Recording Delegate
|
|
28
22
|
API_AVAILABLE(macos(12.3))
|
|
29
23
|
@interface SCKRecorderDelegate : NSObject <SCStreamDelegate, SCStreamOutput>
|
|
30
|
-
>>>>>>> screencapture
|
|
31
24
|
@property (nonatomic, copy) void (^completionHandler)(NSURL *outputURL, NSError *error);
|
|
32
25
|
@property (nonatomic, copy) void (^startedHandler)(void);
|
|
33
26
|
@property (nonatomic, strong) AVAssetWriter *assetWriter;
|
|
@@ -41,20 +34,6 @@ API_AVAILABLE(macos(12.3))
|
|
|
41
34
|
@property (nonatomic, assign) BOOL startFailed;
|
|
42
35
|
@end
|
|
43
36
|
|
|
44
|
-
<<<<<<< HEAD
|
|
45
|
-
@implementation MacRecorderDelegate
|
|
46
|
-
- (void)recordingDidStart {
|
|
47
|
-
NSLog(@"[mac_recorder] ScreenCaptureKit recording started");
|
|
48
|
-
}
|
|
49
|
-
- (void)recordingDidFinish:(NSURL *)outputURL error:(NSError *)error {
|
|
50
|
-
if (error) {
|
|
51
|
-
NSLog(@"[mac_recorder] ScreenCaptureKit recording finished with error: %@", error.localizedDescription);
|
|
52
|
-
} else {
|
|
53
|
-
NSLog(@"[mac_recorder] ScreenCaptureKit recording finished OK → %@", outputURL.path);
|
|
54
|
-
}
|
|
55
|
-
if (self.completionHandler) {
|
|
56
|
-
self.completionHandler(outputURL, error);
|
|
57
|
-
=======
|
|
58
37
|
@implementation SCKRecorderDelegate
|
|
59
38
|
|
|
60
39
|
// Standard SCStreamDelegate method - should be called automatically
|
|
@@ -67,7 +46,6 @@ API_AVAILABLE(macos(12.3))
|
|
|
67
46
|
NSLog(@"🛑 Stream stopped with error: %@", error ? error.localizedDescription : @"none");
|
|
68
47
|
if (self.completionHandler) {
|
|
69
48
|
self.completionHandler(self.outputURL, error);
|
|
70
|
-
>>>>>>> screencapture
|
|
71
49
|
}
|
|
72
50
|
}
|
|
73
51
|
|
|
@@ -142,15 +120,6 @@ API_AVAILABLE(macos(12.3))
|
|
|
142
120
|
|
|
143
121
|
@end
|
|
144
122
|
|
|
145
|
-
<<<<<<< HEAD
|
|
146
|
-
// Global state for recording
|
|
147
|
-
static MacRecorderDelegate *g_delegate = nil;
|
|
148
|
-
static bool g_isRecording = false;
|
|
149
|
-
|
|
150
|
-
// Helper function to cleanup recording resources
|
|
151
|
-
void cleanupRecording() {
|
|
152
|
-
g_delegate = nil;
|
|
153
|
-
=======
|
|
154
123
|
// Global state for ScreenCaptureKit recording
|
|
155
124
|
static SCStream *g_scStream = nil;
|
|
156
125
|
static SCKRecorderDelegate *g_scDelegate = nil;
|
|
@@ -201,7 +170,6 @@ void cleanupSCKRecording() {
|
|
|
201
170
|
}
|
|
202
171
|
g_scDelegate = nil;
|
|
203
172
|
}
|
|
204
|
-
>>>>>>> screencapture
|
|
205
173
|
g_isRecording = false;
|
|
206
174
|
}
|
|
207
175
|
|
|
@@ -246,31 +214,13 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
246
214
|
NSLog(@"✅ Screen recording permission verified");
|
|
247
215
|
|
|
248
216
|
std::string outputPath = info[0].As<Napi::String>().Utf8Value();
|
|
249
|
-
NSLog(@"[mac_recorder] StartRecording: output=%@", [NSString stringWithUTF8String:outputPath.c_str()]);
|
|
250
217
|
|
|
251
|
-
<<<<<<< HEAD
|
|
252
|
-
// Options parsing (shared)
|
|
253
|
-
CGRect captureRect = CGRectNull;
|
|
254
|
-
bool captureCursor = false; // Default olarak cursor gizli
|
|
255
|
-
bool includeMicrophone = false; // Default olarak mikrofon kapalı
|
|
256
|
-
bool includeSystemAudio = true; // Default olarak sistem sesi açık
|
|
257
|
-
CGDirectDisplayID displayID = CGMainDisplayID(); // Default ana ekran
|
|
258
|
-
NSString *audioDeviceId = nil; // Default audio device ID
|
|
259
|
-
NSString *systemAudioDeviceId = nil; // System audio device ID
|
|
260
|
-
bool forceUseSC = false;
|
|
261
|
-
// Exclude options for ScreenCaptureKit (optional, backward compatible)
|
|
262
|
-
NSMutableArray<NSString*> *excludedAppBundleIds = [NSMutableArray array];
|
|
263
|
-
NSMutableArray<NSNumber*> *excludedPIDs = [NSMutableArray array];
|
|
264
|
-
NSMutableArray<NSNumber*> *excludedWindowIds = [NSMutableArray array];
|
|
265
|
-
bool autoExcludeSelf = false;
|
|
266
|
-
=======
|
|
267
218
|
// Default options
|
|
268
219
|
bool captureCursor = false;
|
|
269
220
|
bool includeSystemAudio = true;
|
|
270
221
|
CGDirectDisplayID displayID = 0; // Will be set to first available display
|
|
271
222
|
uint32_t windowID = 0;
|
|
272
223
|
CGRect captureRect = CGRectNull;
|
|
273
|
-
>>>>>>> screencapture
|
|
274
224
|
|
|
275
225
|
// Parse options
|
|
276
226
|
if (info.Length() > 1 && info[1].IsObject()) {
|
|
@@ -285,53 +235,6 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
285
235
|
includeSystemAudio = options.Get("includeSystemAudio").As<Napi::Boolean>();
|
|
286
236
|
}
|
|
287
237
|
|
|
288
|
-
<<<<<<< HEAD
|
|
289
|
-
// System audio device ID
|
|
290
|
-
if (options.Has("systemAudioDeviceId") && !options.Get("systemAudioDeviceId").IsNull()) {
|
|
291
|
-
std::string sysDeviceId = options.Get("systemAudioDeviceId").As<Napi::String>().Utf8Value();
|
|
292
|
-
systemAudioDeviceId = [NSString stringWithUTF8String:sysDeviceId.c_str()];
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// ScreenCaptureKit toggle (optional)
|
|
296
|
-
if (options.Has("useScreenCaptureKit")) {
|
|
297
|
-
forceUseSC = options.Get("useScreenCaptureKit").As<Napi::Boolean>();
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Exclusion lists (optional)
|
|
301
|
-
if (options.Has("excludedAppBundleIds") && options.Get("excludedAppBundleIds").IsArray()) {
|
|
302
|
-
Napi::Array arr = options.Get("excludedAppBundleIds").As<Napi::Array>();
|
|
303
|
-
for (uint32_t i = 0; i < arr.Length(); i++) {
|
|
304
|
-
if (!arr.Get(i).IsUndefined() && !arr.Get(i).IsNull()) {
|
|
305
|
-
std::string s = arr.Get(i).As<Napi::String>().Utf8Value();
|
|
306
|
-
[excludedAppBundleIds addObject:[NSString stringWithUTF8String:s.c_str()]];
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
if (options.Has("excludedPIDs") && options.Get("excludedPIDs").IsArray()) {
|
|
311
|
-
Napi::Array arr = options.Get("excludedPIDs").As<Napi::Array>();
|
|
312
|
-
for (uint32_t i = 0; i < arr.Length(); i++) {
|
|
313
|
-
if (!arr.Get(i).IsUndefined() && !arr.Get(i).IsNull()) {
|
|
314
|
-
double v = arr.Get(i).As<Napi::Number>().DoubleValue();
|
|
315
|
-
[excludedPIDs addObject:@( (pid_t)v )];
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
if (options.Has("excludedWindowIds") && options.Get("excludedWindowIds").IsArray()) {
|
|
320
|
-
Napi::Array arr = options.Get("excludedWindowIds").As<Napi::Array>();
|
|
321
|
-
for (uint32_t i = 0; i < arr.Length(); i++) {
|
|
322
|
-
if (!arr.Get(i).IsUndefined() && !arr.Get(i).IsNull()) {
|
|
323
|
-
double v = arr.Get(i).As<Napi::Number>().DoubleValue();
|
|
324
|
-
[excludedWindowIds addObject:@( (uint32_t)v )];
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
if (options.Has("autoExcludeSelf")) {
|
|
329
|
-
autoExcludeSelf = options.Get("autoExcludeSelf").As<Napi::Boolean>();
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Display ID
|
|
333
|
-
=======
|
|
334
|
-
>>>>>>> screencapture
|
|
335
238
|
if (options.Has("displayId") && !options.Get("displayId").IsNull()) {
|
|
336
239
|
uint32_t tempDisplayID = options.Get("displayId").As<Napi::Number>().Uint32Value();
|
|
337
240
|
if (tempDisplayID != 0) {
|
|
@@ -356,64 +259,6 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
356
259
|
}
|
|
357
260
|
}
|
|
358
261
|
|
|
359
|
-
<<<<<<< HEAD
|
|
360
|
-
@try {
|
|
361
|
-
// Always prefer ScreenCaptureKit if available
|
|
362
|
-
NSLog(@"[mac_recorder] Checking ScreenCaptureKit availability");
|
|
363
|
-
if (@available(macOS 12.3, *)) {
|
|
364
|
-
if ([ScreenCaptureKitRecorder isScreenCaptureKitAvailable]) {
|
|
365
|
-
NSMutableDictionary *scConfig = [@{} mutableCopy];
|
|
366
|
-
scConfig[@"displayId"] = @(displayID);
|
|
367
|
-
if (!CGRectIsNull(captureRect)) {
|
|
368
|
-
scConfig[@"captureArea"] = @{ @"x": @(captureRect.origin.x),
|
|
369
|
-
@"y": @(captureRect.origin.y),
|
|
370
|
-
@"width": @(captureRect.size.width),
|
|
371
|
-
@"height": @(captureRect.size.height) };
|
|
372
|
-
}
|
|
373
|
-
scConfig[@"captureCursor"] = @(captureCursor);
|
|
374
|
-
scConfig[@"includeMicrophone"] = @(includeMicrophone);
|
|
375
|
-
scConfig[@"includeSystemAudio"] = @(includeSystemAudio);
|
|
376
|
-
if (excludedAppBundleIds.count) scConfig[@"excludedAppBundleIds"] = excludedAppBundleIds;
|
|
377
|
-
if (excludedPIDs.count) scConfig[@"excludedPIDs"] = excludedPIDs;
|
|
378
|
-
if (excludedWindowIds.count) scConfig[@"excludedWindowIds"] = excludedWindowIds;
|
|
379
|
-
// Auto exclude current app by PID if requested
|
|
380
|
-
if (autoExcludeSelf) {
|
|
381
|
-
pid_t pid = getpid();
|
|
382
|
-
NSMutableArray *arr = [NSMutableArray arrayWithArray:scConfig[@"excludedPIDs"] ?: @[]];
|
|
383
|
-
[arr addObject:@(pid)];
|
|
384
|
-
scConfig[@"excludedPIDs"] = arr;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// Output path for SC
|
|
388
|
-
std::string outputPathStr = info[0].As<Napi::String>().Utf8Value();
|
|
389
|
-
scConfig[@"outputPath"] = [NSString stringWithUTF8String:outputPathStr.c_str()];
|
|
390
|
-
|
|
391
|
-
NSError *scErr = nil;
|
|
392
|
-
NSLog(@"[mac_recorder] Using ScreenCaptureKit path (displayId=%u)", displayID);
|
|
393
|
-
|
|
394
|
-
// Create and set up delegate
|
|
395
|
-
g_delegate = [[MacRecorderDelegate alloc] init];
|
|
396
|
-
|
|
397
|
-
BOOL ok = [ScreenCaptureKitRecorder startRecordingWithConfiguration:scConfig delegate:g_delegate error:&scErr];
|
|
398
|
-
if (ok) {
|
|
399
|
-
g_isRecording = true;
|
|
400
|
-
NSLog(@"[mac_recorder] ScreenCaptureKit startRecording → OK");
|
|
401
|
-
return Napi::Boolean::New(env, true);
|
|
402
|
-
}
|
|
403
|
-
NSLog(@"[mac_recorder] ScreenCaptureKit startRecording → FAIL: %@", scErr.localizedDescription);
|
|
404
|
-
cleanupRecording();
|
|
405
|
-
return Napi::Boolean::New(env, false);
|
|
406
|
-
}
|
|
407
|
-
} else {
|
|
408
|
-
NSLog(@"[mac_recorder] ScreenCaptureKit not available");
|
|
409
|
-
cleanupRecording();
|
|
410
|
-
return Napi::Boolean::New(env, false);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
} @catch (NSException *exception) {
|
|
414
|
-
cleanupRecording();
|
|
415
|
-
return Napi::Boolean::New(env, false);
|
|
416
|
-
=======
|
|
417
262
|
// Create output URL
|
|
418
263
|
NSURL *outputURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:outputPath.c_str()]];
|
|
419
264
|
NSLog(@"📁 Output URL: %@", outputURL.absoluteString);
|
|
@@ -543,13 +388,8 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
543
388
|
|
|
544
389
|
contentFilter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:excluded];
|
|
545
390
|
NSLog(@"✅ Content filter created for display recording");
|
|
546
|
-
>>>>>>> screencapture
|
|
547
391
|
}
|
|
548
392
|
|
|
549
|
-
<<<<<<< HEAD
|
|
550
|
-
if (!g_isRecording) {
|
|
551
|
-
return Napi::Boolean::New(env, false);
|
|
552
|
-
=======
|
|
553
393
|
// Get actual display dimensions for proper video configuration
|
|
554
394
|
CGRect displayBounds = CGDisplayBounds(displayID);
|
|
555
395
|
NSSize videoSize = NSMakeSize(displayBounds.size.width, displayBounds.size.height);
|
|
@@ -571,27 +411,9 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
571
411
|
NSLog(@"🔊 Audio configuration: capture=%@, excludeProcess=%@", includeSystemAudio ? @"YES" : @"NO", @"YES");
|
|
572
412
|
} else {
|
|
573
413
|
NSLog(@"⚠️ macOS 13.0+ features not available");
|
|
574
|
-
>>>>>>> screencapture
|
|
575
414
|
}
|
|
576
415
|
config.showsCursor = captureCursor;
|
|
577
416
|
|
|
578
|
-
<<<<<<< HEAD
|
|
579
|
-
@try {
|
|
580
|
-
NSLog(@"[mac_recorder] StopRecording called");
|
|
581
|
-
|
|
582
|
-
// Stop ScreenCaptureKit recording
|
|
583
|
-
NSLog(@"[mac_recorder] Stopping ScreenCaptureKit stream");
|
|
584
|
-
if (@available(macOS 12.3, *)) {
|
|
585
|
-
[ScreenCaptureKitRecorder stopRecording];
|
|
586
|
-
}
|
|
587
|
-
g_isRecording = false;
|
|
588
|
-
cleanupRecording();
|
|
589
|
-
NSLog(@"[mac_recorder] ScreenCaptureKit stopped");
|
|
590
|
-
return Napi::Boolean::New(env, true);
|
|
591
|
-
|
|
592
|
-
} @catch (NSException *exception) {
|
|
593
|
-
cleanupRecording();
|
|
594
|
-
=======
|
|
595
417
|
if (!CGRectIsNull(captureRect)) {
|
|
596
418
|
config.sourceRect = captureRect;
|
|
597
419
|
// Update video size if capture rect is specified
|
|
@@ -611,7 +433,6 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
611
433
|
|
|
612
434
|
if (writerError) {
|
|
613
435
|
NSLog(@"❌ Failed to create asset writer: %@", writerError.localizedDescription);
|
|
614
|
-
>>>>>>> screencapture
|
|
615
436
|
return Napi::Boolean::New(env, false);
|
|
616
437
|
}
|
|
617
438
|
|
|
@@ -635,117 +456,6 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
635
456
|
NSLog(@"❌ Cannot add video input to asset writer");
|
|
636
457
|
}
|
|
637
458
|
|
|
638
|
-
<<<<<<< HEAD
|
|
639
|
-
@try {
|
|
640
|
-
NSMutableArray *devices = [NSMutableArray array];
|
|
641
|
-
|
|
642
|
-
// Use CoreAudio to get audio devices since we're removing AVFoundation
|
|
643
|
-
AudioObjectPropertyAddress propertyAddress = {
|
|
644
|
-
kAudioHardwarePropertyDevices,
|
|
645
|
-
kAudioObjectPropertyScopeGlobal,
|
|
646
|
-
kAudioObjectPropertyElementMain
|
|
647
|
-
};
|
|
648
|
-
|
|
649
|
-
UInt32 dataSize = 0;
|
|
650
|
-
OSStatus status = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize);
|
|
651
|
-
if (status != noErr) {
|
|
652
|
-
return Napi::Array::New(env, 0);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
UInt32 deviceCount = dataSize / sizeof(AudioDeviceID);
|
|
656
|
-
AudioDeviceID *audioDevices = (AudioDeviceID *)malloc(dataSize);
|
|
657
|
-
|
|
658
|
-
status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize, audioDevices);
|
|
659
|
-
if (status != noErr) {
|
|
660
|
-
free(audioDevices);
|
|
661
|
-
return Napi::Array::New(env, 0);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
for (UInt32 i = 0; i < deviceCount; i++) {
|
|
665
|
-
AudioDeviceID deviceID = audioDevices[i];
|
|
666
|
-
|
|
667
|
-
// Check if device has input streams
|
|
668
|
-
AudioObjectPropertyAddress streamsAddress = {
|
|
669
|
-
kAudioDevicePropertyStreams,
|
|
670
|
-
kAudioDevicePropertyScopeInput,
|
|
671
|
-
kAudioObjectPropertyElementMain
|
|
672
|
-
};
|
|
673
|
-
|
|
674
|
-
UInt32 streamsSize = 0;
|
|
675
|
-
status = AudioObjectGetPropertyDataSize(deviceID, &streamsAddress, 0, NULL, &streamsSize);
|
|
676
|
-
if (status != noErr || streamsSize == 0) {
|
|
677
|
-
continue; // Skip output-only devices
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// Get device name
|
|
681
|
-
AudioObjectPropertyAddress nameAddress = {
|
|
682
|
-
kAudioDevicePropertyDeviceNameCFString,
|
|
683
|
-
kAudioObjectPropertyScopeGlobal,
|
|
684
|
-
kAudioObjectPropertyElementMain
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
CFStringRef deviceNameRef = NULL;
|
|
688
|
-
UInt32 nameSize = sizeof(CFStringRef);
|
|
689
|
-
status = AudioObjectGetPropertyData(deviceID, &nameAddress, 0, NULL, &nameSize, &deviceNameRef);
|
|
690
|
-
|
|
691
|
-
NSString *deviceName = @"Unknown Device";
|
|
692
|
-
if (status == noErr && deviceNameRef) {
|
|
693
|
-
deviceName = (__bridge NSString *)deviceNameRef;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// Get device UID
|
|
697
|
-
AudioObjectPropertyAddress uidAddress = {
|
|
698
|
-
kAudioDevicePropertyDeviceUID,
|
|
699
|
-
kAudioObjectPropertyScopeGlobal,
|
|
700
|
-
kAudioObjectPropertyElementMain
|
|
701
|
-
};
|
|
702
|
-
|
|
703
|
-
CFStringRef deviceUIDRef = NULL;
|
|
704
|
-
UInt32 uidSize = sizeof(CFStringRef);
|
|
705
|
-
status = AudioObjectGetPropertyData(deviceID, &uidAddress, 0, NULL, &uidSize, &deviceUIDRef);
|
|
706
|
-
|
|
707
|
-
NSString *deviceUID = [NSString stringWithFormat:@"%u", deviceID];
|
|
708
|
-
if (status == noErr && deviceUIDRef) {
|
|
709
|
-
deviceUID = (__bridge NSString *)deviceUIDRef;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// Check if this is the default input device
|
|
713
|
-
AudioObjectPropertyAddress defaultAddress = {
|
|
714
|
-
kAudioHardwarePropertyDefaultInputDevice,
|
|
715
|
-
kAudioObjectPropertyScopeGlobal,
|
|
716
|
-
kAudioObjectPropertyElementMain
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
AudioDeviceID defaultDeviceID = kAudioDeviceUnknown;
|
|
720
|
-
UInt32 defaultSize = sizeof(AudioDeviceID);
|
|
721
|
-
AudioObjectGetPropertyData(kAudioObjectSystemObject, &defaultAddress, 0, NULL, &defaultSize, &defaultDeviceID);
|
|
722
|
-
|
|
723
|
-
BOOL isDefault = (deviceID == defaultDeviceID);
|
|
724
|
-
|
|
725
|
-
[devices addObject:@{
|
|
726
|
-
@"id": deviceUID,
|
|
727
|
-
@"name": deviceName,
|
|
728
|
-
@"manufacturer": @"Unknown",
|
|
729
|
-
@"isDefault": @(isDefault)
|
|
730
|
-
}];
|
|
731
|
-
|
|
732
|
-
if (deviceNameRef) CFRelease(deviceNameRef);
|
|
733
|
-
if (deviceUIDRef) CFRelease(deviceUIDRef);
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
free(audioDevices);
|
|
737
|
-
|
|
738
|
-
// Convert to NAPI array
|
|
739
|
-
Napi::Array result = Napi::Array::New(env, devices.count);
|
|
740
|
-
for (NSUInteger i = 0; i < devices.count; i++) {
|
|
741
|
-
NSDictionary *device = devices[i];
|
|
742
|
-
Napi::Object deviceObj = Napi::Object::New(env);
|
|
743
|
-
deviceObj.Set("id", Napi::String::New(env, [device[@"id"] UTF8String]));
|
|
744
|
-
deviceObj.Set("name", Napi::String::New(env, [device[@"name"] UTF8String]));
|
|
745
|
-
deviceObj.Set("manufacturer", Napi::String::New(env, [device[@"manufacturer"] UTF8String]));
|
|
746
|
-
deviceObj.Set("isDefault", Napi::Boolean::New(env, [device[@"isDefault"] boolValue]));
|
|
747
|
-
result[i] = deviceObj;
|
|
748
|
-
=======
|
|
749
459
|
// Audio input settings (if needed)
|
|
750
460
|
if (includeSystemAudio) {
|
|
751
461
|
NSDictionary *audioSettings = @{
|
|
@@ -785,7 +495,6 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
785
495
|
NSLog(@"✅ Audio output attached to SCStream");
|
|
786
496
|
} else {
|
|
787
497
|
NSLog(@"⚠️ Failed to attach audio output to SCStream (audio may be disabled): %@", outputError.localizedDescription);
|
|
788
|
-
>>>>>>> screencapture
|
|
789
498
|
}
|
|
790
499
|
}
|
|
791
500
|
|
|
@@ -950,19 +659,6 @@ Napi::Value GetWindows(const Napi::CallbackInfo& info) {
|
|
|
950
659
|
Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
|
|
951
660
|
Napi::Env env = info.Env();
|
|
952
661
|
|
|
953
|
-
<<<<<<< HEAD
|
|
954
|
-
@try {
|
|
955
|
-
// Check screen recording permission using ScreenCaptureKit
|
|
956
|
-
bool hasScreenPermission = true;
|
|
957
|
-
|
|
958
|
-
if (@available(macOS 12.3, *)) {
|
|
959
|
-
// Try to get shareable content to test ScreenCaptureKit permissions
|
|
960
|
-
@try {
|
|
961
|
-
SCShareableContent *content = [SCShareableContent currentShareableContent];
|
|
962
|
-
hasScreenPermission = (content != nil && content.displays.count > 0);
|
|
963
|
-
} @catch (NSException *exception) {
|
|
964
|
-
hasScreenPermission = false;
|
|
965
|
-
=======
|
|
966
662
|
// Check screen recording permission
|
|
967
663
|
bool hasPermission = CGPreflightScreenCaptureAccess();
|
|
968
664
|
|
|
@@ -1032,41 +728,8 @@ Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
|
|
|
1032
728
|
|
|
1033
729
|
devices.Set(index++, deviceObj);
|
|
1034
730
|
CFRelease(deviceName);
|
|
1035
|
-
>>>>>>> screencapture
|
|
1036
|
-
}
|
|
1037
|
-
} else {
|
|
1038
|
-
// Fallback for older macOS versions
|
|
1039
|
-
if (@available(macOS 10.15, *)) {
|
|
1040
|
-
// Try to create a display stream to test permissions
|
|
1041
|
-
CGDisplayStreamRef stream = CGDisplayStreamCreate(
|
|
1042
|
-
CGMainDisplayID(),
|
|
1043
|
-
1, 1,
|
|
1044
|
-
kCVPixelFormatType_32BGRA,
|
|
1045
|
-
nil,
|
|
1046
|
-
^(CGDisplayStreamFrameStatus status, uint64_t displayTime, IOSurfaceRef frameSurface, CGDisplayStreamUpdateRef updateRef) {
|
|
1047
|
-
// Empty handler
|
|
1048
|
-
}
|
|
1049
|
-
);
|
|
1050
|
-
|
|
1051
|
-
if (stream) {
|
|
1052
|
-
CFRelease(stream);
|
|
1053
|
-
hasScreenPermission = true;
|
|
1054
|
-
} else {
|
|
1055
|
-
hasScreenPermission = false;
|
|
1056
|
-
}
|
|
1057
731
|
}
|
|
1058
732
|
}
|
|
1059
|
-
<<<<<<< HEAD
|
|
1060
|
-
|
|
1061
|
-
// For audio permission, we'll use a simpler check since we're using CoreAudio
|
|
1062
|
-
bool hasAudioPermission = true;
|
|
1063
|
-
|
|
1064
|
-
return Napi::Boolean::New(env, hasScreenPermission && hasAudioPermission);
|
|
1065
|
-
|
|
1066
|
-
} @catch (NSException *exception) {
|
|
1067
|
-
return Napi::Boolean::New(env, false);
|
|
1068
|
-
=======
|
|
1069
|
-
>>>>>>> screencapture
|
|
1070
733
|
}
|
|
1071
734
|
|
|
1072
735
|
free(audioDevices);
|
|
@@ -1075,29 +738,6 @@ Napi::Value GetAudioDevices(const Napi::CallbackInfo& info) {
|
|
|
1075
738
|
|
|
1076
739
|
// Initialize the addon
|
|
1077
740
|
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
1078
|
-
<<<<<<< HEAD
|
|
1079
|
-
exports.Set(Napi::String::New(env, "startRecording"), Napi::Function::New(env, StartRecording));
|
|
1080
|
-
exports.Set(Napi::String::New(env, "stopRecording"), Napi::Function::New(env, StopRecording));
|
|
1081
|
-
|
|
1082
|
-
exports.Set(Napi::String::New(env, "getAudioDevices"), Napi::Function::New(env, GetAudioDevices));
|
|
1083
|
-
exports.Set(Napi::String::New(env, "getDisplays"), Napi::Function::New(env, GetDisplays));
|
|
1084
|
-
exports.Set(Napi::String::New(env, "getWindows"), Napi::Function::New(env, GetWindows));
|
|
1085
|
-
exports.Set(Napi::String::New(env, "getRecordingStatus"), Napi::Function::New(env, GetRecordingStatus));
|
|
1086
|
-
exports.Set(Napi::String::New(env, "checkPermissions"), Napi::Function::New(env, CheckPermissions));
|
|
1087
|
-
// ScreenCaptureKit availability (optional for clients)
|
|
1088
|
-
exports.Set(Napi::String::New(env, "isScreenCaptureKitAvailable"), Napi::Function::New(env, [](const Napi::CallbackInfo& info){
|
|
1089
|
-
Napi::Env env = info.Env();
|
|
1090
|
-
if (@available(macOS 12.3, *)) {
|
|
1091
|
-
bool available = [ScreenCaptureKitRecorder isScreenCaptureKitAvailable];
|
|
1092
|
-
return Napi::Boolean::New(env, available);
|
|
1093
|
-
}
|
|
1094
|
-
return Napi::Boolean::New(env, false);
|
|
1095
|
-
}));
|
|
1096
|
-
|
|
1097
|
-
// Thumbnail functions
|
|
1098
|
-
exports.Set(Napi::String::New(env, "getWindowThumbnail"), Napi::Function::New(env, GetWindowThumbnail));
|
|
1099
|
-
exports.Set(Napi::String::New(env, "getDisplayThumbnail"), Napi::Function::New(env, GetDisplayThumbnail));
|
|
1100
|
-
=======
|
|
1101
741
|
exports.Set("startRecording", Napi::Function::New(env, StartRecording));
|
|
1102
742
|
exports.Set("stopRecording", Napi::Function::New(env, StopRecording));
|
|
1103
743
|
exports.Set("isRecording", Napi::Function::New(env, IsRecording));
|
|
@@ -1105,7 +745,6 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
1105
745
|
exports.Set("getWindows", Napi::Function::New(env, GetWindows));
|
|
1106
746
|
exports.Set("checkPermissions", Napi::Function::New(env, CheckPermissions));
|
|
1107
747
|
exports.Set("getAudioDevices", Napi::Function::New(env, GetAudioDevices));
|
|
1108
|
-
>>>>>>> screencapture
|
|
1109
748
|
|
|
1110
749
|
// Initialize cursor tracker
|
|
1111
750
|
InitCursorTracker(env, exports);
|
package/src/screen_capture.mm
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
<<<<<<< HEAD
|
|
2
|
-
=======
|
|
3
1
|
#import "screen_capture.h"
|
|
4
2
|
#import <ScreenCaptureKit/ScreenCaptureKit.h>
|
|
5
3
|
#import <AVFoundation/AVFoundation.h>
|
|
6
|
-
>>>>>>> screencapture
|
|
7
4
|
#import <CoreGraphics/CoreGraphics.h>
|
|
8
5
|
#import <AppKit/AppKit.h>
|
|
9
6
|
|
|
@@ -77,11 +74,7 @@
|
|
|
77
74
|
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
|
|
78
75
|
CGImageDestinationRef destination = CGImageDestinationCreateWithURL(
|
|
79
76
|
(__bridge CFURLRef)fileURL,
|
|
80
|
-
<<<<<<< HEAD
|
|
81
|
-
CFSTR("public.png"),
|
|
82
|
-
=======
|
|
83
77
|
(__bridge CFStringRef)@"public.png",
|
|
84
|
-
>>>>>>> screencapture
|
|
85
78
|
1,
|
|
86
79
|
NULL
|
|
87
80
|
);
|
|
@@ -110,14 +103,6 @@
|
|
|
110
103
|
|
|
111
104
|
+ (CGImageRef)createScreenshotFromDisplay:(CGDirectDisplayID)displayID
|
|
112
105
|
rect:(CGRect)rect {
|
|
113
|
-
<<<<<<< HEAD
|
|
114
|
-
if (CGRectIsNull(rect)) {
|
|
115
|
-
// Capture entire display
|
|
116
|
-
return CGDisplayCreateImage(displayID);
|
|
117
|
-
} else {
|
|
118
|
-
// Capture specific rect
|
|
119
|
-
return CGDisplayCreateImageForRect(displayID, rect);
|
|
120
|
-
=======
|
|
121
106
|
if (CGRectIsNull(rect) || CGRectIsEmpty(rect)) {
|
|
122
107
|
rect = CGDisplayBounds(displayID);
|
|
123
108
|
}
|
|
@@ -156,7 +141,6 @@ Napi::Value GetAvailableDisplays(const Napi::CallbackInfo& info) {
|
|
|
156
141
|
displayObj.Set("isPrimary", Napi::Boolean::New(env, [[displayInfo objectForKey:@"isPrimary"] boolValue]));
|
|
157
142
|
|
|
158
143
|
displaysArray.Set(static_cast<uint32_t>(i), displayObj);
|
|
159
|
-
>>>>>>> screencapture
|
|
160
144
|
}
|
|
161
145
|
|
|
162
146
|
return displaysArray;
|
package/test-sck.js
CHANGED
|
@@ -1,55 +1,3 @@
|
|
|
1
|
-
<<<<<<< HEAD
|
|
2
|
-
const nativeBinding = require('./build/Release/mac_recorder.node');
|
|
3
|
-
|
|
4
|
-
console.log('=== ScreenCaptureKit Migration Test ===');
|
|
5
|
-
console.log('ScreenCaptureKit available:', nativeBinding.isScreenCaptureKitAvailable());
|
|
6
|
-
console.log('Displays:', nativeBinding.getDisplays().length);
|
|
7
|
-
console.log('Audio devices:', nativeBinding.getAudioDevices().length);
|
|
8
|
-
console.log('Permissions OK:', nativeBinding.checkPermissions());
|
|
9
|
-
|
|
10
|
-
const displays = nativeBinding.getDisplays();
|
|
11
|
-
console.log('\nDisplay info:', displays[0]);
|
|
12
|
-
|
|
13
|
-
const audioDevices = nativeBinding.getAudioDevices();
|
|
14
|
-
console.log('\nFirst audio device:', audioDevices[0]);
|
|
15
|
-
|
|
16
|
-
// Test starting and stopping recording
|
|
17
|
-
console.log('\n=== Recording Test ===');
|
|
18
|
-
const outputPath = '/tmp/test-recording-sck.mov';
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
console.log('Starting recording...');
|
|
22
|
-
const success = nativeBinding.startRecording(outputPath, {
|
|
23
|
-
displayId: displays[0].id,
|
|
24
|
-
captureCursor: true,
|
|
25
|
-
includeMicrophone: false,
|
|
26
|
-
includeSystemAudio: false
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
console.log('Recording started:', success);
|
|
30
|
-
|
|
31
|
-
if (success) {
|
|
32
|
-
setTimeout(() => {
|
|
33
|
-
console.log('Stopping recording...');
|
|
34
|
-
const stopped = nativeBinding.stopRecording();
|
|
35
|
-
console.log('Recording stopped:', stopped);
|
|
36
|
-
|
|
37
|
-
// Check if file was created
|
|
38
|
-
const fs = require('fs');
|
|
39
|
-
setTimeout(() => {
|
|
40
|
-
if (fs.existsSync(outputPath)) {
|
|
41
|
-
const stats = fs.statSync(outputPath);
|
|
42
|
-
console.log(`Recording file created: ${outputPath} (${stats.size} bytes)`);
|
|
43
|
-
} else {
|
|
44
|
-
console.log('Recording file not found');
|
|
45
|
-
}
|
|
46
|
-
}, 1000);
|
|
47
|
-
}, 3000); // Record for 3 seconds
|
|
48
|
-
}
|
|
49
|
-
} catch (error) {
|
|
50
|
-
console.error('Recording test failed:', error.message);
|
|
51
|
-
}
|
|
52
|
-
=======
|
|
53
1
|
const MacRecorder = require('./index');
|
|
54
2
|
|
|
55
3
|
function testScreenCaptureKit() {
|
|
@@ -105,5 +53,4 @@ function testScreenCaptureKit() {
|
|
|
105
53
|
}
|
|
106
54
|
}
|
|
107
55
|
|
|
108
|
-
testScreenCaptureKit();
|
|
109
|
-
>>>>>>> screencapture
|
|
56
|
+
testScreenCaptureKit();
|
|
Binary file
|
|
Binary file
|
package/scripts/test-exclude.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
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,222 +0,0 @@
|
|
|
1
|
-
#import "screen_capture_kit.h"
|
|
2
|
-
#import <ScreenCaptureKit/ScreenCaptureKit.h>
|
|
3
|
-
#import <AVFoundation/AVFoundation.h>
|
|
4
|
-
|
|
5
|
-
static SCStream *g_scStream = nil;
|
|
6
|
-
static SCRecordingOutput *g_scRecordingOutput = nil;
|
|
7
|
-
static NSURL *g_outputURL = nil;
|
|
8
|
-
static ScreenCaptureKitRecorder *g_scDelegate = nil;
|
|
9
|
-
static NSArray<SCRunningApplication *> *g_appsToExclude = nil;
|
|
10
|
-
static NSArray<SCWindow *> *g_windowsToExclude = nil;
|
|
11
|
-
|
|
12
|
-
@interface ScreenCaptureKitRecorder () <SCRecordingOutputDelegate>
|
|
13
|
-
@end
|
|
14
|
-
|
|
15
|
-
@implementation ScreenCaptureKitRecorder
|
|
16
|
-
|
|
17
|
-
+ (BOOL)isScreenCaptureKitAvailable {
|
|
18
|
-
if (@available(macOS 14.0, *)) {
|
|
19
|
-
Class streamClass = NSClassFromString(@"SCStream");
|
|
20
|
-
Class recordingOutputClass = NSClassFromString(@"SCRecordingOutput");
|
|
21
|
-
return (streamClass != nil && recordingOutputClass != nil);
|
|
22
|
-
}
|
|
23
|
-
return NO;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
+ (BOOL)isRecording {
|
|
27
|
-
return g_scStream != nil;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
+ (BOOL)startRecordingWithConfiguration:(NSDictionary *)config
|
|
31
|
-
delegate:(id)delegate
|
|
32
|
-
error:(NSError **)error {
|
|
33
|
-
if (![self isScreenCaptureKitAvailable]) {
|
|
34
|
-
if (error) {
|
|
35
|
-
*error = [NSError errorWithDomain:@"ScreenCaptureKitRecorder"
|
|
36
|
-
code:-1
|
|
37
|
-
userInfo:@{NSLocalizedDescriptionKey: @"ScreenCaptureKit not available"}];
|
|
38
|
-
}
|
|
39
|
-
return NO;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (g_scStream) {
|
|
43
|
-
return NO;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
@try {
|
|
47
|
-
SCShareableContent *content = nil;
|
|
48
|
-
if (@available(macOS 13.0, *)) {
|
|
49
|
-
NSLog(@"[SCK] Fetching shareable content...");
|
|
50
|
-
content = [SCShareableContent currentShareableContent];
|
|
51
|
-
}
|
|
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");
|
|
55
|
-
return NO;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
NSNumber *displayIdNumber = config[@"displayId"]; // CGDirectDisplayID
|
|
59
|
-
NSDictionary *captureArea = config[@"captureArea"]; // {x,y,width,height}
|
|
60
|
-
NSNumber *captureCursorNum = config[@"captureCursor"];
|
|
61
|
-
NSNumber *includeMicNum = config[@"includeMicrophone"];
|
|
62
|
-
NSNumber *includeSystemAudioNum = config[@"includeSystemAudio"];
|
|
63
|
-
NSArray<NSString *> *excludedBundleIds = config[@"excludedAppBundleIds"];
|
|
64
|
-
NSArray<NSNumber *> *excludedPIDs = config[@"excludedPIDs"];
|
|
65
|
-
NSArray<NSNumber *> *excludedWindowIds = config[@"excludedWindowIds"];
|
|
66
|
-
NSString *outputPath = config[@"outputPath"];
|
|
67
|
-
|
|
68
|
-
SCDisplay *targetDisplay = nil;
|
|
69
|
-
if (displayIdNumber) {
|
|
70
|
-
uint32_t wanted = (uint32_t)displayIdNumber.unsignedIntValue;
|
|
71
|
-
for (SCDisplay *d in content.displays) {
|
|
72
|
-
if (d.displayID == wanted) { targetDisplay = d; break; }
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (!targetDisplay) {
|
|
76
|
-
targetDisplay = content.displays.firstObject;
|
|
77
|
-
if (!targetDisplay) { NSLog(@"[SCK] No displays found"); return NO; }
|
|
78
|
-
}
|
|
79
|
-
NSLog(@"[SCK] Using displayID=%u", targetDisplay.displayID);
|
|
80
|
-
|
|
81
|
-
NSMutableArray<SCRunningApplication*> *appsToExclude = [NSMutableArray array];
|
|
82
|
-
if (excludedBundleIds.count > 0) {
|
|
83
|
-
for (SCRunningApplication *app in content.applications) {
|
|
84
|
-
if ([excludedBundleIds containsObject:app.bundleIdentifier]) {
|
|
85
|
-
[appsToExclude addObject:app];
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
if (excludedPIDs.count > 0) {
|
|
90
|
-
for (SCRunningApplication *app in content.applications) {
|
|
91
|
-
if ([excludedPIDs containsObject:@(app.processID)]) {
|
|
92
|
-
[appsToExclude addObject:app];
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
NSMutableArray<SCWindow*> *windowsToExclude = [NSMutableArray array];
|
|
98
|
-
if (excludedWindowIds.count > 0) {
|
|
99
|
-
for (SCWindow *w in content.windows) {
|
|
100
|
-
if ([excludedWindowIds containsObject:@(w.windowID)]) {
|
|
101
|
-
[windowsToExclude addObject:w];
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Keep strong references to excluded items for the lifetime of the stream
|
|
107
|
-
g_appsToExclude = [appsToExclude copy];
|
|
108
|
-
g_windowsToExclude = [windowsToExclude copy];
|
|
109
|
-
|
|
110
|
-
SCContentFilter *filter = nil;
|
|
111
|
-
if (appsToExclude.count > 0) {
|
|
112
|
-
if (@available(macOS 13.0, *)) {
|
|
113
|
-
filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingApplications:(g_appsToExclude ?: @[]) exceptingWindows:(g_windowsToExclude ?: @[])];
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (!filter) {
|
|
117
|
-
if (@available(macOS 13.0, *)) {
|
|
118
|
-
filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:(g_windowsToExclude ?: @[])];
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (!filter) { NSLog(@"[SCK] Failed to create filter"); return NO; }
|
|
122
|
-
|
|
123
|
-
SCStreamConfiguration *cfg = [[SCStreamConfiguration alloc] init];
|
|
124
|
-
if (captureArea && captureArea[@"width"] && captureArea[@"height"]) {
|
|
125
|
-
CGRect displayFrame = targetDisplay.frame;
|
|
126
|
-
double x = [captureArea[@"x"] doubleValue];
|
|
127
|
-
double yBottom = [captureArea[@"y"] doubleValue];
|
|
128
|
-
double w = [captureArea[@"width"] doubleValue];
|
|
129
|
-
double h = [captureArea[@"height"] doubleValue];
|
|
130
|
-
|
|
131
|
-
// Convert bottom-left origin (used by legacy path) to top-left for SC
|
|
132
|
-
double y = displayFrame.size.height - yBottom - h;
|
|
133
|
-
|
|
134
|
-
// Clamp to display bounds to avoid invalid sourceRect
|
|
135
|
-
if (w < 1) w = 1; if (h < 1) h = 1;
|
|
136
|
-
if (x < 0) x = 0; if (y < 0) y = 0;
|
|
137
|
-
if (x + w > displayFrame.size.width) w = MAX(1, displayFrame.size.width - x);
|
|
138
|
-
if (y + h > displayFrame.size.height) h = MAX(1, displayFrame.size.height - y);
|
|
139
|
-
|
|
140
|
-
CGRect src = CGRectMake(x, y, w, h);
|
|
141
|
-
cfg.sourceRect = src;
|
|
142
|
-
cfg.width = (int)src.size.width;
|
|
143
|
-
cfg.height = (int)src.size.height;
|
|
144
|
-
} else {
|
|
145
|
-
CGRect displayFrame = targetDisplay.frame;
|
|
146
|
-
cfg.width = (int)displayFrame.size.width;
|
|
147
|
-
cfg.height = (int)displayFrame.size.height;
|
|
148
|
-
}
|
|
149
|
-
cfg.showsCursor = captureCursorNum.boolValue;
|
|
150
|
-
if (includeMicNum || includeSystemAudioNum) {
|
|
151
|
-
if (@available(macOS 13.0, *)) {
|
|
152
|
-
cfg.capturesAudio = YES;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
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"));
|
|
159
|
-
|
|
160
|
-
SCRecordingOutputConfiguration *recCfg = [[SCRecordingOutputConfiguration alloc] init];
|
|
161
|
-
g_outputURL = [NSURL fileURLWithPath:outputPath];
|
|
162
|
-
recCfg.outputURL = g_outputURL;
|
|
163
|
-
recCfg.outputFileType = AVFileTypeQuickTimeMovie;
|
|
164
|
-
|
|
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;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
[g_scStream startCaptureWithCompletionHandler:^(NSError * _Nullable err) {
|
|
184
|
-
if (err) {
|
|
185
|
-
NSLog(@"[SCK] startCapture error: %@", err.localizedDescription);
|
|
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");
|
|
190
|
-
}
|
|
191
|
-
}];
|
|
192
|
-
// Return immediately; capture will start asynchronously
|
|
193
|
-
return YES;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return NO;
|
|
197
|
-
} @catch (__unused NSException *ex) {
|
|
198
|
-
return NO;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
+ (void)stopRecording {
|
|
203
|
-
if (!g_scStream) { return; }
|
|
204
|
-
@try {
|
|
205
|
-
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
|
206
|
-
[g_scStream stopCaptureWithCompletionHandler:^(NSError * _Nullable error) {
|
|
207
|
-
dispatch_semaphore_signal(sem);
|
|
208
|
-
}];
|
|
209
|
-
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
|
|
210
|
-
} @catch (__unused NSException *ex) {
|
|
211
|
-
}
|
|
212
|
-
if (g_scRecordingOutput) {
|
|
213
|
-
[g_scStream removeRecordingOutput:g_scRecordingOutput error:nil];
|
|
214
|
-
}
|
|
215
|
-
g_scRecordingOutput = nil;
|
|
216
|
-
g_scStream = nil;
|
|
217
|
-
g_outputURL = nil;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
@end
|
|
221
|
-
|
|
222
|
-
|