/* ============================================================================== This file is part of the JUCE 6 technical preview. Copyright (c) 2020 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For this technical preview, this file is not subject to commercial licensing. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ //============================================================================== // This byte-code is generated from: // // native/java/com/rmsl/juce/CameraCaptureSessionCaptureCallback.java // native/java/com/rmsl/juce/CameraCaptureSessionStateCallback.java // native/java/com/rmsl/juce/CameraDeviceStateCallback.java // native/java/com/rmsl/juce/JuceOrientationEventListener.java // // files with min sdk version 21 // See juce_core/native/java/README.txt on how to generate this byte-code. static const uint8 CameraSupportByteCode[] = {31,139,8,8,187,110,161,94,0,3,67,97,109,101,114,97,83,117,112,112,111,114,116,66,121,116,101,67,111,100,101,46,100,101,120,0, 149,152,93,108,28,213,21,199,207,157,157,221,89,239,174,215,227,181,243,217,36,56,95,182,3,113,54,137,227,40,101,29,59,198,113, 192,102,133,169,215,182,34,3,45,147,221,73,188,176,222,89,102,215,139,171,86,37,68,84,208,151,42,80,90,168,4,136,170,20,181,15, 84,110,9,132,7,30,82,21,149,240,0,10,136,190,165,18,84,133,7,154,7,132,42,20,85,60,244,127,63,198,158,93,175,93,99,233,183, 255,51,115,206,61,247,222,115,239,140,103,38,103,47,68,14,246,246,209,213,103,22,174,255,247,133,96,249,25,246,233,195,151,255, 18,189,241,193,31,255,57,248,226,196,252,238,238,4,81,137,136,22,166,143,192,146,127,79,181,18,157,38,121,190,13,92,99,68,91, 160,219,52,162,32,244,174,0,81,55,247,67,117,232,34,126,110,198,136,14,194,249,86,152,232,10,248,2,36,154,136,14,131,62,112,12, 220,7,170,224,50,248,26,116,69,136,190,15,158,4,127,6,255,0,70,148,232,40,248,1,120,22,188,11,190,2,29,200,191,27,116,129,219, 120,95,160,15,164,192,9,112,39,24,7,167,193,131,32,7,242,192,1,85,240,99,240,24,120,18,252,22,188,7,110,128,182,102,162,65,48,7, 158,6,175,129,143,192,87,160,57,78,212,9,70,192,3,160,10,30,3,191,6,47,131,63,128,55,193,219,224,93,240,33,248,12,124,13,162, 45,68,123,193,16,200,128,7,64,9,252,4,252,28,60,7,126,3,46,131,43,224,125,112,29,124,6,254,13,254,3,190,1,97,147,168,21,236,2, 221,166,172,55,95,3,3,160,196,132,50,18,202,70,40,17,97,58,132,97,19,186,39,52,37,132,19,150,146,18,106,29,219,193,6,176,17, 116,42,109,85,107,190,73,217,139,72,188,89,217,111,25,114,221,185,125,5,246,119,148,125,21,246,86,101,127,236,179,175,251,98, 254,5,123,155,178,191,132,189,93,217,55,97,239,80,182,142,9,220,162,236,118,216,29,202,222,227,179,143,248,236,147,176,119,41, 123,18,246,78,101,223,239,59,159,131,189,91,217,5,216,123,148,189,0,123,175,178,159,240,217,23,125,246,11,190,156,191,247,229, 92,12,243,186,50,234,23,245,77,208,184,168,177,60,110,81,106,42,13,168,122,234,74,67,74,99,98,69,120,251,176,210,56,237,19,26, 163,91,133,54,211,109,66,155,104,191,208,8,245,8,141,210,1,161,27,105,80,104,59,157,16,186,129,134,132,182,210,29,106,92,195,66, 219,232,164,24,159,38,250,137,99,183,116,43,77,42,61,168,244,144,210,195,66,77,26,80,58,162,244,148,210,59,149,166,197,124, 101,222,22,204,172,87,233,17,161,6,245,169,227,163,66,55,137,60,92,239,18,186,153,70,213,241,152,168,151,172,144,137,138,29,83, 245,187,155,228,254,101,190,58,50,165,31,155,164,230,35,207,121,245,213,148,38,212,70,110,81,126,175,238,94,158,83,202,111, 42,127,172,110,125,92,229,15,34,51,247,247,154,242,250,42,153,60,126,6,151,208,253,237,12,189,199,224,231,215,222,113,83,230, 200,92,32,154,122,130,145,241,184,241,75,227,85,227,114,213,8,146,63,238,212,170,113,161,154,184,241,85,227,12,17,23,193,76,248, 88,79,155,242,90,207,184,136,171,32,238,17,227,167,236,87,213,80,88,68,201,245,228,227,126,208,203,247,52,226,126,129,184,139, 198,43,236,13,253,111,213,112,147,136,74,160,87,126,223,200,155,178,166,153,63,33,238,117,196,45,26,239,24,127,215,63,15,226, 198,83,141,68,68,172,142,190,121,77,202,166,172,87,201,100,162,38,154,240,105,98,45,126,164,124,153,14,141,74,39,18,164,29,90, 246,93,168,241,181,213,248,126,86,227,107,175,241,93,172,241,109,168,241,61,87,227,219,168,124,114,156,47,45,141,83,19,227,12, 248,198,249,234,82,187,0,218,109,175,201,249,90,141,111,135,240,5,145,147,223,111,47,249,125,147,183,212,180,123,187,166,93, 135,240,133,68,75,162,191,122,99,57,24,160,161,224,76,66,247,141,229,253,165,118,58,218,117,138,118,222,126,103,10,205,119,61, 48,181,219,229,185,160,82,67,249,12,159,47,33,242,115,13,146,252,127,225,217,222,181,33,143,155,136,137,76,203,199,225,154,248, 38,177,71,252,199,81,213,135,55,174,160,178,131,106,140,94,31,242,255,148,204,31,80,118,120,41,70,230,245,236,168,106,99,168, 92,92,67,253,249,98,190,50,64,221,195,214,156,237,90,195,86,169,50,239,218,25,187,92,206,59,69,117,52,108,21,10,103,172,236,195, 7,30,178,170,22,117,54,138,204,84,172,74,93,220,14,25,119,210,174,230,179,118,3,63,27,37,54,70,59,199,230,179,246,184,155, 183,139,136,64,162,145,42,172,116,190,92,177,139,182,43,3,183,164,173,98,206,117,242,185,100,214,41,226,124,37,57,204,117,161, 146,162,193,37,215,172,229,230,30,181,92,59,153,21,189,30,78,54,26,229,158,186,9,165,232,248,183,76,80,51,143,20,37,191,93,243, 20,245,253,191,6,178,92,245,253,116,173,175,89,138,246,173,21,40,134,114,202,202,23,32,235,9,157,176,31,153,183,203,40,115,247, 58,66,203,243,5,68,246,172,30,57,233,84,172,66,93,248,242,188,170,121,251,209,228,106,251,32,69,189,233,172,51,151,116,231, 202,133,228,67,216,48,13,171,187,98,113,15,173,163,81,93,161,247,53,108,210,96,15,167,104,127,93,232,90,27,57,69,108,154,180, 233,81,48,70,129,233,177,81,254,147,38,29,63,194,156,161,32,126,96,135,132,140,145,193,53,61,54,38,79,164,211,104,152,70,32,34, 244,105,238,15,78,11,47,14,224,99,51,20,146,85,166,173,217,6,115,28,202,86,242,85,155,246,52,242,121,69,115,230,74,5,187,98,231, 104,231,26,81,124,239,32,100,239,26,33,247,186,206,57,23,103,16,118,235,26,97,25,190,181,138,89,123,232,140,227,242,94,247, 175,35,118,121,140,187,214,138,174,88,34,99,195,74,12,23,28,62,178,221,13,125,78,241,108,254,220,242,44,119,172,25,148,163,45, 141,252,19,182,149,251,33,109,202,174,216,54,178,227,237,43,28,39,243,101,220,213,138,118,150,143,121,227,10,247,136,235,58, 110,131,124,227,37,108,171,28,25,89,121,39,164,205,57,225,243,237,191,225,89,171,120,14,33,65,91,164,48,206,202,235,158,162,103, 93,36,187,103,126,238,140,237,146,62,235,148,43,212,196,127,39,157,169,178,77,225,165,221,146,104,176,55,90,234,119,66,91, 163,117,223,188,234,42,111,93,99,77,205,21,43,24,94,90,175,214,149,171,19,171,89,139,184,83,172,41,164,129,235,79,76,27,57,84, 169,218,97,173,172,142,225,45,89,212,89,118,82,115,9,35,200,91,5,121,155,34,163,228,218,252,254,68,186,139,210,147,225,202,251, 34,133,92,233,143,148,213,108,70,145,176,44,55,2,181,148,103,157,249,66,238,14,84,70,53,174,204,230,203,212,84,201,207,161,173, 53,87,162,30,109,96,218,136,247,247,244,208,209,192,212,68,198,136,95,162,126,24,41,110,12,195,24,231,198,168,54,53,105,196, 127,71,227,129,169,201,126,126,98,90,159,154,56,13,235,35,218,207,6,120,115,234,101,83,70,124,134,190,43,101,80,202,136,148,187, 165,116,169,208,3,172,155,31,30,145,114,187,214,125,59,215,19,242,176,51,48,112,236,123,70,252,56,15,187,135,159,32,13,15, 207,26,211,244,243,231,245,197,8,187,128,167,188,6,68,217,181,8,99,55,193,203,81,198,174,130,47,193,243,49,98,161,160,166,181, 162,237,59,177,70,237,54,177,79,98,140,125,3,158,111,102,236,18,184,134,151,191,160,30,208,182,161,205,141,230,250,248,157,236, 169,56,99,175,128,43,224,58,30,225,52,166,107,123,31,63,175,223,140,115,127,23,123,182,69,190,215,120,207,117,158,122,223,88, 248,51,143,247,157,133,63,19,121,223,90,188,119,125,254,189,133,171,247,205,37,68,203,223,93,88,135,124,175,231,223,94,152,41, 223,221,249,59,188,214,33,243,243,239,49,1,21,195,223,105,248,203,52,111,43,222,167,76,57,14,254,189,231,127,249,28,121,55,40,18, 0,0,0,0}; #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ STATICMETHOD (valueOf, "valueOf", "(Ljava/lang/String;)Landroid/graphics/Bitmap$CompressFormat;") DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidBitmapCompressFormat, "android/graphics/Bitmap$CompressFormat", 21) #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (close, "close", "()V") \ METHOD (createCaptureRequest, "createCaptureRequest", "(I)Landroid/hardware/camera2/CaptureRequest$Builder;") \ METHOD (createCaptureSession, "createCaptureSession", "(Ljava/util/List;Landroid/hardware/camera2/CameraCaptureSession$StateCallback;Landroid/os/Handler;)V") DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidCameraDevice, "android/hardware/camera2/CameraDevice", 21) #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (close, "close", "()V") \ METHOD (getPlanes, "getPlanes", "()[Landroid/media/Image$Plane;") DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidImage, "android/media/Image", 21) #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (getBuffer, "getBuffer", "()Ljava/nio/ByteBuffer;") DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidImagePlane, "android/media/Image$Plane", 21) #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (acquireLatestImage, "acquireLatestImage", "()Landroid/media/Image;") \ METHOD (close, "close", "()V") \ METHOD (getSurface, "getSurface", "()Landroid/view/Surface;") \ METHOD (setOnImageAvailableListener, "setOnImageAvailableListener", "(Landroid/media/ImageReader$OnImageAvailableListener;Landroid/os/Handler;)V") \ STATICMETHOD (newInstance, "newInstance", "(IIII)Landroid/media/ImageReader;") DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidImageReader, "android/media/ImageReader", 21) #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (constructor, "", "()V") \ METHOD (getSurface, "getSurface", "()Landroid/view/Surface;") \ METHOD (prepare, "prepare", "()V") \ METHOD (release, "release", "()V") \ METHOD (setAudioEncoder, "setAudioEncoder", "(I)V") \ METHOD (setAudioSource, "setAudioSource", "(I)V") \ METHOD (setOnErrorListener, "setOnErrorListener", "(Landroid/media/MediaRecorder$OnErrorListener;)V") \ METHOD (setOnInfoListener, "setOnInfoListener", "(Landroid/media/MediaRecorder$OnInfoListener;)V") \ METHOD (setOrientationHint, "setOrientationHint", "(I)V") \ METHOD (setOutputFile, "setOutputFile", "(Ljava/lang/String;)V") \ METHOD (setOutputFormat, "setOutputFormat", "(I)V") \ METHOD (setVideoEncoder, "setVideoEncoder", "(I)V") \ METHOD (setVideoEncodingBitRate, "setVideoEncodingBitRate", "(I)V") \ METHOD (setVideoFrameRate, "setVideoFrameRate", "(I)V") \ METHOD (setVideoSize, "setVideoSize", "(II)V") \ METHOD (setVideoSource, "setVideoSource", "(I)V") \ METHOD (start, "start", "()V") \ METHOD (stop, "stop", "()V") DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidMediaRecorder, "android/media/MediaRecorder", 21) #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (constructor, "", "(Landroid/content/Context;)V") \ METHOD (getSurfaceTexture, "getSurfaceTexture", "()Landroid/graphics/SurfaceTexture;") \ METHOD (isAvailable, "isAvailable", "()Z") \ METHOD (setSurfaceTextureListener, "setSurfaceTextureListener", "(Landroid/view/TextureView$SurfaceTextureListener;)V") \ METHOD (setTransform, "setTransform", "(Landroid/graphics/Matrix;)V") DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidTextureView, "android/view/TextureView", 21) #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (constructor, "", "(Landroid/graphics/SurfaceTexture;)V") DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidSurface, "android/view/Surface", 21) #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (setDefaultBufferSize, "setDefaultBufferSize", "(II)V") DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidSurfaceTexture, "android/graphics/SurfaceTexture", 21) #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (getOutputSizesForClass, "getOutputSizes", "(Ljava/lang/Class;)[Landroid/util/Size;") \ METHOD (getOutputSizesForFormat, "getOutputSizes", "(I)[Landroid/util/Size;") \ METHOD (isOutputSupportedFor, "isOutputSupportedFor", "(I)Z") \ METHOD (isOutputSupportedForSurface, "isOutputSupportedFor", "(Landroid/view/Surface;)Z") DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidStreamConfigurationMap, "android/hardware/camera2/params/StreamConfigurationMap", 21) #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (constructor, "", "()V") \ METHOD (toByteArray, "toByteArray", "()[B") \ METHOD (size, "size", "()I") DECLARE_JNI_CLASS_WITH_MIN_SDK (ByteArrayOutputStream, "java/io/ByteArrayOutputStream", 21) #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (abortCaptures, "abortCaptures", "()V") \ METHOD (capture, "capture", "(Landroid/hardware/camera2/CaptureRequest;Landroid/hardware/camera2/CameraCaptureSession$CaptureCallback;Landroid/os/Handler;)I") \ METHOD (close, "close", "()V") \ METHOD (setRepeatingRequest, "setRepeatingRequest", "(Landroid/hardware/camera2/CaptureRequest;Landroid/hardware/camera2/CameraCaptureSession$CaptureCallback;Landroid/os/Handler;)I") \ METHOD (stopRepeating, "stopRepeating", "()V") DECLARE_JNI_CLASS_WITH_MIN_SDK (CameraCaptureSession, "android/hardware/camera2/CameraCaptureSession", 21) #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (get, "get", "(Landroid/hardware/camera2/CameraCharacteristics$Key;)Ljava/lang/Object;") \ METHOD (getKeys, "getKeys", "()Ljava/util/List;") \ STATICFIELD (CONTROL_AF_AVAILABLE_MODES, "CONTROL_AF_AVAILABLE_MODES", "Landroid/hardware/camera2/CameraCharacteristics$Key;") \ STATICFIELD (LENS_FACING, "LENS_FACING", "Landroid/hardware/camera2/CameraCharacteristics$Key;") \ STATICFIELD (SCALER_STREAM_CONFIGURATION_MAP, "SCALER_STREAM_CONFIGURATION_MAP", "Landroid/hardware/camera2/CameraCharacteristics$Key;") \ STATICFIELD (SENSOR_ORIENTATION, "SENSOR_ORIENTATION", "Landroid/hardware/camera2/CameraCharacteristics$Key;") DECLARE_JNI_CLASS_WITH_MIN_SDK (CameraCharacteristics, "android/hardware/camera2/CameraCharacteristics", 21) #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (getName, "getName", "()Ljava/lang/String;") DECLARE_JNI_CLASS_WITH_MIN_SDK (CameraCharacteristicsKey, "android/hardware/camera2/CameraCharacteristics$Key", 21) #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (getCameraCharacteristics, "getCameraCharacteristics", "(Ljava/lang/String;)Landroid/hardware/camera2/CameraCharacteristics;") \ METHOD (getCameraIdList, "getCameraIdList", "()[Ljava/lang/String;") \ METHOD (openCamera, "openCamera", "(Ljava/lang/String;Landroid/hardware/camera2/CameraDevice$StateCallback;Landroid/os/Handler;)V") DECLARE_JNI_CLASS_WITH_MIN_SDK (CameraManager, "android/hardware/camera2/CameraManager", 21) #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ STATICFIELD (CONTROL_AE_PRECAPTURE_TRIGGER, "CONTROL_AE_PRECAPTURE_TRIGGER", "Landroid/hardware/camera2/CaptureRequest$Key;") \ STATICFIELD (CONTROL_AF_MODE, "CONTROL_AF_MODE", "Landroid/hardware/camera2/CaptureRequest$Key;") \ STATICFIELD (CONTROL_AF_TRIGGER, "CONTROL_AF_TRIGGER", "Landroid/hardware/camera2/CaptureRequest$Key;") \ STATICFIELD (CONTROL_MODE, "CONTROL_MODE", "Landroid/hardware/camera2/CaptureRequest$Key;") DECLARE_JNI_CLASS_WITH_MIN_SDK (CaptureRequest, "android/hardware/camera2/CaptureRequest", 21) #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (addTarget, "addTarget", "(Landroid/view/Surface;)V") \ METHOD (build, "build", "()Landroid/hardware/camera2/CaptureRequest;") \ METHOD (set, "set", "(Landroid/hardware/camera2/CaptureRequest$Key;Ljava/lang/Object;)V") DECLARE_JNI_CLASS_WITH_MIN_SDK (CaptureRequestBuilder, "android/hardware/camera2/CaptureRequest$Builder", 21) #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (get, "get", "(Landroid/hardware/camera2/CaptureResult$Key;)Ljava/lang/Object;") \ STATICFIELD (CONTROL_AE_STATE, "CONTROL_AE_STATE", "Landroid/hardware/camera2/CaptureResult$Key;") \ STATICFIELD (CONTROL_AF_STATE, "CONTROL_AF_STATE", "Landroid/hardware/camera2/CaptureResult$Key;") DECLARE_JNI_CLASS_WITH_MIN_SDK (CaptureResult, "android/hardware/camera2/CaptureResult", 21) #undef JNI_CLASS_MEMBERS //============================================================================== class AndroidRunnable : public juce::AndroidInterfaceImplementer { public: struct Owner { virtual ~Owner() {} virtual void run() = 0; }; AndroidRunnable (Owner& ownerToUse) : owner (ownerToUse) {} private: Owner& owner; jobject invoke (jobject proxy, jobject method, jobjectArray args) override { auto* env = getEnv(); auto methodName = juce::juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName)); if (methodName == "run") { owner.run(); return nullptr; } // invoke base class return AndroidInterfaceImplementer::invoke (proxy, method, args); } }; //============================================================================== class TextureViewSurfaceTextureListener : public AndroidInterfaceImplementer { public: struct Owner { virtual ~Owner() {} virtual void onSurfaceTextureAvailable (LocalRef& surface, int width, int height) = 0; virtual bool onSurfaceTextureDestroyed (LocalRef& surface) = 0; virtual void onSurfaceTextureSizeChanged (LocalRef& surface, int width, int height) = 0; virtual void onSurfaceTextureUpdated (LocalRef& surface) = 0; }; TextureViewSurfaceTextureListener (Owner& ownerToUse) : owner (ownerToUse) {} jobject invoke (jobject proxy, jobject method, jobjectArray args) override { auto* env = getEnv(); auto methodName = juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName)); int numArgs = args != nullptr ? env->GetArrayLength (args) : 0; if (methodName == "onSurfaceTextureAvailable" && numArgs == 3) { auto surface = LocalRef (env->GetObjectArrayElement (args, 0)); auto width = LocalRef (env->GetObjectArrayElement (args, 1)); auto height = LocalRef (env->GetObjectArrayElement (args, 2)); auto widthInt = env->CallIntMethod (width, JavaInteger.intValue); auto heightInt = env->CallIntMethod (height, JavaInteger.intValue); owner.onSurfaceTextureAvailable (surface, widthInt, heightInt); return nullptr; } else if (methodName == "onSurfaceTextureDestroyed" && numArgs == 1) { auto surface = LocalRef (env->GetObjectArrayElement (args, 0)); auto result = owner.onSurfaceTextureDestroyed (surface); return env->CallStaticObjectMethod (JavaBoolean, JavaBoolean.valueOf, result); } else if (methodName == "onSurfaceTextureSizeChanged" && numArgs == 3) { auto surface = LocalRef (env->GetObjectArrayElement (args, 0)); auto width = LocalRef (env->GetObjectArrayElement (args, 1)); auto height = LocalRef (env->GetObjectArrayElement (args, 2)); auto widthInt = env->CallIntMethod (width, JavaInteger.intValue); auto heightInt = env->CallIntMethod (height, JavaInteger.intValue); owner.onSurfaceTextureSizeChanged (surface, widthInt, heightInt); return nullptr; } else if (methodName == "onSurfaceTextureUpdated" && numArgs == 1) { auto surface = LocalRef (env->GetObjectArrayElement (args, 0)); owner.onSurfaceTextureUpdated (surface); return nullptr; } return AndroidInterfaceImplementer::invoke (proxy, method, args); } private: Owner& owner; }; //============================================================================== class ImageReaderOnImageAvailableListener : public AndroidInterfaceImplementer { public: struct Owner { virtual ~Owner() {} virtual void onImageAvailable (LocalRef& imageReader) = 0; }; ImageReaderOnImageAvailableListener (Owner& ownerToUse) : owner (ownerToUse) {} jobject invoke (jobject proxy, jobject method, jobjectArray args) override { auto* env = getEnv(); auto methodName = juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName)); int numArgs = args != nullptr ? env->GetArrayLength (args) : 0; if (methodName == "onImageAvailable" && numArgs == 1) { auto imageReader = LocalRef (env->GetObjectArrayElement (args, 0)); owner.onImageAvailable (imageReader); return nullptr; } return AndroidInterfaceImplementer::invoke (proxy, method, args); } private: Owner& owner; }; //============================================================================== class MediaRecorderOnInfoListener : public AndroidInterfaceImplementer { public: struct Owner { virtual ~Owner() {} virtual void onInfo (LocalRef& mediaRecorder, int what, int extra) = 0; }; MediaRecorderOnInfoListener (Owner& ownerToUse) : owner (ownerToUse) {} jobject invoke (jobject proxy, jobject method, jobjectArray args) override { auto* env = getEnv(); auto methodName = juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName)); int numArgs = args != nullptr ? env->GetArrayLength (args) : 0; if (methodName == "onInfo" && numArgs == 3) { auto mediaRecorder = LocalRef (env->GetObjectArrayElement (args, 0)); auto what = LocalRef (env->GetObjectArrayElement (args, 1)); auto extra = LocalRef (env->GetObjectArrayElement (args, 2)); auto whatInt = (int) env->CallIntMethod (what, JavaInteger.intValue); auto extraInt = (int) env->CallIntMethod (extra, JavaInteger.intValue); owner.onInfo (mediaRecorder, whatInt, extraInt); return nullptr; } return AndroidInterfaceImplementer::invoke (proxy, method, args); } private: Owner& owner; }; //============================================================================== class MediaRecorderOnErrorListener : public AndroidInterfaceImplementer { public: struct Owner { virtual ~Owner() {} virtual void onError (LocalRef& mediaRecorder, int what, int extra) = 0; }; MediaRecorderOnErrorListener (Owner& ownerToUse) : owner (ownerToUse) {} jobject invoke (jobject proxy, jobject method, jobjectArray args) override { auto* env = getEnv(); auto methodName = juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName)); int numArgs = args != nullptr ? env->GetArrayLength (args) : 0; if (methodName == "onError" && numArgs == 3) { auto mediaRecorder = LocalRef (env->GetObjectArrayElement (args, 0)); auto what = LocalRef (env->GetObjectArrayElement (args, 1)); auto extra = LocalRef (env->GetObjectArrayElement (args, 2)); auto whatInt = (int) env->CallIntMethod (what, JavaInteger.intValue); auto extraInt = (int) env->CallIntMethod (extra, JavaInteger.intValue); owner.onError (mediaRecorder, whatInt, extraInt); return nullptr; } return AndroidInterfaceImplementer::invoke (proxy, method, args); } private: Owner& owner; }; //============================================================================== struct CameraDevice::Pimpl : private ActivityLifecycleCallbacks { using InternalOpenCameraResultCallback = std::function; Pimpl (CameraDevice& ownerToUse, const String& cameraIdToUse, int /*index*/, int minWidthToUse, int minHeightToUse, int maxWidthToUse, int maxHeightToUse, bool /*useHighQuality*/) : owner (ownerToUse), minWidth (minWidthToUse), minHeight (minHeightToUse), maxWidth (maxWidthToUse), maxHeight (maxHeightToUse), cameraId (cameraIdToUse), activityLifeListener (CreateJavaInterface (this, "android/app/Application$ActivityLifecycleCallbacks")), cameraManager (initialiseCameraManager()), cameraCharacteristics (initialiseCameraCharacteristics (cameraManager, cameraId)), streamConfigurationMap (cameraCharacteristics), previewDisplay (streamConfigurationMap.getPreviewBufferSize()), deviceOrientationChangeListener (previewDisplay) { startBackgroundThread(); } ~Pimpl() override { auto* env = getEnv(); env->CallVoidMethod (getAppContext().get(), AndroidApplication.unregisterActivityLifecycleCallbacks, activityLifeListener.get()); activityLifeListener.clear(); } JUCE_DECLARE_WEAK_REFERENCEABLE (Pimpl) String getCameraId() const noexcept { return cameraId; } void open (InternalOpenCameraResultCallback cameraOpenCallbackToUse) { cameraOpenCallback = std::move (cameraOpenCallbackToUse); // A valid camera open callback must be passed. jassert (cameraOpenCallback != nullptr); // The same camera can be opened only once! jassert (scopedCameraDevice == nullptr); if (cameraOpenCallback == nullptr || scopedCameraDevice != nullptr) return; WeakReference safeThis (this); RuntimePermissions::request (RuntimePermissions::camera, [safeThis] (bool granted) mutable { if (safeThis != nullptr) safeThis->continueOpenRequest (granted); }); } void continueOpenRequest (bool granted) { if (getAndroidSDKVersion() >= 21) { if (granted) { getEnv()->CallVoidMethod (getAppContext().get(), AndroidApplication.registerActivityLifecycleCallbacks, activityLifeListener.get()); scopedCameraDevice.reset (new ScopedCameraDevice (*this, cameraId, cameraManager, handler, getAutoFocusModeToUse())); } else { invokeCameraOpenCallback ("Camera permission not granted"); } } else { invokeCameraOpenCallback ("Camera requires android sdk version 21 or greater"); } } bool openedOk() const noexcept { return scopedCameraDevice->openedOk(); } void takeStillPicture (std::function pictureTakenCallbackToUse) { if (pictureTakenCallbackToUse == nullptr || currentCaptureSessionMode == nullptr) { jassertfalse; return; } if (currentCaptureSessionMode->isVideoRecordSession()) { // Taking still pictures while recording video is not supported on Android. jassertfalse; return; } pictureTakenCallback = std::move (pictureTakenCallbackToUse); triggerStillPictureCapture(); } void startRecordingToFile (const File& file, int /*quality*/) { if (! openedOk()) { jassertfalse; return; } if (! previewDisplay.isReady()) { // Did you remember to create and show a preview display? jassertfalse; return; } file.deleteFile(); file.create(); jassert (file.existsAsFile()); // MediaRecorder can't handle videos larger than 1080p auto videoSize = chooseBestSize (minWidth, minHeight, jmin (maxWidth, 1080), maxHeight, streamConfigurationMap.getSupportedVideoRecordingOutputSizes()); mediaRecorder.reset (new MediaRecorder (file.getFullPathName(), videoSize.getWidth(), videoSize.getHeight(), getCameraSensorOrientation(), getCameraLensFacing())); firstRecordedFrameTimeMs = Time::getCurrentTime(); currentCaptureSessionMode.reset(); startVideoRecordingMode (*mediaRecorder); } void stopRecording() { currentCaptureSessionMode.reset(); mediaRecorder.reset(); startPreviewMode (*imageReader); } Time getTimeOfFirstRecordedFrame() const { return firstRecordedFrameTimeMs; } static StringArray getAvailableDevices() { if (getAndroidSDKVersion() < 21) return StringArray(); // Camera requires SDK version 21 or later StringArray results; auto* env = getEnv(); auto cameraManagerToUse = initialiseCameraManager(); auto cameraIdArray = LocalRef ((jobjectArray) env->CallObjectMethod (cameraManagerToUse, CameraManager.getCameraIdList)); results = javaStringArrayToJuce (cameraIdArray); for (auto& result : results) printDebugCameraInfo (cameraManagerToUse, result); return results; } void addListener (CameraDevice::Listener* listenerToAdd) { const ScopedLock sl (listenerLock); listeners.add (listenerToAdd); if (listeners.size() == 1) triggerStillPictureCapture(); } void removeListener (CameraDevice::Listener* listenerToRemove) { const ScopedLock sl (listenerLock); listeners.remove (listenerToRemove); } private: enum { ERROR_CAMERA_IN_USE = 1, ERROR_MAX_CAMERAS_IN_USE = 2, ERROR_CAMERA_DISABLED = 3, ERROR_CAMERA_DEVICE = 4, ERROR_CAMERA_SERVICE = 5 }; static String cameraErrorCodeToString (int errorCode) { switch (errorCode) { case ERROR_CAMERA_IN_USE: return "Camera already in use."; case ERROR_MAX_CAMERAS_IN_USE: return "Too many opened camera devices."; case ERROR_CAMERA_DISABLED: return "Camera disabled."; case ERROR_CAMERA_DEVICE: return "Fatal error."; case ERROR_CAMERA_SERVICE: return "Fatal error. Reboot required or persistent hardware problem."; default: return "Unknown error."; } } static LocalRef initialiseCameraManager() { return LocalRef (getEnv()->CallObjectMethod (getAppContext().get(), AndroidContext.getSystemService, javaString ("camera").get())); } static LocalRef initialiseCameraCharacteristics (const GlobalRef& cameraManager, const String& cameraId) { return LocalRef (getEnv()->CallObjectMethod (cameraManager, CameraManager.getCameraCharacteristics, javaString (cameraId).get())); } static void printDebugCameraInfo (const LocalRef& cameraManagerToUse, const String& cameraId) { auto* env = getEnv(); auto characteristics = LocalRef (env->CallObjectMethod (cameraManagerToUse, CameraManager.getCameraCharacteristics, javaString (cameraId).get())); auto keysList = LocalRef (env->CallObjectMethod (characteristics, CameraCharacteristics.getKeys)); const int size = env->CallIntMethod (keysList, JavaList.size); JUCE_CAMERA_LOG ("Camera id: " + cameraId + ", characteristics keys num: " + String (size)); for (int i = 0; i < size; ++i) { auto key = LocalRef (env->CallObjectMethod (keysList, JavaList.get, i)); auto jKeyName = LocalRef ((jstring) env->CallObjectMethod (key, CameraCharacteristicsKey.getName)); auto keyName = juceString (jKeyName); auto keyValue = LocalRef (env->CallObjectMethod (characteristics, CameraCharacteristics.get, key.get())); auto jKeyValueString = LocalRef ((jstring) env->CallObjectMethod (keyValue, JavaObject.toString)); auto keyValueString = juceString (jKeyValueString); auto &kvs = keyValueString; if (kvs.startsWith ("[I") || kvs.startsWith ("[F") || kvs.startsWith ("[Z") || kvs.startsWith ("[B")) { printPrimitiveArrayElements (keyValue, keyName, keyValueString); } else if (kvs.startsWith ("[Landroid.util.Range")) { printRangeArrayElements (keyValue, keyName); } else { int chunkSize = 256; if (keyValueString.length() > chunkSize) { JUCE_CAMERA_LOG ("Key: " + keyName); for (int j = 0, k = 1; j < keyValueString.length(); j += chunkSize, ++k) JUCE_CAMERA_LOG ("value part " + String (k) + ": " + keyValueString.substring (j, k + chunkSize)); } else { JUCE_CAMERA_LOG ("Key: " + keyName + ", value: " + keyValueString); } } ignoreUnused (keyName); } } static void printPrimitiveArrayElements (const LocalRef& keyValue, const String& keyName, const String& keyValueString) { ignoreUnused (keyName); String result = "["; auto* env = getEnv(); #define PRINT_ELEMENTS(elem_type, array_type, fun_name_middle) \ { \ elem_type* elements = env->Get##fun_name_middle##ArrayElements ((array_type) keyValue.get(), nullptr); \ int size = env->GetArrayLength ((array_type) keyValue.get()); \ \ for (int i = 0; i < size - 1; ++i) \ result << String (elements[i]) << " "; \ \ if (size > 0) \ result << String (elements[size - 1]); \ \ env->Release##fun_name_middle##ArrayElements ((array_type) keyValue.get(), elements, 0); \ } if (keyValueString.startsWith ("[I")) PRINT_ELEMENTS (jint, jintArray, Int) else if (keyValueString.startsWith ("[F")) PRINT_ELEMENTS (float, jfloatArray, Float) else if (keyValueString.startsWith ("[Z")) PRINT_ELEMENTS (jboolean, jbooleanArray, Boolean) else if (keyValueString.startsWith ("[B")) PRINT_ELEMENTS (jbyte, jbyteArray, Byte); #undef PRINT_ELEMENTS result << "]"; JUCE_CAMERA_LOG ("Key: " + keyName + ", value: " + result); } static void printRangeArrayElements (const LocalRef& rangeArray, const String& keyName) { auto* env = getEnv(); jobjectArray ranges = static_cast (rangeArray.get()); int numRanges = env->GetArrayLength (ranges); String result; for (int i = 0; i < numRanges; ++i) { auto range = LocalRef (env->GetObjectArrayElement (ranges, i)); auto jRangeString = LocalRef ((jstring) env->CallObjectMethod (range, AndroidRange.toString)); result << juceString (jRangeString) << " "; } ignoreUnused (keyName); JUCE_CAMERA_LOG ("Key: " + keyName + ", value: " + result); } //============================================================================== class StreamConfigurationMap { public: StreamConfigurationMap (const GlobalRef& cameraCharacteristicsToUse) : scalerStreamConfigurationMap (getStreamConfigurationMap (cameraCharacteristicsToUse)), supportedPreviewOutputSizes (retrieveOutputSizes (scalerStreamConfigurationMap, getClassForName ("android.graphics.SurfaceTexture"), -1)), supportedStillImageOutputSizes (retrieveOutputSizes (scalerStreamConfigurationMap, LocalRef(), jpegImageFormat)), supportedVideoRecordingOutputSizes (retrieveOutputSizes (scalerStreamConfigurationMap, getClassForName ("android.media.MediaRecorder"), -1)), defaultPreviewSize (getSmallestSize (supportedPreviewOutputSizes)), previewBufferSize (getLargestSize (supportedPreviewOutputSizes)) { printSizesLog (supportedPreviewOutputSizes, "SurfaceTexture"); printSizesLog (supportedStillImageOutputSizes, "JPEG"); printSizesLog (supportedVideoRecordingOutputSizes, "MediaRecorder"); } Array> getSupportedPreviewOutputSizes() const noexcept { return supportedPreviewOutputSizes; } Array> getSupportedStillImageOutputSizes() const noexcept { return supportedStillImageOutputSizes; } Array> getSupportedVideoRecordingOutputSizes() const noexcept { return supportedVideoRecordingOutputSizes; } Rectangle getDefaultPreviewSize() const noexcept { return defaultPreviewSize; } Rectangle getPreviewBufferSize() const noexcept { return previewBufferSize; } bool isOutputSupportedForSurface (const LocalRef& surface) const { return getEnv()->CallBooleanMethod (scalerStreamConfigurationMap, AndroidStreamConfigurationMap.isOutputSupportedForSurface, surface.get()) != 0; } static constexpr int jpegImageFormat = 256; private: GlobalRef scalerStreamConfigurationMap; Array> supportedPreviewOutputSizes; Array> supportedStillImageOutputSizes; Array> supportedVideoRecordingOutputSizes; Rectangle defaultPreviewSize, previewBufferSize; GlobalRef getStreamConfigurationMap (const GlobalRef& cameraCharacteristicsToUse) { auto* env = getEnv(); auto scalerStreamConfigurationMapKey = LocalRef (env->GetStaticObjectField (CameraCharacteristics, CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)); return GlobalRef (LocalRef (env->CallObjectMethod (cameraCharacteristicsToUse, CameraCharacteristics.get, scalerStreamConfigurationMapKey.get()))); } static Array> retrieveOutputSizes (GlobalRef& scalerStreamConfigurationMap, const LocalRef& outputClass, int format) { Array> result; auto* env = getEnv(); auto outputSizes = outputClass.get() != nullptr ? LocalRef ((jobjectArray) env->CallObjectMethod (scalerStreamConfigurationMap, AndroidStreamConfigurationMap.getOutputSizesForClass, outputClass.get())) : LocalRef ((jobjectArray) env->CallObjectMethod (scalerStreamConfigurationMap, AndroidStreamConfigurationMap.getOutputSizesForFormat, (jint) format)); if (format != -1) { auto supported = (env->CallBooleanMethod (scalerStreamConfigurationMap, AndroidStreamConfigurationMap.isOutputSupportedFor, (jint) format) != 0); if (! supported) { // The output format is not supported by this device, still image capture will not work! jassertfalse; return {}; } } int numSizes = env->GetArrayLength (outputSizes); jassert (numSizes > 0); for (int i = 0; i < numSizes; ++i) { auto size = LocalRef (env->GetObjectArrayElement (outputSizes, i)); auto width = env->CallIntMethod (size, AndroidSize.getWidth); auto height = env->CallIntMethod (size, AndroidSize.getHeight); result.add (Rectangle (0, 0, width, height)); } return result; } static LocalRef getClassForName (const String& name) { return LocalRef (getEnv()->CallStaticObjectMethod (JavaClass, JavaClass.forName, javaString (name).get())); } static void printSizesLog (const Array>& sizes, const String& className) { ignoreUnused (sizes, className); JUCE_CAMERA_LOG ("Sizes for class " + className); #if JUCE_CAMERA_LOG_ENABLED for (auto& s : sizes) JUCE_CAMERA_LOG (s.toString() + "\n"); #endif } Rectangle getSmallestSize (const Array>& sizes) const { if (sizes.size() == 0) return {}; auto smallestSize = sizes[0]; for (auto& size : sizes) { if (size.getWidth() < smallestSize.getWidth() && size.getHeight() < smallestSize.getHeight()) smallestSize = size; } return smallestSize; } Rectangle getLargestSize (const Array>& sizes) const { if (sizes.size() == 0) return {}; auto largestSize = sizes[0]; for (auto& size : sizes) { if (size.getWidth() > largestSize.getWidth() && size.getHeight() > largestSize.getHeight()) largestSize = size; } return largestSize; } }; //============================================================================== class PreviewDisplay : private TextureViewSurfaceTextureListener::Owner { public: struct Listener { virtual ~Listener() {} virtual void previewDisplayReady() = 0; virtual void previewDisplayAboutToBeDestroyed() = 0; }; PreviewDisplay (Rectangle bufferSize) : textureViewSurfaceTextureListener (*this), textureView (LocalRef (getEnv()->NewObject (AndroidTextureView, AndroidTextureView.constructor, getAppContext().get()))), bufferWidth (bufferSize.getWidth()), bufferHeight (bufferSize.getHeight()) { auto* env = getEnv(); if (! isReady()) env->CallVoidMethod (textureView, AndroidTextureView.setSurfaceTextureListener, CreateJavaInterface (&textureViewSurfaceTextureListener, "android/view/TextureView$SurfaceTextureListener").get()); } ~PreviewDisplay() override { getEnv()->CallVoidMethod (textureView, AndroidTextureView.setSurfaceTextureListener, nullptr); } void addListener (Listener* l) { if (l == nullptr) { jassertfalse; return; } listeners.add (l); if (isReady()) l->previewDisplayReady(); } void removeListener (Listener* l) { if (l == nullptr) { jassertfalse; return; } listeners.remove (l); } bool isReady() const { return (getEnv()->CallBooleanMethod (textureView, AndroidTextureView.isAvailable) != 0) && width > 0 && height > 0; } LocalRef createSurface() { // Surface may get destroyed while session is being configured, if // the preview gets hidden in the meantime, so bailout. if (! isReady()) return LocalRef (nullptr); auto* env = getEnv(); auto surfaceTexture = LocalRef (env->CallObjectMethod (textureView, AndroidTextureView.getSurfaceTexture)); // NB: too small buffer will result in pixelated preview. A buffer with wrong aspect ratio // can result in a cropped preview. env->CallVoidMethod (surfaceTexture, AndroidSurfaceTexture.setDefaultBufferSize, (jint) bufferWidth, (jint) bufferHeight); auto surface = LocalRef (env->NewObject (AndroidSurface, AndroidSurface.constructor, surfaceTexture.get())); return surface; } const GlobalRef& getNativeView() { return textureView; } void updateSurfaceTransform() { auto* env = getEnv(); auto windowManager = LocalRef (env->CallObjectMethod (getAppContext(), AndroidContext.getSystemService, javaString ("window").get())); auto display = LocalRef (env->CallObjectMethod (windowManager, AndroidWindowManager.getDefaultDisplay)); auto rotation = env->CallIntMethod (display, AndroidDisplay.getRotation); static constexpr int rotation90 = 1; static constexpr int rotation270 = 3; auto matrix = LocalRef (env->NewObject (AndroidMatrix, AndroidMatrix.constructor)); if (rotation == rotation90 || rotation == rotation270) { env->CallBooleanMethod (matrix, AndroidMatrix.postScale, jfloat (height / (float) width), jfloat (width / (float) height), (jfloat) 0, (jfloat) 0); env->CallBooleanMethod (matrix, AndroidMatrix.postRotate, (jfloat) 90 * (rotation - 2), (jfloat) 0, (jfloat) 0); env->CallBooleanMethod (matrix, AndroidMatrix.postTranslate, (jfloat) (rotation == 3 ? width : 0), (jfloat) (rotation == 1 ? height : 0)); } env->CallVoidMethod (textureView, AndroidTextureView.setTransform, matrix.get()); } private: ListenerList listeners; TextureViewSurfaceTextureListener textureViewSurfaceTextureListener; GlobalRef textureView; int width = -1, height = -1; int bufferWidth, bufferHeight; void onSurfaceTextureAvailable (LocalRef& /*surface*/, int widthToUse, int heightToUse) override { JUCE_CAMERA_LOG ("onSurfaceTextureAvailable()"); width = widthToUse; height = heightToUse; updateSurfaceTransform(); listeners.call (&Listener::previewDisplayReady); } bool onSurfaceTextureDestroyed (LocalRef& /*surface*/) override { JUCE_CAMERA_LOG ("onSurfaceTextureDestroyed()"); listeners.call (&Listener::previewDisplayAboutToBeDestroyed); return true; } void onSurfaceTextureSizeChanged (LocalRef& /*surface*/, int widthToUse, int heightToUse) override { JUCE_CAMERA_LOG ("onSurfaceTextureSizeChanged()"); width = widthToUse; height = heightToUse; updateSurfaceTransform(); } void onSurfaceTextureUpdated (LocalRef& /*surface*/) override { JUCE_CAMERA_LOG ("onSurfaceTextureUpdated()"); } JUCE_DECLARE_NON_COPYABLE (PreviewDisplay) }; //============================================================================== class ImageReader : private ImageReaderOnImageAvailableListener::Owner { public: ImageReader (Pimpl& ownerToUse, GlobalRef& handlerToUse, int imageWidth, int imageHeight, int cameraSensorOrientationToUse) : owner (ownerToUse), cameraSensorOrientation (cameraSensorOrientationToUse), imageReader (LocalRef (getEnv()->CallStaticObjectMethod (AndroidImageReader, AndroidImageReader.newInstance, imageWidth, imageHeight, StreamConfigurationMap::jpegImageFormat, numImagesToKeep))), onImageAvailableListener (*this) { getEnv()->CallVoidMethod (imageReader, AndroidImageReader.setOnImageAvailableListener, CreateJavaInterface (&onImageAvailableListener, "android/media/ImageReader$OnImageAvailableListener").get(), handlerToUse.get()); } ~ImageReader() override { getEnv()->CallVoidMethod (imageReader, AndroidImageReader.close); } LocalRef getSurface() const { return LocalRef (getEnv()->CallObjectMethod (imageReader, AndroidImageReader.getSurface)); } void resetNotificationFlag() { hasNotifiedListeners.set (0); } private: Pimpl& owner; int cameraSensorOrientation; GlobalRef imageReader; ImageReaderOnImageAvailableListener onImageAvailableListener; static constexpr int numImagesToKeep = 2; Atomic hasNotifiedListeners { 0 }; JUCE_DECLARE_WEAK_REFERENCEABLE (ImageReader) void onImageAvailable (LocalRef& /*imageReader*/) override { JUCE_CAMERA_LOG ("onImageAvailable()"); auto* env = getEnv(); auto jImage = LocalRef (env->CallObjectMethod (imageReader, AndroidImageReader.acquireLatestImage)); if (jImage.get() == nullptr) return; auto cameraLensFrontFacing = owner.getCameraLensFacing() == 0; // NB: could use sensor orientation here to get real-world orientation, but then the resulting // image could not match the UI orientation. auto image = androidImageToJuceWithFixedOrientation (jImage, owner.deviceOrientationChangeListener.getDeviceOrientation(), Desktop::getInstance().getCurrentOrientation(), cameraLensFrontFacing, cameraSensorOrientation); env->CallVoidMethod (jImage, AndroidImage.close); WeakReference safeThis (this); owner.callListeners (image); // Android may take multiple pictures before it handles a request to stop. if (hasNotifiedListeners.compareAndSetBool (1, 0)) MessageManager::callAsync ([safeThis, image]() mutable { if (safeThis != nullptr) safeThis->owner.notifyPictureTaken (image); }); } struct ImageBuffer { LocalRef byteArray; int size; }; static Image androidImageToJuceWithFixedOrientation (const LocalRef& androidImage, Desktop::DisplayOrientation deviceOrientationFromAccelerometerSensor, Desktop::DisplayOrientation targetOrientation, bool cameraLensFrontFacing, int cameraSensorOrientation) { auto* env = getEnv(); auto planes = LocalRef ((jobjectArray) env->CallObjectMethod (androidImage, AndroidImage.getPlanes)); jassert (env->GetArrayLength (planes) > 0); auto plane = LocalRef (env->GetObjectArrayElement (planes, 0)); auto byteBuffer = LocalRef (env->CallObjectMethod (plane, AndroidImagePlane.getBuffer)); ImageBuffer correctedBuffer = getImageBufferWithCorrectedOrientationFrom (byteBuffer, deviceOrientationFromAccelerometerSensor, targetOrientation, cameraLensFrontFacing, cameraSensorOrientation); jbyte* rawBytes = env->GetByteArrayElements (correctedBuffer.byteArray, nullptr); Image result = ImageFileFormat::loadFrom (rawBytes, (size_t) correctedBuffer.size); env->ReleaseByteArrayElements (correctedBuffer.byteArray, rawBytes, 0); return result; } static ImageBuffer getImageBufferWithCorrectedOrientationFrom (const LocalRef& imagePlaneBuffer, Desktop::DisplayOrientation deviceOrientationFromAccelerometerSensor, Desktop::DisplayOrientation targetOrientation, bool cameraLensFrontFacing, int cameraSensorOrientation) { auto* env = getEnv(); auto bufferSize = env->CallIntMethod (imagePlaneBuffer, JavaByteBuffer.remaining); auto byteArray = LocalRef (env->NewByteArray (bufferSize)); env->CallObjectMethod (imagePlaneBuffer, JavaByteBuffer.get, byteArray.get()); auto rotationAngle = getRotationAngle (deviceOrientationFromAccelerometerSensor, targetOrientation, cameraLensFrontFacing, cameraSensorOrientation); if (rotationAngle == 0) { // Nothing to do, just get the bytes return { byteArray, bufferSize }; } auto origBitmap = LocalRef (env->CallStaticObjectMethod (AndroidBitmapFactory, AndroidBitmapFactory.decodeByteArray, byteArray.get(), (jint) 0, (jint) bufferSize)); if (origBitmap == nullptr) { // Nothing to do, just get the bytes return { byteArray, bufferSize }; } auto correctedBitmap = getBitmapWithCorrectOrientationFrom (origBitmap, rotationAngle); auto byteArrayOutputStream = LocalRef (env->NewObject (ByteArrayOutputStream, ByteArrayOutputStream.constructor)); auto jCompressFormatString = javaString ("JPEG"); auto compressFormat = LocalRef (env->CallStaticObjectMethod (AndroidBitmapCompressFormat, AndroidBitmapCompressFormat.valueOf, jCompressFormatString.get())); if (env->CallBooleanMethod (correctedBitmap, AndroidBitmap.compress, compressFormat.get(), (jint) 100, byteArrayOutputStream.get()) != 0) { auto correctedByteArray = LocalRef ((jbyteArray) env->CallObjectMethod (byteArrayOutputStream, ByteArrayOutputStream.toByteArray)); int correctedByteArraySize = env->CallIntMethod (byteArrayOutputStream, ByteArrayOutputStream.size); return { correctedByteArray, correctedByteArraySize }; } jassertfalse; // fallback, return original bitmap return { byteArray, bufferSize }; } static int getRotationAngle (Desktop::DisplayOrientation deviceOrientationFromAccelerometerSensor, Desktop::DisplayOrientation targetOrientation, bool cameraLensFrontFacing, int cameraSensorOrientation) { auto isSensorOrientationHorizontal = deviceOrientationFromAccelerometerSensor == Desktop::rotatedAntiClockwise || deviceOrientationFromAccelerometerSensor == Desktop::rotatedClockwise; if (cameraLensFrontFacing && isSensorOrientationHorizontal) { // flip angles for front camera return getRotationAngle (deviceOrientationFromAccelerometerSensor, targetOrientation, false, (cameraSensorOrientation + 180) % 360); } switch (targetOrientation) { case Desktop::rotatedAntiClockwise: return cameraSensorOrientation == 90 ? 0 : 180; case Desktop::rotatedClockwise: return cameraSensorOrientation == 90 ? 180 : 0; case Desktop::upright: case Desktop::upsideDown: if ((targetOrientation == Desktop::upright && ! cameraLensFrontFacing) || (targetOrientation == Desktop::upsideDown && cameraLensFrontFacing)) { return cameraSensorOrientation; } else { if (deviceOrientationFromAccelerometerSensor == Desktop::upright || deviceOrientationFromAccelerometerSensor == Desktop::upsideDown) return cameraSensorOrientation; else return (cameraSensorOrientation + 180) % 360; } break; case Desktop::allOrientations: default: return 0; } } static LocalRef getBitmapWithCorrectOrientationFrom (LocalRef& origBitmap, int rotationAngle) { auto* env = getEnv(); auto origBitmapWidth = env->CallIntMethod (origBitmap, AndroidBitmap.getWidth); auto origBitmapHeight = env->CallIntMethod (origBitmap, AndroidBitmap.getHeight); auto matrix = LocalRef (env->NewObject (AndroidMatrix, AndroidMatrix.constructor)); env->CallBooleanMethod (matrix, AndroidMatrix.postRotate, (jfloat) rotationAngle, (jfloat) 0, (jfloat) 0); auto rotatedBitmap = LocalRef (env->CallStaticObjectMethod (AndroidBitmap, AndroidBitmap.createBitmapFrom, origBitmap.get(), (jint) 0, (jint) 0, (jint) origBitmapWidth, (jint) origBitmapHeight, matrix.get(), true)); env->CallVoidMethod (origBitmap, AndroidBitmap.recycle); return rotatedBitmap; } }; //============================================================================== class MediaRecorder : private MediaRecorderOnInfoListener::Owner, private MediaRecorderOnErrorListener::Owner { public: MediaRecorder (const String& outputFilePath, int videoWidth, int videoHeight, int sensorOrientation, int cameraLensFacing) : onInfoListener (*this), onErrorListener (*this), mediaRecorder (LocalRef (getEnv()->NewObject (AndroidMediaRecorder, AndroidMediaRecorder.constructor))) { auto* env = getEnv(); env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setOnInfoListener, CreateJavaInterface (&onInfoListener, "android/media/MediaRecorder$OnInfoListener").get()); env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setOnErrorListener, CreateJavaInterface (&onErrorListener, "android/media/MediaRecorder$OnErrorListener").get()); // NB: the order of function calls here is enforced, and exceptions will be thrown if // the order is changed. static constexpr int audioSourceMic = 1; env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setAudioSource, (jint) audioSourceMic); static constexpr int videoSourceSurface = 2; env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setVideoSource, (jint) videoSourceSurface); static constexpr int outputFormatMPEG4 = 2; env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setOutputFormat, (jint) outputFormatMPEG4); static constexpr int audioEncoderAAC = 3; env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setAudioEncoder, (jint) audioEncoderAAC); static constexpr int videoEncoderH264 = 2; env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setVideoEncoder, (jint) videoEncoderH264); env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setVideoEncodingBitRate, (jint) 10000000); env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setVideoFrameRate, (jint) 30); auto frontFacing = cameraLensFacing == 0; auto useInverseDegrees = frontFacing && sensorOrientation == 90; int orientationHint = getOrientationHint (useInverseDegrees, sensorOrientation); env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setOrientationHint, (jint) orientationHint); getEnv()->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setVideoSize, (jint) videoWidth, (jint) videoHeight); getEnv()->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setOutputFile, javaString (outputFilePath).get()); getEnv()->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.prepare); } ~MediaRecorder() override { getEnv()->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.release); } LocalRef getSurface() { return LocalRef (getEnv()->CallObjectMethod (mediaRecorder, AndroidMediaRecorder.getSurface)); } void start() { lockScreenOrientation(); getEnv()->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.start); hasStartedRecording = true; } void stop() { // A request to stop can be sent before recording has had a chance to start, so // ignore the request rather than calling AndroidMediaRecorder.stop because // otherwise MediaRecorder will throw an exception and... if (! hasStartedRecording) return; hasStartedRecording = false; auto* env = getEnv(); env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.stop); // ... ignore RuntimeException that can be thrown if stop() was called after recording // has started but before any frame was written to a file. This is not an error. jniCheckHasExceptionOccurredAndClear(); unlockScreenOrientation(); } private: MediaRecorderOnInfoListener onInfoListener; MediaRecorderOnErrorListener onErrorListener; GlobalRef mediaRecorder; bool hasStartedRecording = false; int orientationsEnabled = -1; void lockScreenOrientation() { orientationsEnabled = Desktop::getInstance().getOrientationsEnabled(); auto o = Desktop::getInstance().getCurrentOrientation(); Desktop::getInstance().setOrientationsEnabled (o); } static jint juceOrientationToNativeOrientation (int orientations) noexcept { enum { SCREEN_ORIENTATION_LANDSCAPE = 0, SCREEN_ORIENTATION_PORTRAIT = 1, SCREEN_ORIENTATION_USER = 2, SCREEN_ORIENTATION_REVERSE_LANDSCAPE = 8, SCREEN_ORIENTATION_REVERSE_PORTRAIT = 9, SCREEN_ORIENTATION_USER_LANDSCAPE = 11, SCREEN_ORIENTATION_USER_PORTRAIT = 12, }; switch (orientations) { case Desktop::upright: return (jint) SCREEN_ORIENTATION_PORTRAIT; case Desktop::upsideDown: return (jint) SCREEN_ORIENTATION_REVERSE_PORTRAIT; case Desktop::upright + Desktop::upsideDown: return (jint) SCREEN_ORIENTATION_USER_PORTRAIT; case Desktop::rotatedAntiClockwise: return (jint) SCREEN_ORIENTATION_LANDSCAPE; case Desktop::rotatedClockwise: return (jint) SCREEN_ORIENTATION_REVERSE_LANDSCAPE; case Desktop::rotatedClockwise + Desktop::rotatedAntiClockwise: return (jint) SCREEN_ORIENTATION_USER_LANDSCAPE; default: return (jint) SCREEN_ORIENTATION_USER; } } void unlockScreenOrientation() { Desktop::getInstance().setOrientationsEnabled (orientationsEnabled); } void onInfo (LocalRef& recorder, int what, int extra) override { ignoreUnused (recorder, what, extra); JUCE_CAMERA_LOG ("MediaRecorder::OnInfo: " + getInfoStringFromCode (what) + ", extra code = " + String (extra)); } void onError (LocalRef& recorder, int what, int extra) override { ignoreUnused (recorder, what, extra); JUCE_CAMERA_LOG ("MediaRecorder::onError: " + getErrorStringFromCode (what) + ", extra code = " + String (extra)); } static String getInfoStringFromCode (int what) { enum { MEDIA_RECORDER_INFO_UNKNOWN = 1, MEDIA_RECORDER_INFO_MAX_DURATION_REACHED = 800, MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED = 801, MEDIA_RECORDER_INFO_MAX_FILESIZE_APPROACHING = 802, MEDIA_RECORDER_INFO_NEXT_OUTPUT_FILE_STARTED = 803 }; switch (what) { case MEDIA_RECORDER_INFO_UNKNOWN: return { "Unknown info" }; case MEDIA_RECORDER_INFO_MAX_DURATION_REACHED: return { "Max duration reached" }; case MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED: return { "Max filesize reached" }; case MEDIA_RECORDER_INFO_MAX_FILESIZE_APPROACHING: return { "Max filesize approaching" }; case MEDIA_RECORDER_INFO_NEXT_OUTPUT_FILE_STARTED: return { "Next output file started" }; default: return String (what); }; } static String getErrorStringFromCode (int what) { enum { MEDIA_RECORDER_ERROR_UNKNOWN = 1, MEDIA_ERROR_SERVER_DIED = 100 }; switch (what) { case MEDIA_RECORDER_ERROR_UNKNOWN: return { "Unknown error" }; case MEDIA_ERROR_SERVER_DIED: return { "Server died" }; default: return String (what); }; } static int getOrientationHint (bool useInverseDegrees, int cameraSensorOrientation) { auto* env = getEnv(); auto windowManager = LocalRef (env->CallObjectMethod (getAppContext(), AndroidContext.getSystemService, javaString ("window").get())); auto display = LocalRef (env->CallObjectMethod (windowManager, AndroidWindowManager.getDefaultDisplay)); auto rotation = env->CallIntMethod (display, AndroidDisplay.getRotation); enum { ROTATION_0 = 0, ROTATION_90, ROTATION_180, ROTATION_270 }; int hint = 0; switch (rotation) { case ROTATION_0: hint = cameraSensorOrientation; break; case ROTATION_90: hint = useInverseDegrees ? 180 : 0; break; case ROTATION_180: hint = cameraSensorOrientation + 180; break; case ROTATION_270: hint = useInverseDegrees ? 0 : 180; break; default: jassertfalse; } return (hint + 360) % 360; } }; //============================================================================== class ScopedCameraDevice { public: //============================================================================== class CaptureSession { public: struct ConfiguredCallback { virtual ~ConfiguredCallback() {} virtual void captureSessionConfigured (CaptureSession*) = 0; }; ~CaptureSession() { bool calledClose = false; auto* env = getEnv(); { const ScopedLock lock (captureSessionLock); if (captureSession.get() != nullptr) { calledClose = true; env->CallVoidMethod (captureSession, CameraCaptureSession.close); } } // When exception occurs, CameraCaptureSession.close will never finish, so // we should not wait for it. For fatal error an exception does occur, but // it is caught internally in Java... if (jniCheckHasExceptionOccurredAndClear() || scopedCameraDevice.fatalErrorOccurred.get()) { JUCE_CAMERA_LOG ("Exception or fatal error occurred while closing Capture Session, closing by force"); } else if (calledClose) { pendingClose.set (1); closedEvent.wait (-1); } } bool openedOk() const noexcept { return captureSession != nullptr; } const GlobalRef& getNativeSession() const { return captureSession; } bool start (const LocalRef& targetSurfacesList, GlobalRef& handlerToUse) { if (! openedOk()) { jassertfalse; return false; } auto* env = getEnv(); auto numSurfaces = env->CallIntMethod (targetSurfacesList, JavaArrayList.size); for (int i = 0; i < numSurfaces; ++i) { auto surface = LocalRef (env->CallObjectMethod (targetSurfacesList, JavaArrayList.get, (jint) i)); env->CallVoidMethod (captureRequestBuilder, CaptureRequestBuilder.addTarget, surface.get()); } previewCaptureRequest = GlobalRef (LocalRef(env->CallObjectMethod (captureRequestBuilder, CaptureRequestBuilder.build))); env->CallIntMethod (captureSession, CameraCaptureSession.setRepeatingRequest, previewCaptureRequest.get(), nullptr, handlerToUse.get()); return true; } void takeStillPicture (jobject targetSurface) { if (stillPictureTaker == nullptr) { // Can only take picture once session was successfully configured! jassertfalse; return; } auto* env = getEnv(); static constexpr int templateStillCapture = 2; auto builder = LocalRef (env->CallObjectMethod (scopedCameraDevice.cameraDevice, AndroidCameraDevice.createCaptureRequest, (jint) templateStillCapture)); env->CallVoidMethod (builder, CaptureRequestBuilder.addTarget, targetSurface); setCaptureRequestBuilderIntegerKey (builder, CaptureRequest.CONTROL_AF_MODE, autoFocusMode); auto stillPictureCaptureRequest = LocalRef (env->CallObjectMethod (builder, CaptureRequestBuilder.build)); stillPictureTaker->takePicture (stillPictureCaptureRequest); } private: //============================================================================== class StillPictureTaker : private AndroidRunnable::Owner { public: StillPictureTaker (GlobalRef& captureSessionToUse, GlobalRef& captureRequestBuilderToUse, GlobalRef& previewCaptureRequestToUse, GlobalRef& handlerToUse, int autoFocusModeToUse) : captureSession (captureSessionToUse), captureRequestBuilder (captureRequestBuilderToUse), previewCaptureRequest (previewCaptureRequestToUse), handler (handlerToUse), runnable (*this), captureSessionPreviewCaptureCallback (createCaptureSessionCallback (true)), captureSessionStillPictureCaptureCallback (createCaptureSessionCallback (false)), autoFocusMode (autoFocusModeToUse) { } void takePicture (const LocalRef& stillPictureCaptureRequestToUse) { JUCE_CAMERA_LOG ("Taking picture..."); stillPictureCaptureRequest = GlobalRef (LocalRef(stillPictureCaptureRequestToUse)); lockFocus(); } private: GlobalRef& captureSession; GlobalRef& captureRequestBuilder; GlobalRef& previewCaptureRequest; GlobalRef& handler; AndroidRunnable runnable; GlobalRef delayedCaptureRunnable; GlobalRef captureSessionPreviewCaptureCallback; GlobalRef stillPictureCaptureRequest; GlobalRef captureSessionStillPictureCaptureCallback; int autoFocusMode; //============================================================================== #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (constructor, "", "(JZ)V") \ CALLBACK (cameraCaptureSessionCaptureCompletedCallback, "cameraCaptureSessionCaptureCompleted", "(JZLandroid/hardware/camera2/CameraCaptureSession;Landroid/hardware/camera2/CaptureRequest;Landroid/hardware/camera2/TotalCaptureResult;)V") \ CALLBACK (cameraCaptureSessionCaptureFailedCallback, "cameraCaptureSessionCaptureFailed", "(JZLandroid/hardware/camera2/CameraCaptureSession;Landroid/hardware/camera2/CaptureRequest;Landroid/hardware/camera2/CaptureFailure;)V") \ CALLBACK (cameraCaptureSessionCaptureProgressedCallback, "cameraCaptureSessionCaptureProgressed", "(JZLandroid/hardware/camera2/CameraCaptureSession;Landroid/hardware/camera2/CaptureRequest;Landroid/hardware/camera2/CaptureResult;)V") \ CALLBACK (cameraCaptureSessionCaptureStartedCallback, "cameraCaptureSessionCaptureStarted", "(JZLandroid/hardware/camera2/CameraCaptureSession;Landroid/hardware/camera2/CaptureRequest;JJ)V") \ CALLBACK (cameraCaptureSessionCaptureSequenceAbortedCallback, "cameraCaptureSessionCaptureSequenceAborted", "(JZLandroid/hardware/camera2/CameraCaptureSession;I)V") \ CALLBACK (cameraCaptureSessionCaptureSequenceCompletedCallback, "cameraCaptureSessionCaptureSequenceCompleted", "(JZLandroid/hardware/camera2/CameraCaptureSession;IJ)V") DECLARE_JNI_CLASS_WITH_BYTECODE (CameraCaptureSessionCaptureCallback, "com/rmsl/juce/CameraCaptureSessionCaptureCallback", 21, CameraSupportByteCode, sizeof(CameraSupportByteCode)) #undef JNI_CLASS_MEMBERS LocalRef createCaptureSessionCallback (bool createPreviewSession) { return LocalRef(getEnv()->NewObject (CameraCaptureSessionCaptureCallback, CameraCaptureSessionCaptureCallback.constructor, reinterpret_cast (this), createPreviewSession ? 1 : 0)); } //============================================================================== enum class State { idle = 0, pendingFocusLock, pendingExposurePrecapture, pendingExposurePostPrecapture, pictureTaken }; State currentState = State::idle; void lockFocus() { if (jniCheckHasExceptionOccurredAndClear()) return; JUCE_CAMERA_LOG ("Performing auto-focus if possible..."); currentState = State::pendingFocusLock; auto* env = getEnv(); // NB: auto-focus may be unavailable on a device, in which case it may have already // automatically adjusted the exposure. We check for that in updateState(). static constexpr int controlAfTriggerStart = 1; CaptureSession::setCaptureRequestBuilderIntegerKey (captureRequestBuilder.get(), CaptureRequest.CONTROL_AF_TRIGGER, controlAfTriggerStart); auto previewRequest = LocalRef (env->CallObjectMethod (captureRequestBuilder, CaptureRequestBuilder.build)); env->CallIntMethod (captureSession, CameraCaptureSession.capture, previewRequest.get(), captureSessionPreviewCaptureCallback.get(), handler.get()); } void updateState (jobject captureResult) { // IllegalStateException can be thrown when accessing CaptureSession, // claiming that capture session was already closed but we may not // get relevant callback yet, so check for this and bailout when needed. if (jniCheckHasExceptionOccurredAndClear()) return; switch (currentState) { case State::pendingFocusLock: { JUCE_CAMERA_LOG ("Still picture capture, updateState(), State::pendingFocusLock..."); auto controlAfStateValue = getCaptureResultIntegerKeyValue (CaptureResult.CONTROL_AF_STATE, captureResult); if (controlAfStateValue.get() == nullptr) { captureStillPictureDelayed(); return; } auto autoToFocusNotAvailable = autoFocusMode == 0; if (autoToFocusNotAvailable || autoFocusHasFinished (controlAfStateValue)) { auto controlAeStateIntValue = getControlAEState (captureResult); static constexpr int controlAeStateConverged = 2; if (controlAeStateIntValue == -1 || controlAeStateIntValue == controlAeStateConverged) { currentState = State::pictureTaken; captureStillPictureDelayed(); } else { runPrecaptureSequence(); } } break; } case State::pendingExposurePrecapture: { JUCE_CAMERA_LOG ("Still picture capture, updateState(), State::pendingExposurePrecapture..."); auto controlAeStateIntValue = getControlAEState (captureResult); static constexpr int controlAeStateFlashRequired = 4; static constexpr int controlAeStatePrecapture = 5; if (controlAeStateIntValue == -1 || controlAeStateIntValue == controlAeStateFlashRequired || controlAeStateIntValue == controlAeStatePrecapture) { currentState = State::pendingExposurePostPrecapture; } break; } case State::pendingExposurePostPrecapture: { JUCE_CAMERA_LOG ("Still picture capture, updateState(), State::pendingExposurePostPrecapture..."); auto controlAeStateIntValue = getControlAEState (captureResult); static constexpr int controlAeStatePrecapture = 5; if (controlAeStateIntValue == -1 || controlAeStateIntValue != controlAeStatePrecapture) { currentState = State::pictureTaken; captureStillPictureDelayed(); } break; } case State::idle: case State::pictureTaken: { /* do nothing */ break; } }; } static int getControlAEState (jobject captureResult) { auto controlAeStateValue = getCaptureResultIntegerKeyValue (CaptureResult.CONTROL_AE_STATE, captureResult); return controlAeStateValue.get() != nullptr ? getEnv()->CallIntMethod (controlAeStateValue, JavaInteger.intValue) : -1; } static bool autoFocusHasFinished (const LocalRef& controlAfStateValue) { static constexpr int controlAfStateFocusedLocked = 4; static constexpr int controlAfStateNotFocusedLocked = 5; auto controlAfStateIntValue = getEnv()->CallIntMethod (controlAfStateValue, JavaInteger.intValue); return controlAfStateIntValue == controlAfStateFocusedLocked || controlAfStateIntValue == controlAfStateNotFocusedLocked; } static LocalRef getCaptureResultIntegerKeyValue (jfieldID key, jobject captureResult) { auto* env = getEnv(); auto jKey = LocalRef (env->GetStaticObjectField (CaptureResult, key)); return LocalRef (env->CallObjectMethod (captureResult, CaptureResult.get, jKey.get())); } void captureStillPictureDelayed() { if (jniCheckHasExceptionOccurredAndClear()) return; JUCE_CAMERA_LOG ("Still picture capture, device ready, capturing now..."); auto* env = getEnv(); env->CallVoidMethod (captureSession, CameraCaptureSession.stopRepeating); if (jniCheckHasExceptionOccurredAndClear()) return; env->CallVoidMethod (captureSession, CameraCaptureSession.abortCaptures); if (jniCheckHasExceptionOccurredAndClear()) return; // Delay still picture capture for devices that can't handle it right after // stopRepeating/abortCaptures calls. if (delayedCaptureRunnable.get() == nullptr) delayedCaptureRunnable = GlobalRef (CreateJavaInterface (&runnable, "java/lang/Runnable")); env->CallBooleanMethod (handler, AndroidHandler.postDelayed, delayedCaptureRunnable.get(), (jlong) 200); } void runPrecaptureSequence() { if (jniCheckHasExceptionOccurredAndClear()) return; auto* env = getEnv(); static constexpr int controlAePrecaptureTriggerStart = 1; CaptureSession::setCaptureRequestBuilderIntegerKey (captureRequestBuilder.get(), CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, controlAePrecaptureTriggerStart); currentState = State::pendingExposurePrecapture; auto previewRequest = LocalRef (env->CallObjectMethod (captureRequestBuilder, CaptureRequestBuilder.build)); env->CallIntMethod (captureSession, CameraCaptureSession.capture, previewRequest.get(), captureSessionPreviewCaptureCallback.get(), handler.get()); } void unlockFocus() { if (jniCheckHasExceptionOccurredAndClear()) return; JUCE_CAMERA_LOG ("Unlocking focus..."); currentState = State::idle; auto* env = getEnv(); static constexpr int controlAfTriggerCancel = 2; CaptureSession::setCaptureRequestBuilderIntegerKey (captureRequestBuilder.get(), CaptureRequest.CONTROL_AF_TRIGGER, controlAfTriggerCancel); auto resetAutoFocusRequest = LocalRef (env->CallObjectMethod (captureRequestBuilder, CaptureRequestBuilder.build)); env->CallIntMethod (captureSession, CameraCaptureSession.capture, resetAutoFocusRequest.get(), nullptr, handler.get()); if (jniCheckHasExceptionOccurredAndClear()) return; // NB: for preview, using preview capture request again env->CallIntMethod (captureSession, CameraCaptureSession.setRepeatingRequest, previewCaptureRequest.get(), nullptr, handler.get()); } //============================================================================== void run() override { captureStillPicture(); } void captureStillPicture() { getEnv()->CallIntMethod (captureSession, CameraCaptureSession.capture, stillPictureCaptureRequest.get(), captureSessionStillPictureCaptureCallback.get(), nullptr); } //============================================================================== void cameraCaptureSessionCaptureCompleted (bool isPreview, jobject session, jobject request, jobject result) { JUCE_CAMERA_LOG ("cameraCaptureSessionCaptureCompleted()"); ignoreUnused (session, request); if (isPreview) updateState (result); else if (currentState != State::idle) unlockFocus(); } void cameraCaptureSessionCaptureFailed (bool isPreview, jobject session, jobject request, jobject failure) { JUCE_CAMERA_LOG ("cameraCaptureSessionCaptureFailed()"); ignoreUnused (isPreview, session, request, failure); } void cameraCaptureSessionCaptureProgressed (bool isPreview, jobject session, jobject request, jobject partialResult) { JUCE_CAMERA_LOG ("cameraCaptureSessionCaptureProgressed()"); ignoreUnused (session, request); if (isPreview) updateState (partialResult); } void cameraCaptureSessionCaptureSequenceAborted (bool isPreview, jobject session, int sequenceId) { JUCE_CAMERA_LOG ("cameraCaptureSessionCaptureSequenceAborted()"); ignoreUnused (isPreview, isPreview, session, sequenceId); } void cameraCaptureSessionCaptureSequenceCompleted (bool isPreview, jobject session, int sequenceId, int64 frameNumber) { JUCE_CAMERA_LOG ("cameraCaptureSessionCaptureSequenceCompleted()"); ignoreUnused (isPreview, session, sequenceId, frameNumber); } void cameraCaptureSessionCaptureStarted (bool isPreview, jobject session, jobject request, int64 timestamp, int64 frameNumber) { JUCE_CAMERA_LOG ("cameraCaptureSessionCaptureStarted()"); ignoreUnused (isPreview, session, request, timestamp, frameNumber); } //============================================================================== static void cameraCaptureSessionCaptureCompletedCallback (JNIEnv*, jobject /*object*/, jlong host, jboolean isPreview, jobject rawSession, jobject rawRequest, jobject rawResult) { if (auto* myself = reinterpret_cast (host)) { LocalRef session (getEnv()->NewLocalRef(rawSession)); LocalRef request (getEnv()->NewLocalRef(rawRequest)); LocalRef result (getEnv()->NewLocalRef(rawResult)); myself->cameraCaptureSessionCaptureCompleted (isPreview != 0, session, request, result); } } static void cameraCaptureSessionCaptureFailedCallback (JNIEnv*, jobject /*object*/, jlong host, jboolean isPreview, jobject rawSession, jobject rawRequest, jobject rawResult) { if (auto* myself = reinterpret_cast (host)) { LocalRef session (getEnv()->NewLocalRef(rawSession)); LocalRef request (getEnv()->NewLocalRef(rawRequest)); LocalRef result (getEnv()->NewLocalRef(rawResult)); myself->cameraCaptureSessionCaptureFailed (isPreview != 0, session, request, result); } } static void cameraCaptureSessionCaptureProgressedCallback (JNIEnv*, jobject /*object*/, jlong host, jboolean isPreview, jobject rawSession, jobject rawRequest, jobject rawResult) { if (auto* myself = reinterpret_cast (host)) { LocalRef session (getEnv()->NewLocalRef(rawSession)); LocalRef request (getEnv()->NewLocalRef(rawRequest)); LocalRef result (getEnv()->NewLocalRef(rawResult)); myself->cameraCaptureSessionCaptureProgressed (isPreview != 0, session, request, result); } } static void cameraCaptureSessionCaptureSequenceAbortedCallback (JNIEnv*, jobject /*object*/, jlong host, jboolean isPreview, jobject rawSession, jint sequenceId) { if (auto* myself = reinterpret_cast (host)) { LocalRef session (getEnv()->NewLocalRef(rawSession)); myself->cameraCaptureSessionCaptureSequenceAborted (isPreview != 0, session, sequenceId); } } static void cameraCaptureSessionCaptureSequenceCompletedCallback (JNIEnv*, jobject /*object*/, jlong host, jboolean isPreview, jobject rawSession, jint sequenceId, jlong frameNumber) { if (auto* myself = reinterpret_cast (host)) { LocalRef session (getEnv()->NewLocalRef(rawSession)); myself->cameraCaptureSessionCaptureSequenceCompleted (isPreview != 0, session, sequenceId, frameNumber); } } static void cameraCaptureSessionCaptureStartedCallback (JNIEnv*, jobject /*object*/, jlong host, jboolean isPreview, jobject rawSession, jobject rawRequest, jlong timestamp, jlong frameNumber) { if (auto* myself = reinterpret_cast (host)) { LocalRef session (getEnv()->NewLocalRef(rawSession)); LocalRef request (getEnv()->NewLocalRef(rawRequest)); myself->cameraCaptureSessionCaptureStarted (isPreview != 0, session, request, timestamp, frameNumber); } } }; //============================================================================== ScopedCameraDevice& scopedCameraDevice; ConfiguredCallback& configuredCallback; GlobalRef& handler; GlobalRef captureRequestBuilder; GlobalRef previewCaptureRequest; GlobalRef captureSessionStateCallback; int autoFocusMode; GlobalRef captureSession; CriticalSection captureSessionLock; Atomic pendingClose { 0 }; std::unique_ptr stillPictureTaker; WaitableEvent closedEvent; JUCE_DECLARE_WEAK_REFERENCEABLE (CaptureSession) //============================================================================== #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (constructor, "", "(J)V") \ CALLBACK(cameraCaptureSessionActiveCallback, "cameraCaptureSessionActive", "(JLandroid/hardware/camera2/CameraCaptureSession;)V") \ CALLBACK(cameraCaptureSessionClosedCallback, "cameraCaptureSessionClosed", "(JLandroid/hardware/camera2/CameraCaptureSession;)V") \ CALLBACK(cameraCaptureSessionConfigureFailedCallback, "cameraCaptureSessionConfigureFailed", "(JLandroid/hardware/camera2/CameraCaptureSession;)V") \ CALLBACK(cameraCaptureSessionConfiguredCallback, "cameraCaptureSessionConfigured", "(JLandroid/hardware/camera2/CameraCaptureSession;)V") \ CALLBACK(cameraCaptureSessionReadyCallback, "cameraCaptureSessionReady", "(JLandroid/hardware/camera2/CameraCaptureSession;)V") DECLARE_JNI_CLASS_WITH_MIN_SDK (CameraCaptureSessionStateCallback, "com/rmsl/juce/CameraCaptureSessionStateCallback", 21) #undef JNI_CLASS_MEMBERS //============================================================================== CaptureSession (ScopedCameraDevice& scopedCameraDeviceToUse, ConfiguredCallback& configuredCallbackToUse, const LocalRef& surfacesList, GlobalRef& handlerToUse, int captureSessionTemplate, int autoFocusModeToUse) : scopedCameraDevice (scopedCameraDeviceToUse), configuredCallback (configuredCallbackToUse), handler (handlerToUse), captureRequestBuilder (LocalRef (getEnv()->CallObjectMethod (scopedCameraDevice.cameraDevice, AndroidCameraDevice.createCaptureRequest, (jint) captureSessionTemplate))), captureSessionStateCallback (LocalRef (getEnv()->NewObject (CameraCaptureSessionStateCallback, CameraCaptureSessionStateCallback.constructor, reinterpret_cast (this)))), autoFocusMode (autoFocusModeToUse) { auto* env = getEnv(); env->CallVoidMethod (scopedCameraDevice.cameraDevice, AndroidCameraDevice.createCaptureSession, surfacesList.get(), captureSessionStateCallback.get(), handler.get()); static constexpr int controlModeAuto = 1; setCaptureRequestBuilderIntegerKey (captureRequestBuilder.get(), CaptureRequest.CONTROL_MODE, controlModeAuto); setCaptureRequestBuilderIntegerKey (captureRequestBuilder.get(), CaptureRequest.CONTROL_AF_MODE, autoFocusMode); } static void setCaptureRequestBuilderIntegerKey (jobject captureRequestBuilder, jfieldID key, int value) { auto* env = getEnv(); auto jKey = LocalRef (env->GetStaticObjectField (CaptureRequest, key)); auto jValue = LocalRef (env->CallStaticObjectMethod (JavaInteger, JavaInteger.valueOf, (jint) value)); env->CallVoidMethod (captureRequestBuilder, CaptureRequestBuilder.set, jKey.get(), jValue.get()); } void cameraCaptureSessionActive (jobject session) { JUCE_CAMERA_LOG ("cameraCaptureSessionActive()"); ignoreUnused (session); } void cameraCaptureSessionClosed (jobject session) { JUCE_CAMERA_LOG ("cameraCaptureSessionClosed()"); ignoreUnused (session); closedEvent.signal(); } void cameraCaptureSessionConfigureFailed (jobject session) { JUCE_CAMERA_LOG ("cameraCaptureSessionConfigureFailed()"); ignoreUnused (session); WeakReference weakRef (this); MessageManager::callAsync ([this, weakRef]() { if (weakRef == nullptr) return; configuredCallback.captureSessionConfigured (nullptr); }); } void cameraCaptureSessionConfigured (const LocalRef& session) { JUCE_CAMERA_LOG ("cameraCaptureSessionConfigured()"); if (pendingClose.get() == 1) { // Already closing, bailout. closedEvent.signal(); GlobalRef s (session); MessageManager::callAsync ([s]() { getEnv()->CallVoidMethod (s, CameraCaptureSession.close); }); return; } { const ScopedLock lock (captureSessionLock); captureSession = GlobalRef (session); } WeakReference weakRef (this); MessageManager::callAsync ([this, weakRef]() { if (weakRef == nullptr) return; stillPictureTaker.reset (new StillPictureTaker (captureSession, captureRequestBuilder, previewCaptureRequest, handler, autoFocusMode)); configuredCallback.captureSessionConfigured (this); }); } void cameraCaptureSessionReady (const LocalRef& session) { JUCE_CAMERA_LOG ("cameraCaptureSessionReady()"); ignoreUnused (session); } //============================================================================== static void cameraCaptureSessionActiveCallback (JNIEnv*, jobject, jlong host, jobject rawSession) { if (auto* myself = reinterpret_cast (host)) { LocalRef session (getEnv()->NewLocalRef(rawSession)); myself->cameraCaptureSessionActive (session); } } static void cameraCaptureSessionClosedCallback (JNIEnv*, jobject, jlong host, jobject rawSession) { if (auto* myself = reinterpret_cast (host)) { LocalRef session (getEnv()->NewLocalRef(rawSession)); myself->cameraCaptureSessionClosed (session); } } static void cameraCaptureSessionConfigureFailedCallback (JNIEnv*, jobject, jlong host, jobject rawSession) { if (auto* myself = reinterpret_cast (host)) { LocalRef session (getEnv()->NewLocalRef(rawSession)); myself->cameraCaptureSessionConfigureFailed (session); } } static void cameraCaptureSessionConfiguredCallback (JNIEnv*, jobject, jlong host, jobject rawSession) { if (auto* myself = reinterpret_cast (host)) { LocalRef session (getEnv()->NewLocalRef(rawSession)); myself->cameraCaptureSessionConfigured (session); } } static void cameraCaptureSessionReadyCallback (JNIEnv*, jobject, jlong host, jobject rawSession) { if (auto* myself = reinterpret_cast (host)) { LocalRef session (getEnv()->NewLocalRef(rawSession)); myself->cameraCaptureSessionReady (session); } } //============================================================================== friend class ScopedCameraDevice; JUCE_DECLARE_NON_COPYABLE (CaptureSession) }; //============================================================================== ScopedCameraDevice (Pimpl& ownerToUse, const String& cameraIdToUse, GlobalRef& cameraManagerToUse, GlobalRef& handlerToUse, int autoFocusModeToUse) : owner (ownerToUse), cameraId (cameraIdToUse), cameraManager (cameraManagerToUse), handler (handlerToUse), cameraStateCallback (createCameraStateCallbackObject()), autoFocusMode (autoFocusModeToUse) { open(); } ~ScopedCameraDevice() { close(); } void open() { pendingOpen.set (1); auto* env = getEnv(); env->CallVoidMethod (cameraManager, CameraManager.openCamera, javaString (cameraId).get(), cameraStateCallback.get(), handler.get()); // If something went wrong we will be pinged in cameraDeviceStateError() // callback, silence the redundant exception. jniCheckHasExceptionOccurredAndClear(); } void close() { if (pendingClose.compareAndSetBool (1, 0)) { auto* env = getEnv(); if (cameraDevice.get() != nullptr) { env->CallVoidMethod (cameraDevice, AndroidCameraDevice.close); closedEvent.wait (-1); } pendingClose.set (0); pendingOpen .set (0); cameraDevice.clear(); } } bool openedOk() const { return cameraDevice != nullptr; } bool hasErrorOccurred() const { return fatalErrorOccurred.get(); } CaptureSession* createCaptureSession (CaptureSession::ConfiguredCallback& cc, const LocalRef& surfacesList, GlobalRef& handlerToUse, int captureSessionTemplate) { if (! openedOk()) { jassertfalse; return nullptr; } return new CaptureSession (*this, cc, surfacesList, handlerToUse, captureSessionTemplate, autoFocusMode); } private: Pimpl& owner; const String cameraId; GlobalRef& cameraManager; GlobalRef& handler; GlobalRef cameraStateCallback; int autoFocusMode; GlobalRef cameraDevice; Atomic pendingOpen { 0 }; Atomic pendingClose { 0 }; Atomic fatalErrorOccurred { 0 }; String openError; WaitableEvent closedEvent; //============================================================================== #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (constructor, "", "(J)V") \ CALLBACK (cameraDeviceStateClosedCallback, "cameraDeviceStateClosed", "(JLandroid/hardware/camera2/CameraDevice;)V") \ CALLBACK (cameraDeviceStateDisconnectedCallback, "cameraDeviceStateDisconnected", "(JLandroid/hardware/camera2/CameraDevice;)V") \ CALLBACK (cameraDeviceStateErrorCallback, "cameraDeviceStateError", "(JLandroid/hardware/camera2/CameraDevice;I)V") \ CALLBACK (cameraDeviceStateOpenedCallback, "cameraDeviceStateOpened", "(JLandroid/hardware/camera2/CameraDevice;)V") DECLARE_JNI_CLASS_WITH_MIN_SDK (CameraDeviceStateCallback, "com/rmsl/juce/CameraDeviceStateCallback", 21) #undef JNI_CLASS_MEMBERS LocalRef createCameraStateCallbackObject() { return LocalRef (getEnv()->NewObject (CameraDeviceStateCallback, CameraDeviceStateCallback.constructor, reinterpret_cast (this))); } //============================================================================== void cameraDeviceStateClosed() { JUCE_CAMERA_LOG ("cameraDeviceStateClosed()"); closedEvent.signal(); } void cameraDeviceStateDisconnected() { JUCE_CAMERA_LOG ("cameraDeviceStateDisconnected()"); if (pendingOpen.compareAndSetBool (0, 1)) { openError = "Device disconnected"; notifyOpenResult(); } MessageManager::callAsync ([this]() { close(); }); } void cameraDeviceStateError (int errorCode) { String error = cameraErrorCodeToString (errorCode); JUCE_CAMERA_LOG ("cameraDeviceStateError(), error: " + error); if (pendingOpen.compareAndSetBool (0, 1)) { openError = error; notifyOpenResult(); } fatalErrorOccurred.set (1); MessageManager::callAsync ([this, error]() { owner.cameraDeviceError (error); close(); }); } void cameraDeviceStateOpened (const LocalRef& cameraDeviceToUse) { JUCE_CAMERA_LOG ("cameraDeviceStateOpened()"); pendingOpen.set (0); cameraDevice = GlobalRef (cameraDeviceToUse); notifyOpenResult(); } void notifyOpenResult() { MessageManager::callAsync ([this]() { owner.cameraOpenFinished (openError); }); } //============================================================================== static void JNICALL cameraDeviceStateClosedCallback (JNIEnv*, jobject, jlong host, jobject) { if (auto* myself = reinterpret_cast(host)) myself->cameraDeviceStateClosed(); } static void JNICALL cameraDeviceStateDisconnectedCallback (JNIEnv*, jobject, jlong host, jobject) { if (auto* myself = reinterpret_cast(host)) myself->cameraDeviceStateDisconnected(); } static void JNICALL cameraDeviceStateErrorCallback (JNIEnv*, jobject, jlong host, jobject, jint error) { if (auto* myself = reinterpret_cast(host)) myself->cameraDeviceStateError (error); } static void JNICALL cameraDeviceStateOpenedCallback (JNIEnv*, jobject, jlong host, jobject rawCamera) { if (auto* myself = reinterpret_cast(host)) { LocalRef camera(getEnv()->NewLocalRef(rawCamera)); myself->cameraDeviceStateOpened (camera); } } }; //============================================================================== struct CaptureSessionModeBase { virtual ~CaptureSessionModeBase() = default; virtual bool isVideoRecordSession() const = 0; virtual void triggerStillPictureCapture() = 0; }; //============================================================================== template struct CaptureSessionMode : public CaptureSessionModeBase, private PreviewDisplay::Listener, private ScopedCameraDevice::CaptureSession::ConfiguredCallback { ~CaptureSessionMode() override { captureSession.reset(); previewDisplay.removeListener (this); } bool isVideoRecordSession() const override { return Mode::isVideoRecord(); } void triggerStillPictureCapture() override { if (captureSession == nullptr) { // The capture session must be ready before taking a still picture. // Did you remember to create and show a preview display? jassertfalse; return; } crtp().takeStillPicture(); } protected: CaptureSessionMode (Pimpl& ownerToUse, ScopedCameraDevice& cameraDeviceToUse, GlobalRef& handlerToUse, PreviewDisplay& pd, int cameraSensorOrientationToUse, int cameraLensFacingToUse, StreamConfigurationMap& streamConfigurationMapToUse) : owner (ownerToUse), scopedCameraDevice (cameraDeviceToUse), handler (handlerToUse), previewDisplay (pd), cameraSensorOrientation (cameraSensorOrientationToUse), cameraLensFacing (cameraLensFacingToUse), streamConfigurationMap (streamConfigurationMapToUse) { WeakReference> weakRef (this); if (weakRef == nullptr) return; // async so that the object is fully constructed before the callback gets invoked MessageManager::callAsync ([this, weakRef]() { if (weakRef == nullptr) return; previewDisplay.addListener (this); }); } Mode& crtp() { return static_cast (*this); } void previewDisplayReady() override { jassert (previewDisplay.isReady()); JUCE_CAMERA_LOG ("previewDisplayReady()"); // close previous capture session first captureSession.reset(); if (scopedCameraDevice.hasErrorOccurred()) { JUCE_CAMERA_LOG ("Device error detected, not recreating a new camera session. The device needs to be reopened."); return; } captureSession.reset (scopedCameraDevice.createCaptureSession (*this, crtp().getCaptureSessionSurfaces(), handler, Mode::getTemplate())); } void previewDisplayAboutToBeDestroyed() override { JUCE_CAMERA_LOG ("previewDisplayAboutToBeDestroyed()"); stopPreview(); } void captureSessionConfigured (ScopedCameraDevice::CaptureSession* session) override { if (session == nullptr) { owner.cameraDeviceError ("Failed to configure camera session."); return; } jassert (session == captureSession.get()); startSession(); } void startSession() { if (! captureSession->start (crtp().getTargetSurfaces(), handler)) { jassertfalse; JUCE_CAMERA_LOG ("Could not start capture session"); } crtp().sessionStarted(); } void stopPreview() { if (captureSession != nullptr) { auto session = captureSession->getNativeSession(); auto* env = getEnv(); env->CallVoidMethod (session, CameraCaptureSession.stopRepeating); if (jniCheckHasExceptionOccurredAndClear()) return; env->CallVoidMethod (session, CameraCaptureSession.abortCaptures); jniCheckHasExceptionOccurredAndClear(); } } Pimpl& owner; ScopedCameraDevice& scopedCameraDevice; GlobalRef& handler; PreviewDisplay& previewDisplay; int cameraSensorOrientation; int cameraLensFacing; StreamConfigurationMap& streamConfigurationMap; std::unique_ptr captureSession; JUCE_DECLARE_WEAK_REFERENCEABLE (CaptureSessionMode) }; //============================================================================== struct CaptureSessionPreviewMode : public CaptureSessionMode { CaptureSessionPreviewMode (Pimpl& ownerToUse, ScopedCameraDevice& cameraDeviceToUse, GlobalRef& handlerToUse, PreviewDisplay& pd, ImageReader& ir, int sensorOrientation, int cameraLensFacingToUse, StreamConfigurationMap& streamConfigurationMapToUse) : CaptureSessionMode (ownerToUse, cameraDeviceToUse, handlerToUse, pd, sensorOrientation, cameraLensFacingToUse, streamConfigurationMapToUse), imageReader (ir) { } // Surfaces passed to newly created capture session. LocalRef getCaptureSessionSurfaces() const { auto* env = getEnv(); auto previewSurface = LocalRef (previewDisplay.createSurface()); auto imageSurface = LocalRef (imageReader.getSurface()); auto arrayList = LocalRef (env->NewObject (JavaArrayList, JavaArrayList.constructor, 2)); env->CallBooleanMethod (arrayList, JavaArrayList.add, previewSurface.get()); env->CallBooleanMethod (arrayList, JavaArrayList.add, imageSurface.get()); auto supported = streamConfigurationMap.isOutputSupportedForSurface (imageSurface); // Output surface is not supported by this device, still image capture will not work! jassert (supported); return arrayList; } // Surfaces set as target during capture. LocalRef getTargetSurfaces() const { auto* env = getEnv(); auto previewSurface = LocalRef (previewDisplay.createSurface()); auto arrayList = LocalRef (env->NewObject (JavaArrayList, JavaArrayList.constructor, 1)); env->CallBooleanMethod (arrayList, JavaArrayList.add, previewSurface.get()); return arrayList; } static int getTemplate() { static constexpr int templatePreview = 1; return templatePreview; } static bool isVideoRecord() { return false; } void sessionStarted() {} void takeStillPicture() { imageReader.resetNotificationFlag(); captureSession->takeStillPicture (imageReader.getSurface()); } private: ImageReader& imageReader; }; //============================================================================== struct CaptureSessionVideoRecordingMode : public CaptureSessionMode { CaptureSessionVideoRecordingMode (Pimpl& ownerToUse, ScopedCameraDevice& cameraDeviceToUse, GlobalRef& handlerToUse, PreviewDisplay& pd, MediaRecorder& mr, int sensorOrientation, int cameraLensFacingToUse, StreamConfigurationMap& streamConfigurationMapToUse) : CaptureSessionMode (ownerToUse, cameraDeviceToUse, handlerToUse, pd, sensorOrientation, cameraLensFacingToUse, streamConfigurationMapToUse), mediaRecorder (mr) { } ~CaptureSessionVideoRecordingMode() { // We need to explicitly stop the preview before stopping the media recorder, // because legacy devices can't handle recording stop before stopping the preview. stopPreview(); mediaRecorder.stop(); } // Surfaces passed to newly created capture session. LocalRef getCaptureSessionSurfaces() const { auto* env = getEnv(); auto previewSurface = LocalRef (previewDisplay.createSurface()); auto mediaRecorderSurface = LocalRef (mediaRecorder.getSurface()); auto arrayList = LocalRef (env->NewObject (JavaArrayList, JavaArrayList.constructor, 2)); env->CallBooleanMethod (arrayList, JavaArrayList.add, previewSurface.get()); env->CallBooleanMethod (arrayList, JavaArrayList.add, mediaRecorderSurface.get()); return arrayList; } // Surfaces set as target during capture. LocalRef getTargetSurfaces() const { // Same surfaces used. return getCaptureSessionSurfaces(); } static int getTemplate() { static constexpr int templateRecord = 3; return templateRecord; } static bool isVideoRecord() { return true; } void sessionStarted() { MessageManager::callAsync ([this]() { mediaRecorder.start(); }); } void takeStillPicture() { // Taking still pictures while recording video is not supported on Android. jassertfalse; } private: MediaRecorder& mediaRecorder; }; //============================================================================== class DeviceOrientationChangeListener : private Timer { public: DeviceOrientationChangeListener (PreviewDisplay& pd) : previewDisplay (pd), orientationEventListener (createOrientationEventListener()), canDetectChange (getEnv()->CallBooleanMethod (orientationEventListener, OrientationEventListener.canDetectOrientation) != 0), deviceOrientation (Desktop::getInstance().getCurrentOrientation()), lastKnownScreenOrientation (deviceOrientation) { setEnabled (true); } ~DeviceOrientationChangeListener() override { setEnabled (false); } void setEnabled (bool shouldBeEnabled) { if (shouldBeEnabled && ! canDetectChange) { // This device does not support orientation listening, photos may have wrong orientation! jassertfalse; return; } if (shouldBeEnabled) getEnv()->CallVoidMethod (orientationEventListener, OrientationEventListener.enable); else getEnv()->CallVoidMethod (orientationEventListener, OrientationEventListener.disable); } bool isSupported() const noexcept { return canDetectChange; } Desktop::DisplayOrientation getDeviceOrientation() const noexcept { return deviceOrientation; } private: PreviewDisplay& previewDisplay; GlobalRef orientationEventListener; static constexpr jint sensorDelayUI = 2; bool canDetectChange; Desktop::DisplayOrientation deviceOrientation; Desktop::DisplayOrientation lastKnownScreenOrientation; int numChecksForOrientationChange = 10; //============================================================================== #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (canDetectOrientation, "canDetectOrientation", "()Z") \ METHOD (constructor, "", "(JLandroid/content/Context;I)V") \ METHOD (disable, "disable", "()V") \ METHOD (enable, "enable", "()V") \ CALLBACK (deviceOrientationChanged, "deviceOrientationChanged", "(JI)V") DECLARE_JNI_CLASS_WITH_MIN_SDK (OrientationEventListener, "com/rmsl/juce/JuceOrientationEventListener", 21) #undef JNI_CLASS_MEMBERS LocalRef createOrientationEventListener() { return LocalRef (getEnv()->NewObject (OrientationEventListener, OrientationEventListener.constructor, reinterpret_cast (this), getAppContext().get(), sensorDelayUI)); } //============================================================================== void orientationChanged (int orientation) { jassert (orientation < 360); // -1 == unknown if (orientation < 0) return; auto oldOrientation = deviceOrientation; // NB: this assumes natural position to be portrait always, but some devices may be landscape... if (orientation > (360 - 45) || orientation < 45) deviceOrientation = Desktop::upright; else if (orientation < 135) deviceOrientation = Desktop::rotatedClockwise; else if (orientation < 225) deviceOrientation = Desktop::upsideDown; else deviceOrientation = Desktop::rotatedAntiClockwise; if (oldOrientation != deviceOrientation) { lastKnownScreenOrientation = Desktop::getInstance().getCurrentOrientation(); // Need to update preview transform, but screen orientation will change slightly // later than sensor orientation. startTimer (500); } } void timerCallback() override { auto currentOrientation = Desktop::getInstance().getCurrentOrientation(); if (lastKnownScreenOrientation != currentOrientation) { lastKnownScreenOrientation = currentOrientation; stopTimer(); numChecksForOrientationChange = 10; previewDisplay.updateSurfaceTransform(); return; } if (--numChecksForOrientationChange == 0) { stopTimer(); numChecksForOrientationChange = 10; } } static void deviceOrientationChanged (JNIEnv*, jobject /*obj*/, jlong host, jint orientation) { if (auto* myself = reinterpret_cast (host)) myself->orientationChanged (orientation); } }; //============================================================================== CameraDevice& owner; int minWidth, minHeight, maxWidth, maxHeight; String cameraId; InternalOpenCameraResultCallback cameraOpenCallback; GlobalRef activityLifeListener; GlobalRef cameraManager; GlobalRef cameraCharacteristics; GlobalRef handlerThread; GlobalRef handler; StreamConfigurationMap streamConfigurationMap; PreviewDisplay previewDisplay; DeviceOrientationChangeListener deviceOrientationChangeListener; std::unique_ptr imageReader; std::unique_ptr mediaRecorder; std::unique_ptr currentCaptureSessionMode; std::unique_ptr scopedCameraDevice; CriticalSection listenerLock; ListenerList listeners; std::function pictureTakenCallback; Time firstRecordedFrameTimeMs; bool notifiedOfCameraOpening = false; bool appWasPaused = false; //============================================================================== int getCameraSensorOrientation() const { return getCameraCharacteristicsIntegerKeyValue (CameraCharacteristics.SENSOR_ORIENTATION); } int getAutoFocusModeToUse() const { auto supportedModes = getSupportedAutoFocusModes(); enum { CONTROL_AF_MODE_OFF = 0, CONTROL_AF_MODE_AUTO = 1, CONTROL_AF_MODE_CONTINUOUS_PICTURE = 4 }; if (supportedModes.contains (CONTROL_AF_MODE_CONTINUOUS_PICTURE)) return CONTROL_AF_MODE_CONTINUOUS_PICTURE; if (supportedModes.contains (CONTROL_AF_MODE_AUTO)) return CONTROL_AF_MODE_AUTO; return CONTROL_AF_MODE_OFF; } Array getSupportedAutoFocusModes() const { auto* env = getEnv(); auto jKey = LocalRef (env->GetStaticObjectField (CameraCharacteristics, CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)); auto supportedModes = LocalRef ((jintArray) env->CallObjectMethod (cameraCharacteristics, CameraCharacteristics.get, jKey.get())); return jintArrayToJuceArray (supportedModes); } static Array jintArrayToJuceArray (const LocalRef& jArray) { auto* env = getEnv(); auto* jArrayElems = env->GetIntArrayElements (jArray, nullptr); auto numElems = env->GetArrayLength (jArray); Array juceArray; for (int s = 0; s < numElems; ++s) juceArray.add (jArrayElems[s]); env->ReleaseIntArrayElements (jArray, jArrayElems, 0); return juceArray; } int getCameraCharacteristicsIntegerKeyValue (jfieldID key) const { auto* env = getEnv(); auto jKey = LocalRef (env->GetStaticObjectField (CameraCharacteristics, key)); auto jValue = LocalRef (env->CallObjectMethod (cameraCharacteristics, CameraCharacteristics.get, jKey.get())); return env->CallIntMethod (jValue, JavaInteger.intValue); } int getCameraLensFacing() const { return getCameraCharacteristicsIntegerKeyValue (CameraCharacteristics.LENS_FACING); } //============================================================================== void cameraOpenFinished (const String& error) { JUCE_CAMERA_LOG ("cameraOpenFinished(), error = " + error); if (error.isEmpty()) { setupStillImageSize(); startPreviewMode (*imageReader); } // Do not notify about camera being reopened on app resume. if (! notifiedOfCameraOpening) { notifiedOfCameraOpening = true; invokeCameraOpenCallback (error); } } void cameraDeviceError (const String& error) { if (owner.onErrorOccurred != nullptr) owner.onErrorOccurred (error); } void invokeCameraOpenCallback (const String& error) { JUCE_CAMERA_LOG ("invokeCameraOpenCallback(), error = " + error); if (cameraOpenCallback != nullptr) cameraOpenCallback (cameraId, error); } //============================================================================== void callListeners (const Image& image) { const ScopedLock sl (listenerLock); listeners.call ([=] (Listener& l) { l.imageReceived (image); }); } void notifyPictureTaken (const Image& image) { JUCE_CAMERA_LOG ("notifyPictureTaken()"); if (pictureTakenCallback != nullptr) pictureTakenCallback (image); } void triggerStillPictureCapture() { currentCaptureSessionMode->triggerStillPictureCapture(); } //============================================================================== void setupStillImageSize() { imageReader.reset(); auto imageSize = chooseBestSize (minWidth, minHeight, maxWidth, maxHeight, streamConfigurationMap.getSupportedStillImageOutputSizes()); imageReader.reset (new ImageReader (*this, handler, imageSize.getWidth(), imageSize.getHeight(), getCameraSensorOrientation())); } static Rectangle chooseBestSize (int minWidth, int minHeight, int maxWidth, int maxHeight, Array> supportedSizes) { Rectangle result; for (auto& size : supportedSizes) { auto width = size.getWidth(); auto height = size.getHeight(); if (width < minWidth || width > maxWidth || height < minHeight || height > maxHeight) continue; if (size.contains (result)) result = size; } // None of the supported sizes matches required width & height limitations, picking // the first one available... jassert (! result.isEmpty()); if (result.isEmpty()) result = supportedSizes[0]; return result; } //============================================================================== void startPreviewMode (ImageReader& ir) { if (currentCaptureSessionMode != nullptr && ! currentCaptureSessionMode->isVideoRecordSession()) return; // previous mode has to be stopped first jassert (currentCaptureSessionMode.get() == nullptr); if (scopedCameraDevice == nullptr || ! scopedCameraDevice->openedOk()) return; currentCaptureSessionMode.reset (new CaptureSessionPreviewMode (*this, *scopedCameraDevice, handler, previewDisplay, ir, getCameraSensorOrientation(), getCameraLensFacing(), streamConfigurationMap)); } void startVideoRecordingMode (MediaRecorder& mr) { if (currentCaptureSessionMode != nullptr && currentCaptureSessionMode->isVideoRecordSession()) return; // previous mode has to be stopped first jassert (currentCaptureSessionMode.get() == nullptr); jassert (scopedCameraDevice != nullptr && scopedCameraDevice->openedOk()); if (scopedCameraDevice == nullptr || ! scopedCameraDevice->openedOk()) return; currentCaptureSessionMode.reset (new CaptureSessionVideoRecordingMode (*this, *scopedCameraDevice, handler, previewDisplay, mr, getCameraSensorOrientation(), getCameraLensFacing(), streamConfigurationMap)); } //============================================================================== void onActivityPaused (jobject) override { JUCE_CAMERA_LOG ("appPaused, closing camera..."); appWasPaused = true; deviceOrientationChangeListener.setEnabled (false); // We need to restart the whole session mode when the app gets resumed. currentCaptureSessionMode.reset(); if (scopedCameraDevice != nullptr) scopedCameraDevice->close(); stopBackgroundThread(); } void onActivityResumed (jobject) override { // Only care about resumed event when paused event was called first. if (! appWasPaused) return; JUCE_CAMERA_LOG ("appResumed, opening camera..."); deviceOrientationChangeListener.setEnabled (true); startBackgroundThread(); if (scopedCameraDevice != nullptr) scopedCameraDevice->open(); } void startBackgroundThread() { auto* env = getEnv(); handlerThread = GlobalRef (LocalRef (env->NewObject (AndroidHandlerThread, AndroidHandlerThread.constructor, javaString ("JuceCameraDeviceBackgroundThread").get()))); // handler thread has to be started before its looper can be fetched env->CallVoidMethod (handlerThread, AndroidHandlerThread.start); handler = GlobalRef (LocalRef (env->NewObject (AndroidHandler, AndroidHandler.constructorWithLooper, env->CallObjectMethod (handlerThread, AndroidHandlerThread.getLooper)))); } void stopBackgroundThread() { auto* env = getEnv(); auto quitSafelyMethod = env->GetMethodID(AndroidHandlerThread, "quitSafely", "()Z"); // this code will only run on SDK >= 21 jassert(quitSafelyMethod != nullptr); env->CallBooleanMethod (handlerThread, quitSafelyMethod); env->CallVoidMethod (handlerThread, AndroidHandlerThread.join); jniCheckHasExceptionOccurredAndClear(); handlerThread.clear(); handler.clear(); } friend struct CameraDevice::ViewerComponent; JUCE_DECLARE_NON_COPYABLE (Pimpl) }; //============================================================================== struct CameraDevice::ViewerComponent : public Component, private ComponentMovementWatcher { ViewerComponent (CameraDevice& device) : ComponentMovementWatcher (this) { auto previewSize = device.pimpl->streamConfigurationMap.getDefaultPreviewSize(); targetAspectRatio = previewSize.getWidth() / (float) previewSize.getHeight(); if (isOrientationLandscape()) setBounds (previewSize); else setBounds (0, 0, previewSize.getHeight(), previewSize.getWidth()); addAndMakeVisible (viewerComponent); viewerComponent.setView (device.pimpl->previewDisplay.getNativeView()); } private: AndroidViewComponent viewerComponent; float targetAspectRatio = 1.0f; void componentMovedOrResized (bool, bool) override { auto b = getLocalBounds(); auto targetWidth = b.getWidth(); auto targetHeight = b.getHeight(); if (isOrientationLandscape()) { auto currentAspectRatio = b.getWidth() / (float) b.getHeight(); if (currentAspectRatio > targetAspectRatio) targetWidth = static_cast (targetWidth * targetAspectRatio / currentAspectRatio); else targetHeight = static_cast (targetHeight * currentAspectRatio / targetAspectRatio); } else { auto currentAspectRatio = b.getHeight() / (float) b.getWidth(); if (currentAspectRatio > targetAspectRatio) targetHeight = static_cast (targetHeight * targetAspectRatio / currentAspectRatio); else targetWidth = static_cast (targetWidth * currentAspectRatio / targetAspectRatio); } viewerComponent.setBounds (Rectangle (0, 0, targetWidth, targetHeight).withCentre (b.getCentre())); } bool isOrientationLandscape() const { auto o = Desktop::getInstance().getCurrentOrientation(); return o == Desktop::rotatedClockwise || o == Desktop::rotatedAntiClockwise; } void componentPeerChanged() override {} void componentVisibilityChanged() override {} JUCE_DECLARE_NON_COPYABLE (ViewerComponent) }; String CameraDevice::getFileExtension() { return ".mp4"; } //============================================================================== CameraDevice::Pimpl::ScopedCameraDevice::CaptureSession::StillPictureTaker::CameraCaptureSessionCaptureCallback_Class CameraDevice::Pimpl::ScopedCameraDevice::CaptureSession::StillPictureTaker::CameraCaptureSessionCaptureCallback; CameraDevice::Pimpl::ScopedCameraDevice::CameraDeviceStateCallback_Class CameraDevice::Pimpl::ScopedCameraDevice::CameraDeviceStateCallback; CameraDevice::Pimpl::ScopedCameraDevice::CaptureSession::CameraCaptureSessionStateCallback_Class CameraDevice::Pimpl::ScopedCameraDevice::CaptureSession::CameraCaptureSessionStateCallback; CameraDevice::Pimpl::DeviceOrientationChangeListener::OrientationEventListener_Class CameraDevice::Pimpl::DeviceOrientationChangeListener::OrientationEventListener;