diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 0000000..94d6e1b --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + {{#android.push_field_title}} + + {{/android.push_field_title}} + {{#android.push_field_text}} + + {{/android.push_field_text}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/camera/src/android/camera.java b/camera/src/android/camera.java new file mode 100644 index 0000000..5736f09 --- /dev/null +++ b/camera/src/android/camera.java @@ -0,0 +1,381 @@ +package com.defold.android.camera; + +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraDevice; + +import android.hardware.Camera; + +import android.Manifest; +import android.content.pm.PackageManager; +import android.content.Context; +import android.os.Build; +import android.util.Log; +import android.app.Activity; +import android.media.ImageReader; +import android.view.Surface; +import android.view.SurfaceView; +import android.view.SurfaceHolder; +import android.view.ViewGroup; +import android.view.View; +import android.app.Activity; +import android.graphics.ImageFormat; + +import java.util.Arrays; +import java.util.List; +import java.lang.Runnable; +import java.lang.Math; + + + +class CameraExtension implements Camera.PreviewCallback { + + private static final String TAG = "defold"; + + private static final String[] PERMISSIONS = { Manifest.permission.CAMERA }; + + private Camera camera; + private Camera.Size pictureSize; + private Camera.Size previewSize; + private Camera.Parameters parameters; + private SurfaceView surfaceView; + + private byte[] previewPixels; + private byte[] picturePixels; + + private static CameraExtension instance; + + private CameraExtension() { + + } + + private static CameraExtension getInstance() { + if (instance == null) { + Log.v(TAG, "No instance, creating"); + instance = new CameraExtension(); + } + return instance; + } + + private static boolean hasAllPermissions(Context context, String... permissions) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context != null && permissions != null) { + for (String permission : permissions) { + if (context.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { + return false; + } + } + } + return true; + } + + private static void requestPermissions(Context context, String... permissions) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (!hasAllPermissions(context, permissions)) { + ((Activity)context).requestPermissions(permissions, 1); + } + } + } + + + private static Camera openCamera(int facing) { + Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); + int cameraCount = Camera.getNumberOfCameras(); + for (int i = 0; i < cameraCount; i++) { + Camera.getCameraInfo(i, cameraInfo); + if (cameraInfo.facing == facing) { + return Camera.open(i); + } + } + return null; + } + + private static Camera openFrontCamera() { + return openCamera(Camera.CameraInfo.CAMERA_FACING_FRONT); + } + + private static Camera openBackCamera() { + return openCamera(Camera.CameraInfo.CAMERA_FACING_BACK); + } + + private native void helloworld(); + + private native void sendPicture(byte[] picture); + + private native void handleCameraFrame(byte[] cameraFrame, int width, int height); + + //Method from Ketai project! + private void decodeYUV420SP(byte[] rgb, byte[] yuv420sp, int width, int height) { + final int frameSize = width * height; + + for (int j = 0, yp = 0; j < height; j++) { + int uvp = frameSize + (j >> 1) * width; + int u = 0; + int v = 0; + for (int i = 0; i < width; i++, yp++) { + int y = (0xff & ((int) yuv420sp[yp])) - 16; + if (y < 0) { + y = 0; + } + if ((i & 1) == 0) { + v = (0xff & yuv420sp[uvp++]) - 128; + u = (0xff & yuv420sp[uvp++]) - 128; + } + + int y1192 = 1192 * y; + int r = (y1192 + 1634 * v); + int g = (y1192 - 833 * v - 400 * u); + int b = (y1192 + 2066 * u); + + if (r < 0) { + r = 0; + } + else if (r > 262143) { + r = 262143; + } + + if (g < 0) { + g = 0; + } + else if (g > 262143) { + g = 262143; + } + + if (b < 0) { + b = 0; + } + else if (b > 262143) { + b = 262143; + } + + int pixel = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff); + rgb[(yp * 3)+0] = (byte)(pixel & 0x00ff0000); + rgb[(yp * 3)+1] = (byte)(pixel & 0x0000ff00); + rgb[(yp * 3)+2] = (byte)(pixel & 0x000000ff); + } + } + } + + /** + * Repeatedly take pictures + */ + private void takePicture(final Context context) { + ((Activity)context).runOnUiThread(new Runnable() { + @Override + public void run() { + if (camera != null) { + Log.v(TAG, "Start preview"); + surfaceView.setVisibility(View.VISIBLE); + camera.startPreview(); + surfaceView.setVisibility(View.INVISIBLE); + + Log.v(TAG, "Take picture"); + camera.takePicture(null, null, new Camera.PictureCallback() { + @Override + public void onPictureTaken(byte[] data, Camera camera) { + Log.v(TAG, "onPictureTaken"); + if (data != null) { + sendPicture(data); + } + else { + Log.v(TAG, "onPictureTaken - data is null"); + } + takePicture(context); + } + }); + } + } + }); + } + + @Override + public void onPreviewFrame(byte[] data, Camera camera) { + Log.v(TAG, "onPreviewFrame " + previewSize.width + "x" + previewSize.height); + if (data != null) { + handleCameraFrame(data, previewSize.width, previewSize.height); + surfaceView.setVisibility(View.VISIBLE); + camera.startPreview(); + surfaceView.setVisibility(View.INVISIBLE); + } + else { + Log.v(TAG, "onPreviewFrame - data is null"); + } + /*decodeYUV420SP(previewPixels, data, previewSize.width, previewSize.height); + sendPicture(previewPixels); + surfaceView.setVisibility(View.VISIBLE); + camera.startPreview(); + surfaceView.setVisibility(View.INVISIBLE);*/ + } + + + private Camera.Size getPreferredPreviewSize() { + List previewSizes = parameters.getSupportedPreviewSizes(); + return previewSizes.get((int)Math.ceil(previewSizes.size() / 2)); + } + + + private boolean prepareCapture(final Context context, int facing) { + Log.v(TAG, "prepareCapture"); + if (!hasAllPermissions(context, PERMISSIONS)) { + requestPermissions(context, PERMISSIONS); + return false; + } + + if (facing == 0) { + camera = openBackCamera(); + } + else { + camera = openFrontCamera(); + } + if (camera == null) { + Log.v(TAG, "Unable to open camera"); + return false; + } + + try { + parameters = camera.getParameters(); + for(Camera.Size size : parameters.getSupportedPictureSizes()) { + Log.v(TAG, "Supported picture size: " + size.width + "x" + size.height); + } + for(Camera.Size size : parameters.getSupportedPreviewSizes()) { + Log.v(TAG, "Supported preview size: " + size.width + "x" + size.height); + } + for(int format : parameters.getSupportedPictureFormats()) { + Log.v(TAG, "Supported picture format: " + format); + } + for(int format : parameters.getSupportedPreviewFormats()) { + Log.v(TAG, "Supported preview format: " + format); + } + Log.v(TAG, "Current preview format:" + parameters.getPreviewFormat()); + + pictureSize = parameters.getPictureSize(); + previewSize = getPreferredPreviewSize(); + parameters.setPreviewSize(previewSize.width, previewSize.height); + Log.v(TAG, "Camera parameters. picture size: " + pictureSize.width + "x" + pictureSize.height + " preview size: " + previewSize.width + "x" + previewSize.height); + //parameters.setPictureFormat(ImageFormat.RGB_565); + //parameters.setPictureSize(previewSize.width, previewSize.height); + camera.setParameters(parameters); + + previewPixels = new byte[previewSize.width * previewSize.height * 3]; + picturePixels = new byte[pictureSize.width * pictureSize.height * 3]; + + //camera.setDisplayOrientation() // use utility function from Google example + } + catch(Exception e) { + Log.e(TAG, e.toString()); + } + return true; + } + + private int getWidth() { + return (previewSize != null) ? previewSize.width : 0; + } + + private int getHeight() { + return (previewSize != null) ? previewSize.height : 0; + } + + private void startCapture(final Context context) { + Log.v(TAG, "startCapture"); + ((Activity)context).runOnUiThread(new Runnable() { + @Override + public void run() { + Log.v(TAG, "runOnUiThread"); + + try { + Log.v(TAG, "Create surface"); + surfaceView = new SurfaceView(context); + Activity activity = (Activity)context; + ViewGroup viewGroup = (ViewGroup)activity.findViewById(android.R.id.content); + viewGroup.addView(surfaceView); + + final SurfaceHolder surfaceHolder = surfaceView.getHolder(); + surfaceHolder.addCallback(new SurfaceHolder.Callback() { + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + Log.v(TAG, "surfaceChanged"); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + Log.v(TAG, "surfaceCreated"); + try { + camera.stopPreview(); + camera.setPreviewCallback(CameraExtension.this); + camera.setPreviewDisplay(surfaceHolder); + surfaceView.setVisibility(View.VISIBLE); + camera.startPreview(); + surfaceView.setVisibility(View.INVISIBLE); + /*Log.v(TAG, "Take picture"); + camera.takePicture(null, new Camera.PictureCallback() { + @Override + public void onPictureTaken(byte[] data, Camera camera) { + if (data != null) { + Log.v(TAG, "onPictureTaken"); + sendPicture(data); + } + else { + Log.v(TAG, "onPictureTaken - data is null"); + } + surfaceView.setVisibility(View.VISIBLE); + } + }, null);*/ + } + catch(Exception e) { + Log.e(TAG, e.toString()); + } + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + Log.v(TAG, "surfaceDestroyed"); + } + }); + + } + catch(Exception e) { + Log.e(TAG, e.toString()); + } + } + }); + } + + private void stopCapture() { + if (camera != null) { + camera.stopPreview(); + camera.release(); + camera = null; + } + if (surfaceView != null) { + ((ViewGroup)surfaceView.getParent()).removeView(surfaceView); + surfaceView = null; + } + } + + public static boolean PrepareCapture(final Context context, int facing) { + Log.v(TAG, "PrepareCapture " + facing); + return CameraExtension.getInstance().prepareCapture(context, facing); + } + + public static void StartCapture(final Context context) { + Log.v(TAG, "StartCapture"); + CameraExtension.getInstance().startCapture(context); + } + + public static void StopCapture(final Context context) { + Log.v(TAG, "StopCapture"); + CameraExtension.getInstance().stopCapture(); + } + + public static int GetWidth() { + Log.v(TAG, "GetWidth"); + return CameraExtension.getInstance().getWidth(); + } + + public static int GetHeight() { + Log.v(TAG, "GetHeight"); + return CameraExtension.getInstance().getHeight(); + } + +} diff --git a/camera/src/camera.cpp b/camera/src/camera.cpp index fe90517..5c41572 100644 --- a/camera/src/camera.cpp +++ b/camera/src/camera.cpp @@ -11,7 +11,7 @@ #define DLIB_LOG_DOMAIN LIB_NAME #include -#if defined(DM_PLATFORM_IOS) || defined(DM_PLATFORM_OSX) +#if defined(DM_PLATFORM_IOS) || defined(DM_PLATFORM_OSX) || defined(DM_PLATFORM_ANDROID) #include "camera_private.h" @@ -94,7 +94,7 @@ 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); + lua_rawgeti(L,LUA_REGISTRYINDEX, g_DefoldCamera.m_VideoBufferLuaRef); return 1; } diff --git a/camera/src/camera_android.cpp b/camera/src/camera_android.cpp new file mode 100644 index 0000000..f838236 --- /dev/null +++ b/camera/src/camera_android.cpp @@ -0,0 +1,223 @@ +#include + +#if defined(DM_PLATFORM_ANDROID) + +#include "camera_private.h" + +static JNIEnv* Attach() +{ + JNIEnv* env; + JavaVM* vm = dmGraphics::GetNativeAndroidJavaVM(); + vm->AttachCurrentThread(&env, NULL); + return env; +} + +static bool Detach(JNIEnv* env) +{ + bool exception = (bool) env->ExceptionCheck(); + env->ExceptionClear(); + JavaVM* vm = dmGraphics::GetNativeAndroidJavaVM(); + vm->DetachCurrentThread(); + return !exception; +} + +namespace { + struct AttachScope + { + JNIEnv* m_Env; + AttachScope() : m_Env(Attach()) + { + } + ~AttachScope() + { + Detach(m_Env); + } + }; +} + +static jclass GetClass(JNIEnv* env, const char* classname) +{ + jclass activity_class = env->FindClass("android/app/NativeActivity"); + jmethodID get_class_loader = env->GetMethodID(activity_class,"getClassLoader", "()Ljava/lang/ClassLoader;"); + jobject cls = env->CallObjectMethod(dmGraphics::GetNativeAndroidActivity(), get_class_loader); + jclass class_loader = env->FindClass("java/lang/ClassLoader"); + jmethodID find_class = env->GetMethodID(class_loader, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;"); + + jstring str_class_name = env->NewStringUTF(classname); + jclass outcls = (jclass)env->CallObjectMethod(cls, find_class, str_class_name); + env->DeleteLocalRef(str_class_name); + return outcls; +} + + +struct AndroidCamera { + dmBuffer::HBuffer m_VideoBuffer; + + AndroidCamera() : m_VideoBuffer(0) + { + } +}; + +AndroidCamera g_Camera; + + + +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT void JNICALL Java_com_defold_android_camera_CameraExtension_helloworld(JNIEnv *env, jobject obj) { + dmLogError("Hello World!\n"); + return; +} + +JNIEXPORT void JNICALL Java_com_defold_android_camera_CameraExtension_sendPicture(JNIEnv *env, jobject obj, jbyteArray array) { + dmLogError("send Picture!\n"); + + uint8_t* data = 0; + uint32_t datasize = 0; + dmBuffer::GetBytes(g_Camera.m_VideoBuffer, (void**)&data, &datasize); + + int len = env->GetArrayLength(array); + dmLogError("sendPicture received array length %d expected %d", len, datasize); + //unsigned char* buf = new unsigned char[len]; + env->GetByteArrayRegion (array, 0, len, reinterpret_cast(data)); + + dmBuffer::ValidateBuffer(g_Camera.m_VideoBuffer); + return; +} + + + +JNIEXPORT void JNICALL Java_com_defold_android_camera_CameraExtension_handleCameraFrame(JNIEnv *env, jobject obj, jbyteArray yuv420sp, jint width, jint height) { + uint8_t* data = 0; + uint32_t datasize = 0; + dmBuffer::GetBytes(g_Camera.m_VideoBuffer, (void**)&data, &datasize); + + int i; + int j; + int Y; + int Cr = 0; + int Cb = 0; + int pixPtr = 0; + int jDiv2 = 0; + int R = 0; + int G = 0; + int B = 0; + int cOff; + int w = width; + int h = height; + int sz = w * h; + + jbyte* yuv = (jbyte*) (env->GetPrimitiveArrayCritical(yuv420sp, 0)); + + for(j = 0; j < h; j++) { + pixPtr = j * w; + jDiv2 = j >> 1; + for(i = 0; i < w; i++) { + Y = yuv[pixPtr]; + if(Y < 0) Y += 255; + if((i & 0x1) != 1) { + cOff = sz + jDiv2 * w + (i >> 1) * 2; + Cb = yuv[cOff]; + if(Cb < 0) Cb += 127; else Cb -= 128; + Cr = yuv[cOff + 1]; + if(Cr < 0) Cr += 127; else Cr -= 128; + } + + //ITU-R BT.601 conversion + // + //R = 1.164*(Y-16) + 2.018*(Cr-128); + //G = 1.164*(Y-16) - 0.813*(Cb-128) - 0.391*(Cr-128); + //B = 1.164*(Y-16) + 1.596*(Cb-128); + // + Y = Y + (Y >> 3) + (Y >> 5) + (Y >> 7); + R = Y + (Cr << 1) + (Cr >> 6); + if(R < 0) R = 0; else if(R > 255) R = 255; + G = Y - Cb + (Cb >> 3) + (Cb >> 4) - (Cr >> 1) + (Cr >> 3); + if(G < 0) G = 0; else if(G > 255) G = 255; + B = Y + Cb + (Cb >> 1) + (Cb >> 4) + (Cb >> 5); + if(B < 0) B = 0; else if(B > 255) B = 255; + + data[0] = R; + data++; + data[0] = G; + data++; + data[0] = B; + data++; + //data[(pixPtr * 3) + 0] = R; + //data[(pixPtr * 3) + 1] = G; + //data[(pixPtr * 3) + 2] = B; + //data[pixPtr++] = 0xff000000 + (R << 16) + (G << 8) + B; + } + } + + env->ReleasePrimitiveArrayCritical(yuv420sp, yuv, 0); + + dmBuffer::ValidateBuffer(g_Camera.m_VideoBuffer); +} + + +#ifdef __cplusplus +} +#endif + + + +int CameraPlatform_StartCapture(dmBuffer::HBuffer* buffer, CameraType type, CaptureQuality quality, CameraInfo& outparams) +{ + dmLogError("Android start capture"); + + int facing = (type == CAMERA_TYPE_BACK) ? 0 : 1; + + // prepare JNI + AttachScope attachscope; + JNIEnv* env = attachscope.m_Env; + jclass cls = GetClass(env, "com.defold.android.camera.CameraExtension"); + + // prepare camera for capture and get width and height + jmethodID prepare_capture = env->GetStaticMethodID(cls, "PrepareCapture", "(Landroid/content/Context;I)Z"); + jboolean prepare_success = env->CallStaticBooleanMethod(cls, prepare_capture, dmGraphics::GetNativeAndroidActivity(), facing); + jint width = env->CallStaticIntMethod(cls, env->GetStaticMethodID(cls, "GetWidth", "()I")); + jint height = env->CallStaticIntMethod(cls, env->GetStaticMethodID(cls, "GetHeight", "()I")); + + // set out parameters and create video buffer + outparams.m_Width = (uint32_t)width; + outparams.m_Height = (uint32_t)height; + uint32_t size = outparams.m_Width * outparams.m_Height; + dmBuffer::StreamDeclaration streams_decl[] = { + {dmHashString64("rgb"), dmBuffer::VALUE_TYPE_UINT8, 3} + }; + dmBuffer::Create(size, streams_decl, 1, buffer); + g_Camera.m_VideoBuffer = *buffer; + + // Start capture + if (prepare_success == JNI_TRUE) { + jmethodID start_capture = env->GetStaticMethodID(cls, "StartCapture", "(Landroid/content/Context;)V"); + env->CallStaticBooleanMethod(cls, start_capture, dmGraphics::GetNativeAndroidActivity()); + } + + return (prepare_success == JNI_TRUE) ? 1 : 0; +} + +int CameraPlatform_StopCapture() +{ + dmLogError("Android stop capture"); + + // stop Android camera + AttachScope attachscope; + JNIEnv* env = attachscope.m_Env; + jclass cls = GetClass(env, "com.defold.android.camera.CameraExtension"); + jmethodID method = env->GetStaticMethodID(cls, "StopCapture", "(Landroid/content/Context;)V"); + dmLogError("Android stop capture - calling java"); + env->CallStaticVoidMethod(cls, method, dmGraphics::GetNativeAndroidActivity()); + + // destroy the video buffer + dmBuffer::Destroy(g_Camera.m_VideoBuffer); + g_Camera.m_VideoBuffer = 0; + + return 1; +} + + +#endif // DM_PLATFORM_ANDROID diff --git a/camera/src/camera_private.h b/camera/src/camera_private.h index f222aa0..109a579 100644 --- a/camera/src/camera_private.h +++ b/camera/src/camera_private.h @@ -19,7 +19,7 @@ struct CameraInfo { uint32_t m_Width; uint32_t m_Height; - CameraType m_Type; + CameraType m_Type; }; extern int CameraPlatform_StartCapture(dmBuffer::HBuffer* buffer, CameraType type, CaptureQuality quality, CameraInfo& outparams); diff --git a/game.project b/game.project index 421ce8a..bbd5248 100644 --- a/game.project +++ b/game.project @@ -23,6 +23,7 @@ bundle_identifier = com.defold.camera [android] package = com.defold.camera +manifest = /AndroidManifest.xml [osx] bundle_identifier = com.defold.camera diff --git a/main/logo.atlas b/main/logo.atlas index 44e703e..a72a2d8 100644 --- a/main/logo.atlas +++ b/main/logo.atlas @@ -1,3 +1,6 @@ images { image: "/main/images/logo.png" } +margin: 0 +extrude_borders: 0 +inner_padding: 0