diff options
-rwxr-xr-x | binding.gyp | 9 | ||||
-rwxr-xr-x | index.js | 19 | ||||
-rw-r--r-- | index.mm | 151 | ||||
-rw-r--r-- | package.json | 15 | ||||
-rw-r--r-- | test.js | 12 |
5 files changed, 206 insertions, 0 deletions
diff --git a/binding.gyp b/binding.gyp new file mode 100755 index 0000000..c8c5123 --- /dev/null +++ b/binding.gyp @@ -0,0 +1,9 @@ +{ + "targets": [ + { + "target_name": "mojave-permissions", + "sources": [ "index.mm" ], + "libraries": [ "-framework AVFoundation" ] + } + ] +} diff --git a/index.js b/index.js new file mode 100755 index 0000000..66bc582 --- /dev/null +++ b/index.js @@ -0,0 +1,19 @@ +const bin = require('./build/Release/mojave-permissions') + +module.exports = { + /** + * @param {String} mediaType + * @return {String} + */ + getMediaAccessStatus(mediaType) { + return bin.getMediaAccessStatus(mediaType) + }, + + /** + * @param {String} mediaType + * @param {Function} callback + */ + askForMediaAccess(mediaType, callback) { + return bin.askForMediaAccess(mediaType, callback) + } +} diff --git a/index.mm b/index.mm new file mode 100644 index 0000000..6423012 --- /dev/null +++ b/index.mm @@ -0,0 +1,151 @@ +#include <node.h> +#include <v8.h> +#include <stdio.h> +#include <unistd.h> +#include <uv.h> + +#import <AVFoundation/AVFoundation.h> + +using namespace v8; + +AVMediaType ParseMediaType(const std::string& media_type) { + if (media_type == "camera") { + return AVMediaTypeVideo; + } else if (media_type == "microphone") { + return AVMediaTypeAudio; + } else { + return nil; + } +} + +std::string ConvertAuthorizationStatus(AVAuthorizationStatus status) { + switch (status) { + case AVAuthorizationStatusNotDetermined: + return "not-determined"; + case AVAuthorizationStatusRestricted: + return "restricted"; + case AVAuthorizationStatusDenied: + return "denied"; + case AVAuthorizationStatusAuthorized: + return "granted"; + default: + return "unknown"; + } +} + +struct Baton { + uv_work_t request; + Persistent<Function> callback; + AVMediaType type; + bool hasResponse; + bool granted; + Baton() : hasResponse(0), granted(0) {} +}; + +// called by libuv worker in separate thread +static void DelayAsync(uv_work_t *req) { + Baton *baton = static_cast<Baton *>(req->data); + [AVCaptureDevice requestAccessForMediaType:baton->type + completionHandler:^(BOOL granted) { + baton->granted = granted; + baton->hasResponse = true; + }]; + + while (!baton->hasResponse) { + usleep(100000); + } +} + +// called by libuv in event loop when async function completes +static void DelayAsyncAfter(uv_work_t *req,int status) { + Isolate * isolate = Isolate::GetCurrent(); + HandleScope scope(isolate); + + Baton *baton = static_cast<Baton *>(req->data); + + Local<Value> argv[1] = { + v8::Boolean::New(isolate, baton->granted) + }; + + Local<Function>::New(isolate, baton->callback)->Call(isolate->GetCurrentContext()->Global(), 1, argv); + baton->callback.Reset(); + + delete baton; +} + +void AskForMediaAccess(const v8::FunctionCallbackInfo<Value>& args) { + Isolate* isolate = Isolate::GetCurrent(); + HandleScope scope(isolate); + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8(isolate, "argument 0 must be string"))); + return; + } + + if (!args[1]->IsFunction()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8(isolate, "argument 1 must be function"))); + return; + } + + String::Utf8Value mediaTypeValue(args[0]); + std::string mediaType(*mediaTypeValue); + + Local<Function> cbFunc = Local<Function>::Cast(args[1]); + + if (auto type = ParseMediaType(mediaType)) { + if (@available(macOS 10.14, *)) { + Baton *baton = new Baton; + baton->type = type; + baton->callback.Reset(isolate, cbFunc); + baton->request.data = baton; + + // queue the async function to the event loop + // the uv default loop is the node.js event loop + uv_queue_work(uv_default_loop(), &baton->request, DelayAsync, DelayAsyncAfter); + } else { + Local<Value> argv[1] = { v8::True(isolate) }; + cbFunc->Call(isolate->GetCurrentContext()->Global(), 1, argv); + } + } else { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8(isolate, "invalid media type"))); + } + + args.GetReturnValue().Set(Undefined(isolate)); +} + +void GetMediaAccessStatus(const v8::FunctionCallbackInfo<Value>& args) { + Isolate* isolate = Isolate::GetCurrent(); + HandleScope scope(isolate); + + if (!args[0]->IsString()) { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8(isolate, "argument 0 must be string"))); + return; + } + + String::Utf8Value mediaTypeValue(args[0]); + std::string mediaType(*mediaTypeValue); + + if (auto type = ParseMediaType(mediaType)) { + if (@available(macOS 10.14, *)) { + args.GetReturnValue().Set(String::NewFromUtf8(isolate, ConvertAuthorizationStatus( + [AVCaptureDevice authorizationStatusForMediaType:type]).c_str())); + } else { + // access always allowed pre-10.14 Mojave + args.GetReturnValue().Set(String::NewFromUtf8(isolate, ConvertAuthorizationStatus(AVAuthorizationStatusAuthorized).c_str())); + } + } else { + isolate->ThrowException(Exception::TypeError( + String::NewFromUtf8(isolate, "invalid media type"))); + } +} + +void Init(Handle<Object> exports) { + NODE_SET_METHOD(exports, "getMediaAccessStatus", GetMediaAccessStatus); + NODE_SET_METHOD(exports, "askForMediaAccess", AskForMediaAccess); +} + +NODE_MODULE(mojavepermissions, Init) diff --git a/package.json b/package.json new file mode 100644 index 0000000..1a2b5fd --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "description": "", + "devDependencies": {}, + "gypfile": true, + "main": "index.js", + "name": "mojave-permissions", + "optionalDependencies": {}, + "private": true, + "readme": "ERROR: No README data found!", + "scripts": { + "install": "node-gyp rebuild", + "test": "node index.js" + }, + "version": "1.0.0" +} @@ -0,0 +1,12 @@ +const perm = require('./index') + +for (let mediaType of ['camera', 'microphone']) { + let access = perm.getMediaAccessStatus(mediaType) + if (access == 'not-determined') { + perm.askForMediaAccess(mediaType, (granted) => { + console.log(mediaType + ' askForMediaAccess result:', granted) + }) + } else { + console.log(`${mediaType} access status: ${access}`) + } +} |