diff --git a/BREAKING-CHANGES.txt b/BREAKING-CHANGES.txt index ee39738aa4..9a9e932c55 100644 --- a/BREAKING-CHANGES.txt +++ b/BREAKING-CHANGES.txt @@ -4,6 +4,33 @@ JUCE breaking changes Develop ======= +Change +------ +CameraDevice::Listener::imageReceived() has been replaced by a new function +CameraDevice::takeStillPicture(). The callback passed in takeStillPicture() +will always be triggered on the message thread. + +Possible Issues +--------------- +The code handling image capture needs to be adjusted to work well on a message +thread. This means that you should not perform any lengthy operations in your +callback, as this will stall your UI. + +Workaround +---------- +Use CameraDevice::takeStillPicture() instead of old listener callback. Pass +your lambda or other std::function compliant object to takeStillPicture which +will be called for you when the capture has finished. If you want to perform +any time consuming operation upon receiving the picture, schedule it on a +separate worker thread. + +Rationale +--------- +The old Listener interface was not working in a typical listener pattern. It +feels more natural to request still picture capture with a dedicated function. +This is also more compliant with async mobile APIs. + + Change ------ JUCE no longer supports OS X deployment targets earlier than 10.7. diff --git a/examples/DemoRunner/Builds/Android/app/CMakeLists.txt b/examples/DemoRunner/Builds/Android/app/CMakeLists.txt index e9de472a27..378e5dfa58 100644 --- a/examples/DemoRunner/Builds/Android/app/CMakeLists.txt +++ b/examples/DemoRunner/Builds/Android/app/CMakeLists.txt @@ -8,7 +8,7 @@ SET(BINARY_NAME "juce_jni") add_library("cpufeatures" STATIC "${ANDROID_NDK}/sources/android/cpufeatures/cpu-features.c") set_source_files_properties("${ANDROID_NDK}/sources/android/cpufeatures/cpu-features.c" PROPERTIES COMPILE_FLAGS "-Wno-sign-conversion -Wno-gnu-statement-expression") -add_definitions("-DJUCE_ANDROID=1" "-DJUCE_ANDROID_API_VERSION=23" "-DJUCE_ANDROID_ACTIVITY_CLASSNAME=com_roli_juce_demorunner_DemoRunner" "-DJUCE_ANDROID_ACTIVITY_CLASSPATH=\"com/roli/juce/demorunner/DemoRunner\"" "-DJUCE_ANDROID_SHARING_CONTENT_PROVIDER_CLASSNAME=com_roli_juce_demorunner_SharingContentProvider" "-DJUCE_ANDROID_SHARING_CONTENT_PROVIDER_CLASSPATH=\"com/roli/juce/demorunner/SharingContentProvider\"" "-DJUCE_PUSH_NOTIFICATIONS=1" "-DJUCE_ANDROID_GL_ES_VERSION_3_0=1" "-DJUCE_DEMO_RUNNER=1" "-DJUCE_UNIT_TESTS=1" "-DJUCER_ANDROIDSTUDIO_7F0E4A25=1" "-DJUCE_APP_VERSION=5.3.1" "-DJUCE_APP_VERSION_HEX=0x50301") +add_definitions("-DJUCE_ANDROID=1" "-DJUCE_ANDROID_API_VERSION=23" "-DJUCE_ANDROID_ACTIVITY_CLASSNAME=com_juce_demorunner_DemoRunner" "-DJUCE_ANDROID_ACTIVITY_CLASSPATH=\"com/juce/demorunner/DemoRunner\"" "-DJUCE_ANDROID_SHARING_CONTENT_PROVIDER_CLASSNAME=com_juce_demorunner_SharingContentProvider" "-DJUCE_ANDROID_SHARING_CONTENT_PROVIDER_CLASSPATH=\"com/juce/demorunner/SharingContentProvider\"" "-DJUCE_PUSH_NOTIFICATIONS=1" "-DJUCE_ANDROID_GL_ES_VERSION_3_0=1" "-DJUCE_DEMO_RUNNER=1" "-DJUCE_UNIT_TESTS=1" "-DJUCER_ANDROIDSTUDIO_7F0E4A25=1" "-DJUCE_APP_VERSION=5.3.1" "-DJUCE_APP_VERSION_HEX=0x50301") include_directories( AFTER "../../../JuceLibraryCode" @@ -546,6 +546,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_core/containers/juce_ReferenceCountedArray.h" "../../../../../modules/juce_core/containers/juce_ScopedValueSetter.h" "../../../../../modules/juce_core/containers/juce_SortedSet.h" + "../../../../../modules/juce_core/containers/juce_SparseSet.cpp" "../../../../../modules/juce_core/containers/juce_SparseSet.h" "../../../../../modules/juce_core/containers/juce_Variant.cpp" "../../../../../modules/juce_core/containers/juce_Variant.h" @@ -1465,6 +1466,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_video/capture/juce_CameraDevice.cpp" "../../../../../modules/juce_video/capture/juce_CameraDevice.h" "../../../../../modules/juce_video/native/juce_android_CameraDevice.h" + "../../../../../modules/juce_video/native/juce_ios_CameraDevice.h" "../../../../../modules/juce_video/native/juce_mac_CameraDevice.h" "../../../../../modules/juce_video/native/juce_mac_Video.h" "../../../../../modules/juce_video/native/juce_win32_CameraDevice.h" @@ -2008,6 +2010,7 @@ set_source_files_properties("../../../../../modules/juce_core/containers/juce_Pr set_source_files_properties("../../../../../modules/juce_core/containers/juce_ReferenceCountedArray.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/containers/juce_ScopedValueSetter.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/containers/juce_SortedSet.h" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_core/containers/juce_SparseSet.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/containers/juce_SparseSet.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/containers/juce_Variant.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/containers/juce_Variant.h" PROPERTIES HEADER_FILE_ONLY TRUE) @@ -2927,6 +2930,7 @@ set_source_files_properties("../../../../../modules/juce_product_unlocking/juce_ set_source_files_properties("../../../../../modules/juce_video/capture/juce_CameraDevice.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_video/capture/juce_CameraDevice.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_video/native/juce_android_CameraDevice.h" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_video/native/juce_ios_CameraDevice.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_video/native/juce_mac_CameraDevice.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_video/native/juce_mac_Video.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_video/native/juce_win32_CameraDevice.h" PROPERTIES HEADER_FILE_ONLY TRUE) diff --git a/examples/DemoRunner/Builds/Android/app/build.gradle b/examples/DemoRunner/Builds/Android/app/build.gradle index 1bbe7b6c87..b2250b3f20 100644 --- a/examples/DemoRunner/Builds/Android/app/build.gradle +++ b/examples/DemoRunner/Builds/Android/app/build.gradle @@ -19,7 +19,7 @@ android { } defaultConfig { - applicationId "com.roli.juce.demorunner" + applicationId "com.juce.demorunner" minSdkVersion 23 targetSdkVersion 23 externalNativeBuild { diff --git a/examples/DemoRunner/Builds/Android/app/src/main/AndroidManifest.xml b/examples/DemoRunner/Builds/Android/app/src/main/AndroidManifest.xml index b1d2d7dba6..cde58d3c0b 100644 --- a/examples/DemoRunner/Builds/Android/app/src/main/AndroidManifest.xml +++ b/examples/DemoRunner/Builds/Android/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ + package="com.juce.demorunner"> @@ -9,18 +9,19 @@ + + android:screenOrientation="unspecified" android:launchMode="singleTask" android:hardwareAccelerated="true"> - diff --git a/examples/DemoRunner/Builds/Android/app/src/main/java/com/roli/juce/demorunner/DemoRunner.java b/examples/DemoRunner/Builds/Android/app/src/main/java/com/roli/juce/demorunner/DemoRunner.java index af0505a40d..7e4f1ae939 100644 --- a/examples/DemoRunner/Builds/Android/app/src/main/java/com/roli/juce/demorunner/DemoRunner.java +++ b/examples/DemoRunner/Builds/Android/app/src/main/java/com/roli/juce/demorunner/DemoRunner.java @@ -30,6 +30,7 @@ import android.content.Intent; import android.content.res.Configuration; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.hardware.camera2.*; import android.net.http.SslError; import android.net.Uri; import android.os.Bundle; @@ -119,6 +120,7 @@ public class DemoRunner extends Activity private static final int JUCE_PERMISSIONS_BLUETOOTH_MIDI = 2; private static final int JUCE_PERMISSIONS_READ_EXTERNAL_STORAGE = 3; private static final int JUCE_PERMISSIONS_WRITE_EXTERNAL_STORAGE = 4; + private static final int JUCE_PERMISSIONS_CAMERA = 5; private static String getAndroidPermissionName (int permissionID) { @@ -129,6 +131,7 @@ public class DemoRunner extends Activity // use string value as this is not defined in SDKs < 16 case JUCE_PERMISSIONS_READ_EXTERNAL_STORAGE: return "android.permission.READ_EXTERNAL_STORAGE"; case JUCE_PERMISSIONS_WRITE_EXTERNAL_STORAGE: return Manifest.permission.WRITE_EXTERNAL_STORAGE; + case JUCE_PERMISSIONS_CAMERA: return Manifest.permission.CAMERA; } // unknown permission ID! @@ -1205,6 +1208,7 @@ public class DemoRunner extends Activity setVolumeControlStream (AudioManager.STREAM_MUSIC); permissionCallbackPtrMap = new HashMap(); + appPausedResumedListeners = new HashMap(); } @Override @@ -1221,6 +1225,11 @@ public class DemoRunner extends Activity { suspendApp(); + Long[] keys = appPausedResumedListeners.keySet().toArray (new Long[appPausedResumedListeners.keySet().size()]); + + for (Long k : keys) + appPausedResumedListeners.get (k).appPaused(); + try { Thread.sleep (1000); // This is a bit of a hack to avoid some hard-to-track-down @@ -1236,12 +1245,10 @@ public class DemoRunner extends Activity super.onResume(); resumeApp(); - // Ensure that navigation/status bar visibility is correctly restored. - for (int i = 0; i < viewHolder.getChildCount(); ++i) - { - if (viewHolder.getChildAt (i) instanceof ComponentPeerView) - ((ComponentPeerView) viewHolder.getChildAt (i)).appResumed(); - } + Long[] keys = appPausedResumedListeners.keySet().toArray (new Long[appPausedResumedListeners.keySet().size()]); + + for (Long k : keys) + appPausedResumedListeners.get (k).appResumed(); } @Override @@ -1368,11 +1375,14 @@ public class DemoRunner extends Activity { ComponentPeerView v = new ComponentPeerView (this, opaque, host); viewHolder.addView (v); + addAppPausedResumedListener (v, host); return v; } public final void deleteView (ComponentPeerView view) { + removeAppPausedResumedListener (view, view.host); + view.host = 0; ViewGroup group = (ViewGroup) (view.getParent()); @@ -1590,9 +1600,28 @@ public class DemoRunner extends Activity public native void alertDismissed (long callback, int id); + //============================================================================== + public interface AppPausedResumedListener + { + void appPaused(); + void appResumed(); + } + + private Map appPausedResumedListeners; + + public void addAppPausedResumedListener (AppPausedResumedListener l, long listenerHost) + { + appPausedResumedListeners.put (new Long (listenerHost), l); + } + + public void removeAppPausedResumedListener (AppPausedResumedListener l, long listenerHost) + { + appPausedResumedListeners.remove (new Long (listenerHost)); + } + //============================================================================== public final class ComponentPeerView extends ViewGroup - implements View.OnFocusChangeListener + implements View.OnFocusChangeListener, AppPausedResumedListener { public ComponentPeerView (Context context, boolean opaque_, long host) { @@ -1940,13 +1969,25 @@ public class DemoRunner extends Activity } //============================================================================== + private native void handleAppPaused (long host); private native void handleAppResumed (long host); + @Override + public void appPaused() + { + if (host == 0) + return; + + handleAppPaused (host); + } + + @Override public void appResumed() { if (host == 0) return; + // Ensure that navigation/status bar visibility is correctly restored. handleAppResumed (host); } } @@ -2616,6 +2657,179 @@ public class DemoRunner extends Activity private final Object hostLock = new Object(); } + + //============================================================================== + public class CameraDeviceStateCallback extends CameraDevice.StateCallback + { + private native void cameraDeviceStateClosed (long host, CameraDevice camera); + private native void cameraDeviceStateDisconnected (long host, CameraDevice camera); + private native void cameraDeviceStateError (long host, CameraDevice camera, int error); + private native void cameraDeviceStateOpened (long host, CameraDevice camera); + + CameraDeviceStateCallback (long hostToUse) + { + host = hostToUse; + } + + @Override + public void onClosed (CameraDevice camera) + { + cameraDeviceStateClosed (host, camera); + } + + @Override + public void onDisconnected (CameraDevice camera) + { + cameraDeviceStateDisconnected (host, camera); + } + + @Override + public void onError (CameraDevice camera, int error) + { + cameraDeviceStateError (host, camera, error); + } + + @Override + public void onOpened (CameraDevice camera) + { + cameraDeviceStateOpened (host, camera); + } + + private long host; + } + + //============================================================================== + public class CameraCaptureSessionStateCallback extends CameraCaptureSession.StateCallback + { + private native void cameraCaptureSessionActive (long host, CameraCaptureSession session); + private native void cameraCaptureSessionClosed (long host, CameraCaptureSession session); + private native void cameraCaptureSessionConfigureFailed (long host, CameraCaptureSession session); + private native void cameraCaptureSessionConfigured (long host, CameraCaptureSession session); + private native void cameraCaptureSessionReady (long host, CameraCaptureSession session); + + CameraCaptureSessionStateCallback (long hostToUse) + { + host = hostToUse; + } + + @Override + public void onActive (CameraCaptureSession session) + { + cameraCaptureSessionActive (host, session); + } + + @Override + public void onClosed (CameraCaptureSession session) + { + cameraCaptureSessionClosed (host, session); + } + + @Override + public void onConfigureFailed (CameraCaptureSession session) + { + cameraCaptureSessionConfigureFailed (host, session); + } + + @Override + public void onConfigured (CameraCaptureSession session) + { + cameraCaptureSessionConfigured (host, session); + } + + @Override + public void onReady (CameraCaptureSession session) + { + cameraCaptureSessionReady (host, session); + } + + private long host; + } + + //============================================================================== + public class CameraCaptureSessionCaptureCallback extends CameraCaptureSession.CaptureCallback + { + private native void cameraCaptureSessionCaptureCompleted (long host, boolean isPreview, CameraCaptureSession session, + CaptureRequest request, TotalCaptureResult result); + private native void cameraCaptureSessionCaptureFailed (long host, boolean isPreview, CameraCaptureSession session, + CaptureRequest request, CaptureFailure failure); + private native void cameraCaptureSessionCaptureProgressed (long host, boolean isPreview, CameraCaptureSession session, + CaptureRequest request, CaptureResult partialResult); + private native void cameraCaptureSessionCaptureSequenceAborted (long host, boolean isPreview, CameraCaptureSession session, int sequenceId); + private native void cameraCaptureSessionCaptureSequenceCompleted (long host, boolean isPreview, CameraCaptureSession session, int sequenceId, long frameNumber); + private native void cameraCaptureSessionCaptureStarted (long host, boolean isPreview, CameraCaptureSession session, CaptureRequest request, + long timestamp, long frameNumber); + + CameraCaptureSessionCaptureCallback (long hostToUse, boolean shouldBePreview) + { + host = hostToUse; + preview = shouldBePreview; + } + + @Override + public void onCaptureCompleted (CameraCaptureSession session, CaptureRequest request, + TotalCaptureResult result) + { + cameraCaptureSessionCaptureCompleted (host, preview, session, request, result); + } + + @Override + public void onCaptureFailed (CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) + { + cameraCaptureSessionCaptureFailed (host, preview, session, request, failure); + } + + @Override + public void onCaptureProgressed (CameraCaptureSession session, CaptureRequest request, + CaptureResult partialResult) + { + cameraCaptureSessionCaptureProgressed (host, preview, session, request, partialResult); + } + + @Override + public void onCaptureSequenceAborted (CameraCaptureSession session, int sequenceId) + { + cameraCaptureSessionCaptureSequenceAborted (host, preview, session, sequenceId); + } + + @Override + public void onCaptureSequenceCompleted (CameraCaptureSession session, int sequenceId, long frameNumber) + { + cameraCaptureSessionCaptureSequenceCompleted (host, preview, session, sequenceId, frameNumber); + } + + @Override + public void onCaptureStarted (CameraCaptureSession session, CaptureRequest request, long timestamp, + long frameNumber) + { + cameraCaptureSessionCaptureStarted (host, preview, session, request, timestamp, frameNumber); + } + + private long host; + private boolean preview; + } + + //============================================================================== + public class JuceOrientationEventListener extends OrientationEventListener + { + private native void deviceOrientationChanged (long host, int orientation); + + public JuceOrientationEventListener (long hostToUse, Context context, int rate) + { + super (context, rate); + + host = hostToUse; + } + + @Override + public void onOrientationChanged (int orientation) + { + deviceOrientationChanged (host, orientation); + } + + private long host; + } + + //============================================================================== public static final String getLocaleValue (boolean isRegion) { diff --git a/examples/DemoRunner/Builds/MacOSX/DemoRunner.xcodeproj/project.pbxproj b/examples/DemoRunner/Builds/MacOSX/DemoRunner.xcodeproj/project.pbxproj index ae0bdc5c17..c71ea15be6 100644 --- a/examples/DemoRunner/Builds/MacOSX/DemoRunner.xcodeproj/project.pbxproj +++ b/examples/DemoRunner/Builds/MacOSX/DemoRunner.xcodeproj/project.pbxproj @@ -259,7 +259,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.11; MACOSX_DEPLOYMENT_TARGET_ppc = 10.4; OTHER_CPLUSPLUSFLAGS = "-Wall -Wshadow -Wno-missing-field-initializers -Wshadow -Wshorten-64-to-32 -Wstrict-aliasing -Wuninitialized -Wunused-parameter -Wconversion -Wsign-compare -Wint-conversion -Wconditional-uninitialized -Woverloaded-virtual -Wreorder -Wconstant-conversion -Wsign-conversion -Wunused-private-field -Wbool-conversion -Wextra-semi -Wno-ignored-qualifiers -Wunreachable-code"; - PRODUCT_BUNDLE_IDENTIFIER = com.roli.juce.demorunner; + PRODUCT_BUNDLE_IDENTIFIER = com.juce.demorunner; SDKROOT_ppc = macosx10.5; USE_HEADERMAP = NO; }; name = Debug; }; 69330F27DD2C71609336C7D2 = {isa = XCBuildConfiguration; buildSettings = { @@ -296,7 +296,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.11; MACOSX_DEPLOYMENT_TARGET_ppc = 10.4; OTHER_CPLUSPLUSFLAGS = "-Wall -Wshadow -Wno-missing-field-initializers -Wshadow -Wshorten-64-to-32 -Wstrict-aliasing -Wuninitialized -Wunused-parameter -Wconversion -Wsign-compare -Wint-conversion -Wconditional-uninitialized -Woverloaded-virtual -Wreorder -Wconstant-conversion -Wsign-conversion -Wunused-private-field -Wbool-conversion -Wextra-semi -Wno-ignored-qualifiers -Wunreachable-code"; - PRODUCT_BUNDLE_IDENTIFIER = com.roli.juce.demorunner; + PRODUCT_BUNDLE_IDENTIFIER = com.juce.demorunner; SDKROOT_ppc = macosx10.5; USE_HEADERMAP = NO; }; name = Release; }; C01EC82F42B640CA1E54AD53 = {isa = XCBuildConfiguration; buildSettings = { diff --git a/examples/DemoRunner/Builds/MacOSX/Info-App.plist b/examples/DemoRunner/Builds/MacOSX/Info-App.plist index 39ca56ccb1..9dda4fe9be 100644 --- a/examples/DemoRunner/Builds/MacOSX/Info-App.plist +++ b/examples/DemoRunner/Builds/MacOSX/Info-App.plist @@ -8,7 +8,7 @@ CFBundleIconFile Icon.icns CFBundleIdentifier - com.roli.juce.demorunner + com.juce.demorunner CFBundleName DemoRunner CFBundleDisplayName diff --git a/examples/DemoRunner/Builds/VisualStudio2013/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2013/DemoRunner_App.vcxproj index e04300c6a1..280bb2631a 100644 --- a/examples/DemoRunner/Builds/VisualStudio2013/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2013/DemoRunner_App.vcxproj @@ -772,6 +772,9 @@ true + + true + true @@ -2836,6 +2839,7 @@ + diff --git a/examples/DemoRunner/Builds/VisualStudio2013/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2013/DemoRunner_App.vcxproj.filters index a17949c392..1eeee3f9ec 100644 --- a/examples/DemoRunner/Builds/VisualStudio2013/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2013/DemoRunner_App.vcxproj.filters @@ -1141,6 +1141,9 @@ JUCE Modules\juce_core\containers + + JUCE Modules\juce_core\containers + JUCE Modules\juce_core\containers @@ -4821,6 +4824,9 @@ JUCE Modules\juce_video\native + + JUCE Modules\juce_video\native + JUCE Modules\juce_video\native diff --git a/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj index c064e86a7b..7df49e7433 100644 --- a/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj @@ -772,6 +772,9 @@ true + + true + true @@ -2836,6 +2839,7 @@ + diff --git a/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj.filters index fe88ae0055..ef3cce5ab3 100644 --- a/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj.filters @@ -1141,6 +1141,9 @@ JUCE Modules\juce_core\containers + + JUCE Modules\juce_core\containers + JUCE Modules\juce_core\containers @@ -4821,6 +4824,9 @@ JUCE Modules\juce_video\native + + JUCE Modules\juce_video\native + JUCE Modules\juce_video\native diff --git a/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj index 696a7ae1ac..01e6756273 100644 --- a/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj @@ -772,6 +772,9 @@ true + + true + true @@ -2836,6 +2839,7 @@ + diff --git a/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj.filters index e7ed5a8e0d..a738f90fce 100644 --- a/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj.filters @@ -1141,6 +1141,9 @@ JUCE Modules\juce_core\containers + + JUCE Modules\juce_core\containers + JUCE Modules\juce_core\containers @@ -4821,6 +4824,9 @@ JUCE Modules\juce_video\native + + JUCE Modules\juce_video\native + JUCE Modules\juce_video\native diff --git a/examples/DemoRunner/Builds/iOS/DemoRunner.xcodeproj/project.pbxproj b/examples/DemoRunner/Builds/iOS/DemoRunner.xcodeproj/project.pbxproj index 1aee579b01..dd4b0a09a5 100644 --- a/examples/DemoRunner/Builds/iOS/DemoRunner.xcodeproj/project.pbxproj +++ b/examples/DemoRunner/Builds/iOS/DemoRunner.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 6A61CBB4E39BFD392D97528F = {isa = PBXBuildFile; fileRef = 61AE09C749B007B70A265D9B; }; 0B0CE6D5062E5C02A41F24BC = {isa = PBXBuildFile; fileRef = 873F9DD54978E601102353B4; }; 5E4310B3F6BB639875D3E9B8 = {isa = PBXBuildFile; fileRef = 49ECA8B998B339A083674A22; }; + AE7FB2AC3885F4BF53A5DDA1 = {isa = PBXBuildFile; fileRef = 7983C452610C1638B7E78F12; }; 1FB200F4AE3E4E7CDFF629BB = {isa = PBXBuildFile; fileRef = 24D74AF1C95BEF957DC4FA77; }; AC783ECD84496E0B77911EEE = {isa = PBXBuildFile; fileRef = 34F1320BC5C23702C08DF9F0; }; B1981F62F6A91FD2F579A198 = {isa = PBXBuildFile; fileRef = 23CD1A3F9067C3A0ECE7BB67; }; @@ -92,6 +93,7 @@ 6C5E26B4D28F8450435B8AE1 = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "include_juce_cryptography.mm"; path = "../../JuceLibraryCode/include_juce_cryptography.mm"; sourceTree = "SOURCE_ROOT"; }; 72129757D2A553B90A7157C6 = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = AppConfig.h; path = ../../JuceLibraryCode/AppConfig.h; sourceTree = "SOURCE_ROOT"; }; 76A157A111866670A4678F04 = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 7983C452610C1638B7E78F12 = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; }; 7A5AAE9EE573FC6105CC4AAC = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SettingsContent.h; path = ../../Source/UI/SettingsContent.h; sourceTree = "SOURCE_ROOT"; }; 8135645508EEFDBDCDF2ADC6 = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = DemoRunner/Images.xcassets; sourceTree = "SOURCE_ROOT"; }; 831A01C745C905F5715CD822 = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "include_juce_blocks_basics.cpp"; path = "../../JuceLibraryCode/include_juce_blocks_basics.cpp"; sourceTree = "SOURCE_ROOT"; }; @@ -209,6 +211,7 @@ 61AE09C749B007B70A265D9B, 873F9DD54978E601102353B4, 49ECA8B998B339A083674A22, + 7983C452610C1638B7E78F12, 24D74AF1C95BEF957DC4FA77, 34F1320BC5C23702C08DF9F0, 23CD1A3F9067C3A0ECE7BB67, @@ -256,7 +259,7 @@ INFOPLIST_PREPROCESS = NO; INSTALL_PATH = "$(HOME)/Applications"; OTHER_CPLUSPLUSFLAGS = "-Wall -Wshadow -Wno-missing-field-initializers -Wshadow -Wshorten-64-to-32 -Wstrict-aliasing -Wuninitialized -Wunused-parameter -Wconversion -Wsign-compare -Wint-conversion -Wconditional-uninitialized -Woverloaded-virtual -Wreorder -Wconstant-conversion -Wsign-conversion -Wunused-private-field -Wbool-conversion -Wextra-semi -Wno-ignored-qualifiers -Wunreachable-code"; - PRODUCT_BUNDLE_IDENTIFIER = com.roli.juce.demorunner; + PRODUCT_BUNDLE_IDENTIFIER = com.juce.demorunner; USE_HEADERMAP = NO; }; name = Debug; }; 69330F27DD2C71609336C7D2 = {isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -293,7 +296,7 @@ INSTALL_PATH = "$(HOME)/Applications"; LLVM_LTO = YES; OTHER_CPLUSPLUSFLAGS = "-Wall -Wshadow -Wno-missing-field-initializers -Wshadow -Wshorten-64-to-32 -Wstrict-aliasing -Wuninitialized -Wunused-parameter -Wconversion -Wsign-compare -Wint-conversion -Wconditional-uninitialized -Woverloaded-virtual -Wreorder -Wconstant-conversion -Wsign-conversion -Wunused-private-field -Wbool-conversion -Wextra-semi -Wno-ignored-qualifiers -Wunreachable-code"; - PRODUCT_BUNDLE_IDENTIFIER = com.roli.juce.demorunner; + PRODUCT_BUNDLE_IDENTIFIER = com.juce.demorunner; USE_HEADERMAP = NO; }; name = Release; }; C01EC82F42B640CA1E54AD53 = {isa = XCBuildConfiguration; buildSettings = { "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -428,6 +431,7 @@ 6A61CBB4E39BFD392D97528F, 0B0CE6D5062E5C02A41F24BC, 5E4310B3F6BB639875D3E9B8, + AE7FB2AC3885F4BF53A5DDA1, 1FB200F4AE3E4E7CDFF629BB, AC783ECD84496E0B77911EEE, B1981F62F6A91FD2F579A198, diff --git a/examples/DemoRunner/Builds/iOS/Info-App.plist b/examples/DemoRunner/Builds/iOS/Info-App.plist index a864c56f75..50c30b6f34 100644 --- a/examples/DemoRunner/Builds/iOS/Info-App.plist +++ b/examples/DemoRunner/Builds/iOS/Info-App.plist @@ -7,12 +7,14 @@ NSMicrophoneUsageDescription This is an audio app which requires audio input. If you do not have a USB audio interface connected it will use the microphone. + NSCameraUsageDescription + This app requires camera usage to function properly. UIViewControllerBasedStatusBarAppearance CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier - com.roli.juce.demorunner + com.juce.demorunner CFBundleName DemoRunner CFBundleDisplayName @@ -39,6 +41,7 @@ UISupportedInterfaceOrientations + UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight diff --git a/examples/DemoRunner/DemoRunner.jucer b/examples/DemoRunner/DemoRunner.jucer index 03a22f3935..11176d38fc 100644 --- a/examples/DemoRunner/DemoRunner.jucer +++ b/examples/DemoRunner/DemoRunner.jucer @@ -1,9 +1,9 @@ + companyEmail="info@juce.com" id="yj7xMM" reportAppUsage="1"> @@ -88,10 +88,10 @@ + smallIcon="YyqWd2" bigIcon="YyqWd2" cameraPermissionNeeded="1"> @@ -119,10 +119,10 @@ - + diff --git a/examples/DemoRunner/JuceLibraryCode/AppConfig.h b/examples/DemoRunner/JuceLibraryCode/AppConfig.h index 9b1c334db5..de08905c2f 100644 --- a/examples/DemoRunner/JuceLibraryCode/AppConfig.h +++ b/examples/DemoRunner/JuceLibraryCode/AppConfig.h @@ -156,6 +156,10 @@ //#define JUCE_PLUGINHOST_AU 0 #endif +#ifndef JUCE_PLUGINHOST_LADSPA + //#define JUCE_PLUGINHOST_LADSPA 0 +#endif + //============================================================================== // juce_audio_utils flags: diff --git a/examples/DemoRunner/Source/Demos/DemoPIPs2.cpp b/examples/DemoRunner/Source/Demos/DemoPIPs2.cpp index 588d5b997f..98ed93834e 100644 --- a/examples/DemoRunner/Source/Demos/DemoPIPs2.cpp +++ b/examples/DemoRunner/Source/Demos/DemoPIPs2.cpp @@ -34,7 +34,7 @@ #include "../../../GUI/AnimationAppDemo.h" #include "../../../GUI/AnimationDemo.h" #include "../../../GUI/BouncingBallWavetableDemo.h" -#if JUCE_MAC || JUCE_WINDOWS +#if JUCE_USE_CAMERA && ! JUCE_LINUX #include "../../../GUI/CameraDemo.h" #endif #if ! JUCE_ANDROID @@ -73,7 +73,7 @@ void registerDemos_Two() noexcept REGISTER_DEMO (AnimationAppDemo, GUI, false) REGISTER_DEMO (AnimationDemo, GUI, false) REGISTER_DEMO (BouncingBallWavetableDemo, GUI, false) - #if JUCE_MAC || JUCE_WINDOWS + #if JUCE_USE_CAMERA && ! JUCE_LINUX REGISTER_DEMO (CameraDemo, GUI, true) #endif #if ! JUCE_ANDROID diff --git a/examples/DemoRunner/Source/Main.cpp b/examples/DemoRunner/Source/Main.cpp index 71379fba93..ad770d7e00 100644 --- a/examples/DemoRunner/Source/Main.cpp +++ b/examples/DemoRunner/Source/Main.cpp @@ -122,6 +122,7 @@ private: #if JUCE_IOS || JUCE_ANDROID setFullScreen (true); + Desktop::getInstance().setOrientationsEnabled (Desktop::rotatedClockwise | Desktop::rotatedAntiClockwise); #else setBounds ((int) (0.1f * getParentWidth()), (int) (0.1f * getParentHeight()), diff --git a/examples/GUI/CameraDemo.h b/examples/GUI/CameraDemo.h index 300c64cc2e..6c1ef60964 100644 --- a/examples/GUI/CameraDemo.h +++ b/examples/GUI/CameraDemo.h @@ -31,7 +31,7 @@ dependencies: juce_core, juce_cryptography, juce_data_structures, juce_events, juce_graphics, juce_gui_basics, juce_gui_extra, juce_video - exporters: xcode_mac, vs2017, linux_make + exporters: xcode_mac, vs2017, androidstudio, xcode_iphone moduleFlags: JUCE_USE_CAMERA=1 @@ -49,15 +49,18 @@ #include "../Assets/DemoUtilities.h" //============================================================================== -class CameraDemo : public Component, - private CameraDevice::Listener, - private AsyncUpdater +class CameraDemo : public Component { public: CameraDemo() { setOpaque (true); + #if JUCE_ANDROID + // Android requires exclusive access to the audio device when recording videos. + audioDeviceManager.closeAudioDevice(); + #endif + addAndMakeVisible (cameraSelectorComboBox); updateCameraList(); cameraSelectorComboBox.setSelectedId (1); @@ -76,6 +79,21 @@ public: cameraSelectorComboBox.setSelectedId (2); setSize (500, 500); + + #if JUCE_IOS || JUCE_ANDROID + setPortraitOrientationEnabled (true); + #endif + } + + ~CameraDemo() + { + #if JUCE_IOS || JUCE_ANDROID + setPortraitOrientationEnabled (false); + #endif + + #if JUCE_ANDROID + audioDeviceManager.restartLastAudioDevice(); + #endif } //============================================================================== @@ -101,26 +119,66 @@ public: recordMovieButton.setBounds (top.removeFromLeft (recordMovieButton.getWidth())); r.removeFromTop (4); - auto previewArea = r.removeFromTop (r.getHeight() / 2); + auto previewArea = shouldUseLandscapeLayout() ? r.removeFromLeft (r.getWidth() / 2) + : r.removeFromTop (r.getHeight() / 2); if (cameraPreviewComp.get() != nullptr) cameraPreviewComp->setBounds (previewArea); - r.removeFromTop (4); + if (shouldUseLandscapeLayout()) + r.removeFromLeft (4); + else + r.removeFromTop (4); + lastSnapshot.setBounds (r); } private: //============================================================================== + // if this PIP is running inside the demo runner, we'll use the shared device manager instead + #ifndef JUCE_DEMO_RUNNER + AudioDeviceManager audioDeviceManager; + #else + AudioDeviceManager& audioDeviceManager { getSharedAudioDeviceManager (0, 2) }; + #endif + std::unique_ptr cameraDevice; std::unique_ptr cameraPreviewComp; ImageComponent lastSnapshot; ComboBox cameraSelectorComboBox { "Camera" }; TextButton snapshotButton { "Take a snapshot" }; + #if ! JUCE_ANDROID && ! JUCE_IOS TextButton recordMovieButton { "Record a movie (to your desktop)..." }; + #else + TextButton recordMovieButton { "Record a movie" }; + #endif bool recordingMovie = false; + File recordingFile; + bool contentSharingPending = false; + + void setPortraitOrientationEnabled (bool shouldBeEnabled) + { + auto allowedOrientations = Desktop::getInstance().getOrientationsEnabled(); + + if (shouldBeEnabled) + allowedOrientations |= Desktop::upright; + else + allowedOrientations &= ~Desktop::upright; + + Desktop::getInstance().setOrientationsEnabled (allowedOrientations); + } + + bool shouldUseLandscapeLayout() const noexcept + { + #if JUCE_ANDROID || JUCE_IOS + auto orientation = Desktop::getInstance().getCurrentOrientation(); + return orientation == Desktop::rotatedClockwise || orientation == Desktop::rotatedAntiClockwise; + #else + return false; + #endif + } void updateCameraList() { @@ -137,25 +195,68 @@ private: void cameraChanged() { // This is called when the user chooses a camera from the drop-down list. - cameraDevice .reset(); + #if JUCE_IOS + // On iOS, when switching camera, open the new camera first, so that it can + // share the underlying camera session with the old camera. Otherwise, the + // session would have to be closed first, which can take several seconds. + if (cameraSelectorComboBox.getSelectedId() == 1) + cameraDevice.reset(); + #else + cameraDevice.reset(); + #endif cameraPreviewComp.reset(); recordingMovie = false; if (cameraSelectorComboBox.getSelectedId() > 1) { - // Try to open the user's choice of camera.. - cameraDevice.reset (CameraDevice::openDevice (cameraSelectorComboBox.getSelectedId() - 2)); + #if JUCE_ANDROID || JUCE_IOS + openCameraAsync(); + #else + cameraDeviceOpenResult (CameraDevice::openDevice (cameraSelectorComboBox.getSelectedId() - 2), {}); + #endif + } + else + { + snapshotButton .setEnabled (cameraDevice != nullptr && ! contentSharingPending); + recordMovieButton.setEnabled (cameraDevice != nullptr && ! contentSharingPending); + resized(); + } + } - // and if it worked, create a preview component for it.. - if (cameraDevice.get() != nullptr) - { - cameraPreviewComp.reset (cameraDevice->createViewerComponent()); - addAndMakeVisible (cameraPreviewComp.get()); - } + void openCameraAsync() + { + SafePointer safeThis (this); + + CameraDevice::openDeviceAsync (cameraSelectorComboBox.getSelectedId() - 2, + [safeThis] (CameraDevice* device, const String& error) mutable + { + if (safeThis) + safeThis->cameraDeviceOpenResult (device, error); + }); + } + + void cameraDeviceOpenResult (CameraDevice* device, const String& error) + { + // If camera opening worked, create a preview component for it.. + cameraDevice.reset (device); + + if (cameraDevice.get() != nullptr) + { + #if JUCE_ANDROID + SafePointer safeThis (this); + cameraDevice->onErrorOccurred = [safeThis] (const String& error) mutable { if (safeThis) safeThis->errorOccurred (error); }; + #endif + cameraPreviewComp.reset (cameraDevice->createViewerComponent()); + addAndMakeVisible (cameraPreviewComp.get()); + } + else + { + AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, "Camera open failed", + "Camera open failed, reason: " + error); } - snapshotButton .setEnabled (cameraDevice.get() != nullptr); - recordMovieButton.setEnabled (cameraDevice.get() != nullptr); + snapshotButton .setEnabled (cameraDevice.get() != nullptr && ! contentSharingPending); + recordMovieButton.setEnabled (cameraDevice.get() != nullptr && ! contentSharingPending); resized(); } @@ -169,10 +270,20 @@ private: // Start recording to a file on the user's desktop.. recordingMovie = true; - auto file = File::getSpecialLocation (File::userDesktopDirectory) - .getNonexistentChildFile ("JuceCameraDemo", CameraDevice::getFileExtension()); + #if JUCE_ANDROID || JUCE_IOS + recordingFile = File::getSpecialLocation (File::tempDirectory) + #else + recordingFile = File::getSpecialLocation (File::userDesktopDirectory) + #endif + .getNonexistentChildFile ("JuceCameraVideoDemo", CameraDevice::getFileExtension()); - cameraDevice->startRecordingToFile (file); + #if JUCE_ANDROID + // Android does not support taking pictures while recording video. + snapshotButton.setEnabled (false); + #endif + + cameraSelectorComboBox.setEnabled (false); + cameraDevice->startRecordingToFile (recordingFile); recordMovieButton.setButtonText ("Stop Recording"); } else @@ -180,40 +291,99 @@ private: // Already recording, so stop... recordingMovie = false; cameraDevice->stopRecording(); + #if ! JUCE_ANDROID && ! JUCE_IOS recordMovieButton.setButtonText ("Start recording (to a file on your desktop)"); + #else + recordMovieButton.setButtonText ("Record a movie"); + #endif + cameraSelectorComboBox.setEnabled (true); + + #if JUCE_ANDROID + snapshotButton.setEnabled (true); + #endif + + #if JUCE_ANDROID || JUCE_IOS + URL url (recordingFile); + + snapshotButton .setEnabled (false); + recordMovieButton.setEnabled (false); + contentSharingPending = true; + + SafePointer safeThis (this); + + juce::ContentSharer::getInstance()->shareFiles ({url}, + [safeThis] (bool success, const String&) mutable + { + if (safeThis) + safeThis->sharingFinished (success, false); + }); + #endif } } } void takeSnapshot() { - // When the user clicks the snapshot button, we'll attach ourselves to - // the camera as a listener, and wait for an image to arrive... - cameraDevice->addListener (this); + SafePointer safeThis (this); + cameraDevice->takeStillPicture ([safeThis] (const Image& image) mutable { safeThis->imageReceived (image); }); } // This is called by the camera device when a new image arrives - void imageReceived (const Image& image) override + void imageReceived (const Image& image) { - // In this app we just want to take one image, so as soon as this happens, - // we'll unregister ourselves as a listener. - if (cameraDevice.get() != nullptr) - cameraDevice->removeListener (this); + if (! image.isValid()) + return; - // This callback won't be on the message thread, so to get the image back to - // the message thread, we'll stash a pointer to it (which is reference-counted in - // a thead-safe way), and trigger an async callback which will then display the - // new image.. - incomingImage = image; - triggerAsyncUpdate(); + lastSnapshot.setImage (image); + + #if JUCE_ANDROID || JUCE_IOS + auto imageFile = File::getSpecialLocation (File::tempDirectory).getNonexistentChildFile ("JuceCameraPhotoDemo", ".jpg"); + + if (auto stream = std::unique_ptr (imageFile.createOutputStream())) + { + if (JPEGImageFormat().writeImageToStream (image, *stream)) + { + URL url (imageFile); + + snapshotButton .setEnabled (false); + recordMovieButton.setEnabled (false); + contentSharingPending = true; + + SafePointer safeThis (this); + + juce::ContentSharer::getInstance()->shareFiles ({url}, + [safeThis] (bool success, const String&) mutable + { + if (safeThis) + safeThis->sharingFinished (success, true); + }); + } + } + #endif } - Image incomingImage; - - void handleAsyncUpdate() override + void errorOccurred (const String& error) { - if (incomingImage.isValid()) - lastSnapshot.setImage (incomingImage); + AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, + "Camera Device Error", + "An error has occurred: " + error + " Camera will be closed."); + + cameraDevice.reset(); + + cameraSelectorComboBox.setSelectedId (1); + snapshotButton .setEnabled (false); + recordMovieButton.setEnabled (false); + } + + void sharingFinished (bool success, bool isCapture) + { + AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, + isCapture ? "Image sharing result" : "Video sharing result", + success ? "Success!" : "Failed!"); + + contentSharingPending = false; + snapshotButton .setEnabled (true); + recordMovieButton.setEnabled (true); } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CameraDemo) diff --git a/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt b/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt index 1acb8f22b0..f688f097d1 100644 --- a/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt +++ b/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt @@ -399,6 +399,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_core/containers/juce_ReferenceCountedArray.h" "../../../../../modules/juce_core/containers/juce_ScopedValueSetter.h" "../../../../../modules/juce_core/containers/juce_SortedSet.h" + "../../../../../modules/juce_core/containers/juce_SparseSet.cpp" "../../../../../modules/juce_core/containers/juce_SparseSet.h" "../../../../../modules/juce_core/containers/juce_Variant.cpp" "../../../../../modules/juce_core/containers/juce_Variant.h" @@ -1566,6 +1567,7 @@ set_source_files_properties("../../../../../modules/juce_core/containers/juce_Pr set_source_files_properties("../../../../../modules/juce_core/containers/juce_ReferenceCountedArray.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/containers/juce_ScopedValueSetter.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/containers/juce_SortedSet.h" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_core/containers/juce_SparseSet.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/containers/juce_SparseSet.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/containers/juce_Variant.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/containers/juce_Variant.h" PROPERTIES HEADER_FILE_ONLY TRUE) diff --git a/extras/AudioPerformanceTest/Builds/Android/app/src/main/java/com/juce/audioperformancetest/AudioPerformanceTest.java b/extras/AudioPerformanceTest/Builds/Android/app/src/main/java/com/juce/audioperformancetest/AudioPerformanceTest.java index 28e29b6460..2ca9c6d929 100644 --- a/extras/AudioPerformanceTest/Builds/Android/app/src/main/java/com/juce/audioperformancetest/AudioPerformanceTest.java +++ b/extras/AudioPerformanceTest/Builds/Android/app/src/main/java/com/juce/audioperformancetest/AudioPerformanceTest.java @@ -30,6 +30,7 @@ import android.content.Intent; import android.content.res.Configuration; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.hardware.camera2.*; import android.net.http.SslError; import android.net.Uri; import android.os.Bundle; @@ -119,6 +120,7 @@ public class AudioPerformanceTest extends Activity private static final int JUCE_PERMISSIONS_BLUETOOTH_MIDI = 2; private static final int JUCE_PERMISSIONS_READ_EXTERNAL_STORAGE = 3; private static final int JUCE_PERMISSIONS_WRITE_EXTERNAL_STORAGE = 4; + private static final int JUCE_PERMISSIONS_CAMERA = 5; private static String getAndroidPermissionName (int permissionID) { @@ -129,6 +131,7 @@ public class AudioPerformanceTest extends Activity // use string value as this is not defined in SDKs < 16 case JUCE_PERMISSIONS_READ_EXTERNAL_STORAGE: return "android.permission.READ_EXTERNAL_STORAGE"; case JUCE_PERMISSIONS_WRITE_EXTERNAL_STORAGE: return Manifest.permission.WRITE_EXTERNAL_STORAGE; + case JUCE_PERMISSIONS_CAMERA: return Manifest.permission.CAMERA; } // unknown permission ID! @@ -1205,6 +1208,7 @@ public class AudioPerformanceTest extends Activity setVolumeControlStream (AudioManager.STREAM_MUSIC); permissionCallbackPtrMap = new HashMap(); + appPausedResumedListeners = new HashMap(); } @Override @@ -1221,6 +1225,11 @@ public class AudioPerformanceTest extends Activity { suspendApp(); + Long[] keys = appPausedResumedListeners.keySet().toArray (new Long[appPausedResumedListeners.keySet().size()]); + + for (Long k : keys) + appPausedResumedListeners.get (k).appPaused(); + try { Thread.sleep (1000); // This is a bit of a hack to avoid some hard-to-track-down @@ -1236,12 +1245,10 @@ public class AudioPerformanceTest extends Activity super.onResume(); resumeApp(); - // Ensure that navigation/status bar visibility is correctly restored. - for (int i = 0; i < viewHolder.getChildCount(); ++i) - { - if (viewHolder.getChildAt (i) instanceof ComponentPeerView) - ((ComponentPeerView) viewHolder.getChildAt (i)).appResumed(); - } + Long[] keys = appPausedResumedListeners.keySet().toArray (new Long[appPausedResumedListeners.keySet().size()]); + + for (Long k : keys) + appPausedResumedListeners.get (k).appResumed(); } @Override @@ -1368,11 +1375,14 @@ public class AudioPerformanceTest extends Activity { ComponentPeerView v = new ComponentPeerView (this, opaque, host); viewHolder.addView (v); + addAppPausedResumedListener (v, host); return v; } public final void deleteView (ComponentPeerView view) { + removeAppPausedResumedListener (view, view.host); + view.host = 0; ViewGroup group = (ViewGroup) (view.getParent()); @@ -1590,9 +1600,28 @@ public class AudioPerformanceTest extends Activity public native void alertDismissed (long callback, int id); + //============================================================================== + public interface AppPausedResumedListener + { + void appPaused(); + void appResumed(); + } + + private Map appPausedResumedListeners; + + public void addAppPausedResumedListener (AppPausedResumedListener l, long listenerHost) + { + appPausedResumedListeners.put (new Long (listenerHost), l); + } + + public void removeAppPausedResumedListener (AppPausedResumedListener l, long listenerHost) + { + appPausedResumedListeners.remove (new Long (listenerHost)); + } + //============================================================================== public final class ComponentPeerView extends ViewGroup - implements View.OnFocusChangeListener + implements View.OnFocusChangeListener, AppPausedResumedListener { public ComponentPeerView (Context context, boolean opaque_, long host) { @@ -1940,13 +1969,25 @@ public class AudioPerformanceTest extends Activity } //============================================================================== + private native void handleAppPaused (long host); private native void handleAppResumed (long host); + @Override + public void appPaused() + { + if (host == 0) + return; + + handleAppPaused (host); + } + + @Override public void appResumed() { if (host == 0) return; + // Ensure that navigation/status bar visibility is correctly restored. handleAppResumed (host); } } @@ -2616,6 +2657,175 @@ public class AudioPerformanceTest extends Activity private final Object hostLock = new Object(); } + + //============================================================================== + public class CameraDeviceStateCallback extends CameraDevice.StateCallback + { + private native void cameraDeviceStateClosed (long host, CameraDevice camera); + private native void cameraDeviceStateDisconnected (long host, CameraDevice camera); + private native void cameraDeviceStateError (long host, CameraDevice camera, int error); + private native void cameraDeviceStateOpened (long host, CameraDevice camera); + + CameraDeviceStateCallback (long hostToUse) + { + host = hostToUse; + } + + @Override + public void onClosed (CameraDevice camera) + { + cameraDeviceStateClosed (host, camera); + } + + @Override + public void onDisconnected (CameraDevice camera) + { + cameraDeviceStateDisconnected (host, camera); + } + + @Override + public void onError (CameraDevice camera, int error) + { + cameraDeviceStateError (host, camera, error); + } + + @Override + public void onOpened (CameraDevice camera) + { + cameraDeviceStateOpened (host, camera); + } + + private long host; + } + + //============================================================================== + public class CameraCaptureSessionStateCallback extends CameraCaptureSession.StateCallback + { + private native void cameraCaptureSessionActive (long host, CameraCaptureSession session); + private native void cameraCaptureSessionClosed (long host, CameraCaptureSession session); + private native void cameraCaptureSessionConfigureFailed (long host, CameraCaptureSession session); + private native void cameraCaptureSessionConfigured (long host, CameraCaptureSession session); + private native void cameraCaptureSessionReady (long host, CameraCaptureSession session); + + CameraCaptureSessionStateCallback (long hostToUse) + { + host = hostToUse; + } + + @Override + public void onActive (CameraCaptureSession session) + { + cameraCaptureSessionActive (host, session); + } + + @Override + public void onClosed (CameraCaptureSession session) + { + cameraCaptureSessionClosed (host, session); + } + + @Override + public void onConfigureFailed (CameraCaptureSession session) + { + cameraCaptureSessionConfigureFailed (host, session); + } + + @Override + public void onConfigured (CameraCaptureSession session) + { + cameraCaptureSessionConfigured (host, session); + } + + @Override + public void onReady (CameraCaptureSession session) + { + cameraCaptureSessionReady (host, session); + } + + private long host; + } + + //============================================================================== + public class CameraCaptureSessionCaptureCallback extends CameraCaptureSession.CaptureCallback + { + private native void cameraCaptureSessionCaptureCompleted (long host, boolean isPreview, CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result); + private native void cameraCaptureSessionCaptureFailed (long host, boolean isPreview, CameraCaptureSession session, CaptureRequest request, CaptureFailure failure); + private native void cameraCaptureSessionCaptureProgressed (long host, boolean isPreview, CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult); + private native void cameraCaptureSessionCaptureStarted (long host, boolean isPreview, CameraCaptureSession session, CaptureRequest request, long timestamp, long frameNumber); + private native void cameraCaptureSessionCaptureSequenceAborted (long host, boolean isPreview, CameraCaptureSession session, int sequenceId); + private native void cameraCaptureSessionCaptureSequenceCompleted (long host, boolean isPreview, CameraCaptureSession session, int sequenceId, long frameNumber); + + CameraCaptureSessionCaptureCallback (long hostToUse, boolean shouldBePreview) + { + host = hostToUse; + preview = shouldBePreview; + } + + @Override + public void onCaptureCompleted (CameraCaptureSession session, CaptureRequest request, + TotalCaptureResult result) + { + cameraCaptureSessionCaptureCompleted (host, preview, session, request, result); + } + + @Override + public void onCaptureFailed (CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) + { + cameraCaptureSessionCaptureFailed (host, preview, session, request, failure); + } + + @Override + public void onCaptureProgressed (CameraCaptureSession session, CaptureRequest request, + CaptureResult partialResult) + { + cameraCaptureSessionCaptureProgressed (host, preview, session, request, partialResult); + } + + @Override + public void onCaptureSequenceAborted (CameraCaptureSession session, int sequenceId) + { + cameraCaptureSessionCaptureSequenceAborted (host, preview, session, sequenceId); + } + + @Override + public void onCaptureSequenceCompleted (CameraCaptureSession session, int sequenceId, long frameNumber) + { + cameraCaptureSessionCaptureSequenceCompleted (host, preview, session, sequenceId, frameNumber); + } + + @Override + public void onCaptureStarted (CameraCaptureSession session, CaptureRequest request, long timestamp, + long frameNumber) + { + cameraCaptureSessionCaptureStarted (host, preview, session, request, timestamp, frameNumber); + } + + private long host; + private boolean preview; + } + + //============================================================================== + public class JuceOrientationEventListener extends OrientationEventListener + { + private native void deviceOrientationChanged (long host, int orientation); + + public JuceOrientationEventListener (long hostToUse, Context context, int rate) + { + super (context, rate); + + host = hostToUse; + } + + @Override + public void onOrientationChanged (int orientation) + { + deviceOrientationChanged (host, orientation); + } + + private long host; + } + + //============================================================================== public static final String getLocaleValue (boolean isRegion) { diff --git a/extras/AudioPerformanceTest/Builds/VisualStudio2017/AudioPerformanceTest_App.vcxproj b/extras/AudioPerformanceTest/Builds/VisualStudio2017/AudioPerformanceTest_App.vcxproj index e050297cb4..a7c1f197f1 100644 --- a/extras/AudioPerformanceTest/Builds/VisualStudio2017/AudioPerformanceTest_App.vcxproj +++ b/extras/AudioPerformanceTest/Builds/VisualStudio2017/AudioPerformanceTest_App.vcxproj @@ -596,6 +596,9 @@ true + + true + true diff --git a/extras/AudioPerformanceTest/Builds/VisualStudio2017/AudioPerformanceTest_App.vcxproj.filters b/extras/AudioPerformanceTest/Builds/VisualStudio2017/AudioPerformanceTest_App.vcxproj.filters index 7a52572e9f..f593065254 100644 --- a/extras/AudioPerformanceTest/Builds/VisualStudio2017/AudioPerformanceTest_App.vcxproj.filters +++ b/extras/AudioPerformanceTest/Builds/VisualStudio2017/AudioPerformanceTest_App.vcxproj.filters @@ -817,6 +817,9 @@ JUCE Modules\juce_core\containers + + JUCE Modules\juce_core\containers + JUCE Modules\juce_core\containers diff --git a/extras/AudioPerformanceTest/JuceLibraryCode/AppConfig.h b/extras/AudioPerformanceTest/JuceLibraryCode/AppConfig.h index 109ec4a772..29b0ea00b5 100644 --- a/extras/AudioPerformanceTest/JuceLibraryCode/AppConfig.h +++ b/extras/AudioPerformanceTest/JuceLibraryCode/AppConfig.h @@ -147,6 +147,10 @@ //#define JUCE_PLUGINHOST_AU 0 #endif +#ifndef JUCE_PLUGINHOST_LADSPA + //#define JUCE_PLUGINHOST_LADSPA 0 +#endif + //============================================================================== // juce_audio_utils flags: diff --git a/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt b/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt index 11a1f09e18..3b05e76a11 100644 --- a/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt +++ b/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt @@ -415,6 +415,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_core/containers/juce_ReferenceCountedArray.h" "../../../../../modules/juce_core/containers/juce_ScopedValueSetter.h" "../../../../../modules/juce_core/containers/juce_SortedSet.h" + "../../../../../modules/juce_core/containers/juce_SparseSet.cpp" "../../../../../modules/juce_core/containers/juce_SparseSet.h" "../../../../../modules/juce_core/containers/juce_Variant.cpp" "../../../../../modules/juce_core/containers/juce_Variant.h" @@ -1250,6 +1251,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_video/capture/juce_CameraDevice.cpp" "../../../../../modules/juce_video/capture/juce_CameraDevice.h" "../../../../../modules/juce_video/native/juce_android_CameraDevice.h" + "../../../../../modules/juce_video/native/juce_ios_CameraDevice.h" "../../../../../modules/juce_video/native/juce_mac_CameraDevice.h" "../../../../../modules/juce_video/native/juce_mac_Video.h" "../../../../../modules/juce_video/native/juce_win32_CameraDevice.h" @@ -1653,6 +1655,7 @@ set_source_files_properties("../../../../../modules/juce_core/containers/juce_Pr set_source_files_properties("../../../../../modules/juce_core/containers/juce_ReferenceCountedArray.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/containers/juce_ScopedValueSetter.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/containers/juce_SortedSet.h" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_core/containers/juce_SparseSet.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/containers/juce_SparseSet.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/containers/juce_Variant.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/containers/juce_Variant.h" PROPERTIES HEADER_FILE_ONLY TRUE) @@ -2488,6 +2491,7 @@ set_source_files_properties("../../../../../modules/juce_opengl/juce_opengl.h" P set_source_files_properties("../../../../../modules/juce_video/capture/juce_CameraDevice.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_video/capture/juce_CameraDevice.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_video/native/juce_android_CameraDevice.h" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_video/native/juce_ios_CameraDevice.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_video/native/juce_mac_CameraDevice.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_video/native/juce_mac_Video.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_video/native/juce_win32_CameraDevice.h" PROPERTIES HEADER_FILE_ONLY TRUE) diff --git a/extras/AudioPluginHost/Builds/Android/app/src/main/java/com/roli/juce/pluginhost/AudioPluginHost.java b/extras/AudioPluginHost/Builds/Android/app/src/main/java/com/roli/juce/pluginhost/AudioPluginHost.java index 11134d24e2..2b62f5a596 100644 --- a/extras/AudioPluginHost/Builds/Android/app/src/main/java/com/roli/juce/pluginhost/AudioPluginHost.java +++ b/extras/AudioPluginHost/Builds/Android/app/src/main/java/com/roli/juce/pluginhost/AudioPluginHost.java @@ -30,6 +30,7 @@ import android.content.Intent; import android.content.res.Configuration; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.hardware.camera2.*; import android.net.http.SslError; import android.net.Uri; import android.os.Bundle; @@ -119,6 +120,7 @@ public class AudioPluginHost extends Activity private static final int JUCE_PERMISSIONS_BLUETOOTH_MIDI = 2; private static final int JUCE_PERMISSIONS_READ_EXTERNAL_STORAGE = 3; private static final int JUCE_PERMISSIONS_WRITE_EXTERNAL_STORAGE = 4; + private static final int JUCE_PERMISSIONS_CAMERA = 5; private static String getAndroidPermissionName (int permissionID) { @@ -129,6 +131,7 @@ public class AudioPluginHost extends Activity // use string value as this is not defined in SDKs < 16 case JUCE_PERMISSIONS_READ_EXTERNAL_STORAGE: return "android.permission.READ_EXTERNAL_STORAGE"; case JUCE_PERMISSIONS_WRITE_EXTERNAL_STORAGE: return Manifest.permission.WRITE_EXTERNAL_STORAGE; + case JUCE_PERMISSIONS_CAMERA: return Manifest.permission.CAMERA; } // unknown permission ID! @@ -1205,6 +1208,7 @@ public class AudioPluginHost extends Activity setVolumeControlStream (AudioManager.STREAM_MUSIC); permissionCallbackPtrMap = new HashMap(); + appPausedResumedListeners = new HashMap(); } @Override @@ -1221,6 +1225,11 @@ public class AudioPluginHost extends Activity { suspendApp(); + Long[] keys = appPausedResumedListeners.keySet().toArray (new Long[appPausedResumedListeners.keySet().size()]); + + for (Long k : keys) + appPausedResumedListeners.get (k).appPaused(); + try { Thread.sleep (1000); // This is a bit of a hack to avoid some hard-to-track-down @@ -1236,12 +1245,10 @@ public class AudioPluginHost extends Activity super.onResume(); resumeApp(); - // Ensure that navigation/status bar visibility is correctly restored. - for (int i = 0; i < viewHolder.getChildCount(); ++i) - { - if (viewHolder.getChildAt (i) instanceof ComponentPeerView) - ((ComponentPeerView) viewHolder.getChildAt (i)).appResumed(); - } + Long[] keys = appPausedResumedListeners.keySet().toArray (new Long[appPausedResumedListeners.keySet().size()]); + + for (Long k : keys) + appPausedResumedListeners.get (k).appResumed(); } @Override @@ -1368,11 +1375,14 @@ public class AudioPluginHost extends Activity { ComponentPeerView v = new ComponentPeerView (this, opaque, host); viewHolder.addView (v); + addAppPausedResumedListener (v, host); return v; } public final void deleteView (ComponentPeerView view) { + removeAppPausedResumedListener (view, view.host); + view.host = 0; ViewGroup group = (ViewGroup) (view.getParent()); @@ -1590,9 +1600,28 @@ public class AudioPluginHost extends Activity public native void alertDismissed (long callback, int id); + //============================================================================== + public interface AppPausedResumedListener + { + void appPaused(); + void appResumed(); + } + + private Map appPausedResumedListeners; + + public void addAppPausedResumedListener (AppPausedResumedListener l, long listenerHost) + { + appPausedResumedListeners.put (new Long (listenerHost), l); + } + + public void removeAppPausedResumedListener (AppPausedResumedListener l, long listenerHost) + { + appPausedResumedListeners.remove (new Long (listenerHost)); + } + //============================================================================== public final class ComponentPeerView extends ViewGroup - implements View.OnFocusChangeListener + implements View.OnFocusChangeListener, AppPausedResumedListener { public ComponentPeerView (Context context, boolean opaque_, long host) { @@ -1940,13 +1969,25 @@ public class AudioPluginHost extends Activity } //============================================================================== + private native void handleAppPaused (long host); private native void handleAppResumed (long host); + @Override + public void appPaused() + { + if (host == 0) + return; + + handleAppPaused (host); + } + + @Override public void appResumed() { if (host == 0) return; + // Ensure that navigation/status bar visibility is correctly restored. handleAppResumed (host); } } @@ -2616,6 +2657,175 @@ public class AudioPluginHost extends Activity private final Object hostLock = new Object(); } + + //============================================================================== + public class CameraDeviceStateCallback extends CameraDevice.StateCallback + { + private native void cameraDeviceStateClosed (long host, CameraDevice camera); + private native void cameraDeviceStateDisconnected (long host, CameraDevice camera); + private native void cameraDeviceStateError (long host, CameraDevice camera, int error); + private native void cameraDeviceStateOpened (long host, CameraDevice camera); + + CameraDeviceStateCallback (long hostToUse) + { + host = hostToUse; + } + + @Override + public void onClosed (CameraDevice camera) + { + cameraDeviceStateClosed (host, camera); + } + + @Override + public void onDisconnected (CameraDevice camera) + { + cameraDeviceStateDisconnected (host, camera); + } + + @Override + public void onError (CameraDevice camera, int error) + { + cameraDeviceStateError (host, camera, error); + } + + @Override + public void onOpened (CameraDevice camera) + { + cameraDeviceStateOpened (host, camera); + } + + private long host; + } + + //============================================================================== + public class CameraCaptureSessionStateCallback extends CameraCaptureSession.StateCallback + { + private native void cameraCaptureSessionActive (long host, CameraCaptureSession session); + private native void cameraCaptureSessionClosed (long host, CameraCaptureSession session); + private native void cameraCaptureSessionConfigureFailed (long host, CameraCaptureSession session); + private native void cameraCaptureSessionConfigured (long host, CameraCaptureSession session); + private native void cameraCaptureSessionReady (long host, CameraCaptureSession session); + + CameraCaptureSessionStateCallback (long hostToUse) + { + host = hostToUse; + } + + @Override + public void onActive (CameraCaptureSession session) + { + cameraCaptureSessionActive (host, session); + } + + @Override + public void onClosed (CameraCaptureSession session) + { + cameraCaptureSessionClosed (host, session); + } + + @Override + public void onConfigureFailed (CameraCaptureSession session) + { + cameraCaptureSessionConfigureFailed (host, session); + } + + @Override + public void onConfigured (CameraCaptureSession session) + { + cameraCaptureSessionConfigured (host, session); + } + + @Override + public void onReady (CameraCaptureSession session) + { + cameraCaptureSessionReady (host, session); + } + + private long host; + } + + //============================================================================== + public class CameraCaptureSessionCaptureCallback extends CameraCaptureSession.CaptureCallback + { + private native void cameraCaptureSessionCaptureCompleted (long host, boolean isPreview, CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result); + private native void cameraCaptureSessionCaptureFailed (long host, boolean isPreview, CameraCaptureSession session, CaptureRequest request, CaptureFailure failure); + private native void cameraCaptureSessionCaptureProgressed (long host, boolean isPreview, CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult); + private native void cameraCaptureSessionCaptureStarted (long host, boolean isPreview, CameraCaptureSession session, CaptureRequest request, long timestamp, long frameNumber); + private native void cameraCaptureSessionCaptureSequenceAborted (long host, boolean isPreview, CameraCaptureSession session, int sequenceId); + private native void cameraCaptureSessionCaptureSequenceCompleted (long host, boolean isPreview, CameraCaptureSession session, int sequenceId, long frameNumber); + + CameraCaptureSessionCaptureCallback (long hostToUse, boolean shouldBePreview) + { + host = hostToUse; + preview = shouldBePreview; + } + + @Override + public void onCaptureCompleted (CameraCaptureSession session, CaptureRequest request, + TotalCaptureResult result) + { + cameraCaptureSessionCaptureCompleted (host, preview, session, request, result); + } + + @Override + public void onCaptureFailed (CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) + { + cameraCaptureSessionCaptureFailed (host, preview, session, request, failure); + } + + @Override + public void onCaptureProgressed (CameraCaptureSession session, CaptureRequest request, + CaptureResult partialResult) + { + cameraCaptureSessionCaptureProgressed (host, preview, session, request, partialResult); + } + + @Override + public void onCaptureSequenceAborted (CameraCaptureSession session, int sequenceId) + { + cameraCaptureSessionCaptureSequenceAborted (host, preview, session, sequenceId); + } + + @Override + public void onCaptureSequenceCompleted (CameraCaptureSession session, int sequenceId, long frameNumber) + { + cameraCaptureSessionCaptureSequenceCompleted (host, preview, session, sequenceId, frameNumber); + } + + @Override + public void onCaptureStarted (CameraCaptureSession session, CaptureRequest request, long timestamp, + long frameNumber) + { + cameraCaptureSessionCaptureStarted (host, preview, session, request, timestamp, frameNumber); + } + + private long host; + private boolean preview; + } + + //============================================================================== + public class JuceOrientationEventListener extends OrientationEventListener + { + private native void deviceOrientationChanged (long host, int orientation); + + public JuceOrientationEventListener (long hostToUse, Context context, int rate) + { + super (context, rate); + + host = hostToUse; + } + + @Override + public void onOrientationChanged (int orientation) + { + deviceOrientationChanged (host, orientation); + } + + private long host; + } + + //============================================================================== public static final String getLocaleValue (boolean isRegion) { diff --git a/extras/AudioPluginHost/Builds/VisualStudio2013/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2013/AudioPluginHost_App.vcxproj index dcc1d5dd8a..5a14058346 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2013/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2013/AudioPluginHost_App.vcxproj @@ -601,6 +601,9 @@ true + + true + true @@ -2433,6 +2436,7 @@ + diff --git a/extras/AudioPluginHost/Builds/VisualStudio2013/AudioPluginHost_App.vcxproj.filters b/extras/AudioPluginHost/Builds/VisualStudio2013/AudioPluginHost_App.vcxproj.filters index 7415956798..d73bb1e334 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2013/AudioPluginHost_App.vcxproj.filters +++ b/extras/AudioPluginHost/Builds/VisualStudio2013/AudioPluginHost_App.vcxproj.filters @@ -874,6 +874,9 @@ JUCE Modules\juce_core\containers + + JUCE Modules\juce_core\containers + JUCE Modules\juce_core\containers @@ -4062,6 +4065,9 @@ JUCE Modules\juce_video\native + + JUCE Modules\juce_video\native + JUCE Modules\juce_video\native diff --git a/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj index a27197c849..03cea25fd4 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj @@ -601,6 +601,9 @@ true + + true + true @@ -2433,6 +2436,7 @@ + diff --git a/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj.filters b/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj.filters index 87f7846b1a..3425cca7a5 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj.filters +++ b/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj.filters @@ -874,6 +874,9 @@ JUCE Modules\juce_core\containers + + JUCE Modules\juce_core\containers + JUCE Modules\juce_core\containers @@ -4062,6 +4065,9 @@ JUCE Modules\juce_video\native + + JUCE Modules\juce_video\native + JUCE Modules\juce_video\native diff --git a/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj index 989607160f..5d76b0597b 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj @@ -601,6 +601,9 @@ true + + true + true @@ -2433,6 +2436,7 @@ + diff --git a/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj.filters b/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj.filters index 87a6356e2b..33384a787e 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj.filters +++ b/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj.filters @@ -874,6 +874,9 @@ JUCE Modules\juce_core\containers + + JUCE Modules\juce_core\containers + JUCE Modules\juce_core\containers @@ -4062,6 +4065,9 @@ JUCE Modules\juce_video\native + + JUCE Modules\juce_video\native + JUCE Modules\juce_video\native diff --git a/extras/BinaryBuilder/Builds/VisualStudio2017/BinaryBuilder_ConsoleApp.vcxproj b/extras/BinaryBuilder/Builds/VisualStudio2017/BinaryBuilder_ConsoleApp.vcxproj index 3249110293..151ce86a93 100644 --- a/extras/BinaryBuilder/Builds/VisualStudio2017/BinaryBuilder_ConsoleApp.vcxproj +++ b/extras/BinaryBuilder/Builds/VisualStudio2017/BinaryBuilder_ConsoleApp.vcxproj @@ -164,6 +164,9 @@ true + + true + true diff --git a/extras/BinaryBuilder/Builds/VisualStudio2017/BinaryBuilder_ConsoleApp.vcxproj.filters b/extras/BinaryBuilder/Builds/VisualStudio2017/BinaryBuilder_ConsoleApp.vcxproj.filters index 427c4c833e..1150273d70 100644 --- a/extras/BinaryBuilder/Builds/VisualStudio2017/BinaryBuilder_ConsoleApp.vcxproj.filters +++ b/extras/BinaryBuilder/Builds/VisualStudio2017/BinaryBuilder_ConsoleApp.vcxproj.filters @@ -91,6 +91,9 @@ JUCE Modules\juce_core\containers + + JUCE Modules\juce_core\containers + JUCE Modules\juce_core\containers diff --git a/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt b/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt index da821bb39c..0b94bf62c4 100644 --- a/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt +++ b/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt @@ -403,6 +403,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_core/containers/juce_ReferenceCountedArray.h" "../../../../../modules/juce_core/containers/juce_ScopedValueSetter.h" "../../../../../modules/juce_core/containers/juce_SortedSet.h" + "../../../../../modules/juce_core/containers/juce_SparseSet.cpp" "../../../../../modules/juce_core/containers/juce_SparseSet.h" "../../../../../modules/juce_core/containers/juce_Variant.cpp" "../../../../../modules/juce_core/containers/juce_Variant.h" @@ -1645,6 +1646,7 @@ set_source_files_properties("../../../../../modules/juce_core/containers/juce_Pr set_source_files_properties("../../../../../modules/juce_core/containers/juce_ReferenceCountedArray.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/containers/juce_ScopedValueSetter.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/containers/juce_SortedSet.h" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_core/containers/juce_SparseSet.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/containers/juce_SparseSet.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/containers/juce_Variant.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/containers/juce_Variant.h" PROPERTIES HEADER_FILE_ONLY TRUE) diff --git a/extras/NetworkGraphicsDemo/Builds/Android/app/src/main/java/com/juce/networkgraphicsdemo/JUCENetworkGraphicsDemo.java b/extras/NetworkGraphicsDemo/Builds/Android/app/src/main/java/com/juce/networkgraphicsdemo/JUCENetworkGraphicsDemo.java index e37f33ec90..fb24cf7354 100644 --- a/extras/NetworkGraphicsDemo/Builds/Android/app/src/main/java/com/juce/networkgraphicsdemo/JUCENetworkGraphicsDemo.java +++ b/extras/NetworkGraphicsDemo/Builds/Android/app/src/main/java/com/juce/networkgraphicsdemo/JUCENetworkGraphicsDemo.java @@ -113,6 +113,7 @@ public class JUCENetworkGraphicsDemo extends Activity private static final int JUCE_PERMISSIONS_BLUETOOTH_MIDI = 2; private static final int JUCE_PERMISSIONS_READ_EXTERNAL_STORAGE = 3; private static final int JUCE_PERMISSIONS_WRITE_EXTERNAL_STORAGE = 4; + private static final int JUCE_PERMISSIONS_CAMERA = 5; private static String getAndroidPermissionName (int permissionID) { @@ -123,6 +124,7 @@ public class JUCENetworkGraphicsDemo extends Activity // use string value as this is not defined in SDKs < 16 case JUCE_PERMISSIONS_READ_EXTERNAL_STORAGE: return "android.permission.READ_EXTERNAL_STORAGE"; case JUCE_PERMISSIONS_WRITE_EXTERNAL_STORAGE: return Manifest.permission.WRITE_EXTERNAL_STORAGE; + case JUCE_PERMISSIONS_CAMERA: return Manifest.permission.CAMERA; } // unknown permission ID! @@ -273,6 +275,7 @@ public class JUCENetworkGraphicsDemo extends Activity setVolumeControlStream (AudioManager.STREAM_MUSIC); permissionCallbackPtrMap = new HashMap(); + appPausedResumedListeners = new HashMap(); } @Override @@ -289,6 +292,11 @@ public class JUCENetworkGraphicsDemo extends Activity { suspendApp(); + Long[] keys = appPausedResumedListeners.keySet().toArray (new Long[appPausedResumedListeners.keySet().size()]); + + for (Long k : keys) + appPausedResumedListeners.get (k).appPaused(); + try { Thread.sleep (1000); // This is a bit of a hack to avoid some hard-to-track-down @@ -304,12 +312,10 @@ public class JUCENetworkGraphicsDemo extends Activity super.onResume(); resumeApp(); - // Ensure that navigation/status bar visibility is correctly restored. - for (int i = 0; i < viewHolder.getChildCount(); ++i) - { - if (viewHolder.getChildAt (i) instanceof ComponentPeerView) - ((ComponentPeerView) viewHolder.getChildAt (i)).appResumed(); - } + Long[] keys = appPausedResumedListeners.keySet().toArray (new Long[appPausedResumedListeners.keySet().size()]); + + for (Long k : keys) + appPausedResumedListeners.get (k).appResumed(); } @Override @@ -436,11 +442,14 @@ public class JUCENetworkGraphicsDemo extends Activity { ComponentPeerView v = new ComponentPeerView (this, opaque, host); viewHolder.addView (v); + addAppPausedResumedListener (v, host); return v; } public final void deleteView (ComponentPeerView view) { + removeAppPausedResumedListener (view, view.host); + view.host = 0; ViewGroup group = (ViewGroup) (view.getParent()); @@ -658,9 +667,28 @@ public class JUCENetworkGraphicsDemo extends Activity public native void alertDismissed (long callback, int id); + //============================================================================== + public interface AppPausedResumedListener + { + void appPaused(); + void appResumed(); + } + + private Map appPausedResumedListeners; + + public void addAppPausedResumedListener (AppPausedResumedListener l, long listenerHost) + { + appPausedResumedListeners.put (new Long (listenerHost), l); + } + + public void removeAppPausedResumedListener (AppPausedResumedListener l, long listenerHost) + { + appPausedResumedListeners.remove (new Long (listenerHost)); + } + //============================================================================== public final class ComponentPeerView extends ViewGroup - implements View.OnFocusChangeListener + implements View.OnFocusChangeListener, AppPausedResumedListener { public ComponentPeerView (Context context, boolean opaque_, long host) { @@ -1008,13 +1036,25 @@ public class JUCENetworkGraphicsDemo extends Activity } //============================================================================== + private native void handleAppPaused (long host); private native void handleAppResumed (long host); + @Override + public void appPaused() + { + if (host == 0) + return; + + handleAppPaused (host); + } + + @Override public void appResumed() { if (host == 0) return; + // Ensure that navigation/status bar visibility is correctly restored. handleAppResumed (host); } } @@ -1656,6 +1696,7 @@ public class JUCENetworkGraphicsDemo extends Activity private final Object hostLock = new Object(); } + //============================================================================== public static final String getLocaleValue (boolean isRegion) { diff --git a/extras/NetworkGraphicsDemo/Builds/VisualStudio2013/NetworkGraphicsDemo_App.vcxproj b/extras/NetworkGraphicsDemo/Builds/VisualStudio2013/NetworkGraphicsDemo_App.vcxproj index 68a37b87c1..deec8db869 100644 --- a/extras/NetworkGraphicsDemo/Builds/VisualStudio2013/NetworkGraphicsDemo_App.vcxproj +++ b/extras/NetworkGraphicsDemo/Builds/VisualStudio2013/NetworkGraphicsDemo_App.vcxproj @@ -596,6 +596,9 @@ true + + true + true diff --git a/extras/NetworkGraphicsDemo/Builds/VisualStudio2013/NetworkGraphicsDemo_App.vcxproj.filters b/extras/NetworkGraphicsDemo/Builds/VisualStudio2013/NetworkGraphicsDemo_App.vcxproj.filters index 0694a1b515..fdeb11f148 100644 --- a/extras/NetworkGraphicsDemo/Builds/VisualStudio2013/NetworkGraphicsDemo_App.vcxproj.filters +++ b/extras/NetworkGraphicsDemo/Builds/VisualStudio2013/NetworkGraphicsDemo_App.vcxproj.filters @@ -847,6 +847,9 @@ JUCE Modules\juce_core\containers + + JUCE Modules\juce_core\containers + JUCE Modules\juce_core\containers diff --git a/extras/NetworkGraphicsDemo/Builds/VisualStudio2017/NetworkGraphicsDemo_App.vcxproj b/extras/NetworkGraphicsDemo/Builds/VisualStudio2017/NetworkGraphicsDemo_App.vcxproj index db60810df0..2eaf232c5b 100644 --- a/extras/NetworkGraphicsDemo/Builds/VisualStudio2017/NetworkGraphicsDemo_App.vcxproj +++ b/extras/NetworkGraphicsDemo/Builds/VisualStudio2017/NetworkGraphicsDemo_App.vcxproj @@ -596,6 +596,9 @@ true + + true + true diff --git a/extras/NetworkGraphicsDemo/Builds/VisualStudio2017/NetworkGraphicsDemo_App.vcxproj.filters b/extras/NetworkGraphicsDemo/Builds/VisualStudio2017/NetworkGraphicsDemo_App.vcxproj.filters index 0801a22dcc..70136663bb 100644 --- a/extras/NetworkGraphicsDemo/Builds/VisualStudio2017/NetworkGraphicsDemo_App.vcxproj.filters +++ b/extras/NetworkGraphicsDemo/Builds/VisualStudio2017/NetworkGraphicsDemo_App.vcxproj.filters @@ -847,6 +847,9 @@ JUCE Modules\juce_core\containers + + JUCE Modules\juce_core\containers + JUCE Modules\juce_core\containers diff --git a/extras/NetworkGraphicsDemo/JuceLibraryCode/AppConfig.h b/extras/NetworkGraphicsDemo/JuceLibraryCode/AppConfig.h index a7d5fe14aa..4efb82c60a 100644 --- a/extras/NetworkGraphicsDemo/JuceLibraryCode/AppConfig.h +++ b/extras/NetworkGraphicsDemo/JuceLibraryCode/AppConfig.h @@ -150,6 +150,10 @@ //#define JUCE_PLUGINHOST_AU 0 #endif +#ifndef JUCE_PLUGINHOST_LADSPA + //#define JUCE_PLUGINHOST_LADSPA 0 +#endif + //============================================================================== // juce_audio_utils flags: diff --git a/extras/Projucer/Builds/VisualStudio2013/Projucer_App.vcxproj b/extras/Projucer/Builds/VisualStudio2013/Projucer_App.vcxproj index c442d2a2f8..572c130cf4 100644 --- a/extras/Projucer/Builds/VisualStudio2013/Projucer_App.vcxproj +++ b/extras/Projucer/Builds/VisualStudio2013/Projucer_App.vcxproj @@ -276,6 +276,9 @@ true + + true + true diff --git a/extras/Projucer/Builds/VisualStudio2013/Projucer_App.vcxproj.filters b/extras/Projucer/Builds/VisualStudio2013/Projucer_App.vcxproj.filters index 695f3e93aa..4163efab29 100644 --- a/extras/Projucer/Builds/VisualStudio2013/Projucer_App.vcxproj.filters +++ b/extras/Projucer/Builds/VisualStudio2013/Projucer_App.vcxproj.filters @@ -541,6 +541,9 @@ JUCE Modules\juce_core\containers + + JUCE Modules\juce_core\containers + JUCE Modules\juce_core\containers diff --git a/extras/Projucer/Builds/VisualStudio2015/Projucer_App.vcxproj b/extras/Projucer/Builds/VisualStudio2015/Projucer_App.vcxproj index a57dea7877..ef598efd12 100644 --- a/extras/Projucer/Builds/VisualStudio2015/Projucer_App.vcxproj +++ b/extras/Projucer/Builds/VisualStudio2015/Projucer_App.vcxproj @@ -276,6 +276,9 @@ true + + true + true diff --git a/extras/Projucer/Builds/VisualStudio2015/Projucer_App.vcxproj.filters b/extras/Projucer/Builds/VisualStudio2015/Projucer_App.vcxproj.filters index 22073c7afb..8d2da9e4f2 100644 --- a/extras/Projucer/Builds/VisualStudio2015/Projucer_App.vcxproj.filters +++ b/extras/Projucer/Builds/VisualStudio2015/Projucer_App.vcxproj.filters @@ -541,6 +541,9 @@ JUCE Modules\juce_core\containers + + JUCE Modules\juce_core\containers + JUCE Modules\juce_core\containers diff --git a/extras/Projucer/Builds/VisualStudio2017/Projucer_App.vcxproj b/extras/Projucer/Builds/VisualStudio2017/Projucer_App.vcxproj index 055a65ae00..29133a4798 100644 --- a/extras/Projucer/Builds/VisualStudio2017/Projucer_App.vcxproj +++ b/extras/Projucer/Builds/VisualStudio2017/Projucer_App.vcxproj @@ -276,6 +276,9 @@ true + + true + true diff --git a/extras/Projucer/Builds/VisualStudio2017/Projucer_App.vcxproj.filters b/extras/Projucer/Builds/VisualStudio2017/Projucer_App.vcxproj.filters index ecaaaa4f75..42313cdb1e 100644 --- a/extras/Projucer/Builds/VisualStudio2017/Projucer_App.vcxproj.filters +++ b/extras/Projucer/Builds/VisualStudio2017/Projucer_App.vcxproj.filters @@ -541,6 +541,9 @@ JUCE Modules\juce_core\containers + + JUCE Modules\juce_core\containers + JUCE Modules\juce_core\containers diff --git a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h index 7844238797..4e75af75d4 100644 --- a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h +++ b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h @@ -103,7 +103,7 @@ public: ValueWithDefault androidJavaLibs, androidRepositories, androidDependencies, androidScreenOrientation, androidActivityClass, androidActivitySubClassName, androidActivityBaseClassName, androidManifestCustomXmlElements, androidVersionCode, androidMinimumSDK, androidTheme, androidSharedLibraries, androidStaticLibraries, androidExtraAssetsFolder, - androidOboeRepositoryPath, androidInternetNeeded, androidMicNeeded, androidBluetoothNeeded, androidExternalReadPermission, + androidOboeRepositoryPath, androidInternetNeeded, androidMicNeeded, androidCameraNeeded, androidBluetoothNeeded, androidExternalReadPermission, androidExternalWritePermission, androidInAppBillingPermission, androidVibratePermission,androidOtherPermissions, androidEnableRemoteNotifications, androidRemoteNotificationsConfigFile, androidEnableContentSharing, androidKeyStore, androidKeyStorePass, androidKeyAlias, androidKeyAliasPass, gradleVersion, gradleToolchain, androidPluginVersion, buildToolsVersion; @@ -128,6 +128,7 @@ public: androidOboeRepositoryPath (settings, Ids::androidOboeRepositoryPath, getUndoManager()), androidInternetNeeded (settings, Ids::androidInternetNeeded, getUndoManager(), true), androidMicNeeded (settings, Ids::microphonePermissionNeeded, getUndoManager(), false), + androidCameraNeeded (settings, Ids::cameraPermissionNeeded, getUndoManager(), false), androidBluetoothNeeded (settings, Ids::androidBluetoothNeeded, getUndoManager(), true), androidExternalReadPermission (settings, Ids::androidExternalReadNeeded, getUndoManager(), true), androidExternalWritePermission (settings, Ids::androidExternalWriteNeeded, getUndoManager(), true), @@ -920,6 +921,9 @@ private: props.add (new ChoicePropertyComponent (androidMicNeeded, "Audio Input Required"), "If enabled, this will set the android.permission.RECORD_AUDIO flag in the manifest."); + props.add (new ChoicePropertyComponent (androidCameraNeeded, "Camera Required"), + "If enabled, this will set the android.permission.CAMERA flag in the manifest."); + props.add (new ChoicePropertyComponent (androidBluetoothNeeded, "Bluetooth permissions Required"), "If enabled, this will set the android.permission.BLUETOOTH and android.permission.BLUETOOTH_ADMIN flag in the manifest. This is required for Bluetooth MIDI on Android."); @@ -1034,25 +1038,92 @@ private: createDirectoryOrThrow (targetFolder); + auto activityCode = getActivityCode (javaSourceFolder, className, package); + auto javaDestFile = targetFolder.getChildFile (className + ".java"); + overwriteFileIfDifferentOrThrow (javaDestFile, activityCode); + } + String getActivityCode (const File& javaSourceFolder, const String& className, const String& package) const + { + auto runtimePermissionsCode = getRuntimePermissionsCode (javaSourceFolder, className); + auto midiCode = getMidiCode (javaSourceFolder, className); + auto webViewCode = getWebViewCode (javaSourceFolder); + auto cameraCode = getCameraCode (javaSourceFolder); - String juceMidiCode, juceMidiImports, juceRuntimePermissionsCode; + auto javaSourceFile = javaSourceFolder.getChildFile ("JuceAppActivity.java"); + auto javaSourceLines = StringArray::fromLines (javaSourceFile.loadFileAsString()); + + { + MemoryOutputStream newFile; + + for (auto& line : javaSourceLines) + { + if (line.contains ("$$JuceAndroidMidiImports$$")) + newFile << midiCode.imports; + else if (line.contains ("$$JuceAndroidMidiCode$$")) + newFile << midiCode.main; + else if (line.contains ("$$JuceAndroidRuntimePermissionsCode$$")) + newFile << runtimePermissionsCode; + else if (line.contains ("$$JuceAndroidWebViewImports$$")) + newFile << webViewCode.imports; + else if (line.contains ("$$JuceAndroidWebViewNativeCode$$")) + newFile << webViewCode.native; + else if (line.contains ("$$JuceAndroidWebViewCode$$")) + newFile << webViewCode.main; + else if (line.contains ("$$JuceAndroidCameraImports$$")) + newFile << cameraCode.imports; + else if (line.contains ("$$JuceAndroidCameraCode$$")) + newFile << cameraCode.main; + else + newFile << line.replace ("$$JuceAppActivityBaseClass$$", androidActivityBaseClassName.get().toString()) + .replace ("JuceAppActivity", className) + .replace ("package com.juce;", "package " + package + ";") << newLine; + } + + javaSourceLines = StringArray::fromLines (newFile.toString()); + } + + while (javaSourceLines.size() > 2 + && javaSourceLines[javaSourceLines.size() - 1].trim().isEmpty() + && javaSourceLines[javaSourceLines.size() - 2].trim().isEmpty()) + javaSourceLines.remove (javaSourceLines.size() - 1); + + return javaSourceLines.joinIntoString (newLine); + } + + String getRuntimePermissionsCode (const File& javaSourceFolder, const String& className) const + { + if (static_cast (androidMinimumSDK.get()) >= 23) + { + auto javaRuntimePermissions = javaSourceFolder.getChildFile ("AndroidRuntimePermissions.java"); + return javaRuntimePermissions.loadFileAsString().replace ("JuceAppActivity", className); + } + + return {}; + } + + struct MidiCode + { + String imports; + String main; + }; + + MidiCode getMidiCode (const File& javaSourceFolder, const String& className) const + { + String juceMidiCode, juceMidiImports; juceMidiImports << newLine; if (static_cast (androidMinimumSDK.get()) >= 23) { auto javaAndroidMidi = javaSourceFolder.getChildFile ("AndroidMidi.java"); - auto javaRuntimePermissions = javaSourceFolder.getChildFile ("AndroidRuntimePermissions.java"); juceMidiImports << "import android.media.midi.*;" << newLine << "import android.bluetooth.*;" << newLine << "import android.bluetooth.le.*;" << newLine; juceMidiCode = javaAndroidMidi.loadFileAsString().replace ("JuceAppActivity", className); - - juceRuntimePermissionsCode = javaRuntimePermissions.loadFileAsString().replace ("JuceAppActivity", className); } else { @@ -1061,6 +1132,18 @@ private: .replace ("JuceAppActivity", className); } + return { juceMidiImports, juceMidiCode }; + } + + struct WebViewCode + { + String imports; + String native; + String main; + }; + + WebViewCode getWebViewCode (const File& javaSourceFolder) const + { String juceWebViewImports, juceWebViewCodeNative, juceWebViewCode; if (static_cast (androidMinimumSDK.get()) >= 23) @@ -1106,41 +1189,32 @@ private: } } - auto javaSourceFile = javaSourceFolder.getChildFile ("JuceAppActivity.java"); - auto javaSourceLines = StringArray::fromLines (javaSourceFile.loadFileAsString()); + return { juceWebViewImports, juceWebViewCodeNative, juceWebViewCode }; + } + struct CameraCode + { + String imports; + String main; + }; + + CameraCode getCameraCode (const File& javaSourceFolder) const + { + String juceCameraImports, juceCameraCode; + + if (static_cast (androidMinimumSDK.get()) >= 21) + juceCameraImports << "import android.hardware.camera2.*;" << newLine; + + auto javaCameraFile = javaSourceFolder.getChildFile ("AndroidCamera.java"); + auto juceCameraCodeAll = javaCameraFile.loadFileAsString(); + + if (static_cast (androidMinimumSDK.get()) >= 21) { - MemoryOutputStream newFile; - - for (auto& line : javaSourceLines) - { - if (line.contains ("$$JuceAndroidMidiImports$$")) - newFile << juceMidiImports; - else if (line.contains ("$$JuceAndroidMidiCode$$")) - newFile << juceMidiCode; - else if (line.contains ("$$JuceAndroidRuntimePermissionsCode$$")) - newFile << juceRuntimePermissionsCode; - else if (line.contains ("$$JuceAndroidWebViewImports$$")) - newFile << juceWebViewImports; - else if (line.contains ("$$JuceAndroidWebViewNativeCode$$")) - newFile << juceWebViewCodeNative; - else if (line.contains ("$$JuceAndroidWebViewCode$$")) - newFile << juceWebViewCode; - else - newFile << line.replace ("$$JuceAppActivityBaseClass$$", androidActivityBaseClassName.get().toString()) - .replace ("JuceAppActivity", className) - .replace ("package com.juce;", "package " + package + ";") << newLine; - } - - javaSourceLines = StringArray::fromLines (newFile.toString()); + juceCameraCode << juceCameraCodeAll.fromFirstOccurrenceOf ("$$CameraApi21", false, false) + .upToFirstOccurrenceOf ("CameraApi21$$", false, false); } - while (javaSourceLines.size() > 2 - && javaSourceLines[javaSourceLines.size() - 1].trim().isEmpty() - && javaSourceLines[javaSourceLines.size() - 2].trim().isEmpty()) - javaSourceLines.remove (javaSourceLines.size() - 1); - - overwriteFileIfDifferentOrThrow (javaDestFile, javaSourceLines.joinIntoString (newLine)); + return { juceCameraImports, juceCameraCode }; } void copyAdditionalJavaFiles (const File& sourceFolder, const File& targetFolder) const @@ -1882,6 +1956,9 @@ private: if (androidMicNeeded.get()) s.add ("android.permission.RECORD_AUDIO"); + if (androidCameraNeeded.get()) + s.add ("android.permission.CAMERA"); + if (androidBluetoothNeeded.get()) { s.add ("android.permission.BLUETOOTH"); diff --git a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Xcode.h b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Xcode.h index 667e6ed82d..26f767b161 100644 --- a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Xcode.h +++ b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Xcode.h @@ -73,6 +73,9 @@ public: microphonePermissionNeededValue (settings, Ids::microphonePermissionNeeded, getUndoManager()), microphonePermissionsTextValue (settings, Ids::microphonePermissionsText, getUndoManager(), "This is an audio app which requires audio input. If you do not have a USB audio interface connected it will use the microphone."), + cameraPermissionNeededValue (settings, Ids::cameraPermissionNeeded, getUndoManager()), + cameraPermissionTextValue (settings, Ids::cameraPermissionText, getUndoManager(), + "This app requires camera usage to function properly."), uiFileSharingEnabledValue (settings, Ids::UIFileSharingEnabled, getUndoManager()), uiSupportsDocumentBrowserValue (settings, Ids::UISupportsDocumentBrowser, getUndoManager()), uiStatusBarHiddenValue (settings, Ids::UIStatusBarHidden, getUndoManager()), @@ -124,6 +127,9 @@ public: bool isMicrophonePermissionEnabled() const { return microphonePermissionNeededValue.get(); } String getMicrophonePermissionsTextString() const { return microphonePermissionsTextValue.get(); } + bool isCameraPermissionEnabled() const { return cameraPermissionNeededValue.get(); } + String getCameraPermissionTextString() const { return cameraPermissionTextValue.get(); } + bool isInAppPurchasesEnabled() const { return iosInAppPurchasesValue.get(); } bool isBackgroundAudioEnabled() const { return iosBackgroundAudioValue.get(); } bool isBackgroundBleEnabled() const { return iosBackgroundBleValue.get(); } @@ -236,6 +242,14 @@ public: props.add (new TextPropertyComponentWithEnablement (microphonePermissionsTextValue, microphonePermissionNeededValue, "Microphone Access Text", 1024, false), "A short description of why your app requires microphone access."); + + props.add (new ChoicePropertyComponent (cameraPermissionNeededValue, "Camera Access"), + "Enable this to allow your app to use the camera. " + "The user of your app will be prompted to grant camera access permissions."); + + props.add (new TextPropertyComponentWithEnablement (cameraPermissionTextValue, cameraPermissionNeededValue, + "Camera Access Text", 1024, false), + "A short description of why your app requires camera access."); } else if (projectType.isGUIApplication()) { @@ -1280,9 +1294,13 @@ public: if (owner.iOS) { addPlistDictionaryKeyBool (dict, "LSRequiresIPhoneOS", true); + if (owner.isMicrophonePermissionEnabled()) addPlistDictionaryKey (dict, "NSMicrophoneUsageDescription", owner.getMicrophonePermissionsTextString()); + if (owner.isCameraPermissionEnabled()) + addPlistDictionaryKey (dict, "NSCameraUsageDescription", owner.getCameraPermissionTextString()); + if (type != AudioUnitv3PlugIn) addPlistDictionaryKeyBool (dict, "UIViewControllerBasedStatusBarAppearance", false); } @@ -1682,7 +1700,8 @@ private: ValueWithDefault customPListValue, pListPrefixHeaderValue, pListPreprocessValue, extraFrameworksValue, postbuildCommandValue, prebuildCommandValue, duplicateAppExResourcesFolderValue, iosDeviceFamilyValue, iPhoneScreenOrientationValue, - iPadScreenOrientationValue, customXcodeResourceFoldersValue, customXcassetsFolderValue, microphonePermissionNeededValue, microphonePermissionsTextValue, + iPadScreenOrientationValue, customXcodeResourceFoldersValue, customXcassetsFolderValue, + microphonePermissionNeededValue, microphonePermissionsTextValue, cameraPermissionNeededValue, cameraPermissionTextValue, uiFileSharingEnabledValue, uiSupportsDocumentBrowserValue, uiStatusBarHiddenValue, documentExtensionsValue, iosInAppPurchasesValue, iosBackgroundAudioValue, iosBackgroundBleValue, iosPushNotificationsValue, iosAppGroupsValue, iCloudPermissionsValue, iosDevelopmentTeamIDValue, iosAppGroupsIDValue, keepCustomXcodeSchemesValue, useHeaderMapValue; @@ -2299,6 +2318,9 @@ private: if (iOS && isPushNotificationsEnabled()) xcodeFrameworks.addIfNotAlreadyThere ("UserNotifications"); + if (isiOS() && project.getConfigFlag ("JUCE_USE_CAMERA").get()) + xcodeFrameworks.addIfNotAlreadyThere ("ImageIO"); + xcodeFrameworks.addTokens (getExtraFrameworksString(), ",;", "\"'"); xcodeFrameworks.trim(); diff --git a/extras/Projucer/Source/Utility/Helpers/jucer_PresetIDs.h b/extras/Projucer/Source/Utility/Helpers/jucer_PresetIDs.h index 1a2346967d..7b66a4695e 100644 --- a/extras/Projucer/Source/Utility/Helpers/jucer_PresetIDs.h +++ b/extras/Projucer/Source/Utility/Helpers/jucer_PresetIDs.h @@ -178,6 +178,8 @@ namespace Ids DECLARE_ID (overwriteOnSave); DECLARE_ID (microphonePermissionNeeded); DECLARE_ID (microphonePermissionsText); + DECLARE_ID (cameraPermissionNeeded); + DECLARE_ID (cameraPermissionText); DECLARE_ID (androidJavaLibs); DECLARE_ID (androidRepositories); DECLARE_ID (androidDependencies); diff --git a/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj b/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj index b59b827937..dea8878e6d 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj +++ b/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj @@ -628,6 +628,9 @@ true + + true + true @@ -2638,6 +2641,7 @@ + diff --git a/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj.filters b/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj.filters index 47e017ffdd..84b335e3bf 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj.filters +++ b/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj.filters @@ -949,6 +949,9 @@ JUCE Modules\juce_core\containers + + JUCE Modules\juce_core\containers + JUCE Modules\juce_core\containers @@ -4467,6 +4470,9 @@ JUCE Modules\juce_video\native + + JUCE Modules\juce_video\native + JUCE Modules\juce_video\native diff --git a/extras/UnitTestRunner/JuceLibraryCode/AppConfig.h b/extras/UnitTestRunner/JuceLibraryCode/AppConfig.h index d15076fc5f..059f7a28f8 100644 --- a/extras/UnitTestRunner/JuceLibraryCode/AppConfig.h +++ b/extras/UnitTestRunner/JuceLibraryCode/AppConfig.h @@ -155,6 +155,10 @@ //#define JUCE_PLUGINHOST_AU 0 #endif +#ifndef JUCE_PLUGINHOST_LADSPA + //#define JUCE_PLUGINHOST_LADSPA 0 +#endif + //============================================================================== // juce_audio_utils flags: diff --git a/extras/WindowsDLL/Builds/VisualStudio2017/WindowsDLL_StaticLibrary.vcxproj b/extras/WindowsDLL/Builds/VisualStudio2017/WindowsDLL_StaticLibrary.vcxproj index 92109b0740..97696d4e20 100644 --- a/extras/WindowsDLL/Builds/VisualStudio2017/WindowsDLL_StaticLibrary.vcxproj +++ b/extras/WindowsDLL/Builds/VisualStudio2017/WindowsDLL_StaticLibrary.vcxproj @@ -595,6 +595,9 @@ true + + true + true @@ -2420,6 +2423,7 @@ + diff --git a/extras/WindowsDLL/Builds/VisualStudio2017/WindowsDLL_StaticLibrary.vcxproj.filters b/extras/WindowsDLL/Builds/VisualStudio2017/WindowsDLL_StaticLibrary.vcxproj.filters index 898a4b4789..f8c3447503 100644 --- a/extras/WindowsDLL/Builds/VisualStudio2017/WindowsDLL_StaticLibrary.vcxproj.filters +++ b/extras/WindowsDLL/Builds/VisualStudio2017/WindowsDLL_StaticLibrary.vcxproj.filters @@ -844,6 +844,9 @@ JUCE Modules\juce_core\containers + + JUCE Modules\juce_core\containers + JUCE Modules\juce_core\containers @@ -4011,6 +4014,9 @@ JUCE Modules\juce_video\native + + JUCE Modules\juce_video\native + JUCE Modules\juce_video\native diff --git a/extras/WindowsDLL/JuceLibraryCode/AppConfig.h b/extras/WindowsDLL/JuceLibraryCode/AppConfig.h index de3a1d428c..8510bde870 100644 --- a/extras/WindowsDLL/JuceLibraryCode/AppConfig.h +++ b/extras/WindowsDLL/JuceLibraryCode/AppConfig.h @@ -150,6 +150,10 @@ //#define JUCE_PLUGINHOST_AU 0 #endif +#ifndef JUCE_PLUGINHOST_LADSPA + //#define JUCE_PLUGINHOST_LADSPA 0 +#endif + //============================================================================== // juce_audio_utils flags: diff --git a/modules/juce_audio_devices/native/juce_android_Midi.cpp b/modules/juce_audio_devices/native/juce_android_Midi.cpp index f290cb4291..a178abf01f 100644 --- a/modules/juce_audio_devices/native/juce_android_Midi.cpp +++ b/modules/juce_audio_devices/native/juce_android_Midi.cpp @@ -221,22 +221,6 @@ public: } private: - static StringArray javaStringArrayToJuce (jobjectArray jStrings) - { - StringArray retval; - - JNIEnv* env = getEnv(); - const int count = env->GetArrayLength (jStrings); - - for (int i = 0; i < count; ++i) - { - LocalRef string ((jstring) env->GetObjectArrayElement (jStrings, i)); - retval.add (juceString (string)); - } - - return retval; - } - GlobalRef deviceManager; }; diff --git a/modules/juce_core/misc/juce_RuntimePermissions.h b/modules/juce_core/misc/juce_RuntimePermissions.h index 43200ebea8..532a0038ea 100644 --- a/modules/juce_core/misc/juce_RuntimePermissions.h +++ b/modules/juce_core/misc/juce_RuntimePermissions.h @@ -83,7 +83,10 @@ public: readExternalStorage = 3, /** Permission to write to external storage such as SD cards */ - writeExternalStorage = 4 + writeExternalStorage = 4, + + /** Permission to use camera */ + camera = 5 }; //============================================================================== diff --git a/modules/juce_core/native/java/AndroidCamera.java b/modules/juce_core/native/java/AndroidCamera.java new file mode 100644 index 0000000000..bbce71f4fd --- /dev/null +++ b/modules/juce_core/native/java/AndroidCamera.java @@ -0,0 +1,169 @@ +$$CameraApi21 + //============================================================================== + public class CameraDeviceStateCallback extends CameraDevice.StateCallback + { + private native void cameraDeviceStateClosed (long host, CameraDevice camera); + private native void cameraDeviceStateDisconnected (long host, CameraDevice camera); + private native void cameraDeviceStateError (long host, CameraDevice camera, int error); + private native void cameraDeviceStateOpened (long host, CameraDevice camera); + + CameraDeviceStateCallback (long hostToUse) + { + host = hostToUse; + } + + @Override + public void onClosed (CameraDevice camera) + { + cameraDeviceStateClosed (host, camera); + } + + @Override + public void onDisconnected (CameraDevice camera) + { + cameraDeviceStateDisconnected (host, camera); + } + + @Override + public void onError (CameraDevice camera, int error) + { + cameraDeviceStateError (host, camera, error); + } + + @Override + public void onOpened (CameraDevice camera) + { + cameraDeviceStateOpened (host, camera); + } + + private long host; + } + + //============================================================================== + public class CameraCaptureSessionStateCallback extends CameraCaptureSession.StateCallback + { + private native void cameraCaptureSessionActive (long host, CameraCaptureSession session); + private native void cameraCaptureSessionClosed (long host, CameraCaptureSession session); + private native void cameraCaptureSessionConfigureFailed (long host, CameraCaptureSession session); + private native void cameraCaptureSessionConfigured (long host, CameraCaptureSession session); + private native void cameraCaptureSessionReady (long host, CameraCaptureSession session); + + CameraCaptureSessionStateCallback (long hostToUse) + { + host = hostToUse; + } + + @Override + public void onActive (CameraCaptureSession session) + { + cameraCaptureSessionActive (host, session); + } + + @Override + public void onClosed (CameraCaptureSession session) + { + cameraCaptureSessionClosed (host, session); + } + + @Override + public void onConfigureFailed (CameraCaptureSession session) + { + cameraCaptureSessionConfigureFailed (host, session); + } + + @Override + public void onConfigured (CameraCaptureSession session) + { + cameraCaptureSessionConfigured (host, session); + } + + @Override + public void onReady (CameraCaptureSession session) + { + cameraCaptureSessionReady (host, session); + } + + private long host; + } + + //============================================================================== + public class CameraCaptureSessionCaptureCallback extends CameraCaptureSession.CaptureCallback + { + private native void cameraCaptureSessionCaptureCompleted (long host, boolean isPreview, CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result); + private native void cameraCaptureSessionCaptureFailed (long host, boolean isPreview, CameraCaptureSession session, CaptureRequest request, CaptureFailure failure); + private native void cameraCaptureSessionCaptureProgressed (long host, boolean isPreview, CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult); + private native void cameraCaptureSessionCaptureStarted (long host, boolean isPreview, CameraCaptureSession session, CaptureRequest request, long timestamp, long frameNumber); + private native void cameraCaptureSessionCaptureSequenceAborted (long host, boolean isPreview, CameraCaptureSession session, int sequenceId); + private native void cameraCaptureSessionCaptureSequenceCompleted (long host, boolean isPreview, CameraCaptureSession session, int sequenceId, long frameNumber); + + CameraCaptureSessionCaptureCallback (long hostToUse, boolean shouldBePreview) + { + host = hostToUse; + preview = shouldBePreview; + } + + @Override + public void onCaptureCompleted (CameraCaptureSession session, CaptureRequest request, + TotalCaptureResult result) + { + cameraCaptureSessionCaptureCompleted (host, preview, session, request, result); + } + + @Override + public void onCaptureFailed (CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) + { + cameraCaptureSessionCaptureFailed (host, preview, session, request, failure); + } + + @Override + public void onCaptureProgressed (CameraCaptureSession session, CaptureRequest request, + CaptureResult partialResult) + { + cameraCaptureSessionCaptureProgressed (host, preview, session, request, partialResult); + } + + @Override + public void onCaptureSequenceAborted (CameraCaptureSession session, int sequenceId) + { + cameraCaptureSessionCaptureSequenceAborted (host, preview, session, sequenceId); + } + + @Override + public void onCaptureSequenceCompleted (CameraCaptureSession session, int sequenceId, long frameNumber) + { + cameraCaptureSessionCaptureSequenceCompleted (host, preview, session, sequenceId, frameNumber); + } + + @Override + public void onCaptureStarted (CameraCaptureSession session, CaptureRequest request, long timestamp, + long frameNumber) + { + cameraCaptureSessionCaptureStarted (host, preview, session, request, timestamp, frameNumber); + } + + private long host; + private boolean preview; + } + + //============================================================================== + public class JuceOrientationEventListener extends OrientationEventListener + { + private native void deviceOrientationChanged (long host, int orientation); + + public JuceOrientationEventListener (long hostToUse, Context context, int rate) + { + super (context, rate); + + host = hostToUse; + } + + @Override + public void onOrientationChanged (int orientation) + { + deviceOrientationChanged (host, orientation); + } + + private long host; + } + +CameraApi21$$ diff --git a/modules/juce_core/native/java/JuceAppActivity.java b/modules/juce_core/native/java/JuceAppActivity.java index eb2a50a305..4fdeac0d9d 100644 --- a/modules/juce_core/native/java/JuceAppActivity.java +++ b/modules/juce_core/native/java/JuceAppActivity.java @@ -30,6 +30,7 @@ import android.content.Intent; import android.content.res.Configuration; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +$$JuceAndroidCameraImports$$ // If you get an error here, you need to re-save your project with the Projucer! import android.net.http.SslError; import android.net.Uri; import android.os.Bundle; @@ -114,6 +115,7 @@ public class JuceAppActivity extends $$JuceAppActivityBaseClass$$ private static final int JUCE_PERMISSIONS_BLUETOOTH_MIDI = 2; private static final int JUCE_PERMISSIONS_READ_EXTERNAL_STORAGE = 3; private static final int JUCE_PERMISSIONS_WRITE_EXTERNAL_STORAGE = 4; + private static final int JUCE_PERMISSIONS_CAMERA = 5; private static String getAndroidPermissionName (int permissionID) { @@ -124,6 +126,7 @@ public class JuceAppActivity extends $$JuceAppActivityBaseClass$$ // use string value as this is not defined in SDKs < 16 case JUCE_PERMISSIONS_READ_EXTERNAL_STORAGE: return "android.permission.READ_EXTERNAL_STORAGE"; case JUCE_PERMISSIONS_WRITE_EXTERNAL_STORAGE: return Manifest.permission.WRITE_EXTERNAL_STORAGE; + case JUCE_PERMISSIONS_CAMERA: return Manifest.permission.CAMERA; } // unknown permission ID! @@ -191,6 +194,7 @@ public class JuceAppActivity extends $$JuceAppActivityBaseClass$$ setVolumeControlStream (AudioManager.STREAM_MUSIC); permissionCallbackPtrMap = new HashMap(); + appPausedResumedListeners = new HashMap(); } @Override @@ -207,6 +211,11 @@ public class JuceAppActivity extends $$JuceAppActivityBaseClass$$ { suspendApp(); + Long[] keys = appPausedResumedListeners.keySet().toArray (new Long[appPausedResumedListeners.keySet().size()]); + + for (Long k : keys) + appPausedResumedListeners.get (k).appPaused(); + try { Thread.sleep (1000); // This is a bit of a hack to avoid some hard-to-track-down @@ -222,12 +231,10 @@ public class JuceAppActivity extends $$JuceAppActivityBaseClass$$ super.onResume(); resumeApp(); - // Ensure that navigation/status bar visibility is correctly restored. - for (int i = 0; i < viewHolder.getChildCount(); ++i) - { - if (viewHolder.getChildAt (i) instanceof ComponentPeerView) - ((ComponentPeerView) viewHolder.getChildAt (i)).appResumed(); - } + Long[] keys = appPausedResumedListeners.keySet().toArray (new Long[appPausedResumedListeners.keySet().size()]); + + for (Long k : keys) + appPausedResumedListeners.get (k).appResumed(); } @Override @@ -354,11 +361,14 @@ public class JuceAppActivity extends $$JuceAppActivityBaseClass$$ { ComponentPeerView v = new ComponentPeerView (this, opaque, host); viewHolder.addView (v); + addAppPausedResumedListener (v, host); return v; } public final void deleteView (ComponentPeerView view) { + removeAppPausedResumedListener (view, view.host); + view.host = 0; ViewGroup group = (ViewGroup) (view.getParent()); @@ -576,9 +586,28 @@ public class JuceAppActivity extends $$JuceAppActivityBaseClass$$ public native void alertDismissed (long callback, int id); + //============================================================================== + public interface AppPausedResumedListener + { + void appPaused(); + void appResumed(); + } + + private Map appPausedResumedListeners; + + public void addAppPausedResumedListener (AppPausedResumedListener l, long listenerHost) + { + appPausedResumedListeners.put (new Long (listenerHost), l); + } + + public void removeAppPausedResumedListener (AppPausedResumedListener l, long listenerHost) + { + appPausedResumedListeners.remove (new Long (listenerHost)); + } + //============================================================================== public final class ComponentPeerView extends ViewGroup - implements View.OnFocusChangeListener + implements View.OnFocusChangeListener, AppPausedResumedListener { public ComponentPeerView (Context context, boolean opaque_, long host) { @@ -926,13 +955,25 @@ public class JuceAppActivity extends $$JuceAppActivityBaseClass$$ } //============================================================================== + private native void handleAppPaused (long host); private native void handleAppResumed (long host); + @Override + public void appPaused() + { + if (host == 0) + return; + + handleAppPaused (host); + } + + @Override public void appResumed() { if (host == 0) return; + // Ensure that navigation/status bar visibility is correctly restored. handleAppResumed (host); } } @@ -1569,6 +1610,8 @@ $$JuceAndroidWebViewNativeCode$$ // If you get an error here, you need to re-sav private final Object hostLock = new Object(); } + $$JuceAndroidCameraCode$$ // If you get an error here, you need to re-save your project with the Projucer! + //============================================================================== public static final String getLocaleValue (boolean isRegion) { diff --git a/modules/juce_core/native/juce_android_JNIHelpers.h b/modules/juce_core/native/juce_android_JNIHelpers.h index 1b050b9440..1324649685 100644 --- a/modules/juce_core/native/juce_android_JNIHelpers.h +++ b/modules/juce_core/native/juce_android_JNIHelpers.h @@ -163,39 +163,6 @@ private: } }; -//============================================================================== -namespace -{ - inline String juceString (JNIEnv* env, jstring s) - { - if (s == 0) - return {}; - - const char* const utf8 = env->GetStringUTFChars (s, nullptr); - CharPointer_UTF8 utf8CP (utf8); - const String result (utf8CP); - env->ReleaseStringUTFChars (s, utf8); - return result; - } - - inline String juceString (jstring s) - { - return juceString (getEnv(), s); - } - - inline LocalRef javaString (const String& s) - { - return LocalRef (getEnv()->NewStringUTF (s.toUTF8())); - } - - inline LocalRef javaStringFromChar (const juce_wchar c) - { - char utf8[8] = { 0 }; - CharPointer_UTF8 (utf8).write (c); - return LocalRef (getEnv()->NewStringUTF (utf8)); - } -} - //============================================================================== class JNIClassBase { @@ -287,6 +254,7 @@ extern AndroidSystem android; METHOD (deleteView, "deleteView", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$ComponentPeerView;)V") \ METHOD (createNativeSurfaceView, "createNativeSurfaceView", "(J)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$NativeSurfaceView;") \ METHOD (finish, "finish", "()V") \ + METHOD (getWindowManager, "getWindowManager", "()Landroid/view/WindowManager;") \ METHOD (setRequestedOrientation, "setRequestedOrientation", "(I)V") \ METHOD (getClipboardContent, "getClipboardContent", "()Ljava/lang/String;") \ METHOD (setClipboardContent, "setClipboardContent", "(Ljava/lang/String;)V") \ @@ -329,14 +297,21 @@ extern AndroidSystem android; METHOD (startActivity, "startActivity", "(Landroid/content/Intent;)V") \ METHOD (startActivityForResult, "startActivityForResult", "(Landroid/content/Intent;I)V") \ METHOD (getContentResolver, "getContentResolver", "()Landroid/content/ContentResolver;") \ + METHOD (addAppPausedResumedListener, "addAppPausedResumedListener", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$AppPausedResumedListener;J)V") \ + METHOD (removeAppPausedResumedListener, "removeAppPausedResumedListener", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$AppPausedResumedListener;J)V") DECLARE_JNI_CLASS (JuceAppActivity, JUCE_ANDROID_ACTIVITY_CLASSPATH); #undef JNI_CLASS_MEMBERS //============================================================================== #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ - STATICMETHOD (createBitmap, "createBitmap", "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;") \ - METHOD (setPixel, "setPixel", "(III)V") + STATICMETHOD (createBitmap, "createBitmap", "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;") \ + STATICMETHOD (createBitmapFrom, "createBitmap", "(Landroid/graphics/Bitmap;IIIILandroid/graphics/Matrix;Z)Landroid/graphics/Bitmap;") \ + METHOD (compress, "compress", "(Landroid/graphics/Bitmap$CompressFormat;ILjava/io/OutputStream;)Z") \ + METHOD (getHeight, "getHeight", "()I") \ + METHOD (getWidth, "getWidth", "()I") \ + METHOD (recycle, "recycle", "()V") \ + METHOD (setPixel, "setPixel", "(III)V") DECLARE_JNI_CLASS (AndroidBitmap, "android/graphics/Bitmap"); #undef JNI_CLASS_MEMBERS @@ -347,6 +322,12 @@ DECLARE_JNI_CLASS (AndroidBitmap, "android/graphics/Bitmap"); DECLARE_JNI_CLASS (AndroidBitmapConfig, "android/graphics/Bitmap$Config"); #undef JNI_CLASS_MEMBERS +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + STATICMETHOD (decodeByteArray, "decodeByteArray", "([BII)Landroid/graphics/Bitmap;") + +DECLARE_JNI_CLASS (AndroidBitmapFactory, "android/graphics/BitmapFactory"); +#undef JNI_CLASS_MEMBERS + #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ STATICMETHOD (dumpReferenceTables, "dumpReferenceTables", "()V") @@ -355,6 +336,31 @@ DECLARE_JNI_CLASS (AndroidBitmapConfig, "android/graphics/Bitmap$Config"); #define JUCE_LOG_JNI_REFERENCES_TABLE getEnv()->CallStaticVoidMethod (AndroidDebug, AndroidDebug.dumpReferenceTables); +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + METHOD (getRotation, "getRotation", "()I") + +DECLARE_JNI_CLASS (AndroidDisplay, "android/view/Display"); +#undef JNI_CLASS_MEMBERS + +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + METHOD (constructor, "", "()V") \ + METHOD (constructorWithLooper, "", "(Landroid/os/Looper;)V") \ + METHOD (post, "post", "(Ljava/lang/Runnable;)Z") \ + METHOD (postDelayed, "postDelayed", "(Ljava/lang/Runnable;J)Z") \ + +DECLARE_JNI_CLASS (AndroidHandler, "android/os/Handler"); +#undef JNI_CLASS_MEMBERS + +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + METHOD (constructor, "", "(Ljava/lang/String;)V") \ + METHOD (getLooper, "getLooper", "()Landroid/os/Looper;") \ + METHOD (join, "join", "()V") \ + METHOD (quitSafely, "quitSafely", "()Z") \ + METHOD (start, "start", "()V") + +DECLARE_JNI_CLASS (AndroidHandlerThread, "android/os/HandlerThread"); +#undef JNI_CLASS_MEMBERS + #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ STATICMETHOD (createChooser, "createChooser", "(Landroid/content/Intent;Ljava/lang/CharSequence;)Landroid/content/Intent;") \ METHOD (addCategory, "addCategory", "(Ljava/lang/String;)Landroid/content/Intent;") \ @@ -382,8 +388,11 @@ DECLARE_JNI_CLASS (AndroidIntent, "android/content/Intent"); #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ - METHOD (constructor, "", "()V") \ - METHOD (setValues, "setValues", "([F)V") \ + METHOD (constructor, "", "()V") \ + METHOD (postRotate, "postRotate", "(FFF)Z") \ + METHOD (postScale, "postScale", "(FFFF)Z") \ + METHOD (postTranslate, "postTranslate", "(FF)Z") \ + METHOD (setValues, "setValues", "([F)V") DECLARE_JNI_CLASS (AndroidMatrix, "android/graphics/Matrix"); #undef JNI_CLASS_MEMBERS @@ -417,6 +426,12 @@ DECLARE_JNI_CLASS (AndroidPaint, "android/graphics/Paint"); DECLARE_JNI_CLASS (AndroidPendingIntent, "android/app/PendingIntent"); #undef JNI_CLASS_MEMBERS +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + METHOD (toString, "toString", "()Ljava/lang/String;") + +DECLARE_JNI_CLASS (AndroidRange, "android/util/Range"); +#undef JNI_CLASS_MEMBERS + #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ METHOD (constructor, "", "(IIII)V") \ FIELD (left, "left", "I") \ @@ -424,7 +439,7 @@ DECLARE_JNI_CLASS (AndroidPendingIntent, "android/app/PendingIntent"); FIELD (top, "top", "I") \ FIELD (bottom, "bottom", "I") \ -DECLARE_JNI_CLASS (AndroidRectClass, "android/graphics/Rect"); +DECLARE_JNI_CLASS (AndroidRect, "android/graphics/Rect"); #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ @@ -434,6 +449,13 @@ DECLARE_JNI_CLASS (AndroidRectClass, "android/graphics/Rect"); DECLARE_JNI_CLASS (AndroidResources, "android/content/res/Resources") #undef JNI_CLASS_MEMBERS +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + METHOD (getHeight, "getHeight", "()I") \ + METHOD (getWidth, "getWidth", "()I") + +DECLARE_JNI_CLASS (AndroidSize, "android/util/Size"); +#undef JNI_CLASS_MEMBERS + #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ STATICMETHOD (parse, "parse", "(Ljava/lang/String;)Landroid/net/Uri;") \ METHOD (toString, "toString", "()Ljava/lang/String;") @@ -465,6 +487,12 @@ DECLARE_JNI_CLASS (AndroidView, "android/view/View"); DECLARE_JNI_CLASS (AndroidViewGroup, "android/view/ViewGroup") #undef JNI_CLASS_MEMBERS +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + METHOD (getDefaultDisplay, "getDefaultDisplay", "()Landroid/view/Display;") + +DECLARE_JNI_CLASS (AndroidWindowManager, "android/view/WindowManager"); +#undef JNI_CLASS_MEMBERS + //============================================================================== #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ METHOD (constructor, "", "(I)V") \ @@ -477,6 +505,7 @@ DECLARE_JNI_CLASS (JavaArrayList, "java/util/ArrayList"); #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + STATICMETHOD (valueOf, "valueOf", "(Z)Ljava/lang/Boolean;") \ METHOD (booleanValue, "booleanValue", "()Z") DECLARE_JNI_CLASS (JavaBoolean, "java/lang/Boolean"); @@ -507,6 +536,13 @@ DECLARE_JNI_CLASS (JavaBoolean, "java/lang/Boolean"); DECLARE_JNI_CLASS (JavaBundle, "android/os/Bundle"); #undef JNI_CLASS_MEMBERS +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + METHOD (get, "get", "([B)Ljava/nio/ByteBuffer;") \ + METHOD (remaining, "remaining", "()I") + +DECLARE_JNI_CLASS (JavaByteBuffer, "java/nio/ByteBuffer"); +#undef JNI_CLASS_MEMBERS + #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ METHOD (toString, "toString", "()Ljava/lang/String;") @@ -514,6 +550,7 @@ DECLARE_JNI_CLASS (JavaCharSequence, "java/lang/CharSequence"); #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + STATICMETHOD (forName, "forName", "(Ljava/lang/String;)Ljava/lang/Class;") \ METHOD (getName, "getName", "()Ljava/lang/String;") \ METHOD (getModifiers, "getModifiers", "()I") \ METHOD (isAnnotation, "isAnnotation", "()Z") \ @@ -571,7 +608,8 @@ DECLARE_JNI_CLASS (JavaHashMap, "java/util/HashMap"); #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ STATICMETHOD (parseInt, "parseInt", "(Ljava/lang/String;I)I") \ - STATICMETHOD (valueOf, "valueOf", "(I)Ljava/lang/Integer;") + STATICMETHOD (valueOf, "valueOf", "(I)Ljava/lang/Integer;") \ + METHOD (intValue, "intValue", "()I") DECLARE_JNI_CLASS (JavaInteger, "java/lang/Integer"); #undef JNI_CLASS_MEMBERS @@ -583,6 +621,13 @@ DECLARE_JNI_CLASS (JavaInteger, "java/lang/Integer"); DECLARE_JNI_CLASS (JavaIterator, "java/util/Iterator"); #undef JNI_CLASS_MEMBERS +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + METHOD (get, "get", "(I)Ljava/lang/Object;") \ + METHOD (size, "size", "()I") + +DECLARE_JNI_CLASS (JavaList, "java/util/List"); +#undef JNI_CLASS_MEMBERS + #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ METHOD (constructor, "", "(J)V") @@ -633,6 +678,71 @@ DECLARE_JNI_CLASS (JavaSet, "java/util/Set"); DECLARE_JNI_CLASS (JavaString, "java/lang/String"); #undef JNI_CLASS_MEMBERS +//============================================================================== +namespace +{ + inline String juceString (JNIEnv* env, jstring s) + { + if (s == 0) + return {}; + + const char* const utf8 = env->GetStringUTFChars (s, nullptr); + CharPointer_UTF8 utf8CP (utf8); + const String result (utf8CP); + env->ReleaseStringUTFChars (s, utf8); + return result; + } + + inline String juceString (jstring s) + { + return juceString (getEnv(), s); + } + + inline LocalRef javaString (const String& s) + { + return LocalRef (getEnv()->NewStringUTF (s.toUTF8())); + } + + inline LocalRef javaStringFromChar (const juce_wchar c) + { + char utf8[8] = { 0 }; + CharPointer_UTF8 (utf8).write (c); + return LocalRef (getEnv()->NewStringUTF (utf8)); + } + + inline LocalRef juceStringArrayToJava (const StringArray& juceArray) + { + auto* env = getEnv(); + + LocalRef result (env->NewObjectArray ((jsize) juceArray.size(), + JavaString, + javaString ("").get())); + + for (int i = 0; i < juceArray.size(); ++i) + env->SetObjectArrayElement (result, i, javaString (juceArray [i]).get()); + + return result; + } + + inline StringArray javaStringArrayToJuce (const LocalRef& javaArray) + { + if (javaArray.get() == nullptr) + return {}; + + auto* env = getEnv(); + + StringArray result; + + for (int i = 0; i < env->GetArrayLength (javaArray.get()); ++i) + { + LocalRef javaString ((jstring) env->GetObjectArrayElement (javaArray.get(), i)); + result.add (juceString (javaString.get())); + } + + return result; + } +} + //============================================================================== class AndroidInterfaceImplementer; diff --git a/modules/juce_events/native/juce_android_Messaging.cpp b/modules/juce_events/native/juce_android_Messaging.cpp index 3913ed7f81..abc5e6bcee 100644 --- a/modules/juce_events/native/juce_android_Messaging.cpp +++ b/modules/juce_events/native/juce_android_Messaging.cpp @@ -23,14 +23,6 @@ namespace juce { -#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ - METHOD (constructor, "", "()V") \ - METHOD (post, "post", "(Ljava/lang/Runnable;)Z") \ - -DECLARE_JNI_CLASS (JNIHandler, "android/os/Handler"); -#undef JNI_CLASS_MEMBERS - - //============================================================================== namespace Android { @@ -58,14 +50,14 @@ namespace Android struct Handler { - Handler() : nativeHandler (getEnv()->NewObject (JNIHandler, JNIHandler.constructor)) {} + Handler() : nativeHandler (getEnv()->NewObject (AndroidHandler, AndroidHandler.constructor)) {} ~Handler() { clearSingletonInstance(); } JUCE_DECLARE_SINGLETON (Handler, false) bool post (jobject runnable) { - return (getEnv()->CallBooleanMethod (nativeHandler.get(), JNIHandler.post, runnable) != 0); + return (getEnv()->CallBooleanMethod (nativeHandler.get(), AndroidHandler.post, runnable) != 0); } GlobalRef nativeHandler; diff --git a/modules/juce_graphics/native/juce_android_Fonts.cpp b/modules/juce_graphics/native/juce_android_Fonts.cpp index eea53ec6d0..e058f87cea 100644 --- a/modules/juce_graphics/native/juce_android_Fonts.cpp +++ b/modules/juce_graphics/native/juce_android_Fonts.cpp @@ -176,7 +176,7 @@ public: void initialise (JNIEnv* const env) { - rect = GlobalRef (env->NewObject (AndroidRectClass, AndroidRectClass.constructor, 0, 0, 0, 0)); + rect = GlobalRef (env->NewObject (AndroidRect, AndroidRect.constructor, 0, 0, 0, 0)); paint = GlobalRef (GraphicsHelpers::createPaint (Graphics::highResamplingQuality)); const LocalRef ignored (paint.callObjectMethod (AndroidPaint.setTypeface, typeface.get())); @@ -298,10 +298,10 @@ public: env->DeleteLocalRef (matrix); - const int left = env->GetIntField (rect.get(), AndroidRectClass.left); - const int top = env->GetIntField (rect.get(), AndroidRectClass.top); - const int right = env->GetIntField (rect.get(), AndroidRectClass.right); - const int bottom = env->GetIntField (rect.get(), AndroidRectClass.bottom); + const int left = env->GetIntField (rect.get(), AndroidRect.left); + const int top = env->GetIntField (rect.get(), AndroidRect.top); + const int right = env->GetIntField (rect.get(), AndroidRect.right); + const int bottom = env->GetIntField (rect.get(), AndroidRect.bottom); const Rectangle bounds (left, top, right - left, bottom - top); diff --git a/modules/juce_gui_basics/native/juce_android_ContentSharer.cpp b/modules/juce_gui_basics/native/juce_android_ContentSharer.cpp index a78e7a027f..15b18d8fc0 100644 --- a/modules/juce_gui_basics/native/juce_android_ContentSharer.cpp +++ b/modules/juce_gui_basics/native/juce_android_ContentSharer.cpp @@ -486,7 +486,7 @@ public: { ignoreUnused (selection, selectionArgs, sortOrder); - StringArray requestedColumns = javaStringArrayToJuceStringArray (projection); + StringArray requestedColumns = javaStringArrayToJuce (projection); StringArray supportedColumns = getSupportedColumns(); StringArray resultColumns; @@ -501,7 +501,7 @@ public: if (resultColumns.isEmpty()) return nullptr; - auto resultJavaColumns = juceStringArrayToJavaStringArray (resultColumns); + auto resultJavaColumns = juceStringArrayToJava (resultColumns); auto* env = getEnv(); @@ -550,7 +550,7 @@ public: if (extension.isEmpty()) return nullptr; - return juceStringArrayToJavaStringArray (filterMimeTypes (getMimeTypesForFileExtension (extension), + return juceStringArrayToJava (filterMimeTypes (getMimeTypesForFileExtension (extension), juceString (mimeTypeFilter.get()))); } @@ -683,40 +683,6 @@ private: return { index, filename, prepareFilesThread->getFilePaths()[index.getIntValue()] }; } - static LocalRef juceStringArrayToJavaStringArray (const StringArray& juceArray) - { - auto* env = getEnv(); - - auto javaArray = LocalRef (env->NewObjectArray ((jsize) juceArray.size(), - JavaString, - javaString ("").get())); - - for (int i = 0; i < juceArray.size(); ++i) - env->SetObjectArrayElement (javaArray, i, javaString (juceArray [i]).get()); - - return javaArray; - } - - static StringArray javaStringArrayToJuceStringArray (const LocalRef& javaArray) - { - if (javaArray.get() == 0) - return {}; - - auto* env = getEnv(); - - const int size = env->GetArrayLength (javaArray.get()); - - StringArray juceArray; - - for (int i = 0; i < size; ++i) - { - auto javaString = LocalRef ((jstring) env->GetObjectArrayElement (javaArray.get(), i)); - juceArray.add (juceString (javaString.get())); - } - - return juceArray; - } - static StringArray getSupportedColumns() { return StringArray ("_display_name", "_size"); diff --git a/modules/juce_gui_basics/native/juce_android_Windowing.cpp b/modules/juce_gui_basics/native/juce_android_Windowing.cpp index 0c5614cb94..9f8ca138d5 100644 --- a/modules/juce_gui_basics/native/juce_android_Windowing.cpp +++ b/modules/juce_gui_basics/native/juce_android_Windowing.cpp @@ -560,6 +560,8 @@ public: Component::unfocusAllComponents(); } + void handleAppPausedCallback() {} + void handleAppResumedCallback() { if (Component* kiosk = Desktop::getInstance().getKioskModeComponent()) @@ -630,10 +632,10 @@ public: void handlePaintCallback (JNIEnv* env, jobject canvas, jobject paint) { jobject rect = env->CallObjectMethod (canvas, CanvasMinimal.getClipBounds); - const int left = env->GetIntField (rect, AndroidRectClass.left); - const int top = env->GetIntField (rect, AndroidRectClass.top); - const int right = env->GetIntField (rect, AndroidRectClass.right); - const int bottom = env->GetIntField (rect, AndroidRectClass.bottom); + const int left = env->GetIntField (rect, AndroidRect.left); + const int top = env->GetIntField (rect, AndroidRect.top); + const int right = env->GetIntField (rect, AndroidRect.right); + const int bottom = env->GetIntField (rect, AndroidRect.bottom); env->DeleteLocalRef (rect); const Rectangle clip (left, top, right - left, bottom - top); @@ -810,6 +812,7 @@ JUCE_VIEW_CALLBACK (void, handleKeyDown, (JNIEnv* env, jobject /*view*/, JUCE_VIEW_CALLBACK (void, handleKeyUp, (JNIEnv* env, jobject /*view*/, jlong host, jint k, jint kc), handleKeyUpCallback ((int) k, (int) kc)) JUCE_VIEW_CALLBACK (void, handleBackButton, (JNIEnv* env, jobject /*view*/, jlong host), handleBackButtonCallback()) JUCE_VIEW_CALLBACK (void, handleKeyboardHidden, (JNIEnv* env, jobject /*view*/, jlong host), handleKeyboardHiddenCallback()) +JUCE_VIEW_CALLBACK (void, handleAppPaused, (JNIEnv* env, jobject /*view*/, jlong host), handleAppPausedCallback()) JUCE_VIEW_CALLBACK (void, handleAppResumed, (JNIEnv* env, jobject /*view*/, jlong host), handleAppResumedCallback()) //============================================================================== @@ -819,18 +822,6 @@ ComponentPeer* Component::createNewPeer (int styleFlags, void*) } //============================================================================== -#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ - METHOD (getRotation, "getRotation", "()I") - -DECLARE_JNI_CLASS (Display, "android/view/Display"); -#undef JNI_CLASS_MEMBERS - -#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ - METHOD (getDefaultDisplay, "getDefaultDisplay", "()Landroid/view/Display;") - -DECLARE_JNI_CLASS (WindowManager, "android/view/WindowManager"); -#undef JNI_CLASS_MEMBERS - bool Desktop::canUseSemiTransparentWindows() noexcept { return true; @@ -857,11 +848,11 @@ Desktop::DisplayOrientation Desktop::getCurrentOrientation() const if (windowManager.get() != 0) { - LocalRef display = LocalRef (env->CallObjectMethod (windowManager, WindowManager.getDefaultDisplay)); + LocalRef display = LocalRef (env->CallObjectMethod (windowManager, AndroidWindowManager.getDefaultDisplay)); if (display.get() != 0) { - int rotation = env->CallIntMethod (display, Display.getRotation); + int rotation = env->CallIntMethod (display, AndroidDisplay.getRotation); switch (rotation) { diff --git a/modules/juce_gui_extra/native/juce_android_PushNotifications.cpp b/modules/juce_gui_extra/native/juce_android_PushNotifications.cpp index 458edfd73f..b6d6217db0 100644 --- a/modules/juce_gui_extra/native/juce_android_PushNotifications.cpp +++ b/modules/juce_gui_extra/native/juce_android_PushNotifications.cpp @@ -1427,8 +1427,8 @@ struct PushNotifications::Pimpl propertiesDynamicObject->setProperty ("clickAction", juceString (clickAction.get())); propertiesDynamicObject->setProperty ("bodyLocalizationKey", juceString (bodyLocalizationKey.get())); propertiesDynamicObject->setProperty ("titleLocalizationKey", juceString (titleLocalizationKey.get())); - propertiesDynamicObject->setProperty ("bodyLocalizationArgs", jobjectArrayToStringArray (bodyLocalizationArgs)); - propertiesDynamicObject->setProperty ("titleLocalizationArgs", jobjectArrayToStringArray (titleLocalizationArgs)); + propertiesDynamicObject->setProperty ("bodyLocalizationArgs", javaStringArrayToJuce (bodyLocalizationArgs)); + propertiesDynamicObject->setProperty ("titleLocalizationArgs", javaStringArrayToJuce (titleLocalizationArgs)); propertiesDynamicObject->setProperty ("link", link.get() != 0 ? juceString ((jstring) env->CallObjectMethod (link, AndroidUri.toString)) : String()); } @@ -1436,23 +1436,6 @@ struct PushNotifications::Pimpl return n; } - - static StringArray jobjectArrayToStringArray (const LocalRef& array) - { - if (array == 0) - return {}; - - auto* env = getEnv(); - - const int size = env->GetArrayLength (array.get()); - - StringArray stringArray; - - for (int i = 0; i < size; ++i) - stringArray.add (juceString ((jstring) env->GetObjectArrayElement (array.get(), (jsize) i))); - - return stringArray; - } #endif void setupChannels (const Array& groups, const Array& channels) diff --git a/modules/juce_video/capture/juce_CameraDevice.cpp b/modules/juce_video/capture/juce_CameraDevice.cpp index 62d23e7f20..bfed050cb0 100644 --- a/modules/juce_video/capture/juce_CameraDevice.cpp +++ b/modules/juce_video/capture/juce_CameraDevice.cpp @@ -27,22 +27,129 @@ namespace juce { -#if JUCE_MAC || JUCE_IOS +#if JUCE_MAC #include "../native/juce_mac_CameraDevice.h" #elif JUCE_WINDOWS #include "../native/juce_win32_CameraDevice.h" +#elif JUCE_IOS + #if JUCE_CLANG + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wunguarded-availability-new" + #endif + + #include "../native/juce_ios_CameraDevice.h" + + #if JUCE_CLANG + #pragma clang diagnostic pop + #endif #elif JUCE_ANDROID #include "../native/juce_android_CameraDevice.h" #endif +#if JUCE_ANDROID || JUCE_IOS +//============================================================================== +class CameraDevice::CameraFactory +{ +public: + static CameraFactory& getInstance() + { + static CameraFactory factory; + return factory; + } + + void openCamera (int index, OpenCameraResultCallback resultCallback, + int minWidth, int minHeight, int maxWidth, int maxHeight, bool useHighQuality) + { + auto cameraId = getAvailableDevices()[index]; + + if (getCameraIndex (cameraId) != -1) + { + // You are trying to open the same camera twice. + jassertfalse; + return; + } + + std::unique_ptr device (new CameraDevice (cameraId, index, + minWidth, minHeight, maxWidth, + maxHeight, useHighQuality)); + + camerasToOpen.add ({ nextRequestId++, + std::unique_ptr (device.release()), + resultCallback }); + + auto& pendingOpen = camerasToOpen.getReference (camerasToOpen.size() - 1); + + pendingOpen.device->pimpl->open ([this](const String& deviceId, const String& error) + { + int index = getCameraIndex (deviceId); + + if (index == -1) + return; + + auto& pendingOpen = camerasToOpen.getReference (index); + + if (error.isEmpty()) + pendingOpen.resultCallback (pendingOpen.device.release(), error); + else + pendingOpen.resultCallback (nullptr, error); + + int id = pendingOpen.requestId; + + MessageManager::callAsync ([this, id]() { removeRequestWithId (id); }); + }); + } + +private: + int getCameraIndex (const String& cameraId) const + { + for (int i = 0; i < camerasToOpen.size(); ++i) + { + auto& pendingOpen = camerasToOpen.getReference (i); + + if (pendingOpen.device->pimpl->getCameraId() == cameraId) + return i; + } + + return -1; + } + + void removeRequestWithId (int id) + { + for (int i = camerasToOpen.size(); --i >= 0;) + { + if (camerasToOpen.getReference (i).requestId == id) + { + camerasToOpen.remove (i); + return; + } + } + } + + struct PendingCameraOpen + { + int requestId; + std::unique_ptr device; + OpenCameraResultCallback resultCallback; + }; + + Array camerasToOpen; + static int nextRequestId; +}; + +int CameraDevice::CameraFactory::nextRequestId = 0; + +#endif + //============================================================================== CameraDevice::CameraDevice (const String& nm, int index, int minWidth, int minHeight, int maxWidth, int maxHeight, bool useHighQuality) - : name (nm), pimpl (new Pimpl (name, index, minWidth, minHeight, maxWidth, maxHeight, useHighQuality)) + : name (nm), pimpl (new Pimpl (*this, name, index, minWidth, minHeight, maxWidth, maxHeight, useHighQuality)) { } CameraDevice::~CameraDevice() { + jassert (juce::MessageManager::getInstance()->currentThreadHasLockedMessageManager()); + stopRecording(); pimpl.reset(); } @@ -52,6 +159,11 @@ Component* CameraDevice::createViewerComponent() return new ViewerComponent (*this); } +void CameraDevice::takeStillPicture (std::function pictureTakenCallback) +{ + pimpl->takeStillPicture (pictureTakenCallback); +} + void CameraDevice::startRecordingToFile (const File& file, int quality) { stopRecording(); @@ -68,18 +180,6 @@ void CameraDevice::stopRecording() pimpl->stopRecording(); } -void CameraDevice::addListener (Listener* listenerToAdd) -{ - if (listenerToAdd != nullptr) - pimpl->addListener (listenerToAdd); -} - -void CameraDevice::removeListener (Listener* listenerToRemove) -{ - if (listenerToRemove != nullptr) - pimpl->removeListener (listenerToRemove); -} - //============================================================================== StringArray CameraDevice::getAvailableDevices() { @@ -94,12 +194,44 @@ CameraDevice* CameraDevice::openDevice (int index, int maxWidth, int maxHeight, bool useHighQuality) { + jassert (juce::MessageManager::getInstance()->currentThreadHasLockedMessageManager()); + + #if ! JUCE_ANDROID && ! JUCE_IOS std::unique_ptr d (new CameraDevice (getAvailableDevices() [index], index, minWidth, minHeight, maxWidth, maxHeight, useHighQuality)); if (d != nullptr && d->pimpl->openedOk()) return d.release(); + #else + ignoreUnused (index, minWidth, minHeight); + ignoreUnused (maxWidth, maxHeight, useHighQuality); + + // Use openDeviceAsync to open a camera device on iOS or Android. + jassertfalse; + #endif return nullptr; } +void CameraDevice::openDeviceAsync (int index, OpenCameraResultCallback resultCallback, + int minWidth, int minHeight, int maxWidth, int maxHeight, bool useHighQuality) +{ + jassert (juce::MessageManager::getInstance()->currentThreadHasLockedMessageManager()); + + if (resultCallback == nullptr) + { + // A valid callback must be passed. + jassertfalse; + return; + } + + #if JUCE_ANDROID || JUCE_IOS + CameraFactory::getInstance().openCamera (index, static_cast (resultCallback), + minWidth, minHeight, maxWidth, maxHeight, useHighQuality); + #else + auto* device = openDevice (index, minWidth, minHeight, maxWidth, maxHeight, useHighQuality); + + resultCallback (device, device != nullptr ? String() : "Could not open camera device"); + #endif +} + } // namespace juce diff --git a/modules/juce_video/capture/juce_CameraDevice.h b/modules/juce_video/capture/juce_CameraDevice.h index 5f62674b10..751990e7ef 100644 --- a/modules/juce_video/capture/juce_CameraDevice.h +++ b/modules/juce_video/capture/juce_CameraDevice.h @@ -35,9 +35,9 @@ namespace juce Controls any video capture devices that might be available. Use getAvailableDevices() to list the devices that are attached to the - system, then call openDevice to open one for use. Once you have a CameraDevice - object, you can get a viewer component from it, and use its methods to - stream to a file or capture still-frames. + system, then call openDevice() or openDeviceAsync() to open one for use. + Once you have a CameraDevice object, you can get a viewer component from it, + and use its methods to stream to a file or capture still-frames. @tags{Video} */ @@ -50,17 +50,18 @@ public: //============================================================================== /** Returns a list of the available cameras on this machine. - You can open one of these devices by calling openDevice(). + You can open one of these devices by calling openDevice() or openDeviceAsync(). */ static StringArray getAvailableDevices(); - /** Opens a camera device. + /** Synchronously opens a camera device. This function should not be used on iOS or + Android, use openDeviceAsync() instead. The index parameter indicates which of the items returned by getAvailableDevices() to open. The size constraints allow the method to choose between different resolutions if - the camera supports this. If the resolution cam't be specified (e.g. on the Mac) + the camera supports this. If the resolution can't be specified (e.g. on the Mac) then these will be ignored. On Mac, if highQuality is false, then the camera will be opened in preview mode @@ -72,16 +73,62 @@ public: int maxWidth = 1024, int maxHeight = 768, bool highQuality = true); + using OpenCameraResultCallback = std::function; + + /** Asynchronously opens a camera device on iOS (iOS 7+) or Android (API 21+). + On other platforms, the function will simply call openDevice(). Upon completion, + resultCallback will be invoked with valid CameraDevice* and an empty error + String on success, or nullptr CameraDevice and a non-empty error String on failure. + + This is the preferred method of opening a camera device, because it works on all + platforms, whereas synchronous openDevice() does not work on iOS & Android. + + The index parameter indicates which of the items returned by getAvailableDevices() + to open. + + The size constraints allow the method to choose between different resolutions if + the camera supports this. If the resolution can't be specified then these will be + ignored. + + On iOS, if you want to switch a device, it is more efficient to open a new device + before closing the older one, because this way both devices can share the same + underlying camera session. Otherwise, the session needs to be close first, and this + is a lengthy process that can take several seconds. + + The Android implementation currently supports a maximum recording resolution of + 1080p. Choosing a larger size will result in larger pictures taken, but the video + will be capped at 1080p. + */ + static void openDeviceAsync (int deviceIndex, + OpenCameraResultCallback resultCallback, + int minWidth = 128, int minHeight = 64, + int maxWidth = 1024, int maxHeight = 768, + bool highQuality = true); + //============================================================================== /** Returns the name of this device */ const String& getName() const noexcept { return name; } /** Creates a component that can be used to display a preview of the video from this camera. + + Note: while you can change the size of the preview component, the actual + preview display may be smaller than the size requested, because the correct + aspect ratio is maintained automatically. */ Component* createViewerComponent(); //============================================================================== + /** Triggers a still picture capture. Upon completion, pictureTakenCallback will + be invoked on a message thread. + + On Android, before calling takeStillPicture(), you need to create a preview with + createViewerComponent() and you need to make it visible on screen. + + Android does not support simultaneous video recording and still picture capture. + */ + void takeStillPicture (std::function pictureTakenCallback); + /** Starts recording video to the specified file. You should use getFileExtension() to find out the correct extension to @@ -95,6 +142,16 @@ public: The quality parameter can be 0, 1, or 2, to indicate low, medium, or high. It may or may not be used, depending on the driver. + + On Android, before calling startRecordingToFile(), you need to create a preview with + createViewerComponent() and you need to make it visible on screen. + + The Android camera also requires exclusive access to the audio device, so make sure + you close any open audio devices with AudioDeviceManager::closeAudioDevice() first. + + Android does not support simultaneous video recording and still picture capture. + + @see AudioDeviceManager::closeAudioDevice, AudioDeviceManager::restartLastAudioDevice */ void startRecordingToFile (const File& file, int quality = 2); @@ -113,36 +170,9 @@ public: */ Time getTimeOfFirstRecordedFrame() const; - //============================================================================== - /** - Receives callbacks with images from a CameraDevice. - - @see CameraDevice::addListener - */ - class JUCE_API Listener - { - public: - Listener() {} - virtual ~Listener() {} - - /** This method is called when a new image arrives. - - This may be called by any thread, so be careful about thread-safety, - and make sure that you process the data as quickly as possible to - avoid glitching! - */ - virtual void imageReceived (const Image& image) = 0; - }; - - /** Adds a listener to receive images from the camera. - - Be very careful not to delete the listener without first removing it by calling - removeListener(). - */ - void addListener (Listener* listenerToAdd); - - /** Removes a listener that was previously added with addListener(). */ - void removeListener (Listener* listenerToRemove); + /** Set this callback to be notified whenever an error occurs. You may need to close + and reopen the device to be able to use it further. */ + std::function onErrorOccurred; private: String name; @@ -158,6 +188,32 @@ private: CameraDevice (const String& name, int index, int minWidth, int minHeight, int maxWidth, int maxHeight, bool highQuality); + #if JUCE_ANDROID || JUCE_IOS + class CameraFactory; + #endif + + #if JUCE_ANDROID + 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); + #endif + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CameraDevice) }; diff --git a/modules/juce_video/juce_video.h b/modules/juce_video/juce_video.h index 4333e43db4..ec04bc6ab8 100644 --- a/modules/juce_video/juce_video.h +++ b/modules/juce_video/juce_video.h @@ -59,13 +59,23 @@ //============================================================================= /** Config: JUCE_USE_CAMERA - Enables web-cam support using the CameraDevice class (Mac and Windows). + Enables camera support using the CameraDevice class (Mac, Windows, iOS, Android). */ #ifndef JUCE_USE_CAMERA #define JUCE_USE_CAMERA 0 #endif -#if ! (JUCE_MAC || JUCE_WINDOWS) +#ifndef JUCE_CAMERA_LOG_ENABLED + #define JUCE_CAMERA_LOG_ENABLED 0 +#endif + +#if JUCE_CAMERA_LOG_ENABLED + #define JUCE_CAMERA_LOG(x) DBG(x) +#else + #define JUCE_CAMERA_LOG(x) {} +#endif + +#if ! (JUCE_MAC || JUCE_WINDOWS || JUCE_IOS || JUCE_ANDROID) #undef JUCE_USE_CAMERA #endif diff --git a/modules/juce_video/native/juce_android_CameraDevice.h b/modules/juce_video/native/juce_android_CameraDevice.h index 18e402ae76..58c7413b19 100644 --- a/modules/juce_video/native/juce_android_CameraDevice.h +++ b/modules/juce_video/native/juce_android_CameraDevice.h @@ -24,62 +24,3356 @@ ============================================================================== */ -struct CameraDevice::Pimpl +#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, "", "()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, "", "(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, "", "(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, "", "()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, "", "(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, "", "(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, "", "(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, "", "(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 { - Pimpl (const String&, int /*index*/, int /*minWidth*/, int /*minHeight*/, int /*maxWidth*/, int /*maxHeight*/) +public: + struct Owner { + virtual ~Owner() {} + + virtual void run() = 0; + }; + + AndroidRunnable (Owner& ownerToUse) + : owner (ownerToUse) + {} + +private: + Owner& owner; + + jobject invoke (jobject proxy, jobject method, jobjectArray args) override + { + auto* env = getEnv(); + auto methodName = juce::juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName)); + + if (methodName == "run") + { + owner.run(); + return nullptr; + } + + // invoke base class + return AndroidInterfaceImplementer::invoke (proxy, method, args); + } +}; + +//============================================================================== +class TextureViewSurfaceTextureListener : public AndroidInterfaceImplementer +{ +public: + struct Owner + { + virtual ~Owner() {} + + virtual void onSurfaceTextureAvailable (LocalRef& surface, int width, int height) = 0; + virtual bool onSurfaceTextureDestroyed (LocalRef& surface) = 0; + virtual void onSurfaceTextureSizeChanged (LocalRef& surface, int width, int height) = 0; + virtual void onSurfaceTextureUpdated (LocalRef& surface) = 0; + }; + + TextureViewSurfaceTextureListener (Owner& ownerToUse) + : owner (ownerToUse) + {} + + jobject invoke (jobject proxy, jobject method, jobjectArray args) override + { + auto* env = getEnv(); + + auto methodName = juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName)); + + int numArgs = args != nullptr ? env->GetArrayLength (args) : 0; + + if (methodName == "onSurfaceTextureAvailable" && numArgs == 3) + { + auto surface = LocalRef (env->GetObjectArrayElement (args, 0)); + auto width = LocalRef (env->GetObjectArrayElement (args, 1)); + auto height = LocalRef (env->GetObjectArrayElement (args, 2)); + + auto widthInt = env->CallIntMethod (width, JavaInteger.intValue); + auto heightInt = env->CallIntMethod (height, JavaInteger.intValue); + + owner.onSurfaceTextureAvailable (surface, widthInt, heightInt); + return nullptr; + } + else if (methodName == "onSurfaceTextureDestroyed" && numArgs == 1) + { + auto surface = LocalRef (env->GetObjectArrayElement (args, 0)); + auto result = owner.onSurfaceTextureDestroyed (surface); + + return env->CallStaticObjectMethod (JavaBoolean, JavaBoolean.valueOf, result); + } + else if (methodName == "onSurfaceTextureSizeChanged" && numArgs == 3) + { + auto surface = LocalRef (env->GetObjectArrayElement (args, 0)); + auto width = LocalRef (env->GetObjectArrayElement (args, 1)); + auto height = LocalRef (env->GetObjectArrayElement (args, 2)); + + auto widthInt = env->CallIntMethod (width, JavaInteger.intValue); + auto heightInt = env->CallIntMethod (height, JavaInteger.intValue); + + owner.onSurfaceTextureSizeChanged (surface, widthInt, heightInt); + return nullptr; + } + else if (methodName == "onSurfaceTextureUpdated" && numArgs == 1) + { + auto surface = LocalRef (env->GetObjectArrayElement (args, 0)); + + owner.onSurfaceTextureUpdated (surface); + return nullptr; + } + + return AndroidInterfaceImplementer::invoke (proxy, method, args); + } + +private: + Owner& owner; +}; + +//============================================================================== +class ImageReaderOnImageAvailableListener : public AndroidInterfaceImplementer +{ +public: + struct Owner + { + virtual ~Owner() {} + + virtual void onImageAvailable (LocalRef& imageReader) = 0; + }; + + ImageReaderOnImageAvailableListener (Owner& ownerToUse) + : owner (ownerToUse) + {} + + jobject invoke (jobject proxy, jobject method, jobjectArray args) override + { + auto* env = getEnv(); + + auto methodName = juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName)); + + int numArgs = args != nullptr ? env->GetArrayLength (args) : 0; + + if (methodName == "onImageAvailable" && numArgs == 1) + { + auto imageReader = LocalRef (env->GetObjectArrayElement (args, 0)); + + owner.onImageAvailable (imageReader); + return nullptr; + } + + return AndroidInterfaceImplementer::invoke (proxy, method, args); + } + +private: + Owner& owner; +}; + +//============================================================================== +class MediaRecorderOnInfoListener : public AndroidInterfaceImplementer +{ +public: + struct Owner + { + virtual ~Owner() {} + + virtual void onInfo (LocalRef& mediaRecorder, int what, int extra) = 0; + }; + + MediaRecorderOnInfoListener (Owner& ownerToUse) + : owner (ownerToUse) + {} + + jobject invoke (jobject proxy, jobject method, jobjectArray args) override + { + auto* env = getEnv(); + + auto methodName = juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName)); + + int numArgs = args != nullptr ? env->GetArrayLength (args) : 0; + + if (methodName == "onInfo" && numArgs == 3) + { + auto mediaRecorder = LocalRef (env->GetObjectArrayElement (args, 0)); + auto what = LocalRef (env->GetObjectArrayElement (args, 1)); + auto extra = LocalRef (env->GetObjectArrayElement (args, 2)); + + auto whatInt = (int) env->CallIntMethod (what, JavaInteger.intValue); + auto extraInt = (int) env->CallIntMethod (extra, JavaInteger.intValue); + + owner.onInfo (mediaRecorder, whatInt, extraInt); + return nullptr; + } + + return AndroidInterfaceImplementer::invoke (proxy, method, args); + } + +private: + Owner& owner; +}; + +//============================================================================== +class MediaRecorderOnErrorListener : public AndroidInterfaceImplementer +{ +public: + struct Owner + { + virtual ~Owner() {} + + virtual void onError (LocalRef& mediaRecorder, int what, int extra) = 0; + }; + + MediaRecorderOnErrorListener (Owner& ownerToUse) + : owner (ownerToUse) + {} + + jobject invoke (jobject proxy, jobject method, jobjectArray args) override + { + auto* env = getEnv(); + + auto methodName = juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName)); + + int numArgs = args != nullptr ? env->GetArrayLength (args) : 0; + + if (methodName == "onError" && numArgs == 3) + { + auto mediaRecorder = LocalRef (env->GetObjectArrayElement (args, 0)); + auto what = LocalRef (env->GetObjectArrayElement (args, 1)); + auto extra = LocalRef (env->GetObjectArrayElement (args, 2)); + + auto whatInt = (int) env->CallIntMethod (what, JavaInteger.intValue); + auto extraInt = (int) env->CallIntMethod (extra, JavaInteger.intValue); + + owner.onError (mediaRecorder, whatInt, extraInt); + return nullptr; + } + + return AndroidInterfaceImplementer::invoke (proxy, method, args); + } + +private: + Owner& owner; +}; + +//============================================================================== +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; + + 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(this)); + #endif } - void startRecordingToFile (const File&, int /*quality*/) + #if __ANDROID_API__ < 21 + // Dummy implementations for unsupported API levels. + void open (InternalOpenCameraResultCallback) {} + void takeStillPicture (std::function) {} + void startRecordingToFile (const File&, int) {} + void stopRecording() {} + + 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 (cameraOpenCallbackToUse); + + // A valid camera open callback must be passed. + jassert (cameraOpenCallback != nullptr); + + // The same camera can be opened only once! + jassert (scopedCameraDevice == nullptr); + + if (cameraOpenCallback == nullptr || scopedCameraDevice != nullptr) + return; + + WeakReference safeThis (this); + RuntimePermissions::request (RuntimePermissions::camera, [safeThis] (bool granted) mutable + { + if (safeThis != nullptr) + safeThis->continueOpenRequest (granted); + }); + } + + void continueOpenRequest (bool granted) + { + if (granted) + { + getEnv()->CallVoidMethod (android.activity, JuceAppActivity.addAppPausedResumedListener, + appPausedResumedListenerNative.get(), reinterpret_cast (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 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&&> (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 {}; - } - - void addListener (CameraDevice::Listener* listenerToAdd) - { - const ScopedLock sl (listenerLock); - listeners.addIfNotAlreadyThere (listenerToAdd); - } - - void removeListener (CameraDevice::Listener* listenerToRemove) - { - const ScopedLock sl (listenerLock); - listeners.removeFirstMatchingValue (listenerToRemove); + return firstRecordedFrameTimeMs; } static StringArray getAvailableDevices() { StringArray results; + auto* env = getEnv(); + + auto cameraManagerToUse = initialiseCameraManager(); + auto cameraIdArray = LocalRef ((jobjectArray) env->CallObjectMethod (cameraManagerToUse, + CameraManager.getCameraIdList)); + + results = javaStringArrayToJuce (cameraIdArray); + + for (auto& result : results) + printDebugCameraInfo (cameraManagerToUse, result); + return results; } private: + enum + { + ERROR_CAMERA_IN_USE = 1, + ERROR_MAX_CAMERAS_IN_USE = 2, + ERROR_CAMERA_DISABLED = 3, + ERROR_CAMERA_DEVICE = 4, + ERROR_CAMERA_SERVICE = 5 + }; + + static String cameraErrorCodeToString (int errorCode) + { + switch (errorCode) + { + case ERROR_CAMERA_IN_USE: return "Camera already in use."; + case ERROR_MAX_CAMERAS_IN_USE: return "Too many opened camera devices."; + case ERROR_CAMERA_DISABLED: return "Camera disabled."; + case ERROR_CAMERA_DEVICE: return "Fatal error."; + case ERROR_CAMERA_SERVICE: return "Fatal error. Reboot required or persistent hardware problem."; + default: return "Unknown error."; + } + } + + static LocalRef initialiseCameraManager() + { + return LocalRef (getEnv()->CallObjectMethod (android.activity, JuceAppActivity.getSystemService, + javaString ("camera").get())); + } + + static LocalRef initialiseCameraCharacteristics (const GlobalRef& cameraManager, const String& cameraId) + { + return LocalRef (getEnv()->CallObjectMethod (cameraManager, + CameraManager.getCameraCharacteristics, + javaString (cameraId).get())); + } + + static void printDebugCameraInfo (const LocalRef& cameraManagerToUse, const String& cameraId) + { + auto* env = getEnv(); + + auto characteristics = LocalRef (env->CallObjectMethod (cameraManagerToUse, + CameraManager.getCameraCharacteristics, + javaString (cameraId).get())); + + auto keysList = LocalRef (env->CallObjectMethod (characteristics, CameraCharacteristics.getKeys)); + + const int size = env->CallIntMethod (keysList, JavaList.size); + + JUCE_CAMERA_LOG ("Camera id: " + cameraId + ", characteristics keys num: " + String (size)); + + for (int i = 0; i < size; ++i) + { + auto key = LocalRef (env->CallObjectMethod (keysList, JavaList.get, i)); + auto jKeyName = LocalRef ((jstring) env->CallObjectMethod (key, CameraCharacteristicsKey.getName)); + auto keyName = juceString (jKeyName); + + auto keyValue = LocalRef (env->CallObjectMethod (characteristics, CameraCharacteristics.get, key.get())); + auto jKeyValueString = LocalRef ((jstring) env->CallObjectMethod (keyValue, JavaObject.toString)); + auto keyValueString = juceString (jKeyValueString); + + auto &kvs = keyValueString; + + if (kvs.startsWith ("[I") || kvs.startsWith ("[F") || kvs.startsWith ("[Z") || kvs.startsWith ("[B")) + { + printPrimitiveArrayElements (keyValue, keyName, keyValueString); + } + else if (kvs.startsWith ("[Landroid.util.Range")) + { + printRangeArrayElements (keyValue, keyName); + } + else + { + int chunkSize = 256; + + if (keyValueString.length() > chunkSize) + { + JUCE_CAMERA_LOG ("Key: " + keyName); + + for (int 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& 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& rangeArray, const String& keyName) + { + auto* env = getEnv(); + + jobjectArray ranges = static_cast (rangeArray.get()); + + int numRanges = env->GetArrayLength (ranges); + + String result; + + for (int i = 0; i < numRanges; ++i) + { + auto range = LocalRef (env->GetObjectArrayElement (ranges, i)); + + auto jRangeString = LocalRef ((jstring) env->CallObjectMethod (range, AndroidRange.toString)); + + result << juceString (jRangeString) << " "; + } + + 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(), + jpegImageFormat)), + supportedVideoRecordingOutputSizes (retrieveOutputSizes (scalerStreamConfigurationMap, + getClassForName ("android.media.MediaRecorder"), + -1)), + defaultPreviewSize (getSmallestSize (supportedPreviewOutputSizes)), + previewBufferSize (getLargestSize (supportedPreviewOutputSizes)) + { + printSizesLog (supportedPreviewOutputSizes, "SurfaceTexture"); + printSizesLog (supportedStillImageOutputSizes, "JPEG"); + printSizesLog (supportedVideoRecordingOutputSizes, "MediaRecorder"); + } + + Array> getSupportedPreviewOutputSizes() const noexcept { return supportedPreviewOutputSizes; } + Array> getSupportedStillImageOutputSizes() const noexcept { return supportedStillImageOutputSizes; } + Array> getSupportedVideoRecordingOutputSizes() const noexcept { return supportedVideoRecordingOutputSizes; } + + Rectangle getDefaultPreviewSize() const noexcept { return defaultPreviewSize; } + Rectangle getPreviewBufferSize() const noexcept { return previewBufferSize; } + + bool isOutputSupportedForSurface (const LocalRef& surface) const + { + return getEnv()->CallBooleanMethod (scalerStreamConfigurationMap, AndroidStreamConfigurationMap.isOutputSupportedForSurface, surface.get()); + } + + static constexpr int jpegImageFormat = 256; + + private: + GlobalRef scalerStreamConfigurationMap; + + Array> supportedPreviewOutputSizes; + Array> supportedStillImageOutputSizes; + Array> supportedVideoRecordingOutputSizes; + Rectangle defaultPreviewSize, previewBufferSize; + + GlobalRef getStreamConfigurationMap (const GlobalRef& cameraCharacteristics) + { + auto* env = getEnv(); + + auto scalerStreamConfigurationMapKey = LocalRef (env->GetStaticObjectField (CameraCharacteristics, + CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)); + + return GlobalRef (LocalRef (env->CallObjectMethod (cameraCharacteristics, + CameraCharacteristics.get, + scalerStreamConfigurationMapKey.get()))); + } + + static Array> retrieveOutputSizes (GlobalRef& scalerStreamConfigurationMap, + const LocalRef& outputClass, + int format) + { + Array> result; + + auto* env = getEnv(); + + auto outputSizes = outputClass.get() != nullptr + ? LocalRef ((jobjectArray) env->CallObjectMethod (scalerStreamConfigurationMap, + AndroidStreamConfigurationMap.getOutputSizesForClass, + outputClass.get())) + : LocalRef ((jobjectArray) env->CallObjectMethod (scalerStreamConfigurationMap, + AndroidStreamConfigurationMap.getOutputSizesForFormat, + (jint) format)); + + if (format != -1) + { + auto supported = (env->CallBooleanMethod (scalerStreamConfigurationMap, AndroidStreamConfigurationMap.isOutputSupportedFor, (jint) format) != 0); + + if (! supported) + { + // The output format is not supported by this device, still image capture will not work! + jassertfalse; + return {}; + } + } + + int numSizes = env->GetArrayLength (outputSizes); + + jassert (numSizes > 0); + + for (int i = 0; i < numSizes; ++i) + { + auto size = LocalRef (env->GetObjectArrayElement (outputSizes, i)); + + auto width = env->CallIntMethod (size, AndroidSize.getWidth); + auto height = env->CallIntMethod (size, AndroidSize.getHeight); + + result.add (Rectangle (0, 0, width, height)); + } + + return result; + } + + static LocalRef getClassForName (const String& name) + { + return LocalRef (getEnv()->CallStaticObjectMethod (JavaClass, JavaClass.forName, + javaString (name).get())); + } + + static void printSizesLog (const Array>& sizes, const String& className) + { + ignoreUnused (sizes, className); + + JUCE_CAMERA_LOG ("Sizes for class " + className); + + #if JUCE_CAMERA_LOG_ENABLED + for (auto& s : sizes) + JUCE_CAMERA_LOG (s.toString() + "\n"); + #endif + } + + Rectangle getSmallestSize (const Array>& sizes) const + { + if (sizes.size() == 0) + return {}; + + auto smallestSize = sizes[0]; + + for (auto& size : sizes) + { + if (size.getWidth() < smallestSize.getWidth() && size.getHeight() < smallestSize.getHeight()) + smallestSize = size; + } + + return smallestSize; + } + + Rectangle getLargestSize (const Array>& sizes) const + { + if (sizes.size() == 0) + return {}; + + auto largestSize = sizes[0]; + + for (auto& size : sizes) + { + if (size.getWidth() > largestSize.getWidth() && size.getHeight() > largestSize.getHeight()) + largestSize = size; + } + + return largestSize; + } + }; + + //============================================================================== + class PreviewDisplay : private TextureViewSurfaceTextureListener::Owner + { + public: + struct Listener + { + virtual ~Listener() {} + + virtual void previewDisplayReady() = 0; + virtual void previewDisplayAboutToBeDestroyed() = 0; + }; + + PreviewDisplay (Rectangle bufferSize) + : textureViewSurfaceTextureListener (*this), + textureView (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 createSurface() + { + // Surface may get destroyed while session is being configured, if + // the preview gets hidden in the meantime, so bailout. + if (! isReady()) + return LocalRef (nullptr); + + auto* env = getEnv(); + + auto surfaceTexture = LocalRef (env->CallObjectMethod (textureView, + AndroidTextureView.getSurfaceTexture)); + + // NB: too small buffer will result in pixelated preview. A buffer with wrong aspect ratio + // can result in a cropped preview. + env->CallVoidMethod (surfaceTexture, AndroidSurfaceTexture.setDefaultBufferSize, (jint) bufferWidth, (jint) bufferHeight); + + auto surface = LocalRef (env->NewObject (AndroidSurface, AndroidSurface.constructor, surfaceTexture.get())); + + return surface; + } + + const GlobalRef& getNativeView() { return textureView; } + + void updateSurfaceTransform() + { + auto* env = getEnv(); + + auto windowManager = LocalRef (env->CallObjectMethod (android.activity, JuceAppActivity.getWindowManager)); + auto display = LocalRef (env->CallObjectMethod (windowManager, AndroidWindowManager.getDefaultDisplay)); + auto rotation = env->CallIntMethod (display, AndroidDisplay.getRotation); + + static constexpr int rotation90 = 1; + static constexpr int rotation270 = 3; + + auto matrix = LocalRef (env->NewObject (AndroidMatrix, AndroidMatrix.constructor)); + + if (rotation == rotation90 || rotation == rotation270) + { + env->CallBooleanMethod (matrix, AndroidMatrix.postScale, jfloat (height / (float) width), jfloat (width / (float) height), (jfloat) 0, (jfloat) 0); + env->CallBooleanMethod (matrix, AndroidMatrix.postRotate, (jfloat) 90 * (rotation - 2), (jfloat) 0, (jfloat) 0); + env->CallBooleanMethod (matrix, AndroidMatrix.postTranslate, (jfloat) (rotation == 3 ? width : 0), (jfloat) (rotation == 1 ? height : 0)); + } + + env->CallVoidMethod (textureView, AndroidTextureView.setTransform, matrix.get()); + } + + private: + ListenerList listeners; + + TextureViewSurfaceTextureListener textureViewSurfaceTextureListener; + GlobalRef textureView; + int width = -1, height = -1; + int bufferWidth, bufferHeight; + + void onSurfaceTextureAvailable (LocalRef& /*surface*/, int widthToUse, int heightToUse) override + { + JUCE_CAMERA_LOG ("onSurfaceTextureAvailable()"); + + width = widthToUse; + height = heightToUse; + + updateSurfaceTransform(); + + listeners.call (&Listener::previewDisplayReady); + } + + bool onSurfaceTextureDestroyed (LocalRef& /*surface*/) override + { + JUCE_CAMERA_LOG ("onSurfaceTextureDestroyed()"); + + listeners.call (&Listener::previewDisplayAboutToBeDestroyed); + + return true; + } + + void onSurfaceTextureSizeChanged (LocalRef& /*surface*/, int widthToUse, int heightToUse) override + { + JUCE_CAMERA_LOG ("onSurfaceTextureSizeChanged()"); + + width = widthToUse; + height = heightToUse; + + updateSurfaceTransform(); + } + + void onSurfaceTextureUpdated (LocalRef& /*surface*/) override + { + JUCE_CAMERA_LOG ("onSurfaceTextureUpdated()"); + } + + JUCE_DECLARE_NON_COPYABLE (PreviewDisplay) + }; + + //============================================================================== + class ImageReader : private ImageReaderOnImageAvailableListener::Owner + { + public: + ImageReader (Pimpl& ownerToUse, GlobalRef& handlerToUse, + int imageWidth, int imageHeight, int cameraSensorOrientationToUse) + : owner (ownerToUse), + cameraSensorOrientation (cameraSensorOrientationToUse), + imageReader (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 getSurface() const + { + return LocalRef (getEnv()->CallObjectMethod (imageReader, AndroidImageReader.getSurface)); + } + + void resetNotificationFlag() + { + hasNotifiedListeners.set (0); + } + + private: + Pimpl& owner; + int cameraSensorOrientation; + + GlobalRef imageReader; + ImageReaderOnImageAvailableListener onImageAvailableListener; + static constexpr int numImagesToKeep = 2; + Atomic hasNotifiedListeners { 0 }; + + JUCE_DECLARE_WEAK_REFERENCEABLE (ImageReader) + + void onImageAvailable (LocalRef& /*imageReader*/) override + { + JUCE_CAMERA_LOG ("onImageAvailable()"); + + auto* env = getEnv(); + + auto jImage = LocalRef (env->CallObjectMethod (imageReader, AndroidImageReader.acquireLatestImage)); + + if (jImage.get() == nullptr) + return; + + auto cameraLensFrontFacing = owner.getCameraLensFacing() == 0; + + // NB: could use sensor orientation here to get real-world orientation, but then the resulting + // image could not match the UI orientation. + auto image = androidImageToJuceWithFixedOrientation (jImage, owner.deviceOrientationChangeListener.getDeviceOrientation(), + Desktop::getInstance().getCurrentOrientation(), + cameraLensFrontFacing, + cameraSensorOrientation); + + env->CallVoidMethod (jImage, AndroidImage.close); + + WeakReference safeThis (this); + + // 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.notifyImageReceived (image); }); + } + + struct ImageBuffer + { + LocalRef byteArray; + int size; + }; + + static Image androidImageToJuceWithFixedOrientation (const LocalRef& androidImage, + Desktop::DisplayOrientation deviceOrientationFromAccelerometerSensor, + Desktop::DisplayOrientation targetOrientation, + bool cameraLensFrontFacing, + int cameraSensorOrientation) + { + auto* env = getEnv(); + + auto planes = LocalRef ((jobjectArray) env->CallObjectMethod (androidImage, AndroidImage.getPlanes)); + jassert (env->GetArrayLength (planes) > 0); + + auto plane = LocalRef (env->GetObjectArrayElement (planes, 0)); + auto byteBuffer = LocalRef (env->CallObjectMethod (plane, AndroidImagePlane.getBuffer)); + + ImageBuffer correctedBuffer = getImageBufferWithCorrectedOrientationFrom (byteBuffer, deviceOrientationFromAccelerometerSensor, + targetOrientation, cameraLensFrontFacing, cameraSensorOrientation); + + jbyte* rawBytes = env->GetByteArrayElements (correctedBuffer.byteArray, nullptr); + + Image result = ImageFileFormat::loadFrom (rawBytes, (size_t) correctedBuffer.size); + + env->ReleaseByteArrayElements (correctedBuffer.byteArray, rawBytes, 0); + + return result; + } + + static ImageBuffer getImageBufferWithCorrectedOrientationFrom (const LocalRef& imagePlaneBuffer, + Desktop::DisplayOrientation deviceOrientationFromAccelerometerSensor, + Desktop::DisplayOrientation targetOrientation, + bool cameraLensFrontFacing, + int cameraSensorOrientation) + { + auto* env = getEnv(); + + auto bufferSize = env->CallIntMethod (imagePlaneBuffer, JavaByteBuffer.remaining); + auto byteArray = LocalRef (env->NewByteArray (bufferSize)); + env->CallObjectMethod (imagePlaneBuffer, JavaByteBuffer.get, byteArray.get()); + + auto 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 (env->CallStaticObjectMethod (AndroidBitmapFactory, + AndroidBitmapFactory.decodeByteArray, + byteArray.get(), (jint) 0, (jint) bufferSize)); + + auto correctedBitmap = getBitmapWithCorrectOrientationFrom (origBitmap, rotationAngle); + + auto byteArrayOutputStream = LocalRef (env->NewObject (ByteArrayOutputStream, + ByteArrayOutputStream.constructor)); + + auto jCompressFormatString = javaString ("JPEG"); + auto compressFormat = LocalRef (env->CallStaticObjectMethod (AndroidBitmapCompressFormat, + AndroidBitmapCompressFormat.valueOf, + jCompressFormatString.get())); + + if (env->CallBooleanMethod (correctedBitmap, AndroidBitmap.compress, compressFormat.get(), + (jint) 100, byteArrayOutputStream.get()) != 0) + { + auto correctedByteArray = LocalRef ((jbyteArray) env->CallObjectMethod (byteArrayOutputStream, + ByteArrayOutputStream.toByteArray)); + + int correctedByteArraySize = env->CallIntMethod (byteArrayOutputStream, ByteArrayOutputStream.size); + + return { correctedByteArray, correctedByteArraySize }; + } + + jassertfalse; + // fallback, return original bitmap + return { byteArray, bufferSize }; + } + + static int getRotationAngle (Desktop::DisplayOrientation deviceOrientationFromAccelerometerSensor, + Desktop::DisplayOrientation targetOrientation, + bool cameraLensFrontFacing, + int cameraSensorOrientation) + { + auto 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 getBitmapWithCorrectOrientationFrom (LocalRef& 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 (env->NewObject (AndroidMatrix, AndroidMatrix.constructor)); + env->CallBooleanMethod (matrix, AndroidMatrix.postRotate, (jfloat) rotationAngle, (jfloat) 0, (jfloat) 0); + + auto rotatedBitmap = LocalRef (env->CallStaticObjectMethod (AndroidBitmap, AndroidBitmap.createBitmapFrom, + origBitmap.get(), (jint) 0, (jint) 0, + (jint) origBitmapWidth, (jint) origBitmapHeight, + matrix.get(), true)); + + env->CallVoidMethod (origBitmap, AndroidBitmap.recycle); + + return rotatedBitmap; + } + }; + + //============================================================================== + class MediaRecorder : private MediaRecorderOnInfoListener::Owner, + private MediaRecorderOnErrorListener::Owner + { + public: + MediaRecorder (const String& outputFilePath, int videoWidth, int videoHeight, + int sensorOrientation, int cameraLensFacing) + : onInfoListener (*this), + onErrorListener (*this), + mediaRecorder (LocalRef (getEnv()->NewObject (AndroidMediaRecorder, + AndroidMediaRecorder.constructor))) + { + auto* env = getEnv(); + + env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setOnInfoListener, + CreateJavaInterface (&onInfoListener, + "android/media/MediaRecorder$OnInfoListener").get()); + + env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setOnErrorListener, + CreateJavaInterface (&onErrorListener, + "android/media/MediaRecorder$OnErrorListener").get()); + + // NB: the order of function calls here is enforced, and exceptions will be thrown if + // the order is changed. + static constexpr int audioSourceMic = 1; + env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setAudioSource, (jint) audioSourceMic); + + static constexpr int videoSourceSurface = 2; + env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setVideoSource, (jint) videoSourceSurface); + + static constexpr int outputFormatMPEG4 = 2; + env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setOutputFormat, (jint) outputFormatMPEG4); + + static constexpr int audioEncoderAAC = 3; + env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setAudioEncoder, (jint) audioEncoderAAC); + + static constexpr int videoEncoderH264 = 2; + env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setVideoEncoder, (jint) videoEncoderH264); + + env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setVideoEncodingBitRate, (jint) 10000000); + env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setVideoFrameRate, (jint) 30); + + auto frontFacing = cameraLensFacing == 0; + + auto useInverseDegrees = frontFacing && sensorOrientation == 90; + + int orientationHint = getOrientationHint (useInverseDegrees, sensorOrientation); + env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setOrientationHint, (jint) orientationHint); + + getEnv()->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setVideoSize, (jint) videoWidth, (jint) videoHeight); + getEnv()->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.setOutputFile, javaString (outputFilePath).get()); + getEnv()->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.prepare); + } + + ~MediaRecorder() + { + getEnv()->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.release); + } + + LocalRef getSurface() + { + return LocalRef (getEnv()->CallObjectMethod (mediaRecorder, AndroidMediaRecorder.getSurface)); + } + + void start() + { + lockScreenOrientation(); + + getEnv()->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.start); + + hasStartedRecording = true; + } + + void stop() + { + // A request to stop can be sent before recording has had a chance to start, so + // ignore the request rather than calling AndroidMediaRecorder.stop because + // otherwise MediaRecorder will throw an exception and... + if (! hasStartedRecording) + return; + + hasStartedRecording = false; + + auto* env = getEnv(); + env->CallVoidMethod (mediaRecorder, AndroidMediaRecorder.stop); + + // ... ignore RuntimeException that can be thrown if stop() was called after recording + // has started but before any frame was written to a file. This is not an error. + auto exception = LocalRef (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& recorder, int what, int extra) override + { + ignoreUnused (recorder, what, extra); + + JUCE_CAMERA_LOG ("MediaRecorder::OnInfo: " + getInfoStringFromCode (what) + + ", extra code = " + String (extra)); + } + + void onError (LocalRef& recorder, int what, int extra) override + { + ignoreUnused (recorder, what, extra); + + JUCE_CAMERA_LOG ("MediaRecorder::onError: " + getErrorStringFromCode (what) + + ", extra code = " + String (extra)); + } + + static String getInfoStringFromCode (int what) + { + enum + { + MEDIA_RECORDER_INFO_UNKNOWN = 1, + MEDIA_RECORDER_INFO_MAX_DURATION_REACHED = 800, + MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED = 801, + MEDIA_RECORDER_INFO_MAX_FILESIZE_APPROACHING = 802, + MEDIA_RECORDER_INFO_NEXT_OUTPUT_FILE_STARTED = 803 + }; + + switch (what) + { + case MEDIA_RECORDER_INFO_UNKNOWN: return { "Unknown info" }; + case MEDIA_RECORDER_INFO_MAX_DURATION_REACHED: return { "Max duration reached" }; + case MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED: return { "Max filesize reached" }; + case MEDIA_RECORDER_INFO_MAX_FILESIZE_APPROACHING: return { "Max filesize approaching" }; + case MEDIA_RECORDER_INFO_NEXT_OUTPUT_FILE_STARTED: return { "Next output file started" }; + default: return String (what); + }; + } + + static String getErrorStringFromCode (int what) + { + enum + { + MEDIA_RECORDER_ERROR_UNKNOWN = 1, + MEDIA_ERROR_SERVER_DIED = 100 + }; + + switch (what) + { + case MEDIA_RECORDER_ERROR_UNKNOWN: return { "Unknown error" }; + case MEDIA_ERROR_SERVER_DIED: return { "Server died" }; + default: return String (what); + }; + } + + static int getOrientationHint (bool useInverseDegrees, int cameraSensorOrientation) + { + auto* env = getEnv(); + + auto windowManager = LocalRef (env->CallObjectMethod (android.activity, JuceAppActivity.getWindowManager)); + auto display = LocalRef (env->CallObjectMethod (windowManager, AndroidWindowManager.getDefaultDisplay)); + auto rotation = env->CallIntMethod (display, AndroidDisplay.getRotation); + + enum + { + ROTATION_0 = 0, + ROTATION_90, + ROTATION_180, + ROTATION_270 + }; + + int hint = 0; + + switch (rotation) + { + case ROTATION_0: hint = cameraSensorOrientation; break; + case ROTATION_90: hint = useInverseDegrees ? 180 : 0; break; + case ROTATION_180: hint = cameraSensorOrientation + 180; break; + case ROTATION_270: hint = useInverseDegrees ? 0 : 180; break; + default: jassertfalse; + } + + return (hint + 360) % 360; + } + }; + + //============================================================================== + class ScopedCameraDevice + { + public: + //============================================================================== + class CaptureSession + { + public: + struct ConfiguredCallback + { + virtual ~ConfiguredCallback() {} + + virtual void captureSessionConfigured (CaptureSession*) = 0; + }; + + ~CaptureSession() + { + bool calledClose = false; + + auto* env = getEnv(); + + { + const ScopedLock lock (captureSessionLock); + + if (captureSession.get() != nullptr) + { + calledClose = true; + + env->CallVoidMethod (captureSession, CameraCaptureSession.close); + } + } + + auto exception = LocalRef (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& targetSurfacesList, GlobalRef& handlerToUse) + { + if (! openedOk()) + { + jassertfalse; + return false; + } + + auto* env = getEnv(); + + auto numSurfaces = env->CallIntMethod (targetSurfacesList, JavaArrayList.size); + + for (int i = 0; i < numSurfaces; ++i) + { + auto surface = LocalRef (env->CallObjectMethod (targetSurfacesList, JavaArrayList.get, (jint) i)); + env->CallVoidMethod (captureRequestBuilder, CaptureRequestBuilder.addTarget, surface.get()); + } + + previewCaptureRequest = GlobalRef (env->CallObjectMethod (captureRequestBuilder, CaptureRequestBuilder.build)); + + env->CallIntMethod (captureSession, CameraCaptureSession.setRepeatingRequest, + previewCaptureRequest.get(), nullptr, handlerToUse.get()); + + return true; + } + + void takeStillPicture (jobject targetSurface) + { + if (stillPictureTaker == nullptr) + { + // Can only take picture once session was successfully configured! + jassertfalse; + return; + } + + auto* env = getEnv(); + + static constexpr int templateStillCapture = 2; + auto builder = LocalRef (env->CallObjectMethod (scopedCameraDevice.cameraDevice, + AndroidCameraDevice.createCaptureRequest, + (jint) templateStillCapture)); + + env->CallVoidMethod (builder, CaptureRequestBuilder.addTarget, targetSurface); + + setCaptureRequestBuilderIntegerKey (builder.get(), CaptureRequest.CONTROL_AF_MODE, autoFocusMode); + + auto stillPictureCaptureRequest = LocalRef (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 (getEnv()->NewObject (CameraCaptureSessionCaptureCallback, + CameraCaptureSessionCaptureCallback.constructor, + android.activity.get(), + reinterpret_cast (this), + true))), + captureSessionStillPictureCaptureCallback (LocalRef (getEnv()->NewObject (CameraCaptureSessionCaptureCallback, + CameraCaptureSessionCaptureCallback.constructor, + android.activity.get(), + reinterpret_cast (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 (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& controlAfStateValue) + { + static constexpr int controlAfStateFocusedLocked = 4; + static constexpr int controlAfStateNotFocusedLocked = 5; + + auto controlAfStateIntValue = getEnv()->CallIntMethod (controlAfStateValue, JavaInteger.intValue); + + return controlAfStateIntValue == controlAfStateFocusedLocked || controlAfStateIntValue == controlAfStateNotFocusedLocked; + } + + static LocalRef getCaptureResultIntegerKeyValue (jfieldID key, jobject captureResult) + { + auto* env = getEnv(); + + auto jKey = LocalRef (env->GetStaticObjectField (CaptureResult, key)); + return LocalRef (env->CallObjectMethod (captureResult, CaptureResult.get, jKey.get())); + } + + void captureStillPictureDelayed() + { + if (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. + 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 (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 (env->CallObjectMethod (captureRequestBuilder, + CaptureRequestBuilder.build)); + + env->CallIntMethod (captureSession, CameraCaptureSession.capture, resetAutoFocusRequest.get(), + nullptr, handler.get()); + + if (Pimpl::checkHasExceptionOccurred()) + return; + + delayedCaptureRunnable.clear(); + + // 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 pendingClose { 0 }; + + std::unique_ptr stillPictureTaker; + + WaitableEvent closedEvent; + + JUCE_DECLARE_WEAK_REFERENCEABLE (CaptureSession) + + //============================================================================== + CaptureSession (ScopedCameraDevice& scopedCameraDeviceToUse, ConfiguredCallback& configuredCallbackToUse, + const LocalRef& surfacesList, GlobalRef& handlerToUse, + int captureSessionTemplate, int autoFocusModeToUse) + : scopedCameraDevice (scopedCameraDeviceToUse), + configuredCallback (configuredCallbackToUse), + handler (handlerToUse), + captureRequestBuilder (LocalRef (getEnv()->CallObjectMethod (scopedCameraDevice.cameraDevice, + AndroidCameraDevice.createCaptureRequest, + (jint) captureSessionTemplate))), + captureSessionStateCallback (LocalRef (getEnv()->NewObject (CameraCaptureSessionStateCallback, + CameraCaptureSessionStateCallback.constructor, + android.activity.get(), + reinterpret_cast (this)))), + autoFocusMode (autoFocusModeToUse) + { + auto* env = getEnv(); + + env->CallVoidMethod (scopedCameraDevice.cameraDevice, AndroidCameraDevice.createCaptureSession, + surfacesList.get(), captureSessionStateCallback.get(), handler.get()); + + static constexpr int controlModeAuto = 1; + setCaptureRequestBuilderIntegerKey (captureRequestBuilder.get(), CaptureRequest.CONTROL_MODE, controlModeAuto); + + setCaptureRequestBuilderIntegerKey (captureRequestBuilder.get(), CaptureRequest.CONTROL_AF_MODE, autoFocusMode); + } + + static void setCaptureRequestBuilderIntegerKey (jobject captureRequestBuilder, jfieldID key, int value) + { + auto* env = getEnv(); + + auto jKey = LocalRef (env->GetStaticObjectField (CaptureRequest, key)); + auto jValue = LocalRef (env->CallStaticObjectMethod (JavaInteger, JavaInteger.valueOf, (jint) value)); + + env->CallVoidMethod (captureRequestBuilder, CaptureRequestBuilder.set, jKey.get(), jValue.get()); + } + + void cameraCaptureSessionActive (jobject session) + { + JUCE_CAMERA_LOG ("cameraCaptureSessionActive()"); + ignoreUnused (session); + } + + void cameraCaptureSessionClosed (jobject session) + { + JUCE_CAMERA_LOG ("cameraCaptureSessionClosed()"); + ignoreUnused (session); + + closedEvent.signal(); + } + + void cameraCaptureSessionConfigureFailed (jobject session) + { + JUCE_CAMERA_LOG ("cameraCaptureSessionConfigureFailed()"); + ignoreUnused (session); + + WeakReference weakRef (this); + + MessageManager::callAsync ([this, weakRef]() + { + if (weakRef == nullptr) + return; + + configuredCallback.captureSessionConfigured (nullptr); + }); + } + + void cameraCaptureSessionConfigured (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 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 (getEnv()->NewObject (CameraDeviceStateCallback, + CameraDeviceStateCallback.constructor, + android.activity.get(), + reinterpret_cast (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 (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& surfacesList, + GlobalRef& handlerToUse, + int captureSessionTemplate) + { + if (! openedOk()) + { + jassertfalse; + return nullptr; + } + + return new CaptureSession (*this, cc, surfacesList, handlerToUse, captureSessionTemplate, autoFocusMode); + } + + private: + Pimpl& owner; + const String cameraId; + GlobalRef& cameraManager; + GlobalRef& handler; + + GlobalRef cameraStateCallback; + int autoFocusMode; + + GlobalRef cameraDevice; + Atomic pendingOpen { 0 }; + Atomic pendingClose { 0 }; + Atomic fatalErrorOccurred { 0 }; + String openError; + + WaitableEvent closedEvent; + + 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 + 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> weakRef (this); + + if (weakRef == nullptr) + return; + + // async so that the object is fully constructed before the callback gets invoked + MessageManager::callAsync ([this, weakRef]() + { + if (weakRef == nullptr) + return; + + previewDisplay.addListener (this); + }); + } + + Mode& crtp() { return static_cast (*this); } + + void previewDisplayReady() override + { + jassert (previewDisplay.isReady()); + + JUCE_CAMERA_LOG ("previewDisplayReady()"); + + // close previous capture session first + captureSession.reset(); + + if (scopedCameraDevice.hasErrorOccurred()) + { + JUCE_CAMERA_LOG ("Device error detected, not recreating a new camera session. The device needs to be reopened."); + return; + } + + captureSession.reset (scopedCameraDevice.createCaptureSession (*this, crtp().getCaptureSessionSurfaces(), + handler, Mode::getTemplate())); + } + + void previewDisplayAboutToBeDestroyed() override + { + JUCE_CAMERA_LOG ("previewDisplayAboutToBeDestroyed()"); + + stopPreview(); + } + + void captureSessionConfigured (ScopedCameraDevice::CaptureSession* session) override + { + if (session == nullptr) + { + owner.cameraDeviceError ("Failed to configure camera session."); + return; + } + + jassert (session == captureSession.get()); + + startSession(); + } + + void startSession() + { + if (! captureSession->start (crtp().getTargetSurfaces(), handler)) + { + jassertfalse; + JUCE_CAMERA_LOG ("Could not start capture session"); + } + + crtp().sessionStarted(); + } + + void stopPreview() + { + if (captureSession != nullptr) + { + auto session = captureSession->getNativeSession(); + + auto* env = getEnv(); + + env->CallVoidMethod (session, CameraCaptureSession.stopRepeating); + + if (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 captureSession; + + JUCE_DECLARE_WEAK_REFERENCEABLE (CaptureSessionMode) + }; + + //============================================================================== + struct CaptureSessionPreviewMode : public CaptureSessionMode + { + CaptureSessionPreviewMode (Pimpl& ownerToUse, ScopedCameraDevice& cameraDeviceToUse, GlobalRef& handlerToUse, + PreviewDisplay& pd, ImageReader& ir, int cameraSensorOrientation, + int cameraLensFacingToUse, StreamConfigurationMap& streamConfigurationMapToUse) + : CaptureSessionMode (ownerToUse, cameraDeviceToUse, handlerToUse, pd, + cameraSensorOrientation, cameraLensFacingToUse, streamConfigurationMapToUse), + imageReader (ir) + { + } + + // Surfaces passed to newly created capture session. + LocalRef getCaptureSessionSurfaces() const + { + auto* env = getEnv(); + + auto previewSurface = LocalRef (previewDisplay.createSurface()); + auto imageSurface = LocalRef (imageReader.getSurface()); + + auto arrayList = LocalRef (env->NewObject (JavaArrayList, JavaArrayList.constructor, 2)); + env->CallBooleanMethod (arrayList, JavaArrayList.add, previewSurface.get()); + env->CallBooleanMethod (arrayList, JavaArrayList.add, imageSurface.get()); + + auto supported = streamConfigurationMap.isOutputSupportedForSurface (imageSurface); + + // Output surface is not supported by this device, still image capture will not work! + jassert (supported); + + return arrayList; + } + + // Surfaces set as target during capture. + LocalRef getTargetSurfaces() const + { + auto* env = getEnv(); + + auto previewSurface = LocalRef (previewDisplay.createSurface()); + + auto arrayList = LocalRef (env->NewObject (JavaArrayList, JavaArrayList.constructor, 1)); + env->CallBooleanMethod (arrayList, JavaArrayList.add, previewSurface.get()); + + return arrayList; + } + + static int getTemplate() + { + static constexpr int templatePreview = 1; + return templatePreview; + } + + static bool isVideoRecord() { return false; } + + void sessionStarted() {} + + void takeStillPicture() + { + imageReader.resetNotificationFlag(); + captureSession->takeStillPicture (imageReader.getSurface()); + } + + private: + ImageReader& imageReader; + }; + + //============================================================================== + struct CaptureSessionVideoRecordingMode : public CaptureSessionMode + { + CaptureSessionVideoRecordingMode (Pimpl& ownerToUse, ScopedCameraDevice& cameraDeviceToUse, GlobalRef& handlerToUse, + PreviewDisplay& pd, MediaRecorder& mr, int cameraSensorOrientation, + int cameraLensFacingToUse, StreamConfigurationMap& streamConfigurationMapToUse) + : CaptureSessionMode (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 getCaptureSessionSurfaces() const + { + auto* env = getEnv(); + + auto previewSurface = LocalRef (previewDisplay.createSurface()); + auto mediaRecorderSurface = LocalRef (mediaRecorder.getSurface()); + + auto arrayList = LocalRef (env->NewObject (JavaArrayList, JavaArrayList.constructor, 2)); + env->CallBooleanMethod (arrayList, JavaArrayList.add, previewSurface.get()); + env->CallBooleanMethod (arrayList, JavaArrayList.add, mediaRecorderSurface.get()); + + return arrayList; + } + + // Surfaces set as target during capture. + LocalRef getTargetSurfaces() const + { + // Same surfaces used. + return getCaptureSessionSurfaces(); + } + + static int getTemplate() + { + static constexpr int templateRecord = 3; + return templateRecord; + } + + static bool isVideoRecord() { return true; } + + void sessionStarted() + { + MessageManager::callAsync ([this]() { mediaRecorder.start(); }); + } + + void takeStillPicture() + { + // Taking still pictures while recording video is not supported on Android. + jassertfalse; + } + + private: + MediaRecorder& mediaRecorder; + }; + + //============================================================================== + class DeviceOrientationChangeListener : private Timer + { + public: + DeviceOrientationChangeListener (PreviewDisplay& pd) + : previewDisplay (pd), + orientationEventListener (getEnv()->NewObject (OrientationEventListener, + OrientationEventListener.constructor, + android.activity.get(), + reinterpret_cast (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; + std::unique_ptr mediaRecorder; + + std::unique_ptr currentCaptureSessionMode; + + std::unique_ptr scopedCameraDevice; + + std::function 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 getSupportedAutoFocusModes() const + { + auto* env = getEnv(); + + auto jKey = LocalRef (env->GetStaticObjectField (CameraCharacteristics, CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)); + + auto supportedModes = LocalRef ((jintArray) env->CallObjectMethod (cameraCharacteristics, + CameraCharacteristics.get, + jKey.get())); + + return jintArrayToJuceArray (supportedModes); + } + + static Array jintArrayToJuceArray (const LocalRef& jArray) + { + auto* env = getEnv(); + + auto* jArrayElems = env->GetIntArrayElements (jArray, 0); + auto numElems = env->GetArrayLength (jArray); + + Array juceArray; + + for (int s = 0; s < numElems; ++s) + juceArray.add (jArrayElems[s]); + + env->ReleaseIntArrayElements (jArray, jArrayElems, 0); + return juceArray; + } + + int getCameraCharacteristicsIntegerKeyValue (jfieldID key) const + { + auto* env = getEnv(); + + auto jKey = LocalRef (env->GetStaticObjectField (CameraCharacteristics, key)); + + auto jValue = LocalRef (env->CallObjectMethod (cameraCharacteristics, + CameraCharacteristics.get, + jKey.get())); + + return env->CallIntMethod (jValue, JavaInteger.intValue); + } + + int getCameraLensFacing() const + { + return getCameraCharacteristicsIntegerKeyValue (CameraCharacteristics.LENS_FACING); + } + + //============================================================================== + void cameraOpenFinished (const String& error) + { + JUCE_CAMERA_LOG ("cameraOpenFinished(), error = " + error); + + if (error.isEmpty()) + { + setupStillImageSize(); + startPreviewMode (*imageReader); + } + + // Do not notify about camera being reopened on app resume. + if (! notifiedOfCameraOpening) + { + notifiedOfCameraOpening = true; + + invokeCameraOpenCallback (error); + } + } + + void cameraDeviceError (const String& error) + { + if (owner.onErrorOccurred != nullptr) + owner.onErrorOccurred (error); + } + + void invokeCameraOpenCallback (const String& error) + { + JUCE_CAMERA_LOG ("invokeCameraOpenCallback(), error = " + error); + + if (cameraOpenCallback != nullptr) + cameraOpenCallback (cameraId, error); + } + + void notifyImageReceived (const Image& image) + { + JUCE_CAMERA_LOG ("notifyImageReceived()"); + + if (pictureTakenCallback != nullptr) + pictureTakenCallback (image); + } + + void triggerStillPictureCapture() + { + currentCaptureSessionMode->triggerStillPictureCapture(); + } + + //============================================================================== + void setupStillImageSize() + { + imageReader.reset(); + + auto imageSize = chooseBestSize (minWidth, minHeight, maxWidth, maxHeight, + streamConfigurationMap.getSupportedStillImageOutputSizes()); + + imageReader.reset (new ImageReader (*this, handler, imageSize.getWidth(), imageSize.getHeight(), + getCameraSensorOrientation())); + } + + static Rectangle chooseBestSize (int minWidth, int minHeight, int maxWidth, int maxHeight, + Array> supportedSizes) + { + Rectangle result; + + for (auto& size : supportedSizes) + { + auto width = size.getWidth(); + auto height = size.getHeight(); + + if (width < minWidth || width > maxWidth || height < minHeight || height > maxHeight) + continue; + + if (size.contains (result)) + result = size; + } + + // None of the supported sizes matches required width & height limitations, picking + // the first one available... + jassert (! result.isEmpty()); + + if (result.isEmpty()) + result = supportedSizes[0]; + + return result; + } + + //============================================================================== + void startPreviewMode (ImageReader& ir) + { + if (currentCaptureSessionMode != nullptr && ! currentCaptureSessionMode->isVideoRecordSession()) + return; + + // previous mode has to be stopped first + jassert (currentCaptureSessionMode.get() == nullptr); + + if (scopedCameraDevice == nullptr || ! scopedCameraDevice->openedOk()) + return; + + currentCaptureSessionMode.reset (new CaptureSessionPreviewMode (*this, *scopedCameraDevice, handler, + previewDisplay, ir, + getCameraSensorOrientation(), + getCameraLensFacing(), + streamConfigurationMap)); + } + + void startVideoRecordingMode (MediaRecorder& mr) + { + if (currentCaptureSessionMode != nullptr && currentCaptureSessionMode->isVideoRecordSession()) + return; + + // previous mode has to be stopped first + jassert (currentCaptureSessionMode.get() == nullptr); + + jassert (scopedCameraDevice != nullptr && scopedCameraDevice->openedOk()); + + if (scopedCameraDevice == nullptr || ! scopedCameraDevice->openedOk()) + return; + + currentCaptureSessionMode.reset (new CaptureSessionVideoRecordingMode (*this, *scopedCameraDevice, handler, + previewDisplay, mr, + getCameraSensorOrientation(), + getCameraLensFacing(), + streamConfigurationMap)); + } + + //============================================================================== + void 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 (env->NewObject (AndroidHandlerThread, + AndroidHandlerThread.constructor, + javaString ("JuceCameraDeviceBackgroundThread").get()))); + // handler thread has to be started before its looper can be fetched + env->CallVoidMethod (handlerThread, AndroidHandlerThread.start); + handler = GlobalRef (LocalRef (env->NewObject (AndroidHandler, + AndroidHandler.constructorWithLooper, + env->CallObjectMethod (handlerThread, AndroidHandlerThread.getLooper)))); + } + + void stopBackgroundThread() + { + auto* env = getEnv(); + + env->CallBooleanMethod (handlerThread, AndroidHandlerThread.quitSafely); + env->CallVoidMethod (handlerThread, AndroidHandlerThread.join); + + auto exception = LocalRef (env->ExceptionOccurred()); + + if (exception != 0) + env->ExceptionClear(); + + handlerThread.clear(); + handler.clear(); + } + + static bool checkHasExceptionOccurred() + { + auto* env = getEnv(); + + auto exception = LocalRef (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 +//============================================================================== +struct CameraDevice::ViewerComponent : public Component, + private ComponentMovementWatcher { - ViewerComponent (CameraDevice&) + 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 (targetWidth * targetAspectRatio / currentAspectRatio); + else + targetHeight = static_cast (targetHeight * currentAspectRatio / targetAspectRatio); + } + else + { + auto currentAspectRatio = b.getHeight() / (float) b.getWidth(); + + if (currentAspectRatio > targetAspectRatio) + targetHeight = static_cast (targetHeight * targetAspectRatio / currentAspectRatio); + else + targetWidth = static_cast (targetWidth * currentAspectRatio / targetAspectRatio); + } + + viewerComponent.setBounds (Rectangle (0, 0, targetWidth, targetHeight).withCentre (b.getCentre())); + } + + bool isOrientationLandscape() const + { + auto o = Desktop::getInstance().getCurrentOrientation(); + return o == Desktop::rotatedClockwise || o == Desktop::rotatedAntiClockwise; + } + + void componentPeerChanged() override {} + void componentVisibilityChanged() override {} + JUCE_DECLARE_NON_COPYABLE (ViewerComponent) }; String CameraDevice::getFileExtension() { - return ".mov"; + return ".mp4"; } + +#if __ANDROID_API__ >= 21 +//============================================================================== +void juce_cameraDeviceStateClosed (int64 host) +{ + reinterpret_cast (host)->cameraDeviceStateClosed(); +} + +void juce_cameraDeviceStateDisconnected (int64 host) +{ + reinterpret_cast (host)->cameraDeviceStateDisconnected(); +} + +void juce_cameraDeviceStateError (int64 host, int error) +{ + reinterpret_cast (host)->cameraDeviceStateError (error); +} + +void juce_cameraDeviceStateOpened (int64 host, void* camera) +{ + reinterpret_cast (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 (host); + juceCaptureSession->cameraCaptureSessionActive ((jobject) session); +} + +void juce_cameraCaptureSessionClosed (int64 host, void* session) +{ + auto* juceCaptureSession = reinterpret_cast (host); + juceCaptureSession->cameraCaptureSessionClosed ((jobject) session); +} + +void juce_cameraCaptureSessionConfigureFailed (int64 host, void* session) +{ + auto* juceCaptureSession = reinterpret_cast (host); + juceCaptureSession->cameraCaptureSessionConfigureFailed ((jobject) session); +} + +void juce_cameraCaptureSessionConfigured (int64 host, void* session) +{ + auto* juceCaptureSession = reinterpret_cast (host); + juceCaptureSession->cameraCaptureSessionConfigured ((jobject) session); +} + +void juce_cameraCaptureSessionReady (int64 host, void* session) +{ + auto* juceCaptureSession = reinterpret_cast (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 (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 (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 (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 (host); + stillPictureTaker->cameraCaptureSessionCaptureSequenceAborted (isPreview, (jobject) session, sequenceId); +} + +void juce_cameraCaptureSessionCaptureSequenceCompleted (int64 host, bool isPreview, void* session, int sequenceId, int64 frameNumber) +{ + auto* stillPictureTaker = reinterpret_cast (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 (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 (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 diff --git a/modules/juce_video/native/juce_ios_CameraDevice.h b/modules/juce_video/native/juce_ios_CameraDevice.h new file mode 100644 index 0000000000..c44b8c9277 --- /dev/null +++ b/modules/juce_video/native/juce_ios_CameraDevice.h @@ -0,0 +1,1288 @@ +/* + ============================================================================== + + 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. + + ============================================================================== +*/ + +struct CameraDevice::Pimpl +{ + using InternalOpenCameraResultCallback = std::function; + + Pimpl (CameraDevice& ownerToUse, const String& cameraIdToUse, int /*index*/, + int /*minWidth*/, int /*minHeight*/, int /*maxWidth*/, int /*maxHeight*/, + bool useHighQuality) + : owner (ownerToUse), + cameraId (cameraIdToUse), + captureSession (*this, useHighQuality) + { + } + + String getCameraId() const noexcept { return cameraId; } + + void open (InternalOpenCameraResultCallback cameraOpenCallbackToUse) + { + cameraOpenCallback = static_cast (cameraOpenCallbackToUse); + + if (cameraOpenCallback == nullptr) + { + // A valid camera open callback must be passed. + jassertfalse; + return; + } + + [AVCaptureDevice requestAccessForMediaType: AVMediaTypeVideo + completionHandler: ^(BOOL granted) + { + // Access to video is required for camera to work, + // black images will be produced otherwise! + jassert (granted); + + ignoreUnused (granted); + }]; + + [AVCaptureDevice requestAccessForMediaType: AVMediaTypeAudio + completionHandler: ^(BOOL granted) + { + // Access to audio is required for camera to work, + // silence will be produced otherwise! + jassert (granted); + + ignoreUnused (granted); + }]; + + captureSession.startSessionForDeviceWithId (cameraId); + } + + bool openedOk() const noexcept { return captureSession.openedOk(); } + + void takeStillPicture (std::function pictureTakenCallbackToUse) + { + if (pictureTakenCallbackToUse == nullptr) + { + jassertfalse; + return; + } + + pictureTakenCallback = static_cast&&> (pictureTakenCallbackToUse); + + triggerStillPictureCapture(); + } + + void startRecordingToFile (const File& file, int /*quality*/) + { + file.deleteFile(); + + captureSession.startRecording (file); + } + + void stopRecording() + { + captureSession.stopRecording(); + } + + Time getTimeOfFirstRecordedFrame() const + { + return captureSession.getTimeOfFirstRecordedFrame(); + } + + static StringArray getAvailableDevices() + { + StringArray results; + + JUCE_CAMERA_LOG ("Available camera devices: "); + + for (AVCaptureDevice* device in getDevices()) + { + JUCE_CAMERA_LOG ("Device start----------------------------------"); + printDebugCameraInfo (device); + JUCE_CAMERA_LOG ("Device end----------------------------------"); + + results.add (nsStringToJuce (device.uniqueID)); + } + + return results; + } + +private: + static NSArray* getDevices() + { + #if defined (__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + if (iosVersion.major >= 10) + { + std::unique_ptr, NSObjectDeleter> deviceTypes ([[NSMutableArray alloc] initWithCapacity: 2]); + + [deviceTypes.get() addObject: AVCaptureDeviceTypeBuiltInWideAngleCamera]; + [deviceTypes.get() addObject: AVCaptureDeviceTypeBuiltInTelephotoCamera]; + + if ((iosVersion.major == 10 && iosVersion.minor >= 2) || iosVersion.major >= 11) + [deviceTypes.get() addObject: AVCaptureDeviceTypeBuiltInDualCamera]; + + if ((iosVersion.major == 11 && iosVersion.minor >= 1) || iosVersion.major >= 12) + [deviceTypes.get() addObject: AVCaptureDeviceTypeBuiltInTrueDepthCamera]; + + auto discoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes: deviceTypes.get() + mediaType: AVMediaTypeVideo + position: AVCaptureDevicePositionUnspecified]; + + return [discoverySession devices]; + } + #endif + + return [AVCaptureDevice devicesWithMediaType: AVMediaTypeVideo]; + } + + //============================================================================== + static void printDebugCameraInfo (AVCaptureDevice* device) + { + auto position = device.position; + + String positionString = position == AVCaptureDevicePositionBack + ? "Back" + : position == AVCaptureDevicePositionFront + ? "Front" + : "Unspecified"; + + JUCE_CAMERA_LOG ("Position: " + positionString); + JUCE_CAMERA_LOG ("Model ID: " + nsStringToJuce (device.modelID)); + JUCE_CAMERA_LOG ("Localized name: " + nsStringToJuce (device.localizedName)); + JUCE_CAMERA_LOG ("Unique ID: " + nsStringToJuce (device.uniqueID)); + JUCE_CAMERA_LOG ("Lens aperture: " + String (device.lensAperture)); + + JUCE_CAMERA_LOG ("Has flash: " + String ((int)device.hasFlash)); + JUCE_CAMERA_LOG ("Supports flash always on: " + String ((int)[device isFlashModeSupported: AVCaptureFlashModeOn])); + JUCE_CAMERA_LOG ("Supports auto flash: " + String ((int)[device isFlashModeSupported: AVCaptureFlashModeAuto])); + + JUCE_CAMERA_LOG ("Has torch: " + String ((int)device.hasTorch)); + JUCE_CAMERA_LOG ("Supports torch always on: " + String ((int)[device isTorchModeSupported: AVCaptureTorchModeOn])); + JUCE_CAMERA_LOG ("Supports auto torch: " + String ((int)[device isTorchModeSupported: AVCaptureTorchModeAuto])); + + JUCE_CAMERA_LOG ("Low light boost supported: " + String ((int)device.lowLightBoostEnabled)); + + JUCE_CAMERA_LOG ("Supports auto white balance: " + String ((int)[device isWhiteBalanceModeSupported: AVCaptureWhiteBalanceModeAutoWhiteBalance])); + JUCE_CAMERA_LOG ("Supports continuous auto white balance: " + String ((int)[device isWhiteBalanceModeSupported: AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance])); + + JUCE_CAMERA_LOG ("Supports auto focus: " + String ((int)[device isFocusModeSupported: AVCaptureFocusModeAutoFocus])); + JUCE_CAMERA_LOG ("Supports continuous auto focus: " + String ((int)[device isFocusModeSupported: AVCaptureFocusModeContinuousAutoFocus])); + JUCE_CAMERA_LOG ("Supports point of interest focus: " + String ((int)device.focusPointOfInterestSupported)); + JUCE_CAMERA_LOG ("Smooth auto focus supported: " + String ((int)device.smoothAutoFocusSupported)); + JUCE_CAMERA_LOG ("Auto focus range restriction supported: " + String ((int)device.autoFocusRangeRestrictionSupported)); + + JUCE_CAMERA_LOG ("Supports auto exposure: " + String ((int)[device isExposureModeSupported: AVCaptureExposureModeAutoExpose])); + JUCE_CAMERA_LOG ("Supports continuous auto exposure: " + String ((int)[device isExposureModeSupported: AVCaptureExposureModeContinuousAutoExposure])); + JUCE_CAMERA_LOG ("Supports custom exposure: " + String ((int)[device isExposureModeSupported: AVCaptureExposureModeCustom])); + JUCE_CAMERA_LOG ("Supports point of interest exposure: " + String ((int)device.exposurePointOfInterestSupported)); + + #if defined (__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + if (iosVersion.major >= 10) + { + JUCE_CAMERA_LOG ("Device type: " + nsStringToJuce (device.deviceType)); + JUCE_CAMERA_LOG ("Locking focus with custom lens position supported: " + String ((int)device.lockingFocusWithCustomLensPositionSupported)); + } + #endif + + #if defined (__IPHONE_11_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (iosVersion.major >= 11) + { + JUCE_CAMERA_LOG ("Min available video zoom factor: " + String (device.minAvailableVideoZoomFactor)); + JUCE_CAMERA_LOG ("Max available video zoom factor: " + String (device.maxAvailableVideoZoomFactor)); + JUCE_CAMERA_LOG ("Dual camera switch over video zoom factor: " + String (device.dualCameraSwitchOverVideoZoomFactor)); + } + #endif + + JUCE_CAMERA_LOG ("Capture formats start-------------------"); + for (AVCaptureDeviceFormat* format in device.formats) + { + JUCE_CAMERA_LOG ("Capture format start------"); + printDebugCameraFormatInfo (format); + JUCE_CAMERA_LOG ("Capture format end------"); + } + JUCE_CAMERA_LOG ("Capture formats end-------------------"); + } + + static void printDebugCameraFormatInfo (AVCaptureDeviceFormat* format) + { + JUCE_CAMERA_LOG ("Media type: " + nsStringToJuce (format.mediaType)); + + String colourSpaces; + + for (NSNumber* number in format.supportedColorSpaces) + { + switch ([number intValue]) + { + case AVCaptureColorSpace_sRGB: colourSpaces << "sRGB "; break; + case AVCaptureColorSpace_P3_D65: colourSpaces << "P3_D65 "; break; + default: break; + } + } + + JUCE_CAMERA_LOG ("Supported colour spaces: " + colourSpaces); + + JUCE_CAMERA_LOG ("Video field of view: " + String (format.videoFieldOfView)); + JUCE_CAMERA_LOG ("Video max zoom factor: " + String (format.videoMaxZoomFactor)); + JUCE_CAMERA_LOG ("Video zoom factor upscale threshold: " + String (format.videoZoomFactorUpscaleThreshold)); + + String videoFrameRateRangesString = "Video supported frame rate ranges: "; + + for (AVFrameRateRange* range in format.videoSupportedFrameRateRanges) + videoFrameRateRangesString << frameRateRangeToString (range); + JUCE_CAMERA_LOG (videoFrameRateRangesString); + + JUCE_CAMERA_LOG ("Video binned: " + String (int(format.videoBinned))); + + #if defined (__IPHONE_8_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_8_0 + if (iosVersion.major >= 8) + { + JUCE_CAMERA_LOG ("Video HDR supported: " + String (int (format.videoHDRSupported))); + JUCE_CAMERA_LOG ("High resolution still image dimensions: " + getHighResStillImgDimensionsString (format.highResolutionStillImageDimensions)); + JUCE_CAMERA_LOG ("Min ISO: " + String (format.minISO)); + JUCE_CAMERA_LOG ("Max ISO: " + String (format.maxISO)); + JUCE_CAMERA_LOG ("Min exposure duration: " + cmTimeToString (format.minExposureDuration)); + + String autoFocusSystemString; + switch (format.autoFocusSystem) + { + case AVCaptureAutoFocusSystemPhaseDetection: autoFocusSystemString = "PhaseDetection"; break; + case AVCaptureAutoFocusSystemContrastDetection: autoFocusSystemString = "ContrastDetection"; break; + default: autoFocusSystemString = "None"; + } + JUCE_CAMERA_LOG ("Auto focus system: " + autoFocusSystemString); + + JUCE_CAMERA_LOG ("Standard (iOS 5.0) video stabilization supported: " + String ((int) [format isVideoStabilizationModeSupported: AVCaptureVideoStabilizationModeStandard])); + JUCE_CAMERA_LOG ("Cinematic video stabilization supported: " + String ((int) [format isVideoStabilizationModeSupported: AVCaptureVideoStabilizationModeCinematic])); + JUCE_CAMERA_LOG ("Auto video stabilization supported: " + String ((int) [format isVideoStabilizationModeSupported: AVCaptureVideoStabilizationModeAuto])); + } + #endif + + #if defined (__IPHONE_11_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (iosVersion.major >= 11) + { + JUCE_CAMERA_LOG ("Min zoom factor for depth data delivery: " + String (format.videoMinZoomFactorForDepthDataDelivery)); + JUCE_CAMERA_LOG ("Max zoom factor for depth data delivery: " + String (format.videoMaxZoomFactorForDepthDataDelivery)); + } + #endif + } + + static String getHighResStillImgDimensionsString (CMVideoDimensions d) + { + return "[" + String (d.width) + " " + String (d.height) + "]"; + } + + static String cmTimeToString (CMTime time) + { + CFStringRef timeDesc = CMTimeCopyDescription (NULL, time); + String result = String::fromCFString (timeDesc); + + CFRelease (timeDesc); + return result; + } + + static String frameRateRangeToString (AVFrameRateRange* range) + { + String result; + result << "[minFrameDuration: " + cmTimeToString (range.minFrameDuration); + result << " maxFrameDuration: " + cmTimeToString (range.maxFrameDuration); + result << " minFrameRate: " + String (range.minFrameRate); + result << " maxFrameRate: " + String (range.maxFrameRate) << "] "; + + return result; + } + + //============================================================================== + class CaptureSession + { + public: + CaptureSession (Pimpl& ownerToUse, bool useHighQuality) + : owner (ownerToUse), + captureSessionQueue (dispatch_queue_create ("JuceCameraDeviceBackgroundDispatchQueue", DISPATCH_QUEUE_SERIAL)), + captureSession ([[AVCaptureSession alloc] init]), + delegate (nullptr), + stillPictureTaker (*this), + videoRecorder (*this) + { + static SessionDelegateClass cls; + delegate.reset ([cls.createInstance() init]); + SessionDelegateClass::setOwner (delegate.get(), this); + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wundeclared-selector" + [[NSNotificationCenter defaultCenter] addObserver: delegate.get() + selector: @selector (sessionDidStartRunning:) + name: AVCaptureSessionDidStartRunningNotification + object: captureSession.get()]; + + [[NSNotificationCenter defaultCenter] addObserver: delegate.get() + selector: @selector (sessionDidStopRunning:) + name: AVCaptureSessionDidStopRunningNotification + object: captureSession.get()]; + + [[NSNotificationCenter defaultCenter] addObserver: delegate.get() + selector: @selector (sessionRuntimeError:) + name: AVCaptureSessionRuntimeErrorNotification + object: captureSession.get()]; + + [[NSNotificationCenter defaultCenter] addObserver: delegate.get() + selector: @selector (sessionWasInterrupted:) + name: AVCaptureSessionWasInterruptedNotification + object: captureSession.get()]; + + [[NSNotificationCenter defaultCenter] addObserver: delegate.get() + selector: @selector (sessionInterruptionEnded:) + name: AVCaptureSessionInterruptionEndedNotification + object: captureSession.get()]; + #pragma clang diagnostic pop + + dispatch_async (captureSessionQueue,^ + { + [captureSession.get() setSessionPreset: useHighQuality ? AVCaptureSessionPresetHigh + : AVCaptureSessionPresetMedium]; + }); + + ++numCaptureSessions; + } + + ~CaptureSession() + { + [[NSNotificationCenter defaultCenter] removeObserver: delegate.get()]; + + stopRecording(); + + if (--numCaptureSessions == 0) + { + dispatch_async (captureSessionQueue, ^ + { + if (captureSession.get().running) + [captureSession.get() stopRunning]; + + sessionClosedEvent.signal(); + }); + + sessionClosedEvent.wait (-1); + } + } + + bool openedOk() const noexcept { return sessionStarted; } + + void startSessionForDeviceWithId (const String& cameraIdToUse) + { + dispatch_async (captureSessionQueue,^ + { + cameraDevice = [AVCaptureDevice deviceWithUniqueID: juceStringToNS (cameraIdToUse)]; + auto* audioDevice = [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeAudio]; + + [captureSession.get() beginConfiguration]; + + // This will add just video... + auto error = addInputToDevice (cameraDevice); + + if (error.isNotEmpty()) + { + WeakReference weakRef (this); + + MessageManager::callAsync ([weakRef, error]() mutable + { + if (weakRef != nullptr) + weakRef->owner.cameraOpenCallback ({}, error); + }); + + return; + } + + // ... so add audio explicitly here + error = addInputToDevice (audioDevice); + + if (error.isNotEmpty()) + { + WeakReference weakRef (this); + + MessageManager::callAsync ([weakRef, error]() mutable + { + if (weakRef != nullptr) + weakRef->owner.cameraOpenCallback ({}, error); + }); + + return; + } + + [captureSession.get() commitConfiguration]; + + if (! captureSession.get().running) + [captureSession.get() startRunning]; + }); + } + + AVCaptureVideoPreviewLayer* createPreviewLayer() + { + if (! openedOk()) + { + // A session must be started first! + jassertfalse; + return nullptr; + } + + previewLayer = [AVCaptureVideoPreviewLayer layerWithSession: captureSession.get()]; + return previewLayer; + } + + void takeStillPicture() + { + if (! openedOk()) + { + // A session must be started first! + jassert (openedOk()); + return; + } + + stillPictureTaker.takePicture (previewLayer.connection.videoOrientation); + } + + void startRecording (const File& file) + { + if (! openedOk()) + { + // A session must be started first! + jassertfalse; + return; + } + + if (file.existsAsFile()) + { + // File overwriting is not supported by iOS video recorder, the target + // file must not exist. + jassertfalse; + return; + } + + videoRecorder.startRecording (file, previewLayer.connection.videoOrientation); + } + + void stopRecording() + { + videoRecorder.stopRecording(); + } + + Time getTimeOfFirstRecordedFrame() const + { + return videoRecorder.getTimeOfFirstRecordedFrame(); + } + + JUCE_DECLARE_WEAK_REFERENCEABLE (CaptureSession) + + private: + String addInputToDevice (AVCaptureDevice* device) + { + NSError* error = nil; + + auto* input = [AVCaptureDeviceInput deviceInputWithDevice: device + error: &error]; + + if (error != nil) + return nsStringToJuce (error.localizedDescription); + + if (! [captureSession.get() canAddInput: input]) + return "Could not add input to camera session."; + + [captureSession.get() addInput: input]; + return {}; + } + + //============================================================================== + struct SessionDelegateClass : public ObjCClass + { + SessionDelegateClass() : ObjCClass ("SessionDelegateClass_") + { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wundeclared-selector" + addMethod (@selector (sessionDidStartRunning:), started, "v@:@"); + addMethod (@selector (sessionDidStopRunning:), stopped, "v@:@"); + addMethod (@selector (sessionRuntimeError:), runtimeError, "v@:@"); + addMethod (@selector (sessionWasInterrupted:), interrupted, "v@:@"); + addMethod (@selector (sessionDidStartRunning:), interruptionEnded, "v@:@"); + #pragma clang diagnostic pop + + addIvar ("owner"); + + registerClass(); + } + + //============================================================================== + static CaptureSession& getOwner (id self) { return *getIvar (self, "owner"); } + static void setOwner (id self, CaptureSession* s) { object_setInstanceVariable (self, "owner", s); } + + private: + //============================================================================== + static void started (id self, SEL, NSNotification* notification) + { + JUCE_CAMERA_LOG (nsStringToJuce ([notification description])); + + ignoreUnused (notification); + + dispatch_async (dispatch_get_main_queue(), + ^{ + getOwner (self).cameraSessionStarted(); + }); + } + + static void stopped (id, SEL, NSNotification* notification) + { + JUCE_CAMERA_LOG (nsStringToJuce ([notification description])); + + ignoreUnused (notification); + } + + static void runtimeError (id self, SEL, NSNotification* notification) + { + JUCE_CAMERA_LOG (nsStringToJuce ([notification description])); + + dispatch_async (dispatch_get_main_queue(), + ^{ + NSError* error = notification.userInfo[AVCaptureSessionErrorKey]; + auto errorString = error != nil ? nsStringToJuce (error.localizedDescription) : String(); + getOwner (self).cameraSessionRuntimeError (errorString); + }); + } + + static void interrupted (id, SEL, NSNotification* notification) + { + JUCE_CAMERA_LOG (nsStringToJuce ([notification description])); + + ignoreUnused (notification); + } + + static void interruptionEnded (id, SEL, NSNotification* notification) + { + JUCE_CAMERA_LOG (nsStringToJuce ([notification description])); + + ignoreUnused (notification); + } + }; + + //============================================================================== + class StillPictureTaker + { + public: + StillPictureTaker (CaptureSession& cs) + : captureSession (cs), + captureOutput (createCaptureOutput()), + photoOutputDelegate (nullptr) + { + if (Pimpl::getIOSVersion().major >= 10) + { + static PhotoOutputDelegateClass cls; + photoOutputDelegate.reset ([cls.createInstance() init]); + PhotoOutputDelegateClass::setOwner (photoOutputDelegate.get(), this); + } + + captureSession.addOutputIfPossible (captureOutput); + } + + void takePicture (AVCaptureVideoOrientation orientationToUse) + { + if (takingPicture) + { + // Picture taking already in progress! + jassertfalse; + return; + } + + takingPicture = true; + + printImageOutputDebugInfo (captureOutput); + + if (auto* connection = findVideoConnection (captureOutput)) + { + #if defined (__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + if (Pimpl::getIOSVersion().major >= 10 && [captureOutput isKindOfClass: [AVCapturePhotoOutput class]]) + { + auto* photoOutput = (AVCapturePhotoOutput*) captureOutput; + auto* outputConnection = [photoOutput connectionWithMediaType: AVMediaTypeVideo]; + outputConnection.videoOrientation = orientationToUse; + + [photoOutput capturePhotoWithSettings: [AVCapturePhotoSettings photoSettings] + delegate: id (photoOutputDelegate.get())]; + + return; + } + #endif + + auto* stillImageOutput = (AVCaptureStillImageOutput*) captureOutput; + auto* outputConnection = [stillImageOutput connectionWithMediaType: AVMediaTypeVideo]; + outputConnection.videoOrientation = orientationToUse; + + [stillImageOutput captureStillImageAsynchronouslyFromConnection: connection completionHandler: + ^(CMSampleBufferRef imageSampleBuffer, NSError* error) + { + if (error != nil) + { + JUCE_CAMERA_LOG ("Still picture capture failed, error: " + nsStringToJuce (error.localizedDescription)); + jassertfalse; + return; + } + + NSData* imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation: imageSampleBuffer]; + + auto image = ImageFileFormat::loadFrom (imageData.bytes, (size_t) imageData.length); + MessageManager::callAsync ([this, image]() { imageTaken (image); }); + }]; + } + else + { + // Could not find a connection of video type + jassertfalse; + } + } + + private: + static AVCaptureOutput* createCaptureOutput() + { + #if defined (__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + if (Pimpl::getIOSVersion().major >= 10) + return [AVCapturePhotoOutput new]; + #endif + + return [AVCaptureStillImageOutput new]; + } + + static void printImageOutputDebugInfo (AVCaptureOutput* captureOutput) + { + #if defined (__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + if (Pimpl::getIOSVersion().major >= 10 && [captureOutput isKindOfClass: [AVCapturePhotoOutput class]]) + { + auto* photoOutput = (AVCapturePhotoOutput*) captureOutput; + + String typesString; + + for (AVVideoCodecType type in photoOutput.availablePhotoCodecTypes) + typesString << nsStringToJuce (type) << " "; + + JUCE_CAMERA_LOG ("Available image codec types: " + typesString); + + JUCE_CAMERA_LOG ("Still image stabilization supported: " + String ((int) photoOutput.stillImageStabilizationSupported)); + JUCE_CAMERA_LOG ("Dual camera fusion supported: " + String ((int) photoOutput.dualCameraFusionSupported)); + JUCE_CAMERA_LOG ("Supports flash: " + String ((int) [photoOutput.supportedFlashModes containsObject: @(AVCaptureFlashModeOn)])); + JUCE_CAMERA_LOG ("Supports auto flash: " + String ((int) [photoOutput.supportedFlashModes containsObject: @(AVCaptureFlashModeAuto)])); + JUCE_CAMERA_LOG ("Max bracketed photo count: " + String (photoOutput.maxBracketedCapturePhotoCount)); + JUCE_CAMERA_LOG ("Lens stabilization during bracketed capture supported: " + String ((int) photoOutput.lensStabilizationDuringBracketedCaptureSupported)); + JUCE_CAMERA_LOG ("Live photo capture supported: " + String ((int) photoOutput.livePhotoCaptureSupported)); + + + #if defined (__IPHONE_11_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (Pimpl::getIOSVersion().major >= 11) + { + typesString.clear(); + + for (AVFileType type in photoOutput.availablePhotoFileTypes) + typesString << nsStringToJuce (type) << " "; + + JUCE_CAMERA_LOG ("Available photo file types: " + typesString); + + typesString.clear(); + + for (AVFileType type in photoOutput.availableRawPhotoFileTypes) + typesString << nsStringToJuce (type) << " "; + + JUCE_CAMERA_LOG ("Available RAW photo file types: " + typesString); + + typesString.clear(); + + for (AVFileType type in photoOutput.availableLivePhotoVideoCodecTypes) + typesString << nsStringToJuce (type) << " "; + + JUCE_CAMERA_LOG ("Available live photo video codec types: " + typesString); + + JUCE_CAMERA_LOG ("Dual camera dual photo delivery supported: " + String ((int) photoOutput.dualCameraDualPhotoDeliverySupported)); + JUCE_CAMERA_LOG ("Camera calibration data delivery supported: " + String ((int) photoOutput.cameraCalibrationDataDeliverySupported)); + JUCE_CAMERA_LOG ("Depth data delivery supported: " + String ((int) photoOutput.depthDataDeliverySupported)); + } + #endif + + return; + } + #endif + + auto* stillImageOutput = (AVCaptureStillImageOutput*) captureOutput; + + String typesString; + + for (AVVideoCodecType type in stillImageOutput.availableImageDataCodecTypes) + typesString << nsStringToJuce (type) << " "; + + JUCE_CAMERA_LOG ("Available image codec types: " + typesString); + JUCE_CAMERA_LOG ("Still image stabilization supported: " + String ((int) stillImageOutput.stillImageStabilizationSupported)); + JUCE_CAMERA_LOG ("Automatically enableds still image stabilization when available: " + String ((int) stillImageOutput.automaticallyEnablesStillImageStabilizationWhenAvailable)); + + JUCE_CAMERA_LOG ("Output settings for image output: " + nsStringToJuce ([stillImageOutput.outputSettings description])); + } + + //============================================================================== + static AVCaptureConnection* findVideoConnection (AVCaptureOutput* output) + { + for (AVCaptureConnection* connection in output.connections) + for (AVCaptureInputPort* port in connection.inputPorts) + if ([port.mediaType isEqual: AVMediaTypeVideo]) + return connection; + + return nullptr; + } + + //============================================================================== + class PhotoOutputDelegateClass : public ObjCClass + { + public: + PhotoOutputDelegateClass() : ObjCClass ("PhotoOutputDelegateClass_") + { + addMethod (@selector (captureOutput:willBeginCaptureForResolvedSettings:), willBeginCaptureForSettings, "v@:@@"); + addMethod (@selector (captureOutput:willCapturePhotoForResolvedSettings:), willCaptureForSettings, "v@:@@"); + addMethod (@selector (captureOutput:didCapturePhotoForResolvedSettings:), didCaptureForSettings, "v@:@@"); + addMethod (@selector (captureOutput:didFinishCaptureForResolvedSettings:error:), didFinishCaptureForSettings, "v@:@@@"); + + if (Pimpl::getIOSVersion().major >= 11) + addMethod (@selector (captureOutput:didFinishProcessingPhoto:error:), didFinishProcessingPhoto, "v@:@@@"); + else + addMethod (@selector (captureOutput:didFinishProcessingPhotoSampleBuffer:previewPhotoSampleBuffer:resolvedSettings:bracketSettings:error:), didFinishProcessingPhotoSampleBuffer, "v@:@@@@@@"); + + addIvar ("owner"); + + registerClass(); + } + + //============================================================================== + static StillPictureTaker& getOwner (id self) { return *getIvar (self, "owner"); } + static void setOwner (id self, StillPictureTaker* t) { object_setInstanceVariable (self, "owner", t); } + + private: + static void willBeginCaptureForSettings (id, SEL, AVCapturePhotoOutput*, AVCaptureResolvedPhotoSettings*) + { + JUCE_CAMERA_LOG ("willBeginCaptureForSettings()"); + } + + static void willCaptureForSettings (id, SEL, AVCapturePhotoOutput*, AVCaptureResolvedPhotoSettings*) + { + JUCE_CAMERA_LOG ("willCaptureForSettings()"); + } + + static void didCaptureForSettings (id, SEL, AVCapturePhotoOutput*, AVCaptureResolvedPhotoSettings*) + { + JUCE_CAMERA_LOG ("didCaptureForSettings()"); + } + + static void didFinishCaptureForSettings (id, SEL, AVCapturePhotoOutput*, AVCaptureResolvedPhotoSettings*, NSError* error) + { + String errorString = error != nil ? nsStringToJuce (error.localizedDescription) : String(); + ignoreUnused (errorString); + + JUCE_CAMERA_LOG ("didFinishCaptureForSettings(), error = " + errorString); + } + + static void didFinishProcessingPhoto (id self, SEL, AVCapturePhotoOutput*, AVCapturePhoto* capturePhoto, NSError* error) + { + String errorString = error != nil ? nsStringToJuce (error.localizedDescription) : String(); + ignoreUnused (errorString); + + JUCE_CAMERA_LOG ("didFinishProcessingPhoto(), error = " + errorString); + + if (error != nil) + { + JUCE_CAMERA_LOG ("Still picture capture failed, error: " + nsStringToJuce (error.localizedDescription)); + jassertfalse; + return; + } + + auto* imageOrientation = (NSNumber *) capturePhoto.metadata[(NSString*) kCGImagePropertyOrientation]; + + auto* uiImage = getImageWithCorrectOrientation ((CGImagePropertyOrientation) imageOrientation.unsignedIntValue, + [capturePhoto CGImageRepresentation]); + + auto* imageData = UIImageJPEGRepresentation (uiImage, 0.f); + + auto image = ImageFileFormat::loadFrom (imageData.bytes, (size_t) imageData.length); + MessageManager::callAsync ([self, image]() { getOwner (self).imageTaken (image); }); + } + + static UIImage* getImageWithCorrectOrientation (CGImagePropertyOrientation imageOrientation, + CGImageRef imageData) + { + auto origWidth = CGImageGetWidth (imageData); + auto origHeight = CGImageGetHeight (imageData); + + auto targetSize = getTargetImageDimensionFor (imageOrientation, imageData); + + UIGraphicsBeginImageContext (targetSize); + CGContextRef context = UIGraphicsGetCurrentContext(); + + switch (imageOrientation) + { + case kCGImagePropertyOrientationUp: + CGContextScaleCTM (context, 1.0, -1.0); + CGContextTranslateCTM (context, 0.0, -targetSize.height); + break; + case kCGImagePropertyOrientationRight: + CGContextRotateCTM (context, 90 * MathConstants::pi / 180); + CGContextScaleCTM (context, targetSize.height / origHeight, -targetSize.width / origWidth); + break; + case kCGImagePropertyOrientationDown: + CGContextTranslateCTM (context, targetSize.width, 0.0); + CGContextScaleCTM (context, -1.0, 1.0); + break; + case kCGImagePropertyOrientationLeft: + CGContextRotateCTM (context, -90 * MathConstants::pi / 180); + CGContextScaleCTM (context, targetSize.height / origHeight, -targetSize.width / origWidth); + CGContextTranslateCTM (context, -targetSize.width, -targetSize.height); + break; + default: + // Not implemented. + jassertfalse; + break; + } + + CGContextDrawImage (context, CGRectMake (0, 0, targetSize.width, targetSize.height), imageData); + + UIImage* correctedImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return correctedImage; + } + + static CGSize getTargetImageDimensionFor (CGImagePropertyOrientation imageOrientation, + CGImageRef imageData) + { + auto width = CGImageGetWidth (imageData); + auto height = CGImageGetHeight (imageData); + + switch (imageOrientation) + { + case kCGImagePropertyOrientationUp: + case kCGImagePropertyOrientationUpMirrored: + case kCGImagePropertyOrientationDown: + case kCGImagePropertyOrientationDownMirrored: + return CGSizeMake ((CGFloat) width, (CGFloat) height); + case kCGImagePropertyOrientationRight: + case kCGImagePropertyOrientationRightMirrored: + case kCGImagePropertyOrientationLeft: + case kCGImagePropertyOrientationLeftMirrored: + return CGSizeMake ((CGFloat) height, (CGFloat) width); + } + + jassertfalse; + return CGSizeMake ((CGFloat) width, (CGFloat) height); + } + + static void didFinishProcessingPhotoSampleBuffer (id self, SEL, AVCapturePhotoOutput*, + CMSampleBufferRef imageBuffer, CMSampleBufferRef imagePreviewBuffer, + AVCaptureResolvedPhotoSettings*, AVCaptureBracketedStillImageSettings*, + NSError* error) + { + String errorString = error != nil ? nsStringToJuce (error.localizedDescription) : String(); + ignoreUnused (errorString); + + JUCE_CAMERA_LOG ("didFinishProcessingPhotoSampleBuffer(), error = " + errorString); + + if (error != nil) + { + JUCE_CAMERA_LOG ("Still picture capture failed, error: " + nsStringToJuce (error.localizedDescription)); + jassertfalse; + return; + } + + NSData* origImageData = [AVCapturePhotoOutput JPEGPhotoDataRepresentationForJPEGSampleBuffer: imageBuffer previewPhotoSampleBuffer: imagePreviewBuffer]; + auto* origImage = [UIImage imageWithData: origImageData]; + auto imageOrientation = uiImageOrientationToCGImageOrientation (origImage.imageOrientation); + + auto* uiImage = getImageWithCorrectOrientation (imageOrientation, origImage.CGImage); + + auto* imageData = UIImageJPEGRepresentation (uiImage, 0.f); + + auto image = ImageFileFormat::loadFrom (imageData.bytes, (size_t) imageData.length); + MessageManager::callAsync ([self, image]() { getOwner (self).imageTaken (image); }); + } + + static CGImagePropertyOrientation uiImageOrientationToCGImageOrientation (UIImageOrientation orientation) + { + switch (orientation) + { + case UIImageOrientationUp: return kCGImagePropertyOrientationUp; + case UIImageOrientationDown: return kCGImagePropertyOrientationDown; + case UIImageOrientationLeft: return kCGImagePropertyOrientationLeft; + case UIImageOrientationRight: return kCGImagePropertyOrientationRight; + case UIImageOrientationUpMirrored: return kCGImagePropertyOrientationUpMirrored; + case UIImageOrientationDownMirrored: return kCGImagePropertyOrientationDownMirrored; + case UIImageOrientationLeftMirrored: return kCGImagePropertyOrientationLeftMirrored; + case UIImageOrientationRightMirrored: return kCGImagePropertyOrientationRightMirrored; + } + } + }; + + //============================================================================== + void imageTaken (const Image& image) + { + takingPicture = false; + + captureSession.notifyImageReceived (image); + } + + CaptureSession& captureSession; + AVCaptureOutput* captureOutput; + + std::unique_ptr photoOutputDelegate; + + bool takingPicture = false; + }; + + //============================================================================== + // NB: FileOutputRecordingDelegateClass callbacks can be called from any thread (incl. + // the message thread), so waiting for an event when stopping recording is not an + // option and VideoRecorder must be alive at all times in order to get stopped + // recording callback. + class VideoRecorder + { + public: + VideoRecorder (CaptureSession& captureSession) + : movieFileOutput ([AVCaptureMovieFileOutput new]), + delegate (nullptr) + { + static FileOutputRecordingDelegateClass cls; + delegate.reset ([cls.createInstance() init]); + FileOutputRecordingDelegateClass::setOwner (delegate.get(), this); + + captureSession.addOutputIfPossible (movieFileOutput); + } + + ~VideoRecorder() + { + stopRecording(); + + // Shutting down a device while recording will stop the recording + // abruptly and the recording will be lost. + jassert (! recordingInProgress); + } + + void startRecording (const File& file, AVCaptureVideoOrientation orientationToUse) + { + if (Pimpl::getIOSVersion().major >= 10) + printVideoOutputDebugInfo (movieFileOutput); + + auto* url = [NSURL fileURLWithPath: juceStringToNS (file.getFullPathName()) + isDirectory: NO]; + + auto* outputConnection = [movieFileOutput connectionWithMediaType: AVMediaTypeVideo]; + outputConnection.videoOrientation = orientationToUse; + + [movieFileOutput startRecordingToOutputFileURL: url recordingDelegate: delegate.get()]; + } + + void stopRecording() + { + [movieFileOutput stopRecording]; + } + + Time getTimeOfFirstRecordedFrame() const + { + return Time (firstRecordedFrameTimeMs.get()); + } + + private: + static void printVideoOutputDebugInfo (AVCaptureMovieFileOutput* output) + { + ignoreUnused (output); + + JUCE_CAMERA_LOG ("Available video codec types:"); + + #if JUCE_CAMERA_LOG_ENABLED + for (AVVideoCodecType type in output.availableVideoCodecTypes) + JUCE_CAMERA_LOG (nsStringToJuce (type)); + #endif + + JUCE_CAMERA_LOG ("Output settings per video connection:"); + + #if JUCE_CAMERA_LOG_ENABLED + for (AVCaptureConnection* connection in output.connections) + JUCE_CAMERA_LOG (nsStringToJuce ([[output outputSettingsForConnection: connection] description])); + #endif + } + + //============================================================================== + struct FileOutputRecordingDelegateClass : public ObjCClass> + { + FileOutputRecordingDelegateClass() : ObjCClass> ("FileOutputRecordingDelegateClass_") + { + addMethod (@selector (captureOutput:didStartRecordingToOutputFileAtURL:fromConnections:), started, "v@:@@@"); + addMethod (@selector (captureOutput:didFinishRecordingToOutputFileAtURL:fromConnections:error:), stopped, "v@:@@@@"); + + addIvar ("owner"); + + registerClass(); + } + + //============================================================================== + static VideoRecorder& getOwner (id self) { return *getIvar (self, "owner"); } + static void setOwner (id self, VideoRecorder* r) { object_setInstanceVariable (self, "owner", r); } + + private: + static void started (id self, SEL, AVCaptureFileOutput*, NSURL*, NSArray*) + { + JUCE_CAMERA_LOG ("Started recording"); + + getOwner (self).firstRecordedFrameTimeMs.set (Time::getCurrentTime().toMilliseconds()); + getOwner (self).recordingInProgress = true; + } + + static void stopped (id self, SEL, AVCaptureFileOutput*, NSURL*, NSArray*, NSError* error) + { + String errorString; + bool recordingPlayable = true; + + // There might have been an error in the recording, yet there may be a playable file... + if ([error code] != noErr) + { + id value = [[error userInfo] objectForKey: AVErrorRecordingSuccessfullyFinishedKey]; + + if (value != nil && ! [value boolValue]) + recordingPlayable = false; + + errorString = nsStringToJuce (error.localizedDescription) + ", playable: " + String ((int) recordingPlayable); + } + + JUCE_CAMERA_LOG ("Stopped recording, error = " + errorString); + + getOwner (self).recordingInProgress = false; + } + }; + + AVCaptureMovieFileOutput* movieFileOutput; + std::unique_ptr, NSObjectDeleter> delegate; + bool recordingInProgress = false; + Atomic firstRecordedFrameTimeMs { 0 }; + }; + + //============================================================================== + void addOutputIfPossible (AVCaptureOutput* output) + { + dispatch_async (captureSessionQueue,^ + { + if ([captureSession.get() canAddOutput: output]) + { + [captureSession.get() beginConfiguration]; + [captureSession.get() addOutput: output]; + [captureSession.get() commitConfiguration]; + + return; + } + + // Can't add output to camera session! + jassertfalse; + }); + } + + //============================================================================== + void cameraSessionStarted() + { + sessionStarted = true; + + owner.cameraSessionStarted(); + } + + void cameraSessionRuntimeError (const String& error) + { + owner.cameraSessionRuntimeError (error); + } + + void notifyImageReceived (const Image& image) + { + owner.notifyImageReceived (image); + } + + Pimpl& owner; + + dispatch_queue_t captureSessionQueue; + std::unique_ptr captureSession; + std::unique_ptr delegate; + + StillPictureTaker stillPictureTaker; + VideoRecorder videoRecorder; + + AVCaptureDevice* cameraDevice = nil; + AVCaptureVideoPreviewLayer* previewLayer = nil; + + bool sessionStarted = false; + + WaitableEvent sessionClosedEvent; + + static int numCaptureSessions; + }; + + //============================================================================== + void cameraSessionStarted() + { + JUCE_CAMERA_LOG ("cameraSessionStarted()"); + + cameraOpenCallback (cameraId, {}); + } + + void cameraSessionRuntimeError (const String& error) + { + JUCE_CAMERA_LOG ("cameraSessionRuntimeError(), error = " + error); + + if (! notifiedOfCameraOpening) + { + cameraOpenCallback ({}, error); + } + else + { + if (owner.onErrorOccurred != nullptr) + owner.onErrorOccurred (error); + } + } + + void notifyImageReceived (const Image& image) + { + JUCE_CAMERA_LOG ("notifyImageReceived()"); + + if (pictureTakenCallback != nullptr) + pictureTakenCallback (image); + } + + //============================================================================== + void triggerStillPictureCapture() + { + captureSession.takeStillPicture(); + } + + //============================================================================== + CameraDevice& owner; + String cameraId; + InternalOpenCameraResultCallback cameraOpenCallback; + + std::function pictureTakenCallback; + + CaptureSession captureSession; + + bool notifiedOfCameraOpening = false; + + //============================================================================== + struct IOSVersion + { + int major; + int minor; + }; + + static IOSVersion getIOSVersion() + { + auto processInfo = [NSProcessInfo processInfo]; + + if (! [processInfo respondsToSelector: @selector (operatingSystemVersion)]) + return {7, 0}; // Below 8.0 in fact, but only care that it's below 8 + + return { (int)[processInfo operatingSystemVersion].majorVersion, + (int)[processInfo operatingSystemVersion].minorVersion }; + } + + static IOSVersion iosVersion; + + friend struct CameraDevice::ViewerComponent; + + JUCE_DECLARE_NON_COPYABLE (Pimpl) +}; + +CameraDevice::Pimpl::IOSVersion CameraDevice::Pimpl::iosVersion = CameraDevice::Pimpl::getIOSVersion(); +int CameraDevice::Pimpl::CaptureSession::numCaptureSessions = 0; + +//============================================================================== +struct CameraDevice::ViewerComponent : public UIViewComponent +{ + //============================================================================== + struct JuceCameraDeviceViewerClass : public ObjCClass + { + JuceCameraDeviceViewerClass() : ObjCClass ("JuceCameraDeviceViewerClass_") + { + addMethod (@selector (layoutSubviews), layoutSubviews, "v@:"); + + registerClass(); + } + + private: + static void layoutSubviews (id self, SEL) + { + sendSuperclassMessage (self, @selector (layoutSubviews)); + + UIView* asUIView = (UIView*) self; + + updateOrientation (self); + + if (auto* previewLayer = getPreviewLayer (self)) + previewLayer.frame = asUIView.bounds; + } + + static AVCaptureVideoPreviewLayer* getPreviewLayer (id self) + { + UIView* asUIView = (UIView*) self; + + if (asUIView.layer.sublayers != nil && [asUIView.layer.sublayers count] > 0) + if ([asUIView.layer.sublayers[0] isKindOfClass: [AVCaptureVideoPreviewLayer class]]) + return (AVCaptureVideoPreviewLayer*) asUIView.layer.sublayers[0]; + + return nil; + } + + static void updateOrientation (id self) + { + if (auto* previewLayer = getPreviewLayer (self)) + { + UIDeviceOrientation o = [UIDevice currentDevice].orientation; + + if (UIDeviceOrientationIsPortrait (o) || UIDeviceOrientationIsLandscape (o)) + { + if (previewLayer.connection != nil) + previewLayer.connection.videoOrientation = (AVCaptureVideoOrientation) o; + } + } + } + }; + + ViewerComponent (CameraDevice& device) + { + static JuceCameraDeviceViewerClass cls; + + // Initial size that can be overriden later. + setSize (640, 480); + + auto* view = [cls.createInstance() init]; + setView (view); + + auto* previewLayer = device.pimpl->captureSession.createPreviewLayer(); + previewLayer.frame = view.bounds; + + UIInterfaceOrientation statusBarOrientation = [UIApplication sharedApplication].statusBarOrientation; + AVCaptureVideoOrientation videoOrientation = statusBarOrientation != UIInterfaceOrientationUnknown + ? (AVCaptureVideoOrientation) statusBarOrientation + : AVCaptureVideoOrientationPortrait; + + previewLayer.connection.videoOrientation = videoOrientation; + + [view.layer addSublayer: previewLayer]; + } +}; + +//============================================================================== +String CameraDevice::getFileExtension() +{ + return ".mov"; +} diff --git a/modules/juce_video/native/juce_mac_CameraDevice.h b/modules/juce_video/native/juce_mac_CameraDevice.h index fbd812c497..66a8fc3915 100644 --- a/modules/juce_video/native/juce_mac_CameraDevice.h +++ b/modules/juce_video/native/juce_mac_CameraDevice.h @@ -26,8 +26,9 @@ struct CameraDevice::Pimpl { - Pimpl (const String&, int /*index*/, int /*minWidth*/, int /*minHeight*/, + Pimpl (CameraDevice& ownerToUse, const String&, int /*index*/, int /*minWidth*/, int /*minHeight*/, int /*maxWidth*/, int /*maxHeight*/, bool useHighQuality) + : owner (ownerToUse) { JUCE_AUTORELEASEPOOL { @@ -42,11 +43,20 @@ struct CameraDevice::Pimpl static DelegateClass cls; callbackDelegate = (id) [cls.createInstance() init]; DelegateClass::setOwner (callbackDelegate, this); + + SEL runtimeErrorSel = NSSelectorFromString (nsStringLiteral ("captureSessionRuntimeError:")); + + [[NSNotificationCenter defaultCenter] addObserver: callbackDelegate + selector: runtimeErrorSel + name: AVCaptureSessionRuntimeErrorNotification + object: session]; } } ~Pimpl() { + [[NSNotificationCenter defaultCenter] removeObserver: callbackDelegate]; + [session stopRunning]; removeImageCapture(); removeMovieCapture(); @@ -113,6 +123,19 @@ struct CameraDevice::Pimpl refreshConnections(); } + void takeStillPicture (std::function pictureTakenCallbackToUse) + { + if (pictureTakenCallbackToUse == nullptr) + { + jassertfalse; + return; + } + + pictureTakenCallback = static_cast&&> (pictureTakenCallbackToUse); + + triggerImageCapture(); + } + void startRecordingToFile (const File& file, int /*quality*/) { stopRecording(); @@ -150,21 +173,10 @@ struct CameraDevice::Pimpl return nil; } - void handleImageCapture (const void* data, size_t size) + void handleImageCapture (const Image& image) { - auto image = ImageFileFormat::loadFrom (data, size); - - const ScopedLock sl (listenerLock); - - if (! listeners.isEmpty()) - { - for (int i = listeners.size(); --i >= 0;) - if (auto* l = listeners[i]) - l->imageReceived (image); - - if (! listeners.isEmpty()) - triggerImageCapture(); - } + if (pictureTakenCallback != nullptr) + pictureTakenCallback (image); } void triggerImageCapture() @@ -174,33 +186,25 @@ struct CameraDevice::Pimpl if (auto* videoConnection = getVideoConnection()) { [imageOutput captureStillImageAsynchronouslyFromConnection: videoConnection - completionHandler: ^(CMSampleBufferRef sampleBuffer, NSError*) + completionHandler: ^(CMSampleBufferRef sampleBuffer, NSError* error) { - auto buffer = CMSampleBufferGetDataBuffer (sampleBuffer); - size_t size = CMBlockBufferGetDataLength (buffer); - jassert (CMBlockBufferIsRangeContiguous (buffer, 0, size)); // TODO: need to add code to handle this if it happens - char* data = nullptr; - CMBlockBufferGetDataPointer (buffer, 0, &size, nullptr, &data); - handleImageCapture (data, size); + if (error != nil) + { + JUCE_CAMERA_LOG ("Still picture capture failed, error: " + nsStringToJuce (error.localizedDescription)); + jassertfalse; + return; + } + + NSData* imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation: sampleBuffer]; + + auto image = ImageFileFormat::loadFrom (imageData.bytes, (size_t) imageData.length); + + WeakReference weakRef (this); + MessageManager::callAsync ([weakRef, image]() mutable { if (weakRef != nullptr) weakRef->handleImageCapture (image); }); }]; } } - void addListener (CameraDevice::Listener* listenerToAdd) - { - const ScopedLock sl (listenerLock); - listeners.addIfNotAlreadyThere (listenerToAdd); - - if (listeners.size() == 1) - triggerImageCapture(); - } - - void removeListener (CameraDevice::Listener* listenerToRemove) - { - const ScopedLock sl (listenerLock); - listeners.removeFirstMatchingValue (listenerToRemove); - } - static StringArray getAvailableDevices() { StringArray results; @@ -208,6 +212,15 @@ struct CameraDevice::Pimpl return results; } + void cameraSessionRuntimeError (const String& error) + { + JUCE_CAMERA_LOG ("cameraSessionRuntimeError(), error = " + error); + + if (owner.onErrorOccurred != nullptr) + owner.onErrorOccurred (error); + } + + CameraDevice& owner; AVCaptureView* captureView = nil; AVCaptureSession* session = nil; AVCaptureMovieFileOutput* fileOutput = nil; @@ -218,8 +231,9 @@ struct CameraDevice::Pimpl Time firstPresentationTime; bool isRecording = false; - Array listeners; - CriticalSection listenerLock; + std::function pictureTakenCallback; + + JUCE_DECLARE_WEAK_REFERENCEABLE (Pimpl) private: //============================================================================== @@ -235,17 +249,29 @@ private: addMethod (@selector (captureOutput:didResumeRecordingToOutputFileAtURL: fromConnections:), didResumeRecordingToOutputFileAtURL, "v@:@@@"); addMethod (@selector (captureOutput:willFinishRecordingToOutputFileAtURL:fromConnections:error:), willFinishRecordingToOutputFileAtURL, "v@:@@@@"); + SEL runtimeErrorSel = NSSelectorFromString (nsStringLiteral ("captureSessionRuntimeError:")); + addMethod (runtimeErrorSel, sessionRuntimeError, "v@:@"); + registerClass(); } static void setOwner (id self, Pimpl* owner) { object_setInstanceVariable (self, "owner", owner); } - static Pimpl* getOwner (id self) { return getIvar (self, "owner"); } + static Pimpl& getOwner (id self) { return *getIvar (self, "owner"); } private: static void didStartRecordingToOutputFileAtURL (id, SEL, AVCaptureFileOutput*, NSURL*, NSArray*) {} static void didPauseRecordingToOutputFileAtURL (id, SEL, AVCaptureFileOutput*, NSURL*, NSArray*) {} static void didResumeRecordingToOutputFileAtURL (id, SEL, AVCaptureFileOutput*, NSURL*, NSArray*) {} static void willFinishRecordingToOutputFileAtURL (id, SEL, AVCaptureFileOutput*, NSURL*, NSArray*, NSError*) {} + + static void sessionRuntimeError (id self, SEL, NSNotification* notification) + { + JUCE_CAMERA_LOG (nsStringToJuce ([notification description])); + + NSError* error = notification.userInfo[AVCaptureSessionErrorKey]; + auto errorString = error != nil ? nsStringToJuce (error.localizedDescription) : String(); + getOwner (self).cameraSessionRuntimeError (errorString); + } }; JUCE_DECLARE_NON_COPYABLE (Pimpl) diff --git a/modules/juce_video/native/juce_win32_CameraDevice.h b/modules/juce_video/native/juce_win32_CameraDevice.h index 99f0e94659..f775de6fb7 100644 --- a/modules/juce_video/native/juce_win32_CameraDevice.h +++ b/modules/juce_video/native/juce_win32_CameraDevice.h @@ -49,10 +49,11 @@ static const CLSID CLSID_NullRenderer = { 0xC1F400A4, 0x3F08, 0x11d3, { 0x9F, 0 struct CameraDevice::Pimpl : public ChangeBroadcaster { - Pimpl (const String&, int index, - int minWidth, int minHeight, - int maxWidth, int maxHeight, bool /*highQuality*/) - : isRecording (false), + Pimpl (CameraDevice& ownerToUse, const String&, int index, + int minWidth, int minHeight, int maxWidth, int maxHeight, + bool /*highQuality*/) + : owner (ownerToUse), + isRecording (false), openedSuccessfully (false), imageNeedsFlipping (false), width (0), height (0), @@ -191,6 +192,22 @@ struct CameraDevice::Pimpl : public ChangeBroadcaster bool openedOk() const noexcept { return openedSuccessfully; } + void takeStillPicture (std::function pictureTakenCallbackToUse) + { + { + const ScopedLock sl (callbackLock); + + jassert (pictureTakenCallbackToUse != nullptr); + + if (pictureTakenCallbackToUse == nullptr) + return; + + pictureTakenCallback = static_cast&&> (pictureTakenCallbackToUse); + } + + addUser(); + } + void startRecordingToFile (const File& file, int quality) { addUser(); @@ -212,32 +229,26 @@ struct CameraDevice::Pimpl : public ChangeBroadcaster return firstRecordedTime; } - void addListener (CameraDevice::Listener* listenerToAdd) + void notifyImageReceivedIfNeeded (const Image& image) { - const ScopedLock sl (listenerLock); + { + const ScopedLock sl (callbackLock); - if (listeners.size() == 0) - addUser(); + if (pictureTakenCallback == nullptr) + return; + } - listeners.addIfNotAlreadyThere (listenerToAdd); - } + WeakReference weakRef (this); + MessageManager::callAsync ([weakRef, image]() mutable + { + if (weakRef == nullptr) + return; - void removeListener (CameraDevice::Listener* listenerToRemove) - { - const ScopedLock sl (listenerLock); - listeners.removeAllInstancesOf (listenerToRemove); + if (weakRef->pictureTakenCallback != nullptr) + weakRef->pictureTakenCallback (image); - if (listeners.size() == 0) - removeUser(); - } - - void callListeners (const Image& image) - { - const ScopedLock sl (listenerLock); - - for (int i = listeners.size(); --i >= 0;) - if (CameraDevice::Listener* const l = listeners[i]) - l->imageReceived (image); + weakRef->pictureTakenCallback = nullptr; + }); } void addUser() @@ -294,8 +305,7 @@ struct CameraDevice::Pimpl : public ChangeBroadcaster imageNeedsFlipping = true; } - if (listeners.size() > 0) - callListeners (loadingImage); + notifyImageReceivedIfNeeded (loadingImage); sendChangeMessage(); } @@ -520,9 +530,9 @@ struct CameraDevice::Pimpl : public ChangeBroadcaster JUCE_DECLARE_NON_COPYABLE (GrabberCallback) }; + CameraDevice& owner; + ComSmartPtr callback; - Array listeners; - CriticalSection listenerLock; bool isRecording, openedSuccessfully; int width, height; @@ -547,6 +557,11 @@ struct CameraDevice::Pimpl : public ChangeBroadcaster bool recordNextFrameTime; int previewMaxFPS; + CriticalSection callbackLock; + std::function pictureTakenCallback; + + JUCE_DECLARE_WEAK_REFERENCEABLE (Pimpl) + private: void getVideoSizes (IAMStreamConfig* const streamConfig) { diff --git a/modules/juce_video/playback/juce_VideoComponent.h b/modules/juce_video/playback/juce_VideoComponent.h index 7d1638d3dc..b960cdfbac 100644 --- a/modules/juce_video/playback/juce_VideoComponent.h +++ b/modules/juce_video/playback/juce_VideoComponent.h @@ -54,12 +54,12 @@ public: //============================================================================== /** Tries to load a video from a local file. - @returns am error if the file failed to be loaded correctly + @returns an error if the file failed to be loaded correctly */ Result load (const File& file); /** Tries to load a video from a URL. - @returns am error if the file failed to be loaded correctly + @returns an error if the file failed to be loaded correctly */ Result load (const URL& url); @@ -75,7 +75,7 @@ public: File getCurrentVideoFile() const; /** Returns the last URL that was loaded. - If nothing is open, or if it was a file rather than a URL, this will return File(). + If nothing is open, or if it was a file rather than a URL, this will return URL(). */ URL getCurrentVideoURL() const;