/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2020 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 6 End-User License Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). End User License Agreement: www.juce.com/juce-6-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). 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; RuntimePermissions::request (RuntimePermissions::camera, [safeThis = WeakReference { this }] (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 ((float) height / (float) width), jfloat ((float) width / (float) height), (jfloat) 0, (jfloat) 0); env->CallBooleanMethod (matrix, AndroidMatrix.postRotate, (jfloat) 90 * ((float) 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); owner.callListeners (image); // Android may take multiple pictures before it handles a request to stop. if (hasNotifiedListeners.compareAndSetBool (1, 0)) MessageManager::callAsync ([safeThis = WeakReference { this }, 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); MessageManager::callAsync ([weakRef = WeakReference { this }] { if (weakRef != nullptr) weakRef->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); } MessageManager::callAsync ([weakRef = WeakReference { this }] { if (weakRef == nullptr) return; weakRef->stillPictureTaker.reset (new StillPictureTaker (weakRef->captureSession, weakRef->captureRequestBuilder, weakRef->previewCaptureRequest, weakRef->handler, weakRef->autoFocusMode)); weakRef->configuredCallback.captureSessionConfigured (weakRef.get()); }); } 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) { // async so that the object is fully constructed before the callback gets invoked MessageManager::callAsync ([weakRef = WeakReference> { this }] { if (weakRef != nullptr) weakRef->previewDisplay.addListener (weakRef.get()); }); } 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 = (float) 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().toFloat(); auto targetWidth = b.getWidth(); auto targetHeight = b.getHeight(); if (isOrientationLandscape()) { auto currentAspectRatio = b.getWidth() / b.getHeight(); if (currentAspectRatio > targetAspectRatio) targetWidth = targetWidth * targetAspectRatio / currentAspectRatio; else targetHeight = targetHeight * currentAspectRatio / targetAspectRatio; } else { auto currentAspectRatio = b.getHeight() / b.getWidth(); if (currentAspectRatio > targetAspectRatio) targetHeight = targetHeight * targetAspectRatio / currentAspectRatio; else targetWidth = targetWidth * currentAspectRatio / targetAspectRatio; } viewerComponent.setBounds (Rectangle (targetWidth, targetHeight).withCentre (b.getCentre()).toNearestInt()); } 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;