3410 lines
143 KiB
C
3410 lines
143 KiB
C
|
/*
|
||
|
==============================================================================
|
||
|
|
||
|
This file is part of the JUCE library.
|
||
|
Copyright (c) 2017 - ROLI Ltd.
|
||
|
|
||
|
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 5 End-User License
|
||
|
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||
|
27th April 2017).
|
||
|
|
||
|
End User License Agreement: www.juce.com/juce-5-licence
|
||
|
Privacy Policy: www.juce.com/juce-5-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.
|
||
|
|
||
|
==============================================================================
|
||
|
*/
|
||
|
|
||
|
#if __ANDROID_API__ >= 21
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
STATICMETHOD (valueOf, "valueOf", "(Ljava/lang/String;)Landroid/graphics/Bitmap$CompressFormat;")
|
||
|
|
||
|
DECLARE_JNI_CLASS (AndroidBitmapCompressFormat, "android/graphics/Bitmap$CompressFormat");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
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 (AndroidCameraDevice, "android/hardware/camera2/CameraDevice");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
METHOD (close, "close", "()V") \
|
||
|
METHOD (getPlanes, "getPlanes", "()[Landroid/media/Image$Plane;")
|
||
|
|
||
|
DECLARE_JNI_CLASS (AndroidImage, "android/media/Image");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
METHOD (getBuffer, "getBuffer", "()Ljava/nio/ByteBuffer;")
|
||
|
|
||
|
DECLARE_JNI_CLASS (AndroidImagePlane, "android/media/Image$Plane");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
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 (AndroidImageReader, "android/media/ImageReader");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
METHOD (constructor, "<init>", "()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 (AndroidMediaRecorder, "android/media/MediaRecorder");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
METHOD (constructor, "<init>", "(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 (AndroidTextureView, "android/view/TextureView");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
METHOD (constructor, "<init>", "(Landroid/graphics/SurfaceTexture;)V")
|
||
|
|
||
|
DECLARE_JNI_CLASS (AndroidSurface, "android/view/Surface");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
METHOD (setDefaultBufferSize, "setDefaultBufferSize", "(II)V")
|
||
|
|
||
|
DECLARE_JNI_CLASS (AndroidSurfaceTexture, "android/graphics/SurfaceTexture");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
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 (AndroidStreamConfigurationMap, "android/hardware/camera2/params/StreamConfigurationMap");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
METHOD (constructor, "<init>", "()V") \
|
||
|
METHOD (toByteArray, "toByteArray", "()[B") \
|
||
|
METHOD (size, "size", "()I")
|
||
|
|
||
|
DECLARE_JNI_CLASS (ByteArrayOutputStream, "java/io/ByteArrayOutputStream");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
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 (CameraCaptureSession, "android/hardware/camera2/CameraCaptureSession")
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
METHOD (constructor, "<init>", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH ";JZ)V")
|
||
|
|
||
|
DECLARE_JNI_CLASS (CameraCaptureSessionCaptureCallback, JUCE_ANDROID_ACTIVITY_CLASSPATH "$CameraCaptureSessionCaptureCallback");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
METHOD (constructor, "<init>", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH ";J)V")
|
||
|
|
||
|
DECLARE_JNI_CLASS (CameraCaptureSessionStateCallback, JUCE_ANDROID_ACTIVITY_CLASSPATH "$CameraCaptureSessionStateCallback");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
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 (CameraCharacteristics, "android/hardware/camera2/CameraCharacteristics");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
METHOD (getName, "getName", "()Ljava/lang/String;")
|
||
|
|
||
|
DECLARE_JNI_CLASS (CameraCharacteristicsKey, "android/hardware/camera2/CameraCharacteristics$Key");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
METHOD (constructor, "<init>", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH ";J)V")
|
||
|
|
||
|
DECLARE_JNI_CLASS (CameraDeviceStateCallback, JUCE_ANDROID_ACTIVITY_CLASSPATH "$CameraDeviceStateCallback");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
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 (CameraManager, "android/hardware/camera2/CameraManager");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
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 (CaptureRequest, "android/hardware/camera2/CaptureRequest");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
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 (CaptureRequestBuilder, "android/hardware/camera2/CaptureRequest$Builder");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
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 (CaptureResult, "android/hardware/camera2/CaptureResult");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
|
||
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||
|
METHOD (canDetectOrientation, "canDetectOrientation", "()Z") \
|
||
|
METHOD (constructor, "<init>", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH ";JLandroid/content/Context;I)V") \
|
||
|
METHOD (disable, "disable", "()V") \
|
||
|
METHOD (enable, "enable", "()V")
|
||
|
|
||
|
DECLARE_JNI_CLASS (OrientationEventListener, JUCE_ANDROID_ACTIVITY_CLASSPATH "$JuceOrientationEventListener");
|
||
|
#undef JNI_CLASS_MEMBERS
|
||
|
#endif
|
||
|
|
||
|
//==============================================================================
|
||
|
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<jobject>& surface, int width, int height) = 0;
|
||
|
virtual bool onSurfaceTextureDestroyed (LocalRef<jobject>& surface) = 0;
|
||
|
virtual void onSurfaceTextureSizeChanged (LocalRef<jobject>& surface, int width, int height) = 0;
|
||
|
virtual void onSurfaceTextureUpdated (LocalRef<jobject>& 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<jobject> (env->GetObjectArrayElement (args, 0));
|
||
|
auto width = LocalRef<jobject> (env->GetObjectArrayElement (args, 1));
|
||
|
auto height = LocalRef<jobject> (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<jobject> (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<jobject> (env->GetObjectArrayElement (args, 0));
|
||
|
auto width = LocalRef<jobject> (env->GetObjectArrayElement (args, 1));
|
||
|
auto height = LocalRef<jobject> (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<jobject> (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<jobject>& 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<jobject> (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<jobject>& 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<jobject> (env->GetObjectArrayElement (args, 0));
|
||
|
auto what = LocalRef<jobject> (env->GetObjectArrayElement (args, 1));
|
||
|
auto extra = LocalRef<jobject> (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<jobject>& 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<jobject> (env->GetObjectArrayElement (args, 0));
|
||
|
auto what = LocalRef<jobject> (env->GetObjectArrayElement (args, 1));
|
||
|
auto extra = LocalRef<jobject> (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;
|
||
|
};
|
||
|
|
||
|
//==============================================================================
|
||
|
class AppPausedResumedListener : public AndroidInterfaceImplementer
|
||
|
{
|
||
|
public:
|
||
|
struct Owner
|
||
|
{
|
||
|
virtual ~Owner() {}
|
||
|
|
||
|
virtual void appPaused() = 0;
|
||
|
virtual void appResumed() = 0;
|
||
|
};
|
||
|
|
||
|
AppPausedResumedListener (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 == "appPaused" && numArgs == 0)
|
||
|
{
|
||
|
owner.appPaused();
|
||
|
return nullptr;
|
||
|
}
|
||
|
|
||
|
if (methodName == "appResumed" && numArgs == 0)
|
||
|
{
|
||
|
owner.appResumed();
|
||
|
return nullptr;
|
||
|
}
|
||
|
|
||
|
return AndroidInterfaceImplementer::invoke (proxy, method, args);
|
||
|
}
|
||
|
|
||
|
private:
|
||
|
Owner& owner;
|
||
|
};
|
||
|
|
||
|
//==============================================================================
|
||
|
struct CameraDevice::Pimpl
|
||
|
#if __ANDROID_API__ >= 21
|
||
|
: private AppPausedResumedListener::Owner
|
||
|
#endif
|
||
|
{
|
||
|
using InternalOpenCameraResultCallback = std::function<void (const String& /*cameraId*/, const String& /*error*/)>;
|
||
|
|
||
|
Pimpl (CameraDevice& ownerToUse, const String& cameraIdToUse, int /*index*/,
|
||
|
int minWidthToUse, int minHeightToUse, int maxWidthToUse, int maxHeightToUse,
|
||
|
bool /*useHighQuality*/)
|
||
|
#if __ANDROID_API__ >= 21
|
||
|
: owner (ownerToUse),
|
||
|
minWidth (minWidthToUse),
|
||
|
minHeight (minHeightToUse),
|
||
|
maxWidth (maxWidthToUse),
|
||
|
maxHeight (maxHeightToUse),
|
||
|
cameraId (cameraIdToUse),
|
||
|
appPausedResumedListener (*this),
|
||
|
appPausedResumedListenerNative (CreateJavaInterface (&appPausedResumedListener,
|
||
|
JUCE_ANDROID_ACTIVITY_CLASSPATH "$AppPausedResumedListener").get()),
|
||
|
|
||
|
cameraManager (initialiseCameraManager()),
|
||
|
cameraCharacteristics (initialiseCameraCharacteristics (cameraManager, cameraId)),
|
||
|
streamConfigurationMap (cameraCharacteristics),
|
||
|
previewDisplay (streamConfigurationMap.getPreviewBufferSize()),
|
||
|
deviceOrientationChangeListener (previewDisplay)
|
||
|
#endif
|
||
|
{
|
||
|
#if __ANDROID_API__ >= 21
|
||
|
startBackgroundThread();
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
~Pimpl()
|
||
|
{
|
||
|
#if __ANDROID_API__ >= 21
|
||
|
getEnv()->CallVoidMethod (android.activity, JuceAppActivity.removeAppPausedResumedListener,
|
||
|
appPausedResumedListenerNative.get(), reinterpret_cast<jlong>(this));
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
#if __ANDROID_API__ < 21
|
||
|
// Dummy implementations for unsupported API levels.
|
||
|
void open (InternalOpenCameraResultCallback) {}
|
||
|
void takeStillPicture (std::function<void (const Image&)>) {}
|
||
|
void startRecordingToFile (const File&, int) {}
|
||
|
void stopRecording() {}
|
||
|
|
||
|
void addListener (CameraDevice::Listener*) {}
|
||
|
void removeListener (CameraDevice::Listener*) {}
|
||
|
|
||
|
String getCameraId() const noexcept { return {}; }
|
||
|
bool openedOk() const noexcept { return false; }
|
||
|
Time getTimeOfFirstRecordedFrame() const { return {}; }
|
||
|
static StringArray getAvailableDevices()
|
||
|
{
|
||
|
// Camera on Android requires API 21 or above.
|
||
|
jassertfalse;
|
||
|
return {};
|
||
|
}
|
||
|
#else
|
||
|
JUCE_DECLARE_WEAK_REFERENCEABLE (Pimpl)
|
||
|
|
||
|
String getCameraId() const noexcept { return cameraId; }
|
||
|
|
||
|
void open (InternalOpenCameraResultCallback cameraOpenCallbackToUse)
|
||
|
{
|
||
|
cameraOpenCallback = static_cast<InternalOpenCameraResultCallback&&> (cameraOpenCallbackToUse);
|
||
|
|
||
|
// A valid camera open callback must be passed.
|
||
|
jassert (cameraOpenCallback != nullptr);
|
||
|
|
||
|
// The same camera can be opened only once!
|
||
|
jassert (scopedCameraDevice == nullptr);
|
||
|
|
||
|
if (cameraOpenCallback == nullptr || scopedCameraDevice != nullptr)
|
||
|
return;
|
||
|
|
||
|
WeakReference<Pimpl> safeThis (this);
|
||
|
RuntimePermissions::request (RuntimePermissions::camera, [safeThis] (bool granted) mutable
|
||
|
{
|
||
|
if (safeThis != nullptr)
|
||
|
safeThis->continueOpenRequest (granted);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
void continueOpenRequest (bool granted)
|
||
|
{
|
||
|
if (granted)
|
||
|
{
|
||
|
getEnv()->CallVoidMethod (android.activity, JuceAppActivity.addAppPausedResumedListener,
|
||
|
appPausedResumedListenerNative.get(), reinterpret_cast<jlong> (this));
|
||
|
scopedCameraDevice.reset (new ScopedCameraDevice (*this, cameraId, cameraManager, handler, getAutoFocusModeToUse()));
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
invokeCameraOpenCallback ("Camera permission not granted");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bool openedOk() const noexcept { return scopedCameraDevice->openedOk(); }
|
||
|
|
||
|
void takeStillPicture (std::function<void (const Image&)> pictureTakenCallbackToUse)
|
||
|
{
|
||
|
if (pictureTakenCallbackToUse == nullptr)
|
||
|
{
|
||
|
jassertfalse;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (currentCaptureSessionMode->isVideoRecordSession())
|
||
|
{
|
||
|
// Taking still pictures while recording video is not supported on Android.
|
||
|
jassertfalse;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
pictureTakenCallback = static_cast<std::function<void (const Image&)>&&> (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()
|
||
|
{
|
||
|
StringArray results;
|
||
|
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
auto cameraManagerToUse = initialiseCameraManager();
|
||
|
auto cameraIdArray = LocalRef<jobjectArray> ((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<jobject> initialiseCameraManager()
|
||
|
{
|
||
|
return LocalRef<jobject> (getEnv()->CallObjectMethod (android.activity, JuceAppActivity.getSystemService,
|
||
|
javaString ("camera").get()));
|
||
|
}
|
||
|
|
||
|
static LocalRef<jobject> initialiseCameraCharacteristics (const GlobalRef& cameraManager, const String& cameraId)
|
||
|
{
|
||
|
return LocalRef<jobject> (getEnv()->CallObjectMethod (cameraManager,
|
||
|
CameraManager.getCameraCharacteristics,
|
||
|
javaString (cameraId).get()));
|
||
|
}
|
||
|
|
||
|
static void printDebugCameraInfo (const LocalRef<jobject>& cameraManagerToUse, const String& cameraId)
|
||
|
{
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
auto characteristics = LocalRef<jobject> (env->CallObjectMethod (cameraManagerToUse,
|
||
|
CameraManager.getCameraCharacteristics,
|
||
|
javaString (cameraId).get()));
|
||
|
|
||
|
auto keysList = LocalRef<jobject> (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<jobject> (env->CallObjectMethod (keysList, JavaList.get, i));
|
||
|
auto jKeyName = LocalRef<jstring> ((jstring) env->CallObjectMethod (key, CameraCharacteristicsKey.getName));
|
||
|
auto keyName = juceString (jKeyName);
|
||
|
|
||
|
auto keyValue = LocalRef<jobject> (env->CallObjectMethod (characteristics, CameraCharacteristics.get, key.get()));
|
||
|
auto jKeyValueString = LocalRef<jstring> ((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 i = 0, j = 1; i < keyValueString.length(); i += chunkSize, ++j)
|
||
|
JUCE_CAMERA_LOG ("value part " + String (j) + ": " + keyValueString.substring (i, i + chunkSize));
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
JUCE_CAMERA_LOG ("Key: " + keyName + ", value: " + keyValueString);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
ignoreUnused (keyName);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static void printPrimitiveArrayElements (const LocalRef<jobject>& 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(), 0); \
|
||
|
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<jobject>& rangeArray, const String& keyName)
|
||
|
{
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
jobjectArray ranges = static_cast<jobjectArray> (rangeArray.get());
|
||
|
|
||
|
int numRanges = env->GetArrayLength (ranges);
|
||
|
|
||
|
String result;
|
||
|
|
||
|
for (int i = 0; i < numRanges; ++i)
|
||
|
{
|
||
|
auto range = LocalRef<jobject> (env->GetObjectArrayElement (ranges, i));
|
||
|
|
||
|
auto jRangeString = LocalRef<jstring> ((jstring) env->CallObjectMethod (range, AndroidRange.toString));
|
||
|
|
||
|
result << juceString (jRangeString) << " ";
|
||
|
}
|
||
|
|
||
|
JUCE_CAMERA_LOG ("Key: " + keyName + ", value: " + result);
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
class StreamConfigurationMap
|
||
|
{
|
||
|
public:
|
||
|
StreamConfigurationMap (const GlobalRef& cameraCharacteristics)
|
||
|
: scalerStreamConfigurationMap (getStreamConfigurationMap (cameraCharacteristics)),
|
||
|
supportedPreviewOutputSizes (retrieveOutputSizes (scalerStreamConfigurationMap,
|
||
|
getClassForName ("android.graphics.SurfaceTexture"),
|
||
|
-1)),
|
||
|
supportedStillImageOutputSizes (retrieveOutputSizes (scalerStreamConfigurationMap,
|
||
|
LocalRef<jobject>(),
|
||
|
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<Rectangle<int>> getSupportedPreviewOutputSizes() const noexcept { return supportedPreviewOutputSizes; }
|
||
|
Array<Rectangle<int>> getSupportedStillImageOutputSizes() const noexcept { return supportedStillImageOutputSizes; }
|
||
|
Array<Rectangle<int>> getSupportedVideoRecordingOutputSizes() const noexcept { return supportedVideoRecordingOutputSizes; }
|
||
|
|
||
|
Rectangle<int> getDefaultPreviewSize() const noexcept { return defaultPreviewSize; }
|
||
|
Rectangle<int> getPreviewBufferSize() const noexcept { return previewBufferSize; }
|
||
|
|
||
|
bool isOutputSupportedForSurface (const LocalRef<jobject>& surface) const
|
||
|
{
|
||
|
return getEnv()->CallBooleanMethod (scalerStreamConfigurationMap, AndroidStreamConfigurationMap.isOutputSupportedForSurface, surface.get());
|
||
|
}
|
||
|
|
||
|
static constexpr int jpegImageFormat = 256;
|
||
|
|
||
|
private:
|
||
|
GlobalRef scalerStreamConfigurationMap;
|
||
|
|
||
|
Array<Rectangle<int>> supportedPreviewOutputSizes;
|
||
|
Array<Rectangle<int>> supportedStillImageOutputSizes;
|
||
|
Array<Rectangle<int>> supportedVideoRecordingOutputSizes;
|
||
|
Rectangle<int> defaultPreviewSize, previewBufferSize;
|
||
|
|
||
|
GlobalRef getStreamConfigurationMap (const GlobalRef& cameraCharacteristics)
|
||
|
{
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
auto scalerStreamConfigurationMapKey = LocalRef<jobject> (env->GetStaticObjectField (CameraCharacteristics,
|
||
|
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP));
|
||
|
|
||
|
return GlobalRef (LocalRef<jobject> (env->CallObjectMethod (cameraCharacteristics,
|
||
|
CameraCharacteristics.get,
|
||
|
scalerStreamConfigurationMapKey.get())));
|
||
|
}
|
||
|
|
||
|
static Array<Rectangle<int>> retrieveOutputSizes (GlobalRef& scalerStreamConfigurationMap,
|
||
|
const LocalRef<jobject>& outputClass,
|
||
|
int format)
|
||
|
{
|
||
|
Array<Rectangle<int>> result;
|
||
|
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
auto outputSizes = outputClass.get() != nullptr
|
||
|
? LocalRef<jobjectArray> ((jobjectArray) env->CallObjectMethod (scalerStreamConfigurationMap,
|
||
|
AndroidStreamConfigurationMap.getOutputSizesForClass,
|
||
|
outputClass.get()))
|
||
|
: LocalRef<jobjectArray> ((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<jobject> (env->GetObjectArrayElement (outputSizes, i));
|
||
|
|
||
|
auto width = env->CallIntMethod (size, AndroidSize.getWidth);
|
||
|
auto height = env->CallIntMethod (size, AndroidSize.getHeight);
|
||
|
|
||
|
result.add (Rectangle<int> (0, 0, width, height));
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
static LocalRef<jobject> getClassForName (const String& name)
|
||
|
{
|
||
|
return LocalRef<jobject> (getEnv()->CallStaticObjectMethod (JavaClass, JavaClass.forName,
|
||
|
javaString (name).get()));
|
||
|
}
|
||
|
|
||
|
static void printSizesLog (const Array<Rectangle<int>>& 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<int> getSmallestSize (const Array<Rectangle<int>>& 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<int> getLargestSize (const Array<Rectangle<int>>& 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<int> bufferSize)
|
||
|
: textureViewSurfaceTextureListener (*this),
|
||
|
textureView (getEnv()->NewObject (AndroidTextureView, AndroidTextureView.constructor,
|
||
|
android.activity.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()
|
||
|
{
|
||
|
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<jobject> createSurface()
|
||
|
{
|
||
|
// Surface may get destroyed while session is being configured, if
|
||
|
// the preview gets hidden in the meantime, so bailout.
|
||
|
if (! isReady())
|
||
|
return LocalRef<jobject> (nullptr);
|
||
|
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
auto surfaceTexture = LocalRef<jobject> (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<jobject> (env->NewObject (AndroidSurface, AndroidSurface.constructor, surfaceTexture.get()));
|
||
|
|
||
|
return surface;
|
||
|
}
|
||
|
|
||
|
const GlobalRef& getNativeView() { return textureView; }
|
||
|
|
||
|
void updateSurfaceTransform()
|
||
|
{
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
auto windowManager = LocalRef<jobject> (env->CallObjectMethod (android.activity, JuceAppActivity.getWindowManager));
|
||
|
auto display = LocalRef<jobject> (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<jobject> (env->NewObject (AndroidMatrix, AndroidMatrix.constructor));
|
||
|
|
||
|
if (rotation == rotation90 || rotation == rotation270)
|
||
|
{
|
||
|
env->CallBooleanMethod (matrix, AndroidMatrix.postScale, jfloat (height / (float) width), jfloat (width / (float) height), (jfloat) 0, (jfloat) 0);
|
||
|
env->CallBooleanMethod (matrix, AndroidMatrix.postRotate, (jfloat) 90 * (rotation - 2), (jfloat) 0, (jfloat) 0);
|
||
|
env->CallBooleanMethod (matrix, AndroidMatrix.postTranslate, (jfloat) (rotation == 3 ? width : 0), (jfloat) (rotation == 1 ? height : 0));
|
||
|
}
|
||
|
|
||
|
env->CallVoidMethod (textureView, AndroidTextureView.setTransform, matrix.get());
|
||
|
}
|
||
|
|
||
|
private:
|
||
|
ListenerList<Listener> listeners;
|
||
|
|
||
|
TextureViewSurfaceTextureListener textureViewSurfaceTextureListener;
|
||
|
GlobalRef textureView;
|
||
|
int width = -1, height = -1;
|
||
|
int bufferWidth, bufferHeight;
|
||
|
|
||
|
void onSurfaceTextureAvailable (LocalRef<jobject>& /*surface*/, int widthToUse, int heightToUse) override
|
||
|
{
|
||
|
JUCE_CAMERA_LOG ("onSurfaceTextureAvailable()");
|
||
|
|
||
|
width = widthToUse;
|
||
|
height = heightToUse;
|
||
|
|
||
|
updateSurfaceTransform();
|
||
|
|
||
|
listeners.call (&Listener::previewDisplayReady);
|
||
|
}
|
||
|
|
||
|
bool onSurfaceTextureDestroyed (LocalRef<jobject>& /*surface*/) override
|
||
|
{
|
||
|
JUCE_CAMERA_LOG ("onSurfaceTextureDestroyed()");
|
||
|
|
||
|
listeners.call (&Listener::previewDisplayAboutToBeDestroyed);
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
void onSurfaceTextureSizeChanged (LocalRef<jobject>& /*surface*/, int widthToUse, int heightToUse) override
|
||
|
{
|
||
|
JUCE_CAMERA_LOG ("onSurfaceTextureSizeChanged()");
|
||
|
|
||
|
width = widthToUse;
|
||
|
height = heightToUse;
|
||
|
|
||
|
updateSurfaceTransform();
|
||
|
}
|
||
|
|
||
|
void onSurfaceTextureUpdated (LocalRef<jobject>& /*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 (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()
|
||
|
{
|
||
|
getEnv()->CallVoidMethod (imageReader, AndroidImageReader.close);
|
||
|
}
|
||
|
|
||
|
LocalRef<jobject> getSurface() const
|
||
|
{
|
||
|
return LocalRef<jobject> (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<int> hasNotifiedListeners { 0 };
|
||
|
|
||
|
JUCE_DECLARE_WEAK_REFERENCEABLE (ImageReader)
|
||
|
|
||
|
void onImageAvailable (LocalRef<jobject>& /*imageReader*/) override
|
||
|
{
|
||
|
JUCE_CAMERA_LOG ("onImageAvailable()");
|
||
|
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
auto jImage = LocalRef<jobject> (env->CallObjectMethod (imageReader, AndroidImageReader.acquireLatestImage));
|
||
|
|
||
|
if (jImage.get() == nullptr)
|
||
|
return;
|
||
|
|
||
|
auto cameraLensFrontFacing = owner.getCameraLensFacing() == 0;
|
||
|
|
||
|
// NB: could use sensor orientation here to get real-world orientation, but then the resulting
|
||
|
// image could not match the UI orientation.
|
||
|
auto image = androidImageToJuceWithFixedOrientation (jImage, owner.deviceOrientationChangeListener.getDeviceOrientation(),
|
||
|
Desktop::getInstance().getCurrentOrientation(),
|
||
|
cameraLensFrontFacing,
|
||
|
cameraSensorOrientation);
|
||
|
|
||
|
env->CallVoidMethod (jImage, AndroidImage.close);
|
||
|
|
||
|
WeakReference<ImageReader> safeThis (this);
|
||
|
|
||
|
owner.callListeners (image);
|
||
|
|
||
|
// Android may take multiple pictures before it handles a request to stop.
|
||
|
if (hasNotifiedListeners.compareAndSetBool (1, 0))
|
||
|
MessageManager::callAsync ([safeThis, image]() mutable { if (safeThis != nullptr) safeThis->owner.notifyPictureTaken (image); });
|
||
|
}
|
||
|
|
||
|
struct ImageBuffer
|
||
|
{
|
||
|
LocalRef<jbyteArray> byteArray;
|
||
|
int size;
|
||
|
};
|
||
|
|
||
|
static Image androidImageToJuceWithFixedOrientation (const LocalRef<jobject>& androidImage,
|
||
|
Desktop::DisplayOrientation deviceOrientationFromAccelerometerSensor,
|
||
|
Desktop::DisplayOrientation targetOrientation,
|
||
|
bool cameraLensFrontFacing,
|
||
|
int cameraSensorOrientation)
|
||
|
{
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
auto planes = LocalRef<jobjectArray> ((jobjectArray) env->CallObjectMethod (androidImage, AndroidImage.getPlanes));
|
||
|
jassert (env->GetArrayLength (planes) > 0);
|
||
|
|
||
|
auto plane = LocalRef<jobject> (env->GetObjectArrayElement (planes, 0));
|
||
|
auto byteBuffer = LocalRef<jobject> (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<jobject>& imagePlaneBuffer,
|
||
|
Desktop::DisplayOrientation deviceOrientationFromAccelerometerSensor,
|
||
|
Desktop::DisplayOrientation targetOrientation,
|
||
|
bool cameraLensFrontFacing,
|
||
|
int cameraSensorOrientation)
|
||
|
{
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
auto bufferSize = env->CallIntMethod (imagePlaneBuffer, JavaByteBuffer.remaining);
|
||
|
auto byteArray = LocalRef<jbyteArray> (env->NewByteArray (bufferSize));
|
||
|
env->CallObjectMethod (imagePlaneBuffer, JavaByteBuffer.get, byteArray.get());
|
||
|
|
||
|
auto orientationsEnabled = Desktop::getInstance().getOrientationsEnabled() & ~Desktop::upsideDown;
|
||
|
|
||
|
auto rotationAngle = getRotationAngle (deviceOrientationFromAccelerometerSensor, targetOrientation,
|
||
|
cameraLensFrontFacing, cameraSensorOrientation);
|
||
|
|
||
|
if (rotationAngle == 0)
|
||
|
{
|
||
|
// Nothing to do, just get the bytes
|
||
|
return { byteArray, bufferSize };
|
||
|
}
|
||
|
|
||
|
auto origBitmap = LocalRef<jobject> (env->CallStaticObjectMethod (AndroidBitmapFactory,
|
||
|
AndroidBitmapFactory.decodeByteArray,
|
||
|
byteArray.get(), (jint) 0, (jint) bufferSize));
|
||
|
|
||
|
auto correctedBitmap = getBitmapWithCorrectOrientationFrom (origBitmap, rotationAngle);
|
||
|
|
||
|
auto byteArrayOutputStream = LocalRef<jobject> (env->NewObject (ByteArrayOutputStream,
|
||
|
ByteArrayOutputStream.constructor));
|
||
|
|
||
|
auto jCompressFormatString = javaString ("JPEG");
|
||
|
auto compressFormat = LocalRef<jobject> (env->CallStaticObjectMethod (AndroidBitmapCompressFormat,
|
||
|
AndroidBitmapCompressFormat.valueOf,
|
||
|
jCompressFormatString.get()));
|
||
|
|
||
|
if (env->CallBooleanMethod (correctedBitmap, AndroidBitmap.compress, compressFormat.get(),
|
||
|
(jint) 100, byteArrayOutputStream.get()) != 0)
|
||
|
{
|
||
|
auto correctedByteArray = LocalRef<jbyteArray> ((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 orientationsEnabled = Desktop::getInstance().getOrientationsEnabled() & ~Desktop::upsideDown;
|
||
|
|
||
|
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;
|
||
|
default:
|
||
|
return 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static LocalRef<jobject> getBitmapWithCorrectOrientationFrom (LocalRef<jobject>& origBitmap, int rotationAngle)
|
||
|
{
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
auto origBitmapWidth = env->CallIntMethod (origBitmap, AndroidBitmap.getWidth);
|
||
|
auto origBitmapHeight = env->CallIntMethod (origBitmap, AndroidBitmap.getHeight);
|
||
|
|
||
|
auto orientationsEnabled = Desktop::getInstance().getOrientationsEnabled() & ~Desktop::upsideDown;
|
||
|
|
||
|
auto matrix = LocalRef<jobject> (env->NewObject (AndroidMatrix, AndroidMatrix.constructor));
|
||
|
env->CallBooleanMethod (matrix, AndroidMatrix.postRotate, (jfloat) rotationAngle, (jfloat) 0, (jfloat) 0);
|
||
|
|
||
|
auto rotatedBitmap = LocalRef<jobject> (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<jobject> (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()
|
||
|
{
|
||
|
getEnv()->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.release);
|
||
|
}
|
||
|
|
||
|
LocalRef<jobject> getSurface()
|
||
|
{
|
||
|
return LocalRef<jobject> (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.
|
||
|
auto exception = LocalRef<jobject> (env->ExceptionOccurred());
|
||
|
|
||
|
if (exception != 0)
|
||
|
env->ExceptionClear();
|
||
|
|
||
|
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<jobject>& recorder, int what, int extra) override
|
||
|
{
|
||
|
ignoreUnused (recorder, what, extra);
|
||
|
|
||
|
JUCE_CAMERA_LOG ("MediaRecorder::OnInfo: " + getInfoStringFromCode (what)
|
||
|
+ ", extra code = " + String (extra));
|
||
|
}
|
||
|
|
||
|
void onError (LocalRef<jobject>& 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<jobject> (env->CallObjectMethod (android.activity, JuceAppActivity.getWindowManager));
|
||
|
auto display = LocalRef<jobject> (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);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
auto exception = LocalRef<jobject> (env->ExceptionOccurred());
|
||
|
|
||
|
// 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 catched internally in Java...
|
||
|
if (exception != 0 || scopedCameraDevice.fatalErrorOccurred.get())
|
||
|
{
|
||
|
JUCE_CAMERA_LOG ("Exception or fatal error occurred while closing Capture Session, closing by force");
|
||
|
|
||
|
env->ExceptionClear();
|
||
|
}
|
||
|
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<jobject>& 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<jobject> (env->CallObjectMethod (targetSurfacesList, JavaArrayList.get, (jint) i));
|
||
|
env->CallVoidMethod (captureRequestBuilder, CaptureRequestBuilder.addTarget, surface.get());
|
||
|
}
|
||
|
|
||
|
previewCaptureRequest = GlobalRef (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<jobject> (env->CallObjectMethod (scopedCameraDevice.cameraDevice,
|
||
|
AndroidCameraDevice.createCaptureRequest,
|
||
|
(jint) templateStillCapture));
|
||
|
|
||
|
env->CallVoidMethod (builder, CaptureRequestBuilder.addTarget, targetSurface);
|
||
|
|
||
|
setCaptureRequestBuilderIntegerKey (builder.get(), CaptureRequest.CONTROL_AF_MODE, autoFocusMode);
|
||
|
|
||
|
auto stillPictureCaptureRequest = LocalRef<jobject> (env->CallObjectMethod (builder, CaptureRequestBuilder.build));
|
||
|
|
||
|
stillPictureTaker->takePicture (stillPictureCaptureRequest.get());
|
||
|
}
|
||
|
|
||
|
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 (LocalRef<jobject> (getEnv()->NewObject (CameraCaptureSessionCaptureCallback,
|
||
|
CameraCaptureSessionCaptureCallback.constructor,
|
||
|
android.activity.get(),
|
||
|
reinterpret_cast<jlong> (this),
|
||
|
true))),
|
||
|
captureSessionStillPictureCaptureCallback (LocalRef<jobject> (getEnv()->NewObject (CameraCaptureSessionCaptureCallback,
|
||
|
CameraCaptureSessionCaptureCallback.constructor,
|
||
|
android.activity.get(),
|
||
|
reinterpret_cast<jlong> (this),
|
||
|
false))),
|
||
|
autoFocusMode (autoFocusModeToUse)
|
||
|
{
|
||
|
}
|
||
|
|
||
|
void takePicture (jobject stillPictureCaptureRequestToUse)
|
||
|
{
|
||
|
JUCE_CAMERA_LOG ("Taking picture...");
|
||
|
|
||
|
stillPictureCaptureRequest = GlobalRef (stillPictureCaptureRequestToUse);
|
||
|
|
||
|
lockFocus();
|
||
|
}
|
||
|
|
||
|
private:
|
||
|
GlobalRef& captureSession;
|
||
|
GlobalRef& captureRequestBuilder;
|
||
|
GlobalRef& previewCaptureRequest;
|
||
|
GlobalRef& handler;
|
||
|
|
||
|
AndroidRunnable runnable;
|
||
|
GlobalRef delayedCaptureRunnable;
|
||
|
|
||
|
GlobalRef captureSessionPreviewCaptureCallback;
|
||
|
|
||
|
GlobalRef stillPictureCaptureRequest;
|
||
|
GlobalRef captureSessionStillPictureCaptureCallback;
|
||
|
|
||
|
int autoFocusMode;
|
||
|
|
||
|
enum class State
|
||
|
{
|
||
|
idle = 0,
|
||
|
pendingFocusLock,
|
||
|
pendingExposurePrecapture,
|
||
|
pendingExposurePostPrecapture,
|
||
|
pictureTaken
|
||
|
};
|
||
|
|
||
|
State currentState = State::idle;
|
||
|
|
||
|
void lockFocus()
|
||
|
{
|
||
|
if (Pimpl::checkHasExceptionOccurred())
|
||
|
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<jobject> (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 (Pimpl::checkHasExceptionOccurred())
|
||
|
return;
|
||
|
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
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<jobject>& 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<jobject> getCaptureResultIntegerKeyValue (jfieldID key, jobject captureResult)
|
||
|
{
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
auto jKey = LocalRef<jobject> (env->GetStaticObjectField (CaptureResult, key));
|
||
|
return LocalRef<jobject> (env->CallObjectMethod (captureResult, CaptureResult.get, jKey.get()));
|
||
|
}
|
||
|
|
||
|
void captureStillPictureDelayed()
|
||
|
{
|
||
|
if (Pimpl::checkHasExceptionOccurred())
|
||
|
return;
|
||
|
|
||
|
JUCE_CAMERA_LOG ("Still picture capture, device ready, capturing now...");
|
||
|
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
env->CallVoidMethod (captureSession, CameraCaptureSession.stopRepeating);
|
||
|
|
||
|
if (Pimpl::checkHasExceptionOccurred())
|
||
|
return;
|
||
|
|
||
|
env->CallVoidMethod (captureSession, CameraCaptureSession.abortCaptures);
|
||
|
|
||
|
if (Pimpl::checkHasExceptionOccurred())
|
||
|
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").get());
|
||
|
|
||
|
env->CallBooleanMethod (handler, AndroidHandler.postDelayed, delayedCaptureRunnable.get(), (jlong) 200);
|
||
|
}
|
||
|
|
||
|
void runPrecaptureSequence()
|
||
|
{
|
||
|
if (Pimpl::checkHasExceptionOccurred())
|
||
|
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<jobject> (env->CallObjectMethod (captureRequestBuilder,
|
||
|
CaptureRequestBuilder.build));
|
||
|
|
||
|
env->CallIntMethod (captureSession, CameraCaptureSession.capture, previewRequest.get(),
|
||
|
captureSessionPreviewCaptureCallback.get(), handler.get());
|
||
|
}
|
||
|
|
||
|
void unlockFocus()
|
||
|
{
|
||
|
if (Pimpl::checkHasExceptionOccurred())
|
||
|
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<jobject> (env->CallObjectMethod (captureRequestBuilder,
|
||
|
CaptureRequestBuilder.build));
|
||
|
|
||
|
env->CallIntMethod (captureSession, CameraCaptureSession.capture, resetAutoFocusRequest.get(),
|
||
|
nullptr, handler.get());
|
||
|
|
||
|
if (Pimpl::checkHasExceptionOccurred())
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
friend void juce_cameraCaptureSessionCaptureCompleted (int64, bool, void*, void*, void*);
|
||
|
friend void juce_cameraCaptureSessionCaptureFailed (int64, bool, void*, void*, void*);
|
||
|
friend void juce_cameraCaptureSessionCaptureProgressed (int64, bool, void*, void*, void*);
|
||
|
friend void juce_cameraCaptureSessionCaptureSequenceAborted (int64, bool, void*, int);
|
||
|
friend void juce_cameraCaptureSessionCaptureSequenceCompleted (int64, bool, void*, int, int64);
|
||
|
friend void juce_cameraCaptureSessionCaptureStarted (int64, bool, void*, void*, int64, int64);
|
||
|
};
|
||
|
|
||
|
//==============================================================================
|
||
|
ScopedCameraDevice& scopedCameraDevice;
|
||
|
ConfiguredCallback& configuredCallback;
|
||
|
GlobalRef& handler;
|
||
|
|
||
|
GlobalRef captureRequestBuilder;
|
||
|
GlobalRef previewCaptureRequest;
|
||
|
|
||
|
GlobalRef captureSessionStateCallback;
|
||
|
int autoFocusMode;
|
||
|
|
||
|
GlobalRef captureSession;
|
||
|
CriticalSection captureSessionLock;
|
||
|
|
||
|
Atomic<int> pendingClose { 0 };
|
||
|
|
||
|
std::unique_ptr<StillPictureTaker> stillPictureTaker;
|
||
|
|
||
|
WaitableEvent closedEvent;
|
||
|
|
||
|
JUCE_DECLARE_WEAK_REFERENCEABLE (CaptureSession)
|
||
|
|
||
|
//==============================================================================
|
||
|
CaptureSession (ScopedCameraDevice& scopedCameraDeviceToUse, ConfiguredCallback& configuredCallbackToUse,
|
||
|
const LocalRef<jobject>& surfacesList, GlobalRef& handlerToUse,
|
||
|
int captureSessionTemplate, int autoFocusModeToUse)
|
||
|
: scopedCameraDevice (scopedCameraDeviceToUse),
|
||
|
configuredCallback (configuredCallbackToUse),
|
||
|
handler (handlerToUse),
|
||
|
captureRequestBuilder (LocalRef<jobject> (getEnv()->CallObjectMethod (scopedCameraDevice.cameraDevice,
|
||
|
AndroidCameraDevice.createCaptureRequest,
|
||
|
(jint) captureSessionTemplate))),
|
||
|
captureSessionStateCallback (LocalRef<jobject> (getEnv()->NewObject (CameraCaptureSessionStateCallback,
|
||
|
CameraCaptureSessionStateCallback.constructor,
|
||
|
android.activity.get(),
|
||
|
reinterpret_cast<jlong> (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<jobject> (env->GetStaticObjectField (CaptureRequest, key));
|
||
|
auto jValue = LocalRef<jobject> (env->CallStaticObjectMethod (JavaInteger, JavaInteger.valueOf, (jint) value));
|
||
|
|
||
|
env->CallVoidMethod (captureRequestBuilder, CaptureRequestBuilder.set, jKey.get(), jValue.get());
|
||
|
}
|
||
|
|
||
|
void cameraCaptureSessionActive (jobject session)
|
||
|
{
|
||
|
JUCE_CAMERA_LOG ("cameraCaptureSessionActive()");
|
||
|
ignoreUnused (session);
|
||
|
}
|
||
|
|
||
|
void cameraCaptureSessionClosed (jobject session)
|
||
|
{
|
||
|
JUCE_CAMERA_LOG ("cameraCaptureSessionClosed()");
|
||
|
ignoreUnused (session);
|
||
|
|
||
|
closedEvent.signal();
|
||
|
}
|
||
|
|
||
|
void cameraCaptureSessionConfigureFailed (jobject session)
|
||
|
{
|
||
|
JUCE_CAMERA_LOG ("cameraCaptureSessionConfigureFailed()");
|
||
|
ignoreUnused (session);
|
||
|
|
||
|
WeakReference<CaptureSession> weakRef (this);
|
||
|
|
||
|
MessageManager::callAsync ([this, weakRef]()
|
||
|
{
|
||
|
if (weakRef == nullptr)
|
||
|
return;
|
||
|
|
||
|
configuredCallback.captureSessionConfigured (nullptr);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
void cameraCaptureSessionConfigured (jobject session)
|
||
|
{
|
||
|
JUCE_CAMERA_LOG ("cameraCaptureSessionConfigured()");
|
||
|
|
||
|
if (pendingClose.get() == 1)
|
||
|
{
|
||
|
// Already closing, bailout.
|
||
|
closedEvent.signal();
|
||
|
|
||
|
GlobalRef s (session);
|
||
|
|
||
|
MessageManager::callAsync ([s]()
|
||
|
{
|
||
|
getEnv()->CallVoidMethod (s, CameraCaptureSession.close);
|
||
|
});
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
{
|
||
|
const ScopedLock lock (captureSessionLock);
|
||
|
captureSession = GlobalRef (session);
|
||
|
}
|
||
|
|
||
|
WeakReference<CaptureSession> weakRef (this);
|
||
|
|
||
|
MessageManager::callAsync ([this, weakRef]()
|
||
|
{
|
||
|
if (weakRef == nullptr)
|
||
|
return;
|
||
|
|
||
|
stillPictureTaker.reset (new StillPictureTaker (captureSession, captureRequestBuilder,
|
||
|
previewCaptureRequest, handler, autoFocusMode));
|
||
|
|
||
|
configuredCallback.captureSessionConfigured (this);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
void cameraCaptureSessionReady (jobject session)
|
||
|
{
|
||
|
JUCE_CAMERA_LOG ("cameraCaptureSessionReady()");
|
||
|
ignoreUnused (session);
|
||
|
}
|
||
|
|
||
|
friend class ScopedCameraDevice;
|
||
|
|
||
|
friend void juce_cameraCaptureSessionActive (int64, void*);
|
||
|
friend void juce_cameraCaptureSessionClosed (int64, void*);
|
||
|
friend void juce_cameraCaptureSessionConfigureFailed (int64, void*);
|
||
|
friend void juce_cameraCaptureSessionConfigured (int64, void*);
|
||
|
friend void juce_cameraCaptureSessionReady (int64, void*);
|
||
|
|
||
|
friend void juce_cameraCaptureSessionCaptureCompleted (int64, bool, void*, void*, void*);
|
||
|
friend void juce_cameraCaptureSessionCaptureFailed (int64, bool, void*, void*, void*);
|
||
|
friend void juce_cameraCaptureSessionCaptureProgressed (int64, bool, void*, void*, void*);
|
||
|
friend void juce_cameraCaptureSessionCaptureSequenceAborted (int64, bool, void*, int);
|
||
|
friend void juce_cameraCaptureSessionCaptureSequenceCompleted (int64, bool, void*, int, int64);
|
||
|
friend void juce_cameraCaptureSessionCaptureStarted (int64, bool, void*, void*, int64, int64);
|
||
|
|
||
|
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 (LocalRef<jobject> (getEnv()->NewObject (CameraDeviceStateCallback,
|
||
|
CameraDeviceStateCallback.constructor,
|
||
|
android.activity.get(),
|
||
|
reinterpret_cast<jlong> (this)))),
|
||
|
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.
|
||
|
auto exception = LocalRef<jobject> (env->ExceptionOccurred());
|
||
|
|
||
|
if (exception != 0)
|
||
|
env->ExceptionClear();
|
||
|
}
|
||
|
|
||
|
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<jobject>& 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<int> pendingOpen { 0 };
|
||
|
Atomic<int> pendingClose { 0 };
|
||
|
Atomic<int> fatalErrorOccurred { 0 };
|
||
|
String openError;
|
||
|
|
||
|
WaitableEvent closedEvent;
|
||
|
|
||
|
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 (jobject cameraDeviceToUse)
|
||
|
{
|
||
|
JUCE_CAMERA_LOG ("cameraDeviceStateOpened()");
|
||
|
|
||
|
pendingOpen.set (0);
|
||
|
|
||
|
cameraDevice = GlobalRef (cameraDeviceToUse);
|
||
|
|
||
|
notifyOpenResult();
|
||
|
}
|
||
|
|
||
|
void notifyOpenResult()
|
||
|
{
|
||
|
MessageManager::callAsync ([this]() { owner.cameraOpenFinished (openError); });
|
||
|
}
|
||
|
|
||
|
friend void juce_cameraDeviceStateClosed (int64);
|
||
|
friend void juce_cameraDeviceStateDisconnected (int64);
|
||
|
friend void juce_cameraDeviceStateError (int64, int);
|
||
|
friend void juce_cameraDeviceStateOpened (int64, void*);
|
||
|
|
||
|
friend void juce_cameraCaptureSessionActive (int64, void*);
|
||
|
friend void juce_cameraCaptureSessionClosed (int64, void*);
|
||
|
friend void juce_cameraCaptureSessionConfigureFailed (int64, void*);
|
||
|
friend void juce_cameraCaptureSessionConfigured (int64, void*);
|
||
|
friend void juce_cameraCaptureSessionReady (int64, void*);
|
||
|
|
||
|
friend void juce_cameraCaptureSessionCaptureCompleted (int64, bool, void*, void*, void*);
|
||
|
friend void juce_cameraCaptureSessionCaptureFailed (int64, bool, void*, void*, void*);
|
||
|
friend void juce_cameraCaptureSessionCaptureProgressed (int64, bool, void*, void*, void*);
|
||
|
friend void juce_cameraCaptureSessionCaptureSequenceAborted (int64, bool, void*, int);
|
||
|
friend void juce_cameraCaptureSessionCaptureSequenceCompleted (int64, bool, void*, int, int64);
|
||
|
friend void juce_cameraCaptureSessionCaptureStarted (int64, bool, void*, void*, int64, int64);
|
||
|
};
|
||
|
|
||
|
//==============================================================================
|
||
|
struct CaptureSessionModeBase
|
||
|
{
|
||
|
virtual ~CaptureSessionModeBase() { }
|
||
|
|
||
|
virtual bool isVideoRecordSession() const = 0;
|
||
|
|
||
|
virtual void triggerStillPictureCapture() = 0;
|
||
|
};
|
||
|
|
||
|
//==============================================================================
|
||
|
template <typename Mode>
|
||
|
struct CaptureSessionMode : public CaptureSessionModeBase,
|
||
|
private PreviewDisplay::Listener,
|
||
|
private ScopedCameraDevice::CaptureSession::ConfiguredCallback
|
||
|
{
|
||
|
~CaptureSessionMode()
|
||
|
{
|
||
|
captureSession.reset();
|
||
|
|
||
|
previewDisplay.removeListener (this);
|
||
|
}
|
||
|
|
||
|
bool isVideoRecordSession() const override
|
||
|
{
|
||
|
return Mode::isVideoRecord();
|
||
|
}
|
||
|
|
||
|
void triggerStillPictureCapture() override
|
||
|
{
|
||
|
if (captureSession == nullptr)
|
||
|
{
|
||
|
// The capture session must be ready before taking a still picture.
|
||
|
// Did you remember to create and show a preview display?
|
||
|
jassertfalse;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
crtp().takeStillPicture();
|
||
|
}
|
||
|
|
||
|
protected:
|
||
|
CaptureSessionMode (Pimpl& ownerToUse, ScopedCameraDevice& cameraDeviceToUse,
|
||
|
GlobalRef& handlerToUse, PreviewDisplay& pd, int cameraSensorOrientationToUse,
|
||
|
int cameraLensFacingToUse, StreamConfigurationMap& streamConfigurationMapToUse)
|
||
|
: owner (ownerToUse),
|
||
|
scopedCameraDevice (cameraDeviceToUse),
|
||
|
handler (handlerToUse),
|
||
|
previewDisplay (pd),
|
||
|
cameraSensorOrientation (cameraSensorOrientationToUse),
|
||
|
cameraLensFacing (cameraLensFacingToUse),
|
||
|
streamConfigurationMap (streamConfigurationMapToUse)
|
||
|
{
|
||
|
WeakReference<CaptureSessionMode<Mode>> weakRef (this);
|
||
|
|
||
|
if (weakRef == nullptr)
|
||
|
return;
|
||
|
|
||
|
// async so that the object is fully constructed before the callback gets invoked
|
||
|
MessageManager::callAsync ([this, weakRef]()
|
||
|
{
|
||
|
if (weakRef == nullptr)
|
||
|
return;
|
||
|
|
||
|
previewDisplay.addListener (this);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
Mode& crtp() { return static_cast<Mode&> (*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 (Pimpl::checkHasExceptionOccurred())
|
||
|
return;
|
||
|
|
||
|
env->CallVoidMethod (session, CameraCaptureSession.abortCaptures);
|
||
|
|
||
|
Pimpl::checkHasExceptionOccurred();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Pimpl& owner;
|
||
|
ScopedCameraDevice& scopedCameraDevice;
|
||
|
GlobalRef& handler;
|
||
|
PreviewDisplay& previewDisplay;
|
||
|
int cameraSensorOrientation;
|
||
|
int cameraLensFacing;
|
||
|
StreamConfigurationMap& streamConfigurationMap;
|
||
|
|
||
|
std::unique_ptr<ScopedCameraDevice::CaptureSession> captureSession;
|
||
|
|
||
|
JUCE_DECLARE_WEAK_REFERENCEABLE (CaptureSessionMode<Mode>)
|
||
|
};
|
||
|
|
||
|
//==============================================================================
|
||
|
struct CaptureSessionPreviewMode : public CaptureSessionMode<CaptureSessionPreviewMode>
|
||
|
{
|
||
|
CaptureSessionPreviewMode (Pimpl& ownerToUse, ScopedCameraDevice& cameraDeviceToUse, GlobalRef& handlerToUse,
|
||
|
PreviewDisplay& pd, ImageReader& ir, int cameraSensorOrientation,
|
||
|
int cameraLensFacingToUse, StreamConfigurationMap& streamConfigurationMapToUse)
|
||
|
: CaptureSessionMode<CaptureSessionPreviewMode> (ownerToUse, cameraDeviceToUse, handlerToUse, pd,
|
||
|
cameraSensorOrientation, cameraLensFacingToUse, streamConfigurationMapToUse),
|
||
|
imageReader (ir)
|
||
|
{
|
||
|
}
|
||
|
|
||
|
// Surfaces passed to newly created capture session.
|
||
|
LocalRef<jobject> getCaptureSessionSurfaces() const
|
||
|
{
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
auto previewSurface = LocalRef<jobject> (previewDisplay.createSurface());
|
||
|
auto imageSurface = LocalRef<jobject> (imageReader.getSurface());
|
||
|
|
||
|
auto arrayList = LocalRef<jobject> (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<jobject> getTargetSurfaces() const
|
||
|
{
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
auto previewSurface = LocalRef<jobject> (previewDisplay.createSurface());
|
||
|
|
||
|
auto arrayList = LocalRef<jobject> (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>
|
||
|
{
|
||
|
CaptureSessionVideoRecordingMode (Pimpl& ownerToUse, ScopedCameraDevice& cameraDeviceToUse, GlobalRef& handlerToUse,
|
||
|
PreviewDisplay& pd, MediaRecorder& mr, int cameraSensorOrientation,
|
||
|
int cameraLensFacingToUse, StreamConfigurationMap& streamConfigurationMapToUse)
|
||
|
: CaptureSessionMode<CaptureSessionVideoRecordingMode> (ownerToUse, cameraDeviceToUse, handlerToUse, pd,
|
||
|
cameraSensorOrientation, 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<jobject> getCaptureSessionSurfaces() const
|
||
|
{
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
auto previewSurface = LocalRef<jobject> (previewDisplay.createSurface());
|
||
|
auto mediaRecorderSurface = LocalRef<jobject> (mediaRecorder.getSurface());
|
||
|
|
||
|
auto arrayList = LocalRef<jobject> (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<jobject> 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 (getEnv()->NewObject (OrientationEventListener,
|
||
|
OrientationEventListener.constructor,
|
||
|
android.activity.get(),
|
||
|
reinterpret_cast<jlong> (this),
|
||
|
android.activity.get(),
|
||
|
sensorDelayUI)),
|
||
|
canDetectChange (getEnv()->CallBooleanMethod (orientationEventListener,
|
||
|
OrientationEventListener.canDetectOrientation) != 0),
|
||
|
deviceOrientation (Desktop::getInstance().getCurrentOrientation()),
|
||
|
lastKnownScreenOrientation (deviceOrientation)
|
||
|
{
|
||
|
setEnabled (true);
|
||
|
}
|
||
|
|
||
|
~DeviceOrientationChangeListener()
|
||
|
{
|
||
|
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;
|
||
|
|
||
|
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;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
friend void juce_deviceOrientationChanged (int64, int);
|
||
|
};
|
||
|
|
||
|
//==============================================================================
|
||
|
CameraDevice& owner;
|
||
|
int minWidth, minHeight, maxWidth, maxHeight;
|
||
|
|
||
|
String cameraId;
|
||
|
InternalOpenCameraResultCallback cameraOpenCallback;
|
||
|
|
||
|
#if __ANDROID_API__ >= 21
|
||
|
AppPausedResumedListener appPausedResumedListener;
|
||
|
GlobalRef appPausedResumedListenerNative;
|
||
|
|
||
|
GlobalRef cameraManager;
|
||
|
GlobalRef cameraCharacteristics;
|
||
|
GlobalRef handlerThread;
|
||
|
GlobalRef handler;
|
||
|
|
||
|
StreamConfigurationMap streamConfigurationMap;
|
||
|
PreviewDisplay previewDisplay;
|
||
|
DeviceOrientationChangeListener deviceOrientationChangeListener;
|
||
|
std::unique_ptr<ImageReader> imageReader;
|
||
|
std::unique_ptr<MediaRecorder> mediaRecorder;
|
||
|
|
||
|
std::unique_ptr<CaptureSessionModeBase> currentCaptureSessionMode;
|
||
|
|
||
|
std::unique_ptr<ScopedCameraDevice> scopedCameraDevice;
|
||
|
|
||
|
CriticalSection listenerLock;
|
||
|
ListenerList<Listener> listeners;
|
||
|
|
||
|
std::function<void (const Image&)> pictureTakenCallback;
|
||
|
|
||
|
Time firstRecordedFrameTimeMs;
|
||
|
bool notifiedOfCameraOpening = false;
|
||
|
#endif
|
||
|
|
||
|
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<int> getSupportedAutoFocusModes() const
|
||
|
{
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
auto jKey = LocalRef<jobject> (env->GetStaticObjectField (CameraCharacteristics, CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES));
|
||
|
|
||
|
auto supportedModes = LocalRef<jintArray> ((jintArray) env->CallObjectMethod (cameraCharacteristics,
|
||
|
CameraCharacteristics.get,
|
||
|
jKey.get()));
|
||
|
|
||
|
return jintArrayToJuceArray (supportedModes);
|
||
|
}
|
||
|
|
||
|
static Array<int> jintArrayToJuceArray (const LocalRef<jintArray>& jArray)
|
||
|
{
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
auto* jArrayElems = env->GetIntArrayElements (jArray, 0);
|
||
|
auto numElems = env->GetArrayLength (jArray);
|
||
|
|
||
|
Array<int> 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<jobject> (env->GetStaticObjectField (CameraCharacteristics, key));
|
||
|
|
||
|
auto jValue = LocalRef<jobject> (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<int> chooseBestSize (int minWidth, int minHeight, int maxWidth, int maxHeight,
|
||
|
Array<Rectangle<int>> supportedSizes)
|
||
|
{
|
||
|
Rectangle<int> 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 appPaused() 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 appResumed() 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<jobject> (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<jobject> (env->NewObject (AndroidHandler,
|
||
|
AndroidHandler.constructorWithLooper,
|
||
|
env->CallObjectMethod (handlerThread, AndroidHandlerThread.getLooper))));
|
||
|
}
|
||
|
|
||
|
void stopBackgroundThread()
|
||
|
{
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
env->CallBooleanMethod (handlerThread, AndroidHandlerThread.quitSafely);
|
||
|
env->CallVoidMethod (handlerThread, AndroidHandlerThread.join);
|
||
|
|
||
|
auto exception = LocalRef<jobject> (env->ExceptionOccurred());
|
||
|
|
||
|
if (exception != 0)
|
||
|
env->ExceptionClear();
|
||
|
|
||
|
handlerThread.clear();
|
||
|
handler.clear();
|
||
|
}
|
||
|
|
||
|
static bool checkHasExceptionOccurred()
|
||
|
{
|
||
|
auto* env = getEnv();
|
||
|
|
||
|
auto exception = LocalRef<jobject> (env->ExceptionOccurred());
|
||
|
|
||
|
if (exception != 0)
|
||
|
{
|
||
|
env->ExceptionClear();
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
friend struct CameraDevice::ViewerComponent;
|
||
|
|
||
|
friend void juce_cameraDeviceStateClosed (int64);
|
||
|
friend void juce_cameraDeviceStateDisconnected (int64);
|
||
|
friend void juce_cameraDeviceStateError (int64, int);
|
||
|
friend void juce_cameraDeviceStateOpened (int64, void*);
|
||
|
|
||
|
friend void juce_cameraCaptureSessionActive (int64, void*);
|
||
|
friend void juce_cameraCaptureSessionClosed (int64, void*);
|
||
|
friend void juce_cameraCaptureSessionConfigureFailed (int64, void*);
|
||
|
friend void juce_cameraCaptureSessionConfigured (int64, void*);
|
||
|
friend void juce_cameraCaptureSessionReady (int64, void*);
|
||
|
|
||
|
friend void juce_cameraCaptureSessionCaptureCompleted (int64, bool, void*, void*, void*);
|
||
|
friend void juce_cameraCaptureSessionCaptureFailed (int64, bool, void*, void*, void*);
|
||
|
friend void juce_cameraCaptureSessionCaptureProgressed (int64, bool, void*, void*, void*);
|
||
|
friend void juce_cameraCaptureSessionCaptureSequenceAborted (int64, bool, void*, int);
|
||
|
friend void juce_cameraCaptureSessionCaptureSequenceCompleted (int64, bool, void*, int, int64);
|
||
|
friend void juce_cameraCaptureSessionCaptureStarted (int64, bool, void*, void*, int64, int64);
|
||
|
|
||
|
friend void juce_deviceOrientationChanged (int64, int);
|
||
|
|
||
|
JUCE_DECLARE_NON_COPYABLE (Pimpl)
|
||
|
};
|
||
|
|
||
|
//==============================================================================
|
||
|
struct CameraDevice::ViewerComponent : public Component,
|
||
|
private ComponentMovementWatcher
|
||
|
{
|
||
|
ViewerComponent (CameraDevice& device) : ComponentMovementWatcher (this)
|
||
|
{
|
||
|
#if __ANDROID_API__ >= 21
|
||
|
auto previewSize = device.pimpl->streamConfigurationMap.getDefaultPreviewSize();
|
||
|
|
||
|
targetAspectRatio = previewSize.getWidth() / (float) previewSize.getHeight();
|
||
|
|
||
|
if (isOrientationLandscape())
|
||
|
setBounds (previewSize);
|
||
|
else
|
||
|
setBounds (0, 0, previewSize.getHeight(), previewSize.getWidth());
|
||
|
|
||
|
addAndMakeVisible (viewerComponent);
|
||
|
viewerComponent.setView (device.pimpl->previewDisplay.getNativeView());
|
||
|
#else
|
||
|
ignoreUnused (device);
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
private:
|
||
|
AndroidViewComponent viewerComponent;
|
||
|
|
||
|
float targetAspectRatio = 1.0f;
|
||
|
|
||
|
void componentMovedOrResized (bool, bool) override
|
||
|
{
|
||
|
auto b = getLocalBounds();
|
||
|
|
||
|
auto targetWidth = b.getWidth();
|
||
|
auto targetHeight = b.getHeight();
|
||
|
|
||
|
if (isOrientationLandscape())
|
||
|
{
|
||
|
auto currentAspectRatio = b.getWidth() / (float) b.getHeight();
|
||
|
|
||
|
if (currentAspectRatio > targetAspectRatio)
|
||
|
targetWidth = static_cast<int> (targetWidth * targetAspectRatio / currentAspectRatio);
|
||
|
else
|
||
|
targetHeight = static_cast<int> (targetHeight * currentAspectRatio / targetAspectRatio);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
auto currentAspectRatio = b.getHeight() / (float) b.getWidth();
|
||
|
|
||
|
if (currentAspectRatio > targetAspectRatio)
|
||
|
targetHeight = static_cast<int> (targetHeight * targetAspectRatio / currentAspectRatio);
|
||
|
else
|
||
|
targetWidth = static_cast<int> (targetWidth * currentAspectRatio / targetAspectRatio);
|
||
|
}
|
||
|
|
||
|
viewerComponent.setBounds (Rectangle<int> (0, 0, targetWidth, targetHeight).withCentre (b.getCentre()));
|
||
|
}
|
||
|
|
||
|
bool isOrientationLandscape() const
|
||
|
{
|
||
|
auto o = Desktop::getInstance().getCurrentOrientation();
|
||
|
return o == Desktop::rotatedClockwise || o == Desktop::rotatedAntiClockwise;
|
||
|
}
|
||
|
|
||
|
void componentPeerChanged() override {}
|
||
|
void componentVisibilityChanged() override {}
|
||
|
|
||
|
JUCE_DECLARE_NON_COPYABLE (ViewerComponent)
|
||
|
};
|
||
|
|
||
|
String CameraDevice::getFileExtension()
|
||
|
{
|
||
|
return ".mp4";
|
||
|
}
|
||
|
|
||
|
#if __ANDROID_API__ >= 21
|
||
|
//==============================================================================
|
||
|
void juce_cameraDeviceStateClosed (int64 host)
|
||
|
{
|
||
|
reinterpret_cast<CameraDevice::Pimpl::ScopedCameraDevice*> (host)->cameraDeviceStateClosed();
|
||
|
}
|
||
|
|
||
|
void juce_cameraDeviceStateDisconnected (int64 host)
|
||
|
{
|
||
|
reinterpret_cast<CameraDevice::Pimpl::ScopedCameraDevice*> (host)->cameraDeviceStateDisconnected();
|
||
|
}
|
||
|
|
||
|
void juce_cameraDeviceStateError (int64 host, int error)
|
||
|
{
|
||
|
reinterpret_cast<CameraDevice::Pimpl::ScopedCameraDevice*> (host)->cameraDeviceStateError (error);
|
||
|
}
|
||
|
|
||
|
void juce_cameraDeviceStateOpened (int64 host, void* camera)
|
||
|
{
|
||
|
reinterpret_cast<CameraDevice::Pimpl::ScopedCameraDevice*> (host)->cameraDeviceStateOpened ((jobject) camera);
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024CameraDeviceStateCallback), cameraDeviceStateClosed, void, (JNIEnv* env, jobject /*activity*/, jlong host, jobject /*camera*/))
|
||
|
{
|
||
|
setEnv (env);
|
||
|
|
||
|
juce_cameraDeviceStateClosed (host);
|
||
|
}
|
||
|
|
||
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024CameraDeviceStateCallback), cameraDeviceStateDisconnected, void, (JNIEnv* env, jobject /*activity*/, jlong host, jobject /*camera*/))
|
||
|
{
|
||
|
setEnv (env);
|
||
|
|
||
|
juce_cameraDeviceStateDisconnected (host);
|
||
|
}
|
||
|
|
||
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024CameraDeviceStateCallback), cameraDeviceStateError, void, (JNIEnv* env, jobject /*activity*/, jlong host, jobject /*camera*/, int error))
|
||
|
{
|
||
|
setEnv (env);
|
||
|
|
||
|
juce_cameraDeviceStateError (host, error);
|
||
|
}
|
||
|
|
||
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024CameraDeviceStateCallback), cameraDeviceStateOpened, void, (JNIEnv* env, jobject /*activity*/, jlong host, jobject camera))
|
||
|
{
|
||
|
setEnv (env);
|
||
|
|
||
|
juce_cameraDeviceStateOpened (host, camera);
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
void juce_cameraCaptureSessionActive (int64 host, void* session)
|
||
|
{
|
||
|
auto* juceCaptureSession = reinterpret_cast<CameraDevice::Pimpl::ScopedCameraDevice::CaptureSession*> (host);
|
||
|
juceCaptureSession->cameraCaptureSessionActive ((jobject) session);
|
||
|
}
|
||
|
|
||
|
void juce_cameraCaptureSessionClosed (int64 host, void* session)
|
||
|
{
|
||
|
auto* juceCaptureSession = reinterpret_cast<CameraDevice::Pimpl::ScopedCameraDevice::CaptureSession*> (host);
|
||
|
juceCaptureSession->cameraCaptureSessionClosed ((jobject) session);
|
||
|
}
|
||
|
|
||
|
void juce_cameraCaptureSessionConfigureFailed (int64 host, void* session)
|
||
|
{
|
||
|
auto* juceCaptureSession = reinterpret_cast<CameraDevice::Pimpl::ScopedCameraDevice::CaptureSession*> (host);
|
||
|
juceCaptureSession->cameraCaptureSessionConfigureFailed ((jobject) session);
|
||
|
}
|
||
|
|
||
|
void juce_cameraCaptureSessionConfigured (int64 host, void* session)
|
||
|
{
|
||
|
auto* juceCaptureSession = reinterpret_cast<CameraDevice::Pimpl::ScopedCameraDevice::CaptureSession*> (host);
|
||
|
juceCaptureSession->cameraCaptureSessionConfigured ((jobject) session);
|
||
|
}
|
||
|
|
||
|
void juce_cameraCaptureSessionReady (int64 host, void* session)
|
||
|
{
|
||
|
auto* juceCaptureSession = reinterpret_cast<CameraDevice::Pimpl::ScopedCameraDevice::CaptureSession*> (host);
|
||
|
juceCaptureSession->cameraCaptureSessionReady ((jobject) session);
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024CameraCaptureSessionStateCallback), cameraCaptureSessionActive, void, (JNIEnv* env, jobject /*activity*/, jlong host, jobject session))
|
||
|
{
|
||
|
setEnv (env);
|
||
|
|
||
|
juce_cameraCaptureSessionActive (host, session);
|
||
|
}
|
||
|
|
||
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024CameraCaptureSessionStateCallback), cameraCaptureSessionClosed, void, (JNIEnv* env, jobject /*activity*/, jlong host, jobject session))
|
||
|
{
|
||
|
setEnv (env);
|
||
|
|
||
|
juce_cameraCaptureSessionClosed (host, session);
|
||
|
}
|
||
|
|
||
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024CameraCaptureSessionStateCallback), cameraCaptureSessionConfigureFailed, void, (JNIEnv* env, jobject /*activity*/, jlong host, jobject session))
|
||
|
{
|
||
|
setEnv (env);
|
||
|
|
||
|
juce_cameraCaptureSessionConfigureFailed (host, session);
|
||
|
}
|
||
|
|
||
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024CameraCaptureSessionStateCallback), cameraCaptureSessionConfigured, void, (JNIEnv* env, jobject /*activity*/, jlong host, jobject session))
|
||
|
{
|
||
|
setEnv (env);
|
||
|
|
||
|
juce_cameraCaptureSessionConfigured (host, session);
|
||
|
}
|
||
|
|
||
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024CameraCaptureSessionStateCallback), cameraCaptureSessionReady, void, (JNIEnv* env, jobject /*activity*/, jlong host, jobject session))
|
||
|
{
|
||
|
setEnv (env);
|
||
|
|
||
|
juce_cameraCaptureSessionReady (host, session);
|
||
|
}
|
||
|
|
||
|
|
||
|
//==============================================================================
|
||
|
void juce_cameraCaptureSessionCaptureCompleted (int64 host, bool isPreview, void* session, void* request, void* result)
|
||
|
{
|
||
|
auto* stillPictureTaker = reinterpret_cast<CameraDevice::Pimpl::ScopedCameraDevice::CaptureSession::StillPictureTaker*> (host);
|
||
|
stillPictureTaker->cameraCaptureSessionCaptureCompleted (isPreview, (jobject) session, (jobject) request, (jobject) result);
|
||
|
}
|
||
|
|
||
|
void juce_cameraCaptureSessionCaptureFailed (int64 host, bool isPreview, void* session, void* request, void* failure)
|
||
|
{
|
||
|
auto* stillPictureTaker = reinterpret_cast<CameraDevice::Pimpl::ScopedCameraDevice::CaptureSession::StillPictureTaker*> (host);
|
||
|
stillPictureTaker->cameraCaptureSessionCaptureFailed (isPreview, (jobject) session, (jobject) request, (jobject) failure);
|
||
|
}
|
||
|
|
||
|
void juce_cameraCaptureSessionCaptureProgressed (int64 host, bool isPreview, void* session, void* request, void* partialResult)
|
||
|
{
|
||
|
auto* stillPictureTaker = reinterpret_cast<CameraDevice::Pimpl::ScopedCameraDevice::CaptureSession::StillPictureTaker*> (host);
|
||
|
stillPictureTaker->cameraCaptureSessionCaptureProgressed (isPreview, (jobject) session, (jobject) request, (jobject) partialResult);
|
||
|
}
|
||
|
|
||
|
void juce_cameraCaptureSessionCaptureSequenceAborted (int64 host, bool isPreview, void* session, int sequenceId)
|
||
|
{
|
||
|
auto* stillPictureTaker = reinterpret_cast<CameraDevice::Pimpl::ScopedCameraDevice::CaptureSession::StillPictureTaker*> (host);
|
||
|
stillPictureTaker->cameraCaptureSessionCaptureSequenceAborted (isPreview, (jobject) session, sequenceId);
|
||
|
}
|
||
|
|
||
|
void juce_cameraCaptureSessionCaptureSequenceCompleted (int64 host, bool isPreview, void* session, int sequenceId, int64 frameNumber)
|
||
|
{
|
||
|
auto* stillPictureTaker = reinterpret_cast<CameraDevice::Pimpl::ScopedCameraDevice::CaptureSession::StillPictureTaker*> (host);
|
||
|
stillPictureTaker->cameraCaptureSessionCaptureSequenceCompleted (isPreview, (jobject) session, sequenceId, frameNumber);
|
||
|
}
|
||
|
|
||
|
void juce_cameraCaptureSessionCaptureStarted (int64 host, bool isPreview, void* session, void* request, int64 timestamp, int64 frameNumber)
|
||
|
{
|
||
|
auto* stillPictureTaker = reinterpret_cast<CameraDevice::Pimpl::ScopedCameraDevice::CaptureSession::StillPictureTaker*> (host);
|
||
|
stillPictureTaker->cameraCaptureSessionCaptureStarted (isPreview, (jobject) session, (jobject) request, timestamp, frameNumber);
|
||
|
}
|
||
|
|
||
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024CameraCaptureSessionCaptureCallback), cameraCaptureSessionCaptureCompleted, \
|
||
|
void, (JNIEnv* env, jobject /*activity*/, jlong host, bool isPreview, jobject session, jobject request, jobject result))
|
||
|
{
|
||
|
setEnv (env);
|
||
|
|
||
|
juce_cameraCaptureSessionCaptureCompleted (host, isPreview, session, request, result);
|
||
|
}
|
||
|
|
||
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024CameraCaptureSessionCaptureCallback), cameraCaptureSessionCaptureFailed, \
|
||
|
void, (JNIEnv* env, jobject /*activity*/, jlong host, bool isPreview, jobject session, jobject request, jobject failure))
|
||
|
{
|
||
|
setEnv (env);
|
||
|
|
||
|
juce_cameraCaptureSessionCaptureFailed (host, isPreview, session, request, failure);
|
||
|
}
|
||
|
|
||
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024CameraCaptureSessionCaptureCallback), cameraCaptureSessionCaptureProgressed, \
|
||
|
void, (JNIEnv* env, jobject /*activity*/, jlong host, bool isPreview, jobject session, jobject request, jobject partialResult))
|
||
|
{
|
||
|
setEnv (env);
|
||
|
|
||
|
juce_cameraCaptureSessionCaptureProgressed (host, isPreview, session, request, partialResult);
|
||
|
}
|
||
|
|
||
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024CameraCaptureSessionCaptureCallback), cameraCaptureSessionCaptureSequenceAborted, \
|
||
|
void, (JNIEnv* env, jobject /*activity*/, jlong host, bool isPreview, jobject session, jint sequenceId))
|
||
|
{
|
||
|
setEnv (env);
|
||
|
|
||
|
juce_cameraCaptureSessionCaptureSequenceAborted (host, isPreview, session, (int) sequenceId);
|
||
|
}
|
||
|
|
||
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024CameraCaptureSessionCaptureCallback), cameraCaptureSessionCaptureSequenceCompleted, \
|
||
|
void, (JNIEnv* env, jobject /*activity*/, jlong host, bool isPreview, jobject session, jint sequenceId, jlong frameNumber))
|
||
|
{
|
||
|
setEnv (env);
|
||
|
|
||
|
juce_cameraCaptureSessionCaptureSequenceCompleted (host, isPreview, session, (int) sequenceId, frameNumber);
|
||
|
}
|
||
|
|
||
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024CameraCaptureSessionCaptureCallback), cameraCaptureSessionCaptureStarted, \
|
||
|
void, (JNIEnv* env, jobject /*activity*/, jlong host, bool isPreview, jobject session, jobject request, int64 timestamp, int64 frameNumber))
|
||
|
{
|
||
|
setEnv (env);
|
||
|
|
||
|
juce_cameraCaptureSessionCaptureStarted (host, isPreview, session, request, timestamp, frameNumber);
|
||
|
}
|
||
|
|
||
|
//==============================================================================
|
||
|
void juce_deviceOrientationChanged (int64 host, int orientation)
|
||
|
{
|
||
|
auto* listener = reinterpret_cast<CameraDevice::Pimpl::DeviceOrientationChangeListener*> (host);
|
||
|
listener->orientationChanged (orientation);
|
||
|
}
|
||
|
|
||
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024JuceOrientationEventListener), deviceOrientationChanged, \
|
||
|
void, (JNIEnv* env, jobject /*activity*/, jlong host, jint orientation))
|
||
|
{
|
||
|
setEnv (env);
|
||
|
|
||
|
juce_deviceOrientationChanged (host, (int) orientation);
|
||
|
}
|
||
|
#endif
|