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