Skip to content

Commit 0c5a821

Browse files
committed
feat: implement askForMediaAccess
1 parent bbaf3be commit 0c5a821

File tree

4 files changed

+104
-16
lines changed

4 files changed

+104
-16
lines changed

README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ This native Node.js module allows you to manage an app's access to:
1616

1717
## `permissions.getAuthStatus(type)`
1818

19-
* `type` String - The type of system component to which you are requesting access. Can be one of 'contacts', 'full-disk-access', 'photos', 'reminders', or 'calendar'.
19+
* `type` String - The type of system component to which you are requesting access. Can be one of 'contacts', 'full-disk-access', 'photos', 'reminders', 'camera', 'microphone', 'screen-capture', or 'calendar'.
2020

2121
Returns `String` - Can be one of 'not determined', 'denied', 'authorized', or 'restricted'.
2222

@@ -47,6 +47,7 @@ Your app’s `Info.plist` file must provide a value for the `NSContactsUsageDesc
4747
const { askForContactsAccess } = require('node-mac-permissions')
4848

4949
askForContactsAccess((err, status) => {
50+
if (err) throw new Error(err)
5051
console.log(`Access to Contacts is ${status}`)
5152
})
5253
```
@@ -61,6 +62,7 @@ askForContactsAccess((err, status) => {
6162
const { askForCalendarAccess } = require('node-mac-permissions')
6263

6364
askForCalendarAccess((err, status) => {
65+
if (err) throw new Error(err)
6466
console.log(`Access to Calendar is ${status}`)
6567
})
6668
```
@@ -75,6 +77,7 @@ askForCalendarAccess((err, status) => {
7577
const { askForRemindersAccess } = require('node-mac-permissions')
7678

7779
askForRemindersAccess((err, status) => {
80+
if (err) throw new Error(err)
7881
console.log(`Access to Reminders is ${status}`)
7982
})
8083
```
@@ -86,3 +89,31 @@ const { askForFullAccess } = require('node-mac-permissions')
8689

8790
askForRemindersAccess()
8891
```
92+
93+
## `permissions.askForMediaAccess(type, callback)`
94+
95+
* `type` String - The type of media to which you are requesting access. Can be 'microphone' or 'camera'.
96+
97+
* `callback` Function
98+
* `error` String | null - An error in performing the request, if one occurred.
99+
* `status` String - Whether or not the request succeeded or failed; can be 'authorized' or 'denied'.
100+
101+
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.
102+
103+
```
104+
<key>`NSCameraUsageDescription</key>
105+
<string>Your reason for wanting to access the Camera</string>
106+
<key>`NSMicrophoneUsageDescription</key>
107+
<string>Your reason for wanting to access the Microphone</string>
108+
```
109+
110+
```js
111+
const { askForMediaAccess } = require('node-mac-permissions')
112+
113+
for (const type of ['microphone', 'camera']) {
114+
askForMediaAccess(type, (err, status) => {
115+
if (err) throw new Error(err)
116+
console.log(`Access to media type ${type} is ${status}`)
117+
})
118+
}
119+
```

binding.gyp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ],
2020
"xcode_settings": {
2121
"OTHER_CPLUSPLUSFLAGS": ["-std=c++14", "-stdlib=libc++", "-mmacosx-version-min=10.10"],
22-
"OTHER_LDFLAGS": ["-framework CoreFoundation -framework AppKit -framework Contacts -framework EventKit -framework Photos"]
22+
"OTHER_LDFLAGS": ["-framework CoreFoundation -framework AppKit -framework Contacts -framework EventKit -framework Photos -framework AVFoundation"]
2323
}
2424
}]
2525
}

index.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,22 @@ function getAuthStatus(type) {
99
return permissions.getAuthStatus.call(this, type)
1010
}
1111

12+
function askForMediaAccess(type, callback) {
13+
if (['microphone', 'camera'].includes(type)) {
14+
throw new TypeError(`${type} must be either 'camera' or 'microphone'`)
15+
}
16+
if (typeof callback !== 'function') {
17+
throw new TypeError(`callback must be a function`)
18+
}
19+
20+
return permissions.askForMediaAccess.call(this, type, callback)
21+
}
22+
1223
module.exports = {
13-
getAuthStatus,
14-
askForContactsAccess: permissions.askForContactsAccess,
1524
askForCalendarAccess: permissions.askForCalendarAccess,
25+
askForContactsAccess: permissions.askForContactsAccess,
26+
askForFullDiskAccess: permissions.askForFullDiskAccess,
1627
askForRemindersAccess: permissions.askForRemindersAccess,
17-
askForFullDiskAccess: permissions.askForFullDiskAccess
28+
askForMediaAccess,
29+
getAuthStatus
1830
}

permissions.mm

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
// Apple APIs
44
#import <AppKit/AppKit.h>
5+
#import <AVFoundation/AVFoundation.h>
56
#import <Contacts/Contacts.h>
67
#import <EventKit/EventKit.h>
78
#import <Foundation/Foundation.h>
@@ -106,12 +107,17 @@
106107
// Request access to the Contacts store.
107108
void AskForContactsAccess(const Napi::CallbackInfo &info) {
108109
Napi::Env env = info.Env();
109-
Napi::ThreadSafeFunction ts_fn = Napi::ThreadSafeFunction::New(env, info[0].As<Napi::Function>(),
110-
"Resource Name", 0, 1, [](Napi::Env){});
110+
Napi::ThreadSafeFunction ts_fn = Napi::ThreadSafeFunction::New(env,
111+
info[0].As<Napi::Function>(),
112+
"Resource Name",
113+
0,
114+
1,
115+
[](Napi::Env){});
111116

112117
if (@available(macOS 10.11, *)) {
113118
CNContactStore *store = [CNContactStore new];
114-
[store requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError* error) {
119+
[store requestAccessForEntityType:CNEntityTypeContacts
120+
completionHandler:^(BOOL granted, NSError* error) {
115121
auto callback = [](Napi::Env env, Napi::Function js_cb, const char* granted) {
116122
js_cb.Call({Napi::String::New(env, granted)});
117123
};
@@ -126,10 +132,15 @@ void AskForContactsAccess(const Napi::CallbackInfo &info) {
126132
// Request access to Calendar.
127133
void AskForCalendarAccess(const Napi::CallbackInfo &info) {
128134
Napi::Env env = info.Env();
129-
Napi::ThreadSafeFunction ts_fn = Napi::ThreadSafeFunction::New(env, info[0].As<Napi::Function>(),
130-
"Resource Name", 0, 1, [](Napi::Env){});
131-
132-
[[EKEventStore new] requestAccessToEntityType:EKEntityTypeEvent completion:^(BOOL granted, NSError * error) {
135+
Napi::ThreadSafeFunction ts_fn = Napi::ThreadSafeFunction::New(env,
136+
info[0].As<Napi::Function>(),
137+
"Resource Name",
138+
0,
139+
1,
140+
[](Napi::Env){});
141+
142+
[[EKEventStore new] requestAccessToEntityType:EKEntityTypeEvent
143+
completion:^(BOOL granted, NSError * error) {
133144
auto callback = [](Napi::Env env, Napi::Function js_cb, const char* granted) {
134145
js_cb.Call({Napi::String::New(env, granted)});
135146
};
@@ -140,10 +151,15 @@ void AskForCalendarAccess(const Napi::CallbackInfo &info) {
140151
// Request access to Reminders.
141152
void AskForRemindersAccess(const Napi::CallbackInfo &info) {
142153
Napi::Env env = info.Env();
143-
Napi::ThreadSafeFunction ts_fn = Napi::ThreadSafeFunction::New(env, info[0].As<Napi::Function>(),
144-
"Resource Name", 0, 1, [](Napi::Env){});
145-
146-
[[EKEventStore new] requestAccessToEntityType:EKEntityTypeReminder completion:^(BOOL granted, NSError * error) {
154+
Napi::ThreadSafeFunction ts_fn = Napi::ThreadSafeFunction::New(env,
155+
info[0].As<Napi::Function>(),
156+
"Resource Name",
157+
0,
158+
1,
159+
[](Napi::Env){});
160+
161+
[[EKEventStore new] requestAccessToEntityType:EKEntityTypeReminder
162+
completion:^(BOOL granted, NSError * error) {
147163
auto callback = [](Napi::Env env, Napi::Function js_cb, const char* granted) {
148164
js_cb.Call({Napi::String::New(env, granted)});
149165
};
@@ -158,6 +174,32 @@ void AskForFullDiskAccess(const Napi::CallbackInfo &info) {
158174
[workspace openURL:[NSURL URLWithString:pref_string]];
159175
}
160176

177+
// Request access to either the Camera or the Microphone.
178+
void AskForMediaAccess(const Napi::CallbackInfo& info) {
179+
Napi::Env env = info.Env();
180+
const std::string type = info[0].As<Napi::String>().Utf8Value();
181+
Napi::ThreadSafeFunction ts_fn = Napi::ThreadSafeFunction::New(env,
182+
info[1].As<Napi::Function>(),
183+
"Resource Name",
184+
0,
185+
1,
186+
[](Napi::Env){});
187+
188+
if (@available(macOS 10.14, *)) {
189+
AVMediaType media_type = (type == "microphone") ? AVMediaTypeAudio : AVMediaTypeVideo;
190+
[AVCaptureDevice requestAccessForMediaType:media_type
191+
completionHandler:^(BOOL granted) {
192+
auto callback = [](Napi::Env env, Napi::Function js_cb, const char* granted) {
193+
js_cb.Call({Napi::String::New(env, granted)});
194+
};
195+
ts_fn.BlockingCall(granted ? "authorized" : "denied", callback);
196+
}];
197+
} else {
198+
Napi::FunctionReference fn = Napi::Persistent(info[0].As<Napi::Function>());
199+
fn.Call({Napi::String::New(env, "authorized")});
200+
}
201+
}
202+
161203
// Initializes all functions exposed to JS
162204
Napi::Object Init(Napi::Env env, Napi::Object exports) {
163205
exports.Set(
@@ -175,6 +217,9 @@ void AskForFullDiskAccess(const Napi::CallbackInfo &info) {
175217
exports.Set(
176218
Napi::String::New(env, "askForFullDiskAccess"), Napi::Function::New(env, AskForFullDiskAccess)
177219
);
220+
exports.Set(
221+
Napi::String::New(env, "askForMediaAccess"), Napi::Function::New(env, AskForMediaAccess)
222+
);
178223

179224
return exports;
180225
}

0 commit comments

Comments
 (0)