Merge pull request #4 from defold/dev-macos-permissions

macOS and iOS permissions
This commit is contained in:
Björn Ritzl 2020-07-14 20:43:31 +02:00 committed by GitHub
commit 4bf2d79b4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 277 additions and 105 deletions

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2019 Defold Copyright (c) 2019 Defold Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,67 +1,101 @@
# extension-camera # extension-camera
Example of interacting with the camera through native extensions. Native extension to use device camera to capture frames.
# Disclaimer
Although we aim to provide good example functionality in this example, we cannot guarantee the quality/stability at all times. # Installation
Please regard it as just that, an example, and don't rely on this as a dependency for your production code.
# Known issues To use the camera extension in a Defold project this project has to be added as a [Defold library dependency](http://www.defold.com/manuals/libraries/). Open the **game.project** file and in the [Dependencies field in the Project section](https://defold.com/manuals/project-settings/#dependencies) add:
https://github.com/defold/extension-camera/archive/master.zip
Or point to the ZIP file of [a specific release](https://github.com/defold/extension-camera/releases).
# Supported platforms
The currently supported platforms are: OSX + iOS The currently supported platforms are: OSX + iOS
# FAQ # FAQ
## How do I use this extension? ## How do I reset macOS camera permission?
Add the package link (https://github.com/defold/extension-camera/archive/master.zip) To test macOS camera permission popup multiple times you can reset the permission from the terminal:
to the project setting `project.dependencies`, and you should be good to go.
See the [manual](http://www.defold.com/manuals/libraries/) for further info. ```bash
tccutil reset Camera
```
# Lua API # Lua API
## Type constants ## Type constants
Describes what camera should be used Describes what camera should be used.
```lua
camera.CAMERA_TYPE_FRONT -- Selfie
camera.CAMERA_TYPE_BACK
```
camera.CAMERA_TYPE_FRONT -- Selfie
camera.CAMERA_TYPE_BACK
## Quality constants ## Quality constants
camera.CAPTURE_QUALITY_HIGH ```lua
camera.CAPTURE_QUALITY_MEDIUM camera.CAPTURE_QUALITY_HIGH
camera.CAPTURE_QUALITY_LOW camera.CAPTURE_QUALITY_MEDIUM
camera.CAPTURE_QUALITY_LOW
```
## camera.start_capture(type, quality)
Returns true if the capture starts well ## Status constants
if camera.start_capture(camera.CAMERA_TYPE_BACK, camera.CAPTURE_QUALITY_HIGH) then ```lua
camera.STATUS_STARTED
camera.STATUS_STOPPED
camera.STATUS_NOT_PERMITTED
camera.STATUS_ERROR
```
## camera.start_capture(type, quality, callback)
Start camera capture using the specified camera (front/back) and capture quality. This may trigger a camera usage permission popup. When the popup has been dismissed the callback will be invoked with camera start status.
```lua
camera.start_capture(camera.CAMERA_TYPE_BACK, camera.CAPTURE_QUALITY_HIGH, function(self, status)
if status == camera.STATUS_STARTED then
-- do stuff -- do stuff
end end
end)
```
## camera.stop_capture() ## camera.stop_capture()
Stops a previously started capture session Stops a previously started capture session.
```lua
camera.stop_capture()
```
## camera.get_info() ## camera.get_info()
Gets the info from the current capture session Gets the info from the current capture session.
```lua
local info = camera.get_info()
print("width", info.width)
print("height", info.height)
```
local info = camera.get_info()
print("width", info.width)
print("height", info.height)
## camera.get_frame() ## camera.get_frame()
Retrieves the camera pixel buffer Retrieves the camera pixel buffer. This buffer has one stream named "rgb", and is of type `buffer.VALUE_TYPE_UINT8` and has the value count of 1.
This buffer has one stream named "rgb", and is of type buffer.VALUE_TYPE_UINT8 and has the value count of 1
self.cameraframe = camera.get_frame() ```lua
self.cameraframe = camera.get_frame()
```

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>{{project.title}} would like to access the camera.</string>
</dict>
</plist>

View File

@ -26,31 +26,82 @@ struct DefoldCamera
// Information about the currently set camera // Information about the currently set camera
CameraInfo m_Params; CameraInfo m_Params;
dmArray<CameraStatus> m_MessageQueue;
dmScript::LuaCallbackInfo* m_Callback;
dmMutex::HMutex m_Mutex;
}; };
DefoldCamera g_DefoldCamera; DefoldCamera g_DefoldCamera;
void Camera_QueueMessage(CameraStatus status)
{
DM_MUTEX_SCOPED_LOCK(g_DefoldCamera.m_Mutex);
if (g_DefoldCamera.m_MessageQueue.Full())
{
g_DefoldCamera.m_MessageQueue.OffsetCapacity(1);
}
g_DefoldCamera.m_MessageQueue.Push(status);
}
static void Camera_ProcessQueue()
{
DM_MUTEX_SCOPED_LOCK(g_DefoldCamera.m_Mutex);
for (uint32_t i = 0; i != g_DefoldCamera.m_MessageQueue.Size(); ++i)
{
lua_State* L = dmScript::GetCallbackLuaContext(g_DefoldCamera.m_Callback);
if (!dmScript::SetupCallback(g_DefoldCamera.m_Callback))
{
break;
}
CameraStatus status = g_DefoldCamera.m_MessageQueue[i];
if (status == STATUS_STARTED)
{
// Increase ref count
dmScript::LuaHBuffer luabuffer = {g_DefoldCamera.m_VideoBuffer, false};
dmScript::PushBuffer(L, luabuffer);
g_DefoldCamera.m_VideoBufferLuaRef = dmScript::Ref(L, LUA_REGISTRYINDEX);
}
else if (status == STATUS_STOPPED)
{
dmScript::Unref(L, LUA_REGISTRYINDEX, g_DefoldCamera.m_VideoBufferLuaRef); // We want it destroyed by the GC
g_DefoldCamera.m_VideoBufferLuaRef = 0;
}
lua_pushnumber(L, (lua_Number)status);
int ret = lua_pcall(L, 2, 0, 0);
if (ret != 0)
{
lua_pop(L, 1);
}
dmScript::TeardownCallback(g_DefoldCamera.m_Callback);
}
g_DefoldCamera.m_MessageQueue.SetSize(0);
}
static void Camera_DestroyCallback()
{
if (g_DefoldCamera.m_Callback != 0)
{
dmScript::DestroyCallback(g_DefoldCamera.m_Callback);
g_DefoldCamera.m_Callback = 0;
}
}
static int StartCapture(lua_State* L) static int StartCapture(lua_State* L)
{ {
DM_LUA_STACK_CHECK(L, 1); DM_LUA_STACK_CHECK(L, 0);
CameraType type = (CameraType) luaL_checkint(L, 1); CameraType type = (CameraType) luaL_checkint(L, 1);
CaptureQuality quality = (CaptureQuality)luaL_checkint(L, 2); CaptureQuality quality = (CaptureQuality)luaL_checkint(L, 2);
int status = CameraPlatform_StartCapture(&g_DefoldCamera.m_VideoBuffer, type, quality, g_DefoldCamera.m_Params); Camera_DestroyCallback();
g_DefoldCamera.m_Callback = dmScript::CreateCallback(L, 3);
lua_pushboolean(L, status > 0); CameraPlatform_StartCapture(&g_DefoldCamera.m_VideoBuffer, type, quality, g_DefoldCamera.m_Params);
if( status == 0 )
{
dmLogError("capture failed!\n");
return 1;
}
// Increase ref count
dmScript::LuaHBuffer luabuffer = {g_DefoldCamera.m_VideoBuffer, false};
dmScript::PushBuffer(L, luabuffer);
g_DefoldCamera.m_VideoBufferLuaRef = dmScript::Ref(L, LUA_REGISTRYINDEX);
return 1; return 1;
} }
@ -59,13 +110,7 @@ static int StopCapture(lua_State* L)
{ {
DM_LUA_STACK_CHECK(L, 0); DM_LUA_STACK_CHECK(L, 0);
int status = CameraPlatform_StopCapture(); CameraPlatform_StopCapture();
if( !status )
{
return luaL_error(L, "Failed to stop capture. Was it started?");
}
dmScript::Unref(L, LUA_REGISTRYINDEX, g_DefoldCamera.m_VideoBufferLuaRef); // We want it destroyed by the GC
return 0; return 0;
} }
@ -94,7 +139,14 @@ static int GetInfo(lua_State* L)
static int GetFrame(lua_State* L) static int GetFrame(lua_State* L)
{ {
DM_LUA_STACK_CHECK(L, 1); DM_LUA_STACK_CHECK(L, 1);
lua_rawgeti(L,LUA_REGISTRYINDEX, g_DefoldCamera.m_VideoBufferLuaRef); if (g_DefoldCamera.m_VideoBufferLuaRef != 0)
{
lua_rawgeti(L, LUA_REGISTRYINDEX, g_DefoldCamera.m_VideoBufferLuaRef);
}
else
{
lua_pushnil(L);
}
return 1; return 1;
} }
@ -113,9 +165,9 @@ static void LuaInit(lua_State* L)
int top = lua_gettop(L); int top = lua_gettop(L);
luaL_register(L, MODULE_NAME, Module_methods); luaL_register(L, MODULE_NAME, Module_methods);
#define SETCONSTANT(name) \ #define SETCONSTANT(name) \
lua_pushnumber(L, (lua_Number) name); \ lua_pushnumber(L, (lua_Number) name); \
lua_setfield(L, -2, #name);\ lua_setfield(L, -2, #name);\
SETCONSTANT(CAMERA_TYPE_FRONT) SETCONSTANT(CAMERA_TYPE_FRONT)
SETCONSTANT(CAMERA_TYPE_BACK) SETCONSTANT(CAMERA_TYPE_BACK)
@ -124,7 +176,12 @@ static void LuaInit(lua_State* L)
SETCONSTANT(CAPTURE_QUALITY_MEDIUM) SETCONSTANT(CAPTURE_QUALITY_MEDIUM)
SETCONSTANT(CAPTURE_QUALITY_HIGH) SETCONSTANT(CAPTURE_QUALITY_HIGH)
#undef SETCONSTANT SETCONSTANT(STATUS_STARTED)
SETCONSTANT(STATUS_STOPPED)
SETCONSTANT(STATUS_NOT_PERMITTED)
SETCONSTANT(STATUS_ERROR)
#undef SETCONSTANT
lua_pop(L, 1); lua_pop(L, 1);
assert(top == lua_gettop(L)); assert(top == lua_gettop(L));
@ -132,12 +189,20 @@ static void LuaInit(lua_State* L)
dmExtension::Result AppInitializeCamera(dmExtension::AppParams* params) dmExtension::Result AppInitializeCamera(dmExtension::AppParams* params)
{ {
dmLogInfo("Registered %s Extension", MODULE_NAME);
return dmExtension::RESULT_OK; return dmExtension::RESULT_OK;
} }
dmExtension::Result InitializeCamera(dmExtension::Params* params) dmExtension::Result InitializeCamera(dmExtension::Params* params)
{ {
LuaInit(params->m_L); LuaInit(params->m_L);
g_DefoldCamera.m_Mutex = dmMutex::New();
return dmExtension::RESULT_OK;
}
static dmExtension::Result UpdateCamera(dmExtension::Params* params)
{
Camera_ProcessQueue();
return dmExtension::RESULT_OK; return dmExtension::RESULT_OK;
} }
@ -148,6 +213,8 @@ dmExtension::Result AppFinalizeCamera(dmExtension::AppParams* params)
dmExtension::Result FinalizeCamera(dmExtension::Params* params) dmExtension::Result FinalizeCamera(dmExtension::Params* params)
{ {
dmMutex::Delete(g_DefoldCamera.m_Mutex);
Camera_DestroyCallback();
return dmExtension::RESULT_OK; return dmExtension::RESULT_OK;
} }
@ -156,7 +223,7 @@ dmExtension::Result FinalizeCamera(dmExtension::Params* params)
static dmExtension::Result AppInitializeCamera(dmExtension::AppParams* params) static dmExtension::Result AppInitializeCamera(dmExtension::AppParams* params)
{ {
dmLogInfo("Registered %s (null) Extension\n", MODULE_NAME); dmLogInfo("Registered %s (null) Extension", MODULE_NAME);
return dmExtension::RESULT_OK; return dmExtension::RESULT_OK;
} }
@ -165,6 +232,12 @@ static dmExtension::Result InitializeCamera(dmExtension::Params* params)
return dmExtension::RESULT_OK; return dmExtension::RESULT_OK;
} }
static dmExtension::Result UpdateCamera(dmExtension::Params* params)
{
Camera_ProcessQueue()
return dmExtension::RESULT_OK;
}
static dmExtension::Result AppFinalizeCamera(dmExtension::AppParams* params) static dmExtension::Result AppFinalizeCamera(dmExtension::AppParams* params)
{ {
return dmExtension::RESULT_OK; return dmExtension::RESULT_OK;
@ -178,4 +251,4 @@ static dmExtension::Result FinalizeCamera(dmExtension::Params* params)
#endif // platforms #endif // platforms
DM_DECLARE_EXTENSION(EXTENSION_NAME, LIB_NAME, AppInitializeCamera, AppFinalizeCamera, InitializeCamera, 0, 0, FinalizeCamera) DM_DECLARE_EXTENSION(EXTENSION_NAME, LIB_NAME, AppInitializeCamera, AppFinalizeCamera, InitializeCamera, UpdateCamera, 0, FinalizeCamera)

View File

@ -121,11 +121,10 @@ IOSCamera g_Camera;
} }
} }
- (void)captureOutput:(AVCaptureOutput *)captureOutput - (void)captureOutput:(AVCaptureOutput *)captureOutput
didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection fromConnection:(AVCaptureConnection *)connection
{ {
//NSLog(@"DROPPING FRAME!!!"); //NSLog(@"DROPPING FRAME!!!");
} }
@ -256,7 +255,7 @@ IOSCamera g_Camera;
static CMVideoDimensions FlipCoords(AVCaptureVideoDataOutput* output, const CMVideoDimensions& in) static CMVideoDimensions FlipCoords(AVCaptureVideoDataOutput* output, const CMVideoDimensions& in)
{ {
CMVideoDimensions out = in; CMVideoDimensions out = in;
#if defined(DM_PLATFORM_IOS) #if defined(DM_PLATFORM_IOS)
AVCaptureConnection* conn = [output connectionWithMediaType:AVMediaTypeVideo]; AVCaptureConnection* conn = [output connectionWithMediaType:AVMediaTypeVideo];
switch (conn.videoOrientation) { switch (conn.videoOrientation) {
@ -274,7 +273,7 @@ static CMVideoDimensions FlipCoords(AVCaptureVideoDataOutput* output, const CMVi
- ( BOOL ) startCamera: (AVCaptureDevicePosition) cameraPosition - ( BOOL ) startCamera: (AVCaptureDevicePosition) cameraPosition
quality: (CaptureQuality)quality quality: (CaptureQuality)quality
{ {
// 1. Find the back camera // 1. Find the back camera
if ( ![ self findCamera: cameraPosition ] ) if ( ![ self findCamera: cameraPosition ] )
@ -352,7 +351,7 @@ static CMVideoDimensions FlipCoords(AVCaptureVideoDataOutput* output, const CMVi
@end @end
int CameraPlatform_StartCapture(dmBuffer::HBuffer* buffer, CameraType type, CaptureQuality quality, CameraInfo& outparams) void CameraPlatform_StartCaptureAuthorized(dmBuffer::HBuffer* buffer, CameraType type, CaptureQuality quality, CameraInfo& outparams)
{ {
if(g_Camera.m_Delegate == 0) if(g_Camera.m_Delegate == 0)
{ {
@ -381,10 +380,65 @@ int CameraPlatform_StartCapture(dmBuffer::HBuffer* buffer, CameraType type, Capt
g_Camera.m_VideoBuffer = *buffer; g_Camera.m_VideoBuffer = *buffer;
return started ? 1 : 0; if (started)
{
Camera_QueueMessage(STATUS_STARTED);
}
else
{
Camera_QueueMessage(STATUS_ERROR);
}
} }
int CameraPlatform_StopCapture() void CameraPlatform_StartCapture(dmBuffer::HBuffer* buffer, CameraType type, CaptureQuality quality, CameraInfo& outparams)
{
// Only check for permission on iOS 7+ and macOS 10.14+
if ([AVCaptureDevice respondsToSelector:@selector(authorizationStatusForMediaType:)])
{
// Request permission to access the camera.
int status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
if (status == AVAuthorizationStatusAuthorized)
{
// The user has previously granted access to the camera.
dmLogInfo("AVAuthorizationStatusAuthorized");
CameraPlatform_StartCaptureAuthorized(buffer, type, quality, outparams);
}
else if (status == AVAuthorizationStatusNotDetermined)
{
dmLogInfo("AVAuthorizationStatusNotDetermined");
// The app hasn't yet asked the user for camera access.
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
if (granted) {
dmLogInfo("AVAuthorizationStatusNotDetermined - granted!");
CameraPlatform_StartCaptureAuthorized(buffer, type, quality, outparams);
}
else
{
dmLogInfo("AVAuthorizationStatusNotDetermined - not granted!");
Camera_QueueMessage(STATUS_NOT_PERMITTED);
}
}];
}
else if (status == AVAuthorizationStatusDenied)
{
// The user has previously denied access.
dmLogInfo("AVAuthorizationStatusDenied");
Camera_QueueMessage(STATUS_NOT_PERMITTED);
}
else if (status == AVAuthorizationStatusRestricted)
{
// The user can't grant access due to restrictions.
dmLogInfo("AVAuthorizationStatusRestricted");
Camera_QueueMessage(STATUS_NOT_PERMITTED);
}
}
else
{
CameraPlatform_StartCaptureAuthorized(buffer, type, quality, outparams);
}
}
void CameraPlatform_StopCapture()
{ {
if(g_Camera.m_Delegate != 0) if(g_Camera.m_Delegate != 0)
{ {
@ -395,7 +449,6 @@ int CameraPlatform_StopCapture()
dmBuffer::Destroy(g_Camera.m_VideoBuffer); dmBuffer::Destroy(g_Camera.m_VideoBuffer);
g_Camera.m_VideoBuffer = 0; g_Camera.m_VideoBuffer = 0;
} }
return 1;
} }
#endif // DM_PLATFORM_IOS/DM_PLATFORM_OSX #endif // DM_PLATFORM_IOS/DM_PLATFORM_OSX

View File

@ -22,5 +22,15 @@ struct CameraInfo
CameraType m_Type; CameraType m_Type;
}; };
extern int CameraPlatform_StartCapture(dmBuffer::HBuffer* buffer, CameraType type, CaptureQuality quality, CameraInfo& outparams); enum CameraStatus
extern int CameraPlatform_StopCapture(); {
STATUS_STARTED,
STATUS_STOPPED,
STATUS_NOT_PERMITTED,
STATUS_ERROR
};
extern void CameraPlatform_StartCapture(dmBuffer::HBuffer* buffer, CameraType type, CaptureQuality quality, CameraInfo& outparams);
extern void CameraPlatform_StopCapture();
void Camera_QueueMessage(CameraStatus message);

View File

@ -29,4 +29,3 @@ bundle_identifier = com.defold.camera
[library] [library]
include_dirs = camera include_dirs = camera

View File

@ -6,44 +6,41 @@ local function stop_capture(self)
self.cameraframe = nil self.cameraframe = nil
camera.stop_capture() camera.stop_capture()
label.set_text("logo#status", "Capture Status: OFF") label.set_text("logo#status", "Capture Status: OFF")
end end
local function start_capture(self) local function start_capture(self)
if self.cameraframe ~= nil then if not camera then
label.set_text("logo#status", "Capture Status: UNAVAILABLE")
return return
end end
if camera ~= nil then
local sysinfo = sys.get_sys_info() local quality = camera.CAPTURE_QUALITY_HIGH
local quality = camera.CAPTURE_QUALITY_HIGH local type = camera.CAMERA_TYPE_FRONT
local type = camera.CAMERA_TYPE_FRONT if sys.get_sys_info().system_name == 'iPhone OS' then
if sysinfo.system_name == 'iPhone OS' then type = camera.CAMERA_TYPE_BACK
type = camera.CAMERA_TYPE_BACK quality = camera.CAPTURE_QUALITY_MEDIUM
quality = camera.CAPTURE_QUALITY_MEDIUM end
end
camera.start_capture(type, quality, function(self, status)
if camera.start_capture(type, quality) then if status == camera.STATUS_STARTED then
self.cameraframe = camera.get_frame() self.cameraframe = camera.get_frame()
self.camerainfo = camera.get_info() self.camerainfo = camera.get_info()
print("Initialized camera") self.cameratextureheader = {
pprint(self.camerainfo) width=self.camerainfo.width,
height=self.camerainfo.height,
self.cameratextureheader = {width=self.camerainfo.width, type=resource.TEXTURE_TYPE_2D,
height=self.camerainfo.height, format=resource.TEXTURE_FORMAT_RGB,
type=resource.TEXTURE_TYPE_2D, num_mip_maps=1
format=resource.TEXTURE_FORMAT_RGB, }
num_mip_maps=1 } label.set_text("logo#status", "Capture Status: ON")
else
label.set_text("logo#status", "Capture Status: ERROR")
end end
label.set_text("logo#status", "Capture Status: ON") end)
else
print("could not start camera capture")
label.set_text("logo#status", "Capture Status: UNAVAILABLE")
end
end end
function init(self) function init(self)
@ -54,35 +51,33 @@ function init(self)
local screen_height = sys.get_config("display.height", 800) local screen_height = sys.get_config("display.height", 800)
local scale_width = screen_width / logosize local scale_width = screen_width / logosize
local scale_height = screen_height / logosize local scale_height = screen_height / logosize
go.set("#sprite", "scale", vmath.vector3(scale_width, scale_height, 1) )
go.set("#sprite", "scale", vmath.vector3(scale_width, scale_height, 1) )
start_capture(self) start_capture(self)
end end
function final(self) function final(self)
if self.cameraframe ~= nil then if self.cameraframe then
camera.stop_capture() camera.stop_capture()
end end
end end
function update(self, dt) function update(self, dt)
if self.cameraframe ~= nil then if self.cameraframe then
local pathmodelcamera = go.get("#sprite", "texture0") local pathmodelcamera = go.get("#sprite", "texture0")
resource.set_texture(pathmodelcamera, self.cameratextureheader, self.cameraframe) resource.set_texture(pathmodelcamera, self.cameratextureheader, self.cameraframe)
end end
end end
function on_input(self, action_id, action) function on_input(self, action_id, action)
if (action_id == hash("space") or action_id == hash("touch")) and action.pressed then if (action_id == hash("space") or action_id == hash("touch")) and action.pressed then
if self.cameraframe == nil then if self.cameraframe then
start_capture(self)
else
stop_capture(self) stop_capture(self)
else
start_capture(self)
end end
end end
end end