node-mac-recorder 1.0.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/index.js ADDED
@@ -0,0 +1,372 @@
1
+ const { EventEmitter } = require("events");
2
+ const path = require("path");
3
+ const fs = require("fs");
4
+
5
+ // Native modülü yükle
6
+ let nativeBinding;
7
+ try {
8
+ nativeBinding = require("./build/Release/mac_recorder.node");
9
+ } catch (error) {
10
+ try {
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
+ );
18
+ }
19
+ }
20
+
21
+ class MacRecorder extends EventEmitter {
22
+ constructor() {
23
+ super();
24
+ this.isRecording = false;
25
+ this.outputPath = null;
26
+ this.recordingTimer = null;
27
+ this.recordingStartTime = null;
28
+ this.options = {
29
+ includeMicrophone: false, // Default olarak mikrofon kapalı
30
+ includeSystemAudio: true, // Default olarak sistem sesi açık
31
+ quality: "medium",
32
+ frameRate: 30,
33
+ captureArea: null, // { x, y, width, height }
34
+ captureCursor: false, // Default olarak cursor gizli
35
+ showClicks: false,
36
+ displayId: null, // Hangi ekranı kaydedeceği (null = ana ekran)
37
+ windowId: null, // Hangi pencereyi kaydedeceği (null = tam ekran)
38
+ };
39
+ }
40
+
41
+ /**
42
+ * macOS ses cihazlarını listeler
43
+ */
44
+ async getAudioDevices() {
45
+ return new Promise((resolve, reject) => {
46
+ try {
47
+ const devices = nativeBinding.getAudioDevices();
48
+ const formattedDevices = devices.map((device) => ({
49
+ name: typeof device === "string" ? device : device.name || device,
50
+ id: typeof device === "object" ? device.id : device,
51
+ type: typeof device === "object" ? device.type : "Audio Device",
52
+ }));
53
+ resolve(formattedDevices);
54
+ } catch (error) {
55
+ reject(error);
56
+ }
57
+ });
58
+ }
59
+
60
+ /**
61
+ * macOS ekranlarını listeler
62
+ */
63
+ async getDisplays() {
64
+ return new Promise((resolve, reject) => {
65
+ try {
66
+ const displays = nativeBinding.getDisplays();
67
+ const formattedDisplays = displays.map((display, index) => ({
68
+ id: index,
69
+ name:
70
+ typeof display === "string"
71
+ ? display
72
+ : display.name || `Display ${index + 1}`,
73
+ resolution:
74
+ typeof display === "object"
75
+ ? `${display.width}x${display.height}`
76
+ : "Unknown",
77
+ width: typeof display === "object" ? display.width : null,
78
+ height: typeof display === "object" ? display.height : null,
79
+ x: typeof display === "object" ? display.x : 0,
80
+ y: typeof display === "object" ? display.y : 0,
81
+ isPrimary:
82
+ typeof display === "object" ? display.isPrimary : index === 0,
83
+ }));
84
+ resolve(formattedDisplays);
85
+ } catch (error) {
86
+ reject(error);
87
+ }
88
+ });
89
+ }
90
+
91
+ /**
92
+ * macOS açık pencerelerini listeler
93
+ */
94
+ async getWindows() {
95
+ return new Promise((resolve, reject) => {
96
+ try {
97
+ const windows = nativeBinding.getWindows();
98
+ resolve(windows);
99
+ } catch (error) {
100
+ reject(error);
101
+ }
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Kayıt seçeneklerini ayarlar
107
+ */
108
+ setOptions(options) {
109
+ this.options = { ...this.options, ...options };
110
+ }
111
+
112
+ /**
113
+ * Ekran kaydını başlatır (macOS native AVFoundation kullanarak)
114
+ */
115
+ async startRecording(outputPath, options = {}) {
116
+ if (this.isRecording) {
117
+ throw new Error("Recording is already in progress");
118
+ }
119
+
120
+ if (!outputPath) {
121
+ throw new Error("Output path is required");
122
+ }
123
+
124
+ // Seçenekleri güncelle
125
+ this.setOptions(options);
126
+
127
+ // WindowId varsa captureArea'yı otomatik ayarla
128
+ if (this.options.windowId && !this.options.captureArea) {
129
+ try {
130
+ const windows = await this.getWindows();
131
+ const displays = await this.getDisplays();
132
+ const targetWindow = windows.find(
133
+ (w) => w.id === this.options.windowId
134
+ );
135
+
136
+ if (targetWindow) {
137
+ // Pencere hangi display'de olduğunu bul
138
+ let targetDisplayId = null;
139
+ let adjustedX = targetWindow.x;
140
+ let adjustedY = targetWindow.y;
141
+
142
+ // Pencere hangi display'de?
143
+ for (let i = 0; i < displays.length; i++) {
144
+ const display = displays[i];
145
+ const displayWidth = parseInt(display.resolution.split("x")[0]);
146
+ const displayHeight = parseInt(display.resolution.split("x")[1]);
147
+
148
+ // Pencere bu display sınırları içinde mi?
149
+ if (
150
+ targetWindow.x >= display.x &&
151
+ targetWindow.x < display.x + displayWidth &&
152
+ targetWindow.y >= display.y &&
153
+ targetWindow.y < display.y + displayHeight
154
+ ) {
155
+ targetDisplayId = i;
156
+ // Koordinatları display'e göre normalize et
157
+ adjustedX = targetWindow.x - display.x;
158
+ adjustedY = targetWindow.y - display.y;
159
+ break;
160
+ }
161
+ }
162
+
163
+ // Eğer display bulunamadıysa ana display kullan
164
+ if (targetDisplayId === null) {
165
+ const mainDisplay = displays.find((d) => d.x === 0 && d.y === 0);
166
+ if (mainDisplay) {
167
+ targetDisplayId = displays.indexOf(mainDisplay);
168
+ adjustedX = Math.max(
169
+ 0,
170
+ Math.min(
171
+ targetWindow.x,
172
+ parseInt(mainDisplay.resolution.split("x")[0]) -
173
+ targetWindow.width
174
+ )
175
+ );
176
+ adjustedY = Math.max(
177
+ 0,
178
+ Math.min(
179
+ targetWindow.y,
180
+ parseInt(mainDisplay.resolution.split("x")[1]) -
181
+ targetWindow.height
182
+ )
183
+ );
184
+ }
185
+ }
186
+
187
+ // DisplayId'yi ayarla
188
+ if (targetDisplayId !== null) {
189
+ this.options.displayId = targetDisplayId;
190
+ }
191
+
192
+ this.options.captureArea = {
193
+ x: Math.max(0, adjustedX),
194
+ y: Math.max(0, adjustedY),
195
+ width: targetWindow.width,
196
+ height: targetWindow.height,
197
+ };
198
+
199
+ console.log(
200
+ `Window ${targetWindow.appName}: display=${targetDisplayId}, coords=${targetWindow.x},${targetWindow.y} -> ${adjustedX},${adjustedY}`
201
+ );
202
+ }
203
+ } catch (error) {
204
+ console.warn(
205
+ "Pencere bilgisi alınamadı, tam ekran kaydedilecek:",
206
+ error.message
207
+ );
208
+ }
209
+ }
210
+
211
+ // Çıkış dizinini oluştur
212
+ const outputDir = path.dirname(outputPath);
213
+ if (!fs.existsSync(outputDir)) {
214
+ fs.mkdirSync(outputDir, { recursive: true });
215
+ }
216
+
217
+ this.outputPath = outputPath;
218
+
219
+ return new Promise((resolve, reject) => {
220
+ try {
221
+ // Native kayıt başlat
222
+ const recordingOptions = {
223
+ includeMicrophone: this.options.includeMicrophone || false,
224
+ includeSystemAudio: this.options.includeSystemAudio !== false, // Default true
225
+ captureCursor: this.options.captureCursor || false,
226
+ displayId: this.options.displayId || null, // null = ana ekran
227
+ windowId: this.options.windowId || null, // null = tam ekran
228
+ };
229
+
230
+ // Manuel captureArea varsa onu kullan
231
+ if (this.options.captureArea) {
232
+ recordingOptions.captureArea = {
233
+ x: this.options.captureArea.x,
234
+ y: this.options.captureArea.y,
235
+ width: this.options.captureArea.width,
236
+ height: this.options.captureArea.height,
237
+ };
238
+ }
239
+
240
+ const success = nativeBinding.startRecording(
241
+ outputPath,
242
+ recordingOptions
243
+ );
244
+
245
+ if (success) {
246
+ this.isRecording = true;
247
+ this.recordingStartTime = Date.now();
248
+
249
+ // Timer başlat (progress tracking için)
250
+ this.recordingTimer = setInterval(() => {
251
+ const elapsed = Math.floor(
252
+ (Date.now() - this.recordingStartTime) / 1000
253
+ );
254
+ this.emit("timeUpdate", elapsed);
255
+ }, 1000);
256
+
257
+ this.emit("started", this.outputPath);
258
+ resolve(this.outputPath);
259
+ } else {
260
+ reject(
261
+ new Error(
262
+ "Failed to start recording. Check permissions and try again."
263
+ )
264
+ );
265
+ }
266
+ } catch (error) {
267
+ reject(error);
268
+ }
269
+ });
270
+ }
271
+
272
+ /**
273
+ * Ekran kaydını durdurur
274
+ */
275
+ async stopRecording() {
276
+ if (!this.isRecording) {
277
+ throw new Error("No recording in progress");
278
+ }
279
+
280
+ return new Promise((resolve, reject) => {
281
+ try {
282
+ const success = nativeBinding.stopRecording();
283
+
284
+ // Timer durdur
285
+ if (this.recordingTimer) {
286
+ clearInterval(this.recordingTimer);
287
+ this.recordingTimer = null;
288
+ }
289
+
290
+ this.isRecording = false;
291
+
292
+ const result = {
293
+ code: success ? 0 : 1,
294
+ outputPath: this.outputPath,
295
+ };
296
+
297
+ this.emit("stopped", result);
298
+
299
+ if (success) {
300
+ // Dosyanın oluşturulmasını bekle
301
+ setTimeout(() => {
302
+ if (fs.existsSync(this.outputPath)) {
303
+ this.emit("completed", this.outputPath);
304
+ }
305
+ }, 1000);
306
+ }
307
+
308
+ resolve(result);
309
+ } catch (error) {
310
+ this.isRecording = false;
311
+ if (this.recordingTimer) {
312
+ clearInterval(this.recordingTimer);
313
+ this.recordingTimer = null;
314
+ }
315
+ reject(error);
316
+ }
317
+ });
318
+ }
319
+
320
+ /**
321
+ * Kayıt durumunu döndürür
322
+ */
323
+ getStatus() {
324
+ const nativeStatus = nativeBinding.getRecordingStatus();
325
+ return {
326
+ isRecording: this.isRecording && nativeStatus,
327
+ outputPath: this.outputPath,
328
+ options: this.options,
329
+ recordingTime: this.recordingStartTime
330
+ ? Math.floor((Date.now() - this.recordingStartTime) / 1000)
331
+ : 0,
332
+ };
333
+ }
334
+
335
+ /**
336
+ * macOS'ta kayıt izinlerini kontrol eder
337
+ */
338
+ async checkPermissions() {
339
+ return new Promise((resolve) => {
340
+ try {
341
+ const hasPermission = nativeBinding.checkPermissions();
342
+ resolve({
343
+ screenRecording: hasPermission,
344
+ accessibility: hasPermission,
345
+ microphone: hasPermission, // Native modül ses izinlerini de kontrol ediyor
346
+ });
347
+ } catch (error) {
348
+ resolve({
349
+ screenRecording: false,
350
+ accessibility: false,
351
+ microphone: false,
352
+ error: error.message,
353
+ });
354
+ }
355
+ });
356
+ }
357
+
358
+ /**
359
+ * Native modül bilgilerini döndürür
360
+ */
361
+ getModuleInfo() {
362
+ return {
363
+ version: require("./package.json").version,
364
+ platform: process.platform,
365
+ arch: process.arch,
366
+ nodeVersion: process.version,
367
+ nativeModule: "mac_recorder.node",
368
+ };
369
+ }
370
+ }
371
+
372
+ module.exports = MacRecorder;
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "node-mac-recorder",
3
+ "version": "1.0.0",
4
+ "description": "Native macOS screen recording package for Node.js applications",
5
+ "main": "index.js",
6
+ "keywords": [
7
+ "macos",
8
+ "screen",
9
+ "recording",
10
+ "capture",
11
+ "video",
12
+ "native",
13
+ "electron"
14
+ ],
15
+ "author": "aslanonur",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/aslanon/node-mac-recorder.git"
20
+ },
21
+ "homepage": "https://github.com/aslanon/node-mac-recorder#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/aslanon/node-mac-recorder/issues"
24
+ },
25
+ "engines": {
26
+ "node": ">=14.0.0"
27
+ },
28
+ "os": [
29
+ "darwin"
30
+ ],
31
+ "scripts": {
32
+ "test": "node test.js",
33
+ "install": "node install.js",
34
+ "build": "node-gyp build",
35
+ "rebuild": "node-gyp rebuild",
36
+ "clean": "node-gyp clean"
37
+ },
38
+ "dependencies": {
39
+ "node-addon-api": "^7.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "node-gyp": "^10.0.0"
43
+ },
44
+ "gypfile": true
45
+ }
@@ -0,0 +1,116 @@
1
+ #import <AVFoundation/AVFoundation.h>
2
+ #import <CoreAudio/CoreAudio.h>
3
+
4
+ @interface AudioCapture : NSObject
5
+
6
+ + (NSArray *)getAudioDevices;
7
+ + (BOOL)hasAudioPermission;
8
+ + (void)requestAudioPermission:(void(^)(BOOL granted))completion;
9
+
10
+ @end
11
+
12
+ @implementation AudioCapture
13
+
14
+ + (NSArray *)getAudioDevices {
15
+ NSMutableArray *devices = [NSMutableArray array];
16
+
17
+ // Get all audio devices
18
+ NSArray *audioDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
19
+
20
+ for (AVCaptureDevice *device in audioDevices) {
21
+ NSDictionary *deviceInfo = @{
22
+ @"id": device.uniqueID,
23
+ @"name": device.localizedName,
24
+ @"manufacturer": device.manufacturer ?: @"Unknown",
25
+ @"isDefault": @([device isEqual:[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]])
26
+ };
27
+
28
+ [devices addObject:deviceInfo];
29
+ }
30
+
31
+ // Also get system audio devices using Core Audio
32
+ AudioObjectPropertyAddress propertyAddress = {
33
+ kAudioHardwarePropertyDevices,
34
+ kAudioObjectPropertyScopeGlobal,
35
+ kAudioObjectPropertyElementMaster
36
+ };
37
+
38
+ UInt32 dataSize = 0;
39
+ OSStatus status = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize);
40
+
41
+ if (status == kAudioHardwareNoError) {
42
+ UInt32 deviceCount = dataSize / sizeof(AudioDeviceID);
43
+ AudioDeviceID *audioDeviceIDs = (AudioDeviceID *)malloc(dataSize);
44
+
45
+ status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize, audioDeviceIDs);
46
+
47
+ if (status == kAudioHardwareNoError) {
48
+ for (UInt32 i = 0; i < deviceCount; i++) {
49
+ AudioDeviceID deviceID = audioDeviceIDs[i];
50
+
51
+ // Get device name
52
+ propertyAddress.mSelector = kAudioDevicePropertyDeviceNameCFString;
53
+ propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
54
+
55
+ CFStringRef deviceName = NULL;
56
+ dataSize = sizeof(deviceName);
57
+
58
+ status = AudioObjectGetPropertyData(deviceID, &propertyAddress, 0, NULL, &dataSize, &deviceName);
59
+
60
+ if (status == kAudioHardwareNoError && deviceName) {
61
+ // Check if it's an input device
62
+ propertyAddress.mSelector = kAudioDevicePropertyStreamConfiguration;
63
+ propertyAddress.mScope = kAudioDevicePropertyScopeInput;
64
+
65
+ AudioObjectGetPropertyDataSize(deviceID, &propertyAddress, 0, NULL, &dataSize);
66
+
67
+ if (dataSize > 0) {
68
+ AudioBufferList *bufferList = (AudioBufferList *)malloc(dataSize);
69
+ AudioObjectGetPropertyData(deviceID, &propertyAddress, 0, NULL, &dataSize, bufferList);
70
+
71
+ if (bufferList->mNumberBuffers > 0) {
72
+ NSDictionary *deviceInfo = @{
73
+ @"id": @(deviceID),
74
+ @"name": (__bridge NSString *)deviceName,
75
+ @"type": @"System Audio Input",
76
+ @"isSystemDevice": @YES
77
+ };
78
+
79
+ [devices addObject:deviceInfo];
80
+ }
81
+
82
+ free(bufferList);
83
+ }
84
+
85
+ CFRelease(deviceName);
86
+ }
87
+ }
88
+ }
89
+
90
+ free(audioDeviceIDs);
91
+ }
92
+
93
+ return [devices copy];
94
+ }
95
+
96
+ + (BOOL)hasAudioPermission {
97
+ if (@available(macOS 10.14, *)) {
98
+ AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
99
+ return status == AVAuthorizationStatusAuthorized;
100
+ }
101
+ return YES; // Older versions don't require explicit permission
102
+ }
103
+
104
+ + (void)requestAudioPermission:(void(^)(BOOL granted))completion {
105
+ if (@available(macOS 10.14, *)) {
106
+ [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {
107
+ dispatch_async(dispatch_get_main_queue(), ^{
108
+ completion(granted);
109
+ });
110
+ }];
111
+ } else {
112
+ completion(YES);
113
+ }
114
+ }
115
+
116
+ @end