diff --git a/LICENSE.md b/LICENSE.md
index 3e328f1..c03d905 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2019 Defold
+Copyright (c) 2019 Defold Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index a496490..3d5c9d5 100644
--- a/README.md
+++ b/README.md
@@ -1,67 +1,101 @@
# 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.
-Please regard it as just that, an example, and don't rely on this as a dependency for your production code.
+# Installation
-# 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
# 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 the project setting `project.dependencies`, and you should be good to go.
+To test macOS camera permission popup multiple times you can reset the permission from the terminal:
-See the [manual](http://www.defold.com/manuals/libraries/) for further info.
+```bash
+tccutil reset Camera
+```
# Lua API
## 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
- camera.CAPTURE_QUALITY_HIGH
- camera.CAPTURE_QUALITY_MEDIUM
- camera.CAPTURE_QUALITY_LOW
+```lua
+camera.CAPTURE_QUALITY_HIGH
+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
end
-
+end)
+```
+
+
## camera.stop_capture()
-Stops a previously started capture session
+Stops a previously started capture session.
+
+```lua
+camera.stop_capture()
+```
+
## 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()
-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
+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.
- self.cameraframe = camera.get_frame()
-
-
+```lua
+self.cameraframe = camera.get_frame()
+```
diff --git a/camera/manifests/osx/Info.plist b/camera/manifests/osx/Info.plist
new file mode 100644
index 0000000..4c0af91
--- /dev/null
+++ b/camera/manifests/osx/Info.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ NSCameraUsageDescription
+ {{project.title}} would like to access the camera.
+
+
diff --git a/camera/src/camera.cpp b/camera/src/camera.cpp
index a4945fd..bd1c3b2 100644
--- a/camera/src/camera.cpp
+++ b/camera/src/camera.cpp
@@ -26,31 +26,82 @@ struct DefoldCamera
// Information about the currently set camera
CameraInfo m_Params;
+
+ dmArray m_MessageQueue;
+ dmScript::LuaCallbackInfo* m_Callback;
+ dmMutex::HMutex m_Mutex;
};
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)
{
- DM_LUA_STACK_CHECK(L, 1);
+ DM_LUA_STACK_CHECK(L, 0);
CameraType type = (CameraType) luaL_checkint(L, 1);
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);
- 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);
+ CameraPlatform_StartCapture(&g_DefoldCamera.m_VideoBuffer, type, quality, g_DefoldCamera.m_Params);
return 1;
}
@@ -59,13 +110,7 @@ static int StopCapture(lua_State* L)
{
DM_LUA_STACK_CHECK(L, 0);
- int status = 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
+ CameraPlatform_StopCapture();
return 0;
}
@@ -94,7 +139,14 @@ static int GetInfo(lua_State* L)
static int GetFrame(lua_State* L)
{
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;
}
@@ -113,9 +165,9 @@ static void LuaInit(lua_State* L)
int top = lua_gettop(L);
luaL_register(L, MODULE_NAME, Module_methods);
-#define SETCONSTANT(name) \
- lua_pushnumber(L, (lua_Number) name); \
- lua_setfield(L, -2, #name);\
+ #define SETCONSTANT(name) \
+ lua_pushnumber(L, (lua_Number) name); \
+ lua_setfield(L, -2, #name);\
SETCONSTANT(CAMERA_TYPE_FRONT)
SETCONSTANT(CAMERA_TYPE_BACK)
@@ -124,7 +176,12 @@ static void LuaInit(lua_State* L)
SETCONSTANT(CAPTURE_QUALITY_MEDIUM)
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);
assert(top == lua_gettop(L));
@@ -132,12 +189,20 @@ static void LuaInit(lua_State* L)
dmExtension::Result AppInitializeCamera(dmExtension::AppParams* params)
{
+ dmLogInfo("Registered %s Extension", MODULE_NAME);
return dmExtension::RESULT_OK;
}
dmExtension::Result InitializeCamera(dmExtension::Params* params)
{
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;
}
@@ -148,6 +213,8 @@ dmExtension::Result AppFinalizeCamera(dmExtension::AppParams* params)
dmExtension::Result FinalizeCamera(dmExtension::Params* params)
{
+ dmMutex::Delete(g_DefoldCamera.m_Mutex);
+ Camera_DestroyCallback();
return dmExtension::RESULT_OK;
}
@@ -156,7 +223,7 @@ dmExtension::Result FinalizeCamera(dmExtension::Params* 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;
}
@@ -165,6 +232,12 @@ static dmExtension::Result InitializeCamera(dmExtension::Params* params)
return dmExtension::RESULT_OK;
}
+static dmExtension::Result UpdateCamera(dmExtension::Params* params)
+{
+ Camera_ProcessQueue()
+ return dmExtension::RESULT_OK;
+}
+
static dmExtension::Result AppFinalizeCamera(dmExtension::AppParams* params)
{
return dmExtension::RESULT_OK;
@@ -178,4 +251,4 @@ static dmExtension::Result FinalizeCamera(dmExtension::Params* params)
#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)
diff --git a/camera/src/camera.mm b/camera/src/camera.mm
index 9c466ae..dfc4065 100644
--- a/camera/src/camera.mm
+++ b/camera/src/camera.mm
@@ -121,11 +121,10 @@ IOSCamera g_Camera;
}
}
-- (void)captureOutput:(AVCaptureOutput *)captureOutput
- didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer
+- (void)captureOutput:(AVCaptureOutput *)captureOutput
+ didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection
{
-
//NSLog(@"DROPPING FRAME!!!");
}
@@ -256,7 +255,7 @@ IOSCamera g_Camera;
static CMVideoDimensions FlipCoords(AVCaptureVideoDataOutput* output, const CMVideoDimensions& in)
{
- CMVideoDimensions out = in;
+ CMVideoDimensions out = in;
#if defined(DM_PLATFORM_IOS)
AVCaptureConnection* conn = [output connectionWithMediaType:AVMediaTypeVideo];
switch (conn.videoOrientation) {
@@ -274,7 +273,7 @@ static CMVideoDimensions FlipCoords(AVCaptureVideoDataOutput* output, const CMVi
- ( BOOL ) startCamera: (AVCaptureDevicePosition) cameraPosition
- quality: (CaptureQuality)quality
+ quality: (CaptureQuality)quality
{
// 1. Find the back camera
if ( ![ self findCamera: cameraPosition ] )
@@ -352,7 +351,7 @@ static CMVideoDimensions FlipCoords(AVCaptureVideoDataOutput* output, const CMVi
@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)
{
@@ -381,10 +380,65 @@ int CameraPlatform_StartCapture(dmBuffer::HBuffer* buffer, CameraType type, Capt
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)
{
@@ -395,7 +449,6 @@ int CameraPlatform_StopCapture()
dmBuffer::Destroy(g_Camera.m_VideoBuffer);
g_Camera.m_VideoBuffer = 0;
}
- return 1;
}
#endif // DM_PLATFORM_IOS/DM_PLATFORM_OSX
diff --git a/camera/src/camera_private.h b/camera/src/camera_private.h
index f0f6131..206a178 100644
--- a/camera/src/camera_private.h
+++ b/camera/src/camera_private.h
@@ -22,5 +22,15 @@ struct CameraInfo
CameraType m_Type;
};
-extern int CameraPlatform_StartCapture(dmBuffer::HBuffer* buffer, CameraType type, CaptureQuality quality, CameraInfo& outparams);
-extern int CameraPlatform_StopCapture();
+enum CameraStatus
+{
+ 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);
diff --git a/game.project b/game.project
index 421ce8a..5d6119e 100644
--- a/game.project
+++ b/game.project
@@ -29,4 +29,3 @@ bundle_identifier = com.defold.camera
[library]
include_dirs = camera
-
diff --git a/main/main.script b/main/main.script
index 6bbd636..51dad22 100644
--- a/main/main.script
+++ b/main/main.script
@@ -6,44 +6,41 @@ local function stop_capture(self)
self.cameraframe = nil
camera.stop_capture()
-
+
label.set_text("logo#status", "Capture Status: OFF")
end
local function start_capture(self)
- if self.cameraframe ~= nil then
+ if not camera then
+ label.set_text("logo#status", "Capture Status: UNAVAILABLE")
return
end
-
- if camera ~= nil then
- local sysinfo = sys.get_sys_info()
-
- local quality = camera.CAPTURE_QUALITY_HIGH
- local type = camera.CAMERA_TYPE_FRONT
- if sysinfo.system_name == 'iPhone OS' then
- type = camera.CAMERA_TYPE_BACK
- quality = camera.CAPTURE_QUALITY_MEDIUM
- end
-
- if camera.start_capture(type, quality) then
+ local quality = camera.CAPTURE_QUALITY_HIGH
+
+ local type = camera.CAMERA_TYPE_FRONT
+ if sys.get_sys_info().system_name == 'iPhone OS' then
+ type = camera.CAMERA_TYPE_BACK
+ quality = camera.CAPTURE_QUALITY_MEDIUM
+ end
+
+ camera.start_capture(type, quality, function(self, status)
+ if status == camera.STATUS_STARTED then
self.cameraframe = camera.get_frame()
self.camerainfo = camera.get_info()
- print("Initialized camera")
- pprint(self.camerainfo)
-
- self.cameratextureheader = {width=self.camerainfo.width,
- height=self.camerainfo.height,
- type=resource.TEXTURE_TYPE_2D,
- format=resource.TEXTURE_FORMAT_RGB,
- num_mip_maps=1 }
+ self.cameratextureheader = {
+ width=self.camerainfo.width,
+ height=self.camerainfo.height,
+ type=resource.TEXTURE_TYPE_2D,
+ 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
- label.set_text("logo#status", "Capture Status: ON")
- else
- print("could not start camera capture")
- label.set_text("logo#status", "Capture Status: UNAVAILABLE")
- end
+ end)
end
function init(self)
@@ -54,35 +51,33 @@ function init(self)
local screen_height = sys.get_config("display.height", 800)
local scale_width = screen_width / 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)
end
function final(self)
- if self.cameraframe ~= nil then
+ if self.cameraframe then
camera.stop_capture()
end
end
function update(self, dt)
- if self.cameraframe ~= nil then
+ if self.cameraframe then
local pathmodelcamera = go.get("#sprite", "texture0")
resource.set_texture(pathmodelcamera, self.cameratextureheader, self.cameraframe)
end
-
end
function on_input(self, action_id, action)
if (action_id == hash("space") or action_id == hash("touch")) and action.pressed then
- if self.cameraframe == nil then
- start_capture(self)
- else
+ if self.cameraframe then
stop_capture(self)
+ else
+ start_capture(self)
end
end
end