Skip to content

Commit ab7b1f4

Browse files
author
Geoff Lawson
authored
feat: split camera and mic permission requests into two fns (#3)
1 parent 789327c commit ab7b1f4

File tree

5 files changed

+116
-61
lines changed

5 files changed

+116
-61
lines changed

README.md

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -120,32 +120,62 @@ const { askForFullDiskAccess } = require('node-mac-permissions')
120120
askForFullDiskAccess()
121121
```
122122

123-
## `permissions.askForMediaAccess(type)`
123+
## `permissions.askForCameraAccess()`
124124

125-
* `type` String - The type of media to which you are requesting access. Can be `microphone` or `camera`.
125+
Returns `Promise<String>` - Current permission status; can be `authorized`, `denied`, or `restricted`.
126126

127-
Returns `Promise<String>` - Whether or not the request succeeded or failed; can be `authorized` or `denied`.
127+
Checks the authorization status for camera access. If the status check returns:
128+
129+
- `not determined`, the camera access authorization will prompt the user to authorize or deny. The Promise is resolved after the user selection with either `authorized` or `denied`.
130+
- `denied`, the `Security & Privacy` System Preferences window is opened with the Camera privacy key highlighted. On open of the `Security & Privacy` window, the Promise is resolved as `denied`.
131+
- `restricted`, the Promise is resolved as `restricted`.
128132

129-
Your app must provide an explanation for its use of capture devices using the `NSCameraUsageDescription` or `NSMicrophoneUsageDescription` `Info.plist` keys; Calling this method or attempting to start a capture session without a usage description raises an exception.
133+
Your app must provide an explanation for its use of capture devices using the `NSCameraUsageDescription` `Info.plist` key; Calling this method or attempting to start a capture session without a usage description raises an exception.
130134

131135
```
132-
<key>`NSCameraUsageDescription</key>
136+
<key>NSCameraUsageDescription</key>
133137
<string>Your reason for wanting to access the Camera</string>
134-
<key>`NSMicrophoneUsageDescription</key>
138+
```
139+
140+
**Note:**
141+
142+
- `status` will be resolved back as `authorized` prior to macOS 10.14 High Sierra, as access to the camera and microphone was unilaterally allowed until that version.
143+
144+
Example:
145+
146+
```js
147+
const { askForCameraAccess } = require("node-mac-permissions");
148+
149+
const status = await askForCameraAccess();
150+
```
151+
152+
## `permissions.askForMicrophoneAccess()`
153+
154+
Returns `Promise<String>` - Current permission status; can be `authorized`, `denied`, or `restricted`.
155+
156+
Checks the authorization status for microphone access. If the status check returns:
157+
158+
- `not determined`, the microphone access authorization will prompt the user to authorize or deny. The Promise is resolved after the user selection with either `authorized` or `denied`.
159+
- `denied`, the `Security & Privacy` System Preferences window is opened with the Microphone privacy key highlighted. On open of the `Security & Privacy` window, the Promise is resolved as `denied`.
160+
- `restricted`, the Promise is resolved as `restricted`.
161+
162+
Your app must provide an explanation for its use of capture devices using the `NSMicrophoneUsageDescription` `Info.plist` key; Calling this method or attempting to start a capture session without a usage description raises an exception.
163+
164+
```
165+
<key>NSMicrophoneUsageDescription</key>
135166
<string>Your reason for wanting to access the Microphone</string>
136167
```
137168

138-
**Note:** `status` will be resolved back as `authorized` prior to macOS 10.14 High Sierra, as access to the camera and microphone was unilaterally allowed until that version.
169+
**Note:**
170+
171+
- `status` will be resolved back as `authorized` prior to macOS 10.14 High Sierra, as access to the camera and microphone was unilaterally allowed until that version.
139172

140173
Example:
174+
141175
```js
142-
const { askForMediaAccess } = require('node-mac-permissions')
176+
const { askForMicrophoneAccess } = require("node-mac-permissions");
143177

144-
for (const type of ['microphone', 'camera']) {
145-
askForMediaAccess(type).then(status => {
146-
console.log(`Access to media type ${type} is ${status}`)
147-
})
148-
}
178+
const status = await askForMicrophoneAccess();
149179
```
150180

151181
## `permissions.askForScreenCaptureAccess()`

index.js

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const permissions = require('bindings')('permissions.node')
1+
const permissions = require('bindings')('permissions.node');
22

33
function getAuthStatus(type) {
44
const validTypes = [
@@ -10,31 +10,24 @@ function getAuthStatus(type) {
1010
'microphone',
1111
'accessibility',
1212
'location',
13-
'screen'
14-
]
13+
'screen',
14+
];
1515

1616
if (!validTypes.includes(type)) {
17-
throw new TypeError(`${type} is not a valid type`)
17+
throw new TypeError(`${type} is not a valid type`);
1818
}
1919

20-
return permissions.getAuthStatus.call(this, type)
21-
}
22-
23-
function askForMediaAccess(type) {
24-
if (!['microphone', 'camera'].includes(type)) {
25-
throw new TypeError(`${type} must be either 'camera' or 'microphone'`)
26-
}
27-
28-
return permissions.askForMediaAccess.call(this, type)
20+
return permissions.getAuthStatus.call(this, type);
2921
}
3022

3123
module.exports = {
3224
askForCalendarAccess: permissions.askForCalendarAccess,
3325
askForContactsAccess: permissions.askForContactsAccess,
3426
askForFullDiskAccess: permissions.askForFullDiskAccess,
3527
askForRemindersAccess: permissions.askForRemindersAccess,
28+
askForCameraAccess: permissions.askForCameraAccess,
29+
askForMicrophoneAccess: permissions.askForMicrophoneAccess,
3630
askForScreenCaptureAccess: permissions.askForScreenCaptureAccess,
3731
askForAccessibilityAccess: permissions.askForAccessibilityAccess,
38-
askForMediaAccess,
39-
getAuthStatus
40-
}
32+
getAuthStatus,
33+
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "node-mac-permissions",
3-
"version": "1.4.0",
3+
"version": "1.5.0",
44
"description": "A native node module to manage system permissions on macOS",
55
"main": "index.js",
66
"scripts": {

permissions.mm

Lines changed: 63 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -283,28 +283,69 @@ void AskForFullDiskAccess(const Napi::CallbackInfo &info) {
283283
[workspace openURL:[NSURL URLWithString:pref_string]];
284284
}
285285

286-
// Request access to either the Camera or the Microphone.
287-
Napi::Promise AskForMediaAccess(const Napi::CallbackInfo &info) {
286+
// Request Camera Access.
287+
Napi::Promise AskForCameraAccess(const Napi::CallbackInfo &info) {
288288
Napi::Env env = info.Env();
289-
const std::string type = info[0].As<Napi::String>().Utf8Value();
290289
Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
291-
Napi::ThreadSafeFunction ts_fn = Napi::ThreadSafeFunction::New(
292-
env, Napi::Function::New(env, NoOp), "mediaAccessCallback", 0, 1,
293-
[](Napi::Env) {});
290+
Napi::ThreadSafeFunction ts_fn = Napi::ThreadSafeFunction::New(env, Napi::Function::New(env, NoOp), "cameraAccessCallback", 0, 1, [](Napi::Env) {});
294291

295292
if (@available(macOS 10.14, *)) {
296-
AVMediaType media_type =
297-
(type == "microphone") ? AVMediaTypeAudio : AVMediaTypeVideo;
298-
[AVCaptureDevice
299-
requestAccessForMediaType:media_type
300-
completionHandler:^(BOOL granted) {
301-
auto callback = [=](Napi::Env env, Napi::Function js_cb,
302-
const char *granted) {
303-
deferred.Resolve(Napi::String::New(env, granted));
304-
};
305-
ts_fn.BlockingCall(granted ? "authorized" : "denied",
306-
callback);
307-
}];
293+
std::string auth_status = MediaAuthStatus("camera");
294+
295+
if (auth_status == "not determined") {
296+
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
297+
auto callback = [=](Napi::Env env, Napi::Function js_cb, const char *granted) {
298+
deferred.Resolve(Napi::String::New(env, granted));
299+
};
300+
301+
ts_fn.BlockingCall(granted ? "authorized" : "denied", callback);
302+
}];
303+
} else if (auth_status == "denied"){
304+
NSWorkspace *workspace = [[NSWorkspace alloc] init];
305+
NSString *pref_string = @"x-apple.systempreferences:com.apple.preference.security?Privacy_Camera";
306+
307+
[workspace openURL:[NSURL URLWithString:pref_string]];
308+
309+
deferred.Resolve(Napi::String::New(env, "denied"));
310+
} else {
311+
deferred.Resolve(Napi::String::New(env, auth_status));
312+
}
313+
} else {
314+
deferred.Resolve(Napi::String::New(env, "authorized"));
315+
}
316+
317+
return deferred.Promise();
318+
}
319+
320+
321+
322+
// Request Microphone Access.
323+
Napi::Promise AskForMicrophoneAccess(const Napi::CallbackInfo &info) {
324+
Napi::Env env = info.Env();
325+
Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
326+
Napi::ThreadSafeFunction ts_fn = Napi::ThreadSafeFunction::New(env, Napi::Function::New(env, NoOp), "microphoneAccessCallback", 0, 1, [](Napi::Env) {});
327+
328+
if (@available(macOS 10.14, *)) {
329+
std::string auth_status = MediaAuthStatus("microphone");
330+
331+
if (auth_status == "not determined") {
332+
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {
333+
auto callback = [=](Napi::Env env, Napi::Function js_cb, const char *granted) {
334+
deferred.Resolve(Napi::String::New(env, granted));
335+
};
336+
337+
ts_fn.BlockingCall(granted ? "authorized" : "denied", callback);
338+
}];
339+
} else if (auth_status == "denied") {
340+
NSWorkspace *workspace = [[NSWorkspace alloc] init];
341+
NSString *pref_string = @"x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone";
342+
343+
[workspace openURL:[NSURL URLWithString:pref_string]];
344+
345+
deferred.Resolve(Napi::String::New(env, "denied"));
346+
} else {
347+
deferred.Resolve(Napi::String::New(env, auth_status));
348+
}
308349
} else {
309350
deferred.Resolve(Napi::String::New(env, "authorized"));
310351
}
@@ -347,8 +388,10 @@ void AskForAccessibilityAccess(const Napi::CallbackInfo &info) {
347388
Napi::Function::New(env, AskForRemindersAccess));
348389
exports.Set(Napi::String::New(env, "askForFullDiskAccess"),
349390
Napi::Function::New(env, AskForFullDiskAccess));
350-
exports.Set(Napi::String::New(env, "askForMediaAccess"),
351-
Napi::Function::New(env, AskForMediaAccess));
391+
exports.Set(Napi::String::New(env, "askForCameraAccess"),
392+
Napi::Function::New(env, AskForCameraAccess));
393+
exports.Set(Napi::String::New(env, "askForMicrophoneAccess"),
394+
Napi::Function::New(env, AskForMicrophoneAccess));
352395
exports.Set(Napi::String::New(env, "askForScreenCaptureAccess"),
353396
Napi::Function::New(env, AskForScreenCaptureAccess));
354397
exports.Set(Napi::String::New(env, "askForAccessibilityAccess"),

test/module.spec.js

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
const { expect } = require('chai')
22
const {
33
getAuthStatus,
4-
askForMediaAccess
54
} = require('../index')
65

76
describe('node-mac-permissions', () => {
@@ -32,14 +31,4 @@ describe('node-mac-permissions', () => {
3231
}
3332
})
3433
})
35-
36-
describe('askForMediaAccess(type, callback)', () => {
37-
it ('throws on invalid media types', () => {
38-
expect(() => {
39-
askForMediaAccess('bad-type').then(status =>{
40-
console.log(status)
41-
})
42-
}).to.throw(/bad-type must be either 'camera' or 'microphone'/)
43-
})
44-
})
4534
})

0 commit comments

Comments
 (0)