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/LICENSE +21 -0
- package/README.md +492 -0
- package/binding.gyp +39 -0
- package/index.js +372 -0
- package/package.json +45 -0
- package/src/audio_capture.mm +116 -0
- package/src/mac_recorder.mm +472 -0
- package/src/screen_capture.mm +141 -0
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
|