From 1a8e5181809b835f63ed158c77b1266f3ed3f838 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 1 Feb 2024 11:55:31 +0100 Subject: [PATCH 01/71] chore: Remove unused `frameProcessor` variable --- .../src/main/java/com/mrousavy/camera/core/CameraSession.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt index 5b8d76e..bdea193 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt @@ -38,7 +38,6 @@ import com.mrousavy.camera.extensions.getVideoSizes import com.mrousavy.camera.extensions.openCamera import com.mrousavy.camera.extensions.setZoom import com.mrousavy.camera.frameprocessor.Frame -import com.mrousavy.camera.frameprocessor.FrameProcessor import com.mrousavy.camera.types.Flash import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.QualityPrioritization @@ -101,11 +100,6 @@ class CameraSession(private val context: Context, private val cameraManager: Cam field = value updateVideoOutputs() } - var frameProcessor: FrameProcessor? = null - set(value) { - field = value - updateVideoOutputs() - } val orientation: Orientation get() { From 8c66d36d8f7ff3f2cb9a658e64f9c5ebc374d22a Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 1 Feb 2024 12:40:52 +0100 Subject: [PATCH 02/71] chore: release 3.9.0-beta.0 --- package/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/package.json b/package/package.json index f5eb461..133a504 100644 --- a/package/package.json +++ b/package/package.json @@ -1,6 +1,6 @@ { "name": "react-native-vision-camera", - "version": "3.8.2", + "version": "3.9.0-beta.0", "description": "A powerful, high-performance React Native Camera library.", "main": "lib/commonjs/index", "module": "lib/module/index", From e95264f782c67c8f756be3aad59064090ff50aaa Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 2 Feb 2024 09:54:17 +0100 Subject: [PATCH 03/71] fix: Fix `minFocusDistance` being `NaN` on some emulators --- .../main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt index 2f86c72..3b66a1b 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt @@ -102,6 +102,7 @@ class CameraDeviceDetails(val cameraManager: CameraManager, val cameraId: String private fun getMinFocusDistanceCm(): Double { val distance = characteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE) if (distance == null || distance == 0f) return 0.0 + if (distance.isNaN() || distance.isInfinite()) return 0.0 // distance is in "diopters", meaning 1/meter. Convert to meters, then centi-meters return 1.0 / distance * 100.0 } From 14554fa31a68f63ec3373de99f57dcd2a191bb0e Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 2 Feb 2024 10:06:53 +0100 Subject: [PATCH 04/71] docs: Be more explicit about video + preview link and add aspect ratio --- docs/docs/guides/FORMATS.mdx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/docs/guides/FORMATS.mdx b/docs/docs/guides/FORMATS.mdx index e2efed1..6a42ea6 100644 --- a/docs/docs/guides/FORMATS.mdx +++ b/docs/docs/guides/FORMATS.mdx @@ -34,6 +34,7 @@ To understand a bit more about camera formats, you first need to understand a fe * 4k Photos, 1080p Videos, 240 FPS (ultra high FPS/slow motion) * 720p Photos, 720p Videos, 30 FPS (smaller buffers/e.g. faster face detection) * Each app has different requirements, so the format filtering is up to you. +* The `videoResolution` and `videoAspectRatio` options also affect the preview, as preview is also running in the video stream. To get all available formats, simply use the `CameraDevice`'s [`formats` property](/docs/api/interfaces/CameraDevice#formats). These are a [CameraFormat's](/docs/api/interfaces/CameraDeviceFormat) props: @@ -61,6 +62,7 @@ You can either find a matching format manually by looping through your `CameraDe ```ts const device = ... const format = useCameraFormat(device, [ + { videoAspectRatio: 16 / 9 }, { videoResolution: { width: 3048, height: 2160 } }, { fps: 60 } ]) @@ -72,6 +74,7 @@ const format = useCameraFormat(device, [ ```ts const device = ... const format = getCameraFormat(device, [ + { videoAspectRatio: 16 / 9 }, { videoResolution: { width: 3048, height: 2160 } }, { fps: 60 } ]) From 97168c647c4fc8cd48c9989e6d6ef70ac65d88b4 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 2 Feb 2024 10:10:18 +0100 Subject: [PATCH 05/71] docs: Be more explicit about `videoHeight`/`videoWidth` --- docs/docs/guides/FORMATS.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/guides/FORMATS.mdx b/docs/docs/guides/FORMATS.mdx index 6a42ea6..bdfd092 100644 --- a/docs/docs/guides/FORMATS.mdx +++ b/docs/docs/guides/FORMATS.mdx @@ -39,8 +39,8 @@ To understand a bit more about camera formats, you first need to understand a fe To get all available formats, simply use the `CameraDevice`'s [`formats` property](/docs/api/interfaces/CameraDevice#formats). These are a [CameraFormat's](/docs/api/interfaces/CameraDeviceFormat) props: - [`photoHeight`](/docs/api/interfaces/CameraDeviceFormat#photoheight)/[`photoWidth`](/docs/api/interfaces/CameraDeviceFormat#photoWidth): The resolution that will be used for taking photos. Choose a format with your desired resolution. -- [`videoHeight`](/docs/api/interfaces/CameraDeviceFormat#videoheight)/[`videoWidth`](/docs/api/interfaces/CameraDeviceFormat#videoWidth): The resolution that will be used for recording videos. Choose a format with your desired resolution. -- [`minFps`](/docs/api/interfaces/CameraDeviceFormat#minfps)/[`maxFps`](/docs/api/interfaces/CameraDeviceFormat#maxfps): A range of possible values for the `fps` property. For example, if your format has `minFps: 1` and `maxFps: 60`, you can either use `fps={30}`, `fps={60}` or any other value in between for recording videos. +- [`videoHeight`](/docs/api/interfaces/CameraDeviceFormat#videoheight)/[`videoWidth`](/docs/api/interfaces/CameraDeviceFormat#videoWidth): The resolution that will be used for recording videos and streaming into frame processors. This also affects the preview's aspect ratio. Choose a format with your desired resolution. +- [`minFps`](/docs/api/interfaces/CameraDeviceFormat#minfps)/[`maxFps`](/docs/api/interfaces/CameraDeviceFormat#maxfps): A range of possible values for the `fps` property. For example, if your format has `minFps: 1` and `maxFps: 60`, you can either use `fps={30}`, `fps={60}` or any other value in between for recording videos and streaming into frame processors. - [`videoStabilizationModes`](/docs/api/interfaces/CameraDeviceFormat#videostabilizationmodes): All supported Video Stabilization Modes, digital and optical. If this specific format contains your desired [`VideoStabilizationMode`](/docs/api/#videostabilizationmode), you can pass it to your `` via the [`videoStabilizationMode` property](/docs/api/interfaces/CameraProps#videoStabilizationMode). - [`pixelFormats`](/docs/api/interfaces/CameraDeviceFormat#pixelformats): All supported Pixel Formats. If this specific format contains your desired [`PixelFormat`](/docs/api/#PixelFormat), you can pass it to your `` via the [`pixelFormat` property](/docs/api/interfaces/CameraProps#pixelFormat). - [`supportsVideoHdr`](/docs/api/interfaces/CameraDeviceFormat#supportsvideohdr): Whether this specific format supports true 10-bit HDR for video capture. If this is `true`, you can enable `videoHdr` on your ``. From d8c95c901f11b5bf6c04146e8f826bc8b6593d2d Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Mon, 5 Feb 2024 12:34:32 +0100 Subject: [PATCH 06/71] feat: Synchronize `Frame` properly (#2501) * feat: Synchronize `Frame` properly * Update CameraError.ts * Image is not valid if `refCount` < 0 --- .../com/mrousavy/camera/core/CameraError.kt | 10 ++ .../mrousavy/camera/frameprocessor/Frame.java | 118 +++++++++--------- package/src/CameraError.ts | 1 + 3 files changed, 71 insertions(+), 58 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt index 70dc1ed..c6bb6c2 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt @@ -113,6 +113,16 @@ class RecordingInProgressError : "recording-in-progress", "There is already an active video recording in progress! Did you call startRecording() twice?" ) +class FrameInvalidError : + CameraError( + "capture", + "frame-invalid", + "Trying to access an already closed Frame! " + + "Are you trying to access the Image data outside of a Frame Processor's lifetime?\n" + + "- If you want to use `console.log(frame)`, use `console.log(frame.toString())` instead.\n" + + "- If you want to do async processing, use `runAsync(...)` instead.\n" + + "- If you want to use runOnJS, increment it's ref-count: `frame.incrementRefCount()`" + ) class CodeTypeNotSupportedError(codeType: String) : CameraError( diff --git a/package/android/src/main/java/com/mrousavy/camera/frameprocessor/Frame.java b/package/android/src/main/java/com/mrousavy/camera/frameprocessor/Frame.java index 763b9cc..c8f6ecf 100644 --- a/package/android/src/main/java/com/mrousavy/camera/frameprocessor/Frame.java +++ b/package/android/src/main/java/com/mrousavy/camera/frameprocessor/Frame.java @@ -4,6 +4,7 @@ import android.hardware.HardwareBuffer; import android.media.Image; import android.os.Build; import com.facebook.proguard.annotations.DoNotStrip; +import com.mrousavy.camera.core.FrameInvalidError; import com.mrousavy.camera.core.HardwareBuffersNotAvailableError; import com.mrousavy.camera.types.PixelFormat; import com.mrousavy.camera.types.Orientation; @@ -23,42 +24,17 @@ public class Frame { this.isMirrored = isMirrored; } - public Image getImage() { - synchronized (this) { - Image img = image; - if (!getIsImageValid(img)) { - throw new RuntimeException("Frame is already closed! " + - "Are you trying to access the Image data outside of a Frame Processor's lifetime?\n" + - "- If you want to use `console.log(frame)`, use `console.log(frame.toString())` instead.\n" + - "- If you want to do async processing, use `runAsync(...)` instead.\n" + - "- If you want to use runOnJS, increment it's ref-count: `frame.incrementRefCount()`"); - } - return img; + private void assertIsValid() throws FrameInvalidError { + if (!getIsImageValid(image)) { + throw new FrameInvalidError(); } } - @SuppressWarnings("unused") - @DoNotStrip - public int getWidth() { - return getImage().getWidth(); - } - - @SuppressWarnings("unused") - @DoNotStrip - public int getHeight() { - return getImage().getHeight(); - } - - @SuppressWarnings("unused") - @DoNotStrip - public boolean getIsValid() { - return getIsImageValid(getImage()); - } - - private boolean getIsImageValid(Image image) { + private synchronized boolean getIsImageValid(Image image) { + if (refCount <= 0) return false; try { // will throw an exception if the image is already closed - synchronized (this) { image.getFormat(); } + image.getFormat(); // no exception thrown, image must still be valid. return true; } catch (IllegalStateException e) { @@ -67,78 +43,104 @@ public class Frame { } } + public synchronized Image getImage() { + return image; + } + @SuppressWarnings("unused") @DoNotStrip - public boolean getIsMirrored() { + public synchronized int getWidth() throws FrameInvalidError { + assertIsValid(); + return image.getWidth(); + } + + @SuppressWarnings("unused") + @DoNotStrip + public synchronized int getHeight() throws FrameInvalidError { + assertIsValid(); + return image.getHeight(); + } + + @SuppressWarnings("unused") + @DoNotStrip + public synchronized boolean getIsValid() throws FrameInvalidError { + assertIsValid(); + return getIsImageValid(image); + } + + @SuppressWarnings("unused") + @DoNotStrip + public synchronized boolean getIsMirrored() throws FrameInvalidError { + assertIsValid(); return isMirrored; } @SuppressWarnings("unused") @DoNotStrip - public long getTimestamp() { + public synchronized long getTimestamp() throws FrameInvalidError { + assertIsValid(); return timestamp; } @SuppressWarnings("unused") @DoNotStrip - public Orientation getOrientation() { + public synchronized Orientation getOrientation() throws FrameInvalidError { + assertIsValid(); return orientation; } @SuppressWarnings("unused") @DoNotStrip - public PixelFormat getPixelFormat() { - return PixelFormat.Companion.fromImageFormat(getImage().getFormat()); + public synchronized PixelFormat getPixelFormat() throws FrameInvalidError { + assertIsValid(); + return PixelFormat.Companion.fromImageFormat(image.getFormat()); } @SuppressWarnings("unused") @DoNotStrip - public int getPlanesCount() { - return getImage().getPlanes().length; + public synchronized int getPlanesCount() throws FrameInvalidError { + assertIsValid(); + return image.getPlanes().length; } @SuppressWarnings("unused") @DoNotStrip - public int getBytesPerRow() { - return getImage().getPlanes()[0].getRowStride(); + public synchronized int getBytesPerRow() throws FrameInvalidError { + assertIsValid(); + return image.getPlanes()[0].getRowStride(); } @SuppressWarnings("unused") @DoNotStrip - public Object getHardwareBufferBoxed() throws HardwareBuffersNotAvailableError { + private Object getHardwareBufferBoxed() throws HardwareBuffersNotAvailableError, FrameInvalidError { return getHardwareBuffer(); } - public HardwareBuffer getHardwareBuffer() throws HardwareBuffersNotAvailableError { + public synchronized HardwareBuffer getHardwareBuffer() throws HardwareBuffersNotAvailableError, FrameInvalidError { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { throw new HardwareBuffersNotAvailableError(); } - return getImage().getHardwareBuffer(); + assertIsValid(); + return image.getHardwareBuffer(); } @SuppressWarnings("unused") @DoNotStrip - public void incrementRefCount() { - synchronized (this) { - refCount++; - } + public synchronized void incrementRefCount() { + refCount++; } @SuppressWarnings("unused") @DoNotStrip - public void decrementRefCount() { - synchronized (this) { - refCount--; - if (refCount <= 0) { - // If no reference is held on this Image, close it. - close(); - } + public synchronized void decrementRefCount() { + refCount--; + if (refCount <= 0) { + // If no reference is held on this Image, close it. + close(); } } - private void close() { - synchronized (this) { - image.close(); - } + private synchronized void close() { + image.close(); } } diff --git a/package/src/CameraError.ts b/package/src/CameraError.ts index 94b205a..baa02ce 100644 --- a/package/src/CameraError.ts +++ b/package/src/CameraError.ts @@ -40,6 +40,7 @@ export type CaptureError = | 'capture/recorder-error' | 'capture/video-not-enabled' | 'capture/photo-not-enabled' + | 'capture/frame-invalid' | 'capture/aborted' | 'capture/unknown' export type SystemError = From cd5fdd4924f103604afcea25f3c89ae358c373c7 Mon Sep 17 00:00:00 2001 From: Lucas Garcez Date: Mon, 5 Feb 2024 22:37:42 +1100 Subject: [PATCH 07/71] fix: Properly type Format `Templates` (#2499) add PredefinedTemplates type --- package/src/devices/Templates.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/package/src/devices/Templates.ts b/package/src/devices/Templates.ts index 3a1df24..274c419 100644 --- a/package/src/devices/Templates.ts +++ b/package/src/devices/Templates.ts @@ -1,9 +1,17 @@ import { Dimensions } from 'react-native' import { FormatFilter } from './getCameraFormat' -type TTemplates = { - [key: string]: FormatFilter[] -} +type PredefinedTemplates = + | 'Video' + | 'Video60Fps' + | 'VideoSlowMotion' + | 'VideoStabilized' + | 'Photo' + | 'PhotoPortrait' + | 'FrameProcessingYUV' + | 'FrameProcessingRGB' + | 'Snapchat' + | 'Instagram' const SnapchatResolution = { width: 1920, height: 1080 } const InstagramResolution = { width: 3840, height: 2160 } @@ -16,7 +24,7 @@ const ScreenAspectRatio = Dimensions.get('window').height / Dimensions.get('wind * const format = useCameraFormat(device, Templates.Snapchat) * ``` */ -export const Templates: TTemplates = { +export const Templates: Record = { /** * Highest resolution video recordings (e.g. 4k) */ From 5acc64e0317542fd4e5450cab1c4770a1d44cde3 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 6 Feb 2024 14:19:25 +0100 Subject: [PATCH 08/71] feat: Create persistent `CaptureSession` to avoid any blackscreen issues or errors (#2494) * feat: Create custom `CaptureSession` wrapper * Create `PersistentCameraCaptureSession` * Update VideoStabilizationMode.kt * Create RepeatingRequest.kt * Update CaptureSession.kt * Delete CaptureSession.kt * Update PersistentCameraCaptureSession.kt * Update PersistentCameraCaptureSession.kt * fix: Add `isRepeating` * Update CameraSession.kt * Make `SurfaceOutput` not `Closable` anymore * Update PersistentCameraCaptureSession.kt * Stub out the rest * Format * Set `isRunning` properly * Close previous outputs * onError callback * Format * Started/Stopped * Update CameraPage.tsx * Add `isValid` * Log `isActive` * Add `tryAbortCaptures` * Configure() * Try? * Add `didDestroyFromOutside` * Disable FP for testing * fix: Call `super.onAttachedToWindow` first * Hm * Update CameraSession.kt * Update PersistentCameraCaptureSession.kt * Try catch `didDestroyFromOutside` * Update PersistentCameraCaptureSession.kt * Session can only be active with a preview * Update PersistentCameraCaptureSession.kt * Throw `no-outputs` if needed * Update logs * fix: Check for CAMERA permission * fix: Close session when opening a new device * perf: Make everything `by lazy` in CameraDeviceDetails * Update CameraDeviceDetails.kt * Update PersistentCameraCaptureSession.kt * Update PersistentCameraCaptureSession.kt * Move * Update Podfile.lock * Implement `capture()` * Format * fix: Fix orientation not being applied * fix: Fix `isMirrored` * fix: Fix getting size * fix: Close `Surface` in `VideoPipeline` * Format * fix: Fix `VideoPipeline` not properly destroying itself * Use FP again * Update CameraConfiguration.kt * Rename * Clean up * Format * Update CameraConfiguration.kt * fix: Don't stop repeating request when capturing --- .../android/src/main/cpp/OpenGLRenderer.cpp | 3 +- .../java/com/mrousavy/camera/CameraView.kt | 4 +- .../camera/core/CameraConfiguration.kt | 11 +- .../camera/core/CameraDeviceDetails.kt | 108 +++-- .../com/mrousavy/camera/core/CameraError.kt | 2 + .../com/mrousavy/camera/core/CameraSession.kt | 376 ++++-------------- .../core/PersistentCameraCaptureSession.kt | 266 +++++++++++++ .../com/mrousavy/camera/core/VideoPipeline.kt | 5 +- .../core/capture/CameraCaptureRequest.kt | 86 ++++ .../core/capture/PhotoCaptureRequest.kt | 113 ++++++ .../core/capture/RepeatingCaptureRequest.kt | 79 ++++ .../camera/core/outputs/SurfaceOutput.kt | 20 +- .../CameraCaptureSession+tryAbortCaptures.kt | 9 + .../CameraDevice+createPhotoCaptureRequest.kt | 104 ----- .../camera/extensions/CameraDevice+isValid.kt | 13 + .../com/mrousavy/camera/types/Orientation.kt | 13 +- .../camera/types/VideoStabilizationMode.kt | 15 - package/example/ios/Podfile.lock | 4 +- package/example/src/CameraPage.tsx | 10 +- package/src/CameraError.ts | 1 + 20 files changed, 746 insertions(+), 496 deletions(-) create mode 100644 package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt create mode 100644 package/android/src/main/java/com/mrousavy/camera/core/capture/CameraCaptureRequest.kt create mode 100644 package/android/src/main/java/com/mrousavy/camera/core/capture/PhotoCaptureRequest.kt create mode 100644 package/android/src/main/java/com/mrousavy/camera/core/capture/RepeatingCaptureRequest.kt create mode 100644 package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+tryAbortCaptures.kt delete mode 100644 package/android/src/main/java/com/mrousavy/camera/extensions/CameraDevice+createPhotoCaptureRequest.kt create mode 100644 package/android/src/main/java/com/mrousavy/camera/extensions/CameraDevice+isValid.kt diff --git a/package/android/src/main/cpp/OpenGLRenderer.cpp b/package/android/src/main/cpp/OpenGLRenderer.cpp index 3768399..e9d6bd3 100644 --- a/package/android/src/main/cpp/OpenGLRenderer.cpp +++ b/package/android/src/main/cpp/OpenGLRenderer.cpp @@ -29,10 +29,11 @@ OpenGLRenderer::OpenGLRenderer(std::shared_ptr context, ANativeWi } OpenGLRenderer::~OpenGLRenderer() { + __android_log_print(ANDROID_LOG_INFO, TAG, "Destroying OpenGLRenderer..."); + destroy(); if (_outputSurface != nullptr) { ANativeWindow_release(_outputSurface); } - destroy(); } void OpenGLRenderer::destroy() { diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt index a3d86ff..1f24448 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt @@ -113,17 +113,17 @@ class CameraView(context: Context) : } override fun onAttachedToWindow() { + super.onAttachedToWindow() if (!isMounted) { isMounted = true invokeOnViewReady() } update() - super.onAttachedToWindow() } override fun onDetachedFromWindow() { - update() super.onDetachedFromWindow() + update() } fun destroy() { diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt index a948b41..f1dca59 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt @@ -67,7 +67,7 @@ data class CameraConfiguration( } data class Difference( - // Input Camera (cameraId, isActive) + // Input Camera (cameraId) val deviceChanged: Boolean, // Outputs & Session (Photo, Video, CodeScanner, HDR, Format) val outputsChanged: Boolean, @@ -75,14 +75,17 @@ data class CameraConfiguration( val sidePropsChanged: Boolean, // (isActive) changed val isActiveChanged: Boolean - ) + ) { + val hasChanges: Boolean + get() = deviceChanged || outputsChanged || sidePropsChanged || isActiveChanged + } companion object { fun copyOf(other: CameraConfiguration?): CameraConfiguration = other?.copy() ?: CameraConfiguration() fun difference(left: CameraConfiguration?, right: CameraConfiguration): Difference { // input device - val deviceChanged = left?.cameraId != right.cameraId || left?.isActive != right.isActive + val deviceChanged = left?.cameraId != right.cameraId // outputs val outputsChanged = deviceChanged || @@ -101,7 +104,7 @@ data class CameraConfiguration( left.videoStabilizationMode != right.videoStabilizationMode || left.exposure != right.exposure - val isActiveChanged = left?.isActive != right.isActive + val isActiveChanged = sidePropsChanged || left?.isActive != right.isActive return Difference( deviceChanged, diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt index 3b66a1b..25856f8 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt @@ -1,7 +1,9 @@ package com.mrousavy.camera.core +import android.annotation.SuppressLint import android.graphics.ImageFormat import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraExtensionCharacteristics import android.hardware.camera2.CameraManager import android.hardware.camera2.CameraMetadata import android.os.Build @@ -23,61 +25,73 @@ import com.mrousavy.camera.types.VideoStabilizationMode import kotlin.math.atan2 import kotlin.math.sqrt -class CameraDeviceDetails(val cameraManager: CameraManager, val cameraId: String) { - val characteristics = cameraManager.getCameraCharacteristics(cameraId) - val hardwareLevel = HardwareLevel.fromCameraCharacteristics(characteristics) - val capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) ?: IntArray(0) - val extensions = getSupportedExtensions() +@SuppressLint("InlinedApi") +class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId: String) { + val characteristics by lazy { cameraManager.getCameraCharacteristics(cameraId) } + val hardwareLevel by lazy { HardwareLevel.fromCameraCharacteristics(characteristics) } + val capabilities by lazy { characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) ?: IntArray(0) } + val extensions by lazy { getSupportedExtensions() } // device characteristics - val isMultiCam = capabilities.contains(11) // TODO: CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA - val supportsDepthCapture = capabilities.contains(8) // TODO: CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT - val supportsRawCapture = capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW) - val supportsLowLightBoost = extensions.contains(4) // TODO: CameraExtensionCharacteristics.EXTENSION_NIGHT - val lensFacing = LensFacing.fromCameraCharacteristics(characteristics) - val hasFlash = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false - val focalLengths = - characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) - // 35mm is the film standard sensor size - ?: floatArrayOf(35f) - val sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!! - val sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!! - val minFocusDistance = getMinFocusDistanceCm() - val name = ( - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - characteristics.get(CameraCharacteristics.INFO_VERSION) - } else { - null - } - ) ?: "$lensFacing ($cameraId)" + val isMultiCam by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA) } + val supportsDepthCapture by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT) } + val supportsRawCapture by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW) } + val supportsLowLightBoost by lazy { extensions.contains(CameraExtensionCharacteristics.EXTENSION_NIGHT) } + val lensFacing by lazy { LensFacing.fromCameraCharacteristics(characteristics) } + val hasFlash by lazy { characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false } + val focalLengths by lazy { + // 35mm is the film standard sensor size + characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(35f) + } + val sensorSize by lazy { characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!! } + val sensorOrientation by lazy { characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0 } + val minFocusDistance by lazy { getMinFocusDistanceCm() } + val name by lazy { + val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) characteristics.get(CameraCharacteristics.INFO_VERSION) else null + return@lazy info ?: "$lensFacing ($cameraId)" + } // "formats" (all possible configurations for this device) - val zoomRange = ( - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val maxDigitalZoom by lazy { characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) ?: 1f } + val zoomRange by lazy { + val range = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE) } else { null } - ) ?: Range(1f, characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) ?: 1f) - val physicalDevices = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && characteristics.physicalCameraIds.isNotEmpty()) { - characteristics.physicalCameraIds - } else { - setOf(cameraId) + return@lazy range ?: Range(1f, maxDigitalZoom) } - val minZoom = zoomRange.lower.toDouble() - val maxZoom = zoomRange.upper.toDouble() + val physicalDevices by lazy { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && characteristics.physicalCameraIds.isNotEmpty()) { + characteristics.physicalCameraIds + } else { + setOf(cameraId) + } + } + val minZoom by lazy { zoomRange.lower.toDouble() } + val maxZoom by lazy { zoomRange.upper.toDouble() } - val cameraConfig = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! - val isoRange = characteristics.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE) ?: Range(0, 0) - val exposureRange = characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE) ?: Range(0, 0) - val digitalStabilizationModes = + val cameraConfig by lazy { characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! } + val isoRange by lazy { characteristics.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE) ?: Range(0, 0) } + val exposureRange by lazy { characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE) ?: Range(0, 0) } + val digitalStabilizationModes by lazy { characteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES) ?: IntArray(0) - val opticalStabilizationModes = + } + val opticalStabilizationModes by lazy { characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION) ?: IntArray(0) - val supportsPhotoHdr = extensions.contains(3) // TODO: CameraExtensionCharacteristics.EXTENSION_HDR - val supportsVideoHdr = getHasVideoHdr() - val autoFocusSystem = getAutoFocusSystemMode() + } + val supportsPhotoHdr by lazy { extensions.contains(CameraExtensionCharacteristics.EXTENSION_HDR) } + val supportsVideoHdr by lazy { getHasVideoHdr() } + val autoFocusSystem by lazy { getAutoFocusSystemMode() } + val supportsYuvProcessing by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING) } + val supportsPrivateProcessing by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING) } + val supportsZsl by lazy { supportsYuvProcessing || supportsPrivateProcessing } + + val isBackwardsCompatible by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) } + val supportsSnapshotCapture by lazy { supportsSnapshotCapture() } + + // TODO: Also add 10-bit YUV here? val videoFormat = ImageFormat.YUV_420_888 // get extensions (HDR, Night Mode, ..) @@ -107,6 +121,14 @@ class CameraDeviceDetails(val cameraManager: CameraManager, val cameraId: String return 1.0 / distance * 100.0 } + @Suppress("RedundantIf") + private fun supportsSnapshotCapture(): Boolean { + // As per CameraDevice.TEMPLATE_VIDEO_SNAPSHOT in documentation: + if (hardwareLevel == HardwareLevel.LEGACY) return false + if (supportsDepthCapture && !isBackwardsCompatible) return false + return true + } + private fun createStabilizationModes(): ReadableArray { val array = Arguments.createArray() digitalStabilizationModes.forEach { videoStabilizationMode -> @@ -176,8 +198,6 @@ class CameraDeviceDetails(val cameraManager: CameraManager, val cameraId: String } } - // TODO: Add high-speed video ranges (high-fps / slow-motion) - return array } diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt index c6bb6c2..182477b 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt @@ -71,6 +71,8 @@ class CameraSessionCannotBeConfiguredError(cameraId: String) : CameraError("session", "cannot-create-session", "Failed to create a Camera Session for Camera #$cameraId!") class CameraDisconnectedError(cameraId: String, error: CameraDeviceError) : CameraError("session", "camera-has-been-disconnected", "The given Camera device (id: $cameraId) has been disconnected! Error: $error") +class NoOutputsError : + CameraError("session", "no-outputs", "Cannot create a CameraCaptureSession without any outputs! (PREVIEW, PHOTO, VIDEO, ...)") class PropRequiresFormatToBeNonNullError(propName: String) : CameraError("format", "format-required", "The prop \"$propName\" requires a format to be set, but format was null!") diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt index bdea193..12f4b77 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt @@ -5,49 +5,36 @@ import android.content.Context import android.content.pm.PackageManager import android.graphics.ImageFormat import android.graphics.Point -import android.hardware.camera2.CameraCaptureSession import android.hardware.camera2.CameraCharacteristics -import android.hardware.camera2.CameraDevice import android.hardware.camera2.CameraManager -import android.hardware.camera2.CameraMetadata -import android.hardware.camera2.CaptureRequest import android.hardware.camera2.CaptureResult import android.hardware.camera2.TotalCaptureResult -import android.hardware.camera2.params.MeteringRectangle import android.media.Image import android.media.ImageReader -import android.os.Build import android.util.Log -import android.util.Range import android.util.Size import android.view.Surface import android.view.SurfaceHolder import androidx.core.content.ContextCompat import com.google.mlkit.vision.barcode.common.Barcode +import com.mrousavy.camera.core.capture.RepeatingCaptureRequest import com.mrousavy.camera.core.outputs.BarcodeScannerOutput import com.mrousavy.camera.core.outputs.PhotoOutput import com.mrousavy.camera.core.outputs.SurfaceOutput import com.mrousavy.camera.core.outputs.VideoPipelineOutput -import com.mrousavy.camera.extensions.capture import com.mrousavy.camera.extensions.closestToOrMax -import com.mrousavy.camera.extensions.createCaptureSession -import com.mrousavy.camera.extensions.createPhotoCaptureRequest import com.mrousavy.camera.extensions.getPhotoSizes import com.mrousavy.camera.extensions.getPreviewTargetSize import com.mrousavy.camera.extensions.getVideoSizes -import com.mrousavy.camera.extensions.openCamera -import com.mrousavy.camera.extensions.setZoom import com.mrousavy.camera.frameprocessor.Frame import com.mrousavy.camera.types.Flash +import com.mrousavy.camera.types.LensFacing import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.QualityPrioritization import com.mrousavy.camera.types.RecordVideoOptions -import com.mrousavy.camera.types.Torch -import com.mrousavy.camera.types.VideoStabilizationMode import com.mrousavy.camera.utils.ImageFormatUtils import java.io.Closeable -import java.lang.IllegalStateException -import java.util.concurrent.CancellationException +import kotlin.coroutines.cancellation.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -55,8 +42,8 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock class CameraSession(private val context: Context, private val cameraManager: CameraManager, private val callback: Callback) : - CameraManager.AvailabilityCallback(), - Closeable { + Closeable, + PersistentCameraCaptureSession.Callback { companion object { private const val TAG = "CameraSession" } @@ -65,14 +52,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam private var configuration: CameraConfiguration? = null // Camera State - private var cameraDevice: CameraDevice? = null - set(value) { - field = value - cameraDeviceDetails = if (value != null) CameraDeviceDetails(cameraManager, value.id) else null - } - private var cameraDeviceDetails: CameraDeviceDetails? = null - private var captureSession: CameraCaptureSession? = null - private var previewRequest: CaptureRequest.Builder? = null + private val captureSession = PersistentCameraCaptureSession(cameraManager, this) private var photoOutput: PhotoOutput? = null private var videoOutput: VideoPipelineOutput? = null private var codeScannerOutput: BarcodeScannerOutput? = null @@ -109,14 +89,9 @@ class CameraSession(private val context: Context, private val cameraManager: Cam return Orientation.fromRotationDegrees(sensorRotation) } - init { - cameraManager.registerAvailabilityCallback(this, CameraQueues.cameraQueue.handler) - } - override fun close() { Log.i(TAG, "Closing CameraSession...") isDestroyed = true - cameraManager.unregisterAvailabilityCallback(this) runBlocking { mutex.withLock { destroy() @@ -126,18 +101,6 @@ class CameraSession(private val context: Context, private val cameraManager: Cam Log.i(TAG, "CameraSession closed!") } - override fun onCameraAvailable(cameraId: String) { - super.onCameraAvailable(cameraId) - if (this.configuration?.cameraId == cameraId && cameraDevice == null && configuration?.isActive == true) { - Log.i(TAG, "Camera #$cameraId is now available again, trying to re-open it now...") - coroutineScope.launch { - configure { - // re-open CameraDevice if needed - } - } - } - } - suspend fun configure(lambda: (configuration: CameraConfiguration) -> Unit) { Log.i(TAG, "configure { ... }: Waiting for lock...") @@ -146,6 +109,12 @@ class CameraSession(private val context: Context, private val cameraManager: Cam val config = CameraConfiguration.copyOf(this.configuration) lambda(config) val diff = CameraConfiguration.difference(this.configuration, config) + this.configuration = config + + if (!diff.hasChanges) { + Log.i(TAG, "Nothing changed, aborting configure { ... }") + return@withLock + } if (isDestroyed) { Log.i(TAG, "CameraSession is already destroyed. Skipping configure { ... }") @@ -155,29 +124,11 @@ class CameraSession(private val context: Context, private val cameraManager: Cam Log.i(TAG, "configure { ... }: Updating CameraSession Configuration... $diff") try { - val needsRebuild = cameraDevice == null || captureSession == null - if (needsRebuild) { - Log.i(TAG, "Need to rebuild CameraDevice and CameraCaptureSession...") - } - - // Since cameraDevice and captureSession are OS resources, we have three possible paths here: - if (needsRebuild) { - if (config.isActive) { - // A: The Camera has been torn down by the OS and we want it to be active - rebuild everything - Log.i(TAG, "Need to rebuild CameraDevice and CameraCaptureSession...") - configureCameraDevice(config) - configureOutputs(config) - configureCaptureRequest(config) - } else { - // B: The Camera has been torn down by the OS but it's currently in the background - ignore this - Log.i(TAG, "CameraDevice and CameraCaptureSession is torn down but Camera is not active, skipping update...") - } - } else { - // C: The Camera has not been torn down and we just want to update some props - update incrementally + captureSession.withConfiguration { // Build up session or update any props if (diff.deviceChanged) { // 1. cameraId changed, open device - configureCameraDevice(config) + configureInput(config) } if (diff.outputsChanged) { // 2. outputs changed, build new session @@ -187,10 +138,18 @@ class CameraSession(private val context: Context, private val cameraManager: Cam // 3. zoom etc changed, update repeating request configureCaptureRequest(config) } + if (diff.isActiveChanged) { + // 4. Either start or stop the session + val isActive = config.isActive && config.preview.isEnabled + captureSession.setIsActive(isActive) + } } - Log.i(TAG, "Successfully updated CameraSession Configuration! isActive: ${config.isActive}") - this.configuration = config + Log.i( + TAG, + "configure { ... }: Completed CameraSession Configuration! (isActive: ${config.isActive}, isRunning: ${captureSession.isRunning})" + ) + isRunning = captureSession.isRunning // Notify about Camera initialization if (diff.deviceChanged) { @@ -205,8 +164,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam private fun destroy() { Log.i(TAG, "Destroying session..") - cameraDevice?.close() - cameraDevice = null + captureSession.close() photoOutput?.close() photoOutput = null @@ -262,66 +220,20 @@ class CameraSession(private val context: Context, private val cameraManager: Cam Log.i(TAG, "Preview Output destroyed!") } - /** - * Set up the `CameraDevice` (`cameraId`) - */ - private suspend fun configureCameraDevice(configuration: CameraConfiguration) { - if (!configuration.isActive) { - // If isActive=false, we don't care if the device is opened or closed. - // Android OS can close the CameraDevice if it needs it, otherwise we keep it warm. - Log.i(TAG, "isActive is false, skipping CameraDevice configuration.") - return - } - - if (cameraDevice != null) { - // Close existing device - Log.i(TAG, "Closing previous Camera #${cameraDevice?.id}...") - cameraDevice?.close() - cameraDevice = null - } - isRunning = false - - // Check Camera Permission - val cameraPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) - if (cameraPermission != PackageManager.PERMISSION_GRANTED) throw CameraPermissionError() - - // Open new device + private fun configureInput(configuration: CameraConfiguration) { + Log.i(TAG, "Configuring inputs for CameraSession...") val cameraId = configuration.cameraId ?: throw NoCameraDeviceError() - Log.i(TAG, "Configuring Camera #$cameraId...") - cameraDevice = cameraManager.openCamera(cameraId, { device, error -> - if (cameraDevice != device) { - // a previous device has been disconnected, but we already have a new one. - // this is just normal behavior - return@openCamera - } - - this.cameraDevice = null - isRunning = false - - if (error != null) { - Log.e(TAG, "Camera #${device.id} has been unexpectedly disconnected!", error) - callback.onError(error) - } else { - Log.i(TAG, "Camera #${device.id} has been gracefully disconnected!") - } - }, CameraQueues.cameraQueue) - - Log.i(TAG, "Successfully configured Camera #$cameraId!") + val status = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) + if (status != PackageManager.PERMISSION_GRANTED) throw CameraPermissionError() + isRunning = false + captureSession.setInput(cameraId) } /** * Set up the `CaptureSession` with all outputs (preview, photo, video, codeScanner) and their HDR/Format settings. */ - private suspend fun configureOutputs(configuration: CameraConfiguration) { - if (!configuration.isActive) { - Log.i(TAG, "isActive is false, skipping CameraCaptureSession configuration.") - return - } - val cameraDevice = cameraDevice - if (cameraDevice == null) { - Log.i(TAG, "CameraSession hasn't configured a CameraDevice, skipping session configuration...") - return - } + private fun configureOutputs(configuration: CameraConfiguration) { + val cameraId = configuration.cameraId ?: throw NoCameraDeviceError() // Destroy previous outputs Log.i(TAG, "Destroying previous outputs...") @@ -333,10 +245,10 @@ class CameraSession(private val context: Context, private val cameraManager: Cam codeScannerOutput = null isRunning = false - val characteristics = cameraManager.getCameraCharacteristics(cameraDevice.id) + val characteristics = cameraManager.getCameraCharacteristics(cameraId) val format = configuration.format - Log.i(TAG, "Creating outputs for Camera #${cameraDevice.id}...") + Log.i(TAG, "Creating outputs for Camera #$cameraId...") val isSelfie = characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT @@ -366,7 +278,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam val video = configuration.video as? CameraConfiguration.Output.Enabled if (video != null) { val imageFormat = video.config.pixelFormat.toImageFormat() - val sizes = characteristics.getVideoSizes(cameraDevice.id, imageFormat) + val sizes = characteristics.getVideoSizes(cameraId, imageFormat) val size = sizes.closestToOrMax(format?.videoSize) Log.i(TAG, "Adding ${size.width}x${size.height} Video Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...") @@ -414,7 +326,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam } val imageFormat = ImageFormat.YUV_420_888 - val sizes = characteristics.getVideoSizes(cameraDevice.id, imageFormat) + val sizes = characteristics.getVideoSizes(cameraId, imageFormat) val size = sizes.closestToOrMax(Size(1280, 720)) Log.i(TAG, "Adding ${size.width}x${size.height} CodeScanner Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...") @@ -425,175 +337,63 @@ class CameraSession(private val context: Context, private val cameraManager: Cam } // Create session - captureSession = cameraDevice.createCaptureSession(cameraManager, outputs, { session -> - if (this.captureSession != session) { - // a previous session has been closed, but we already have a new one. - // this is just normal behavior - return@createCaptureSession - } + captureSession.setOutputs(outputs) - // onClosed - this.captureSession = null - isRunning = false - - Log.i(TAG, "Camera Session $session has been closed.") - }, CameraQueues.cameraQueue) - - Log.i(TAG, "Successfully configured Session with ${outputs.size} outputs for Camera #${cameraDevice.id}!") + Log.i(TAG, "Successfully configured Session with ${outputs.size} outputs for Camera #$cameraId!") // Update Frame Processor and RecordingSession for newly changed output updateVideoOutputs() } - private fun createRepeatingRequest(device: CameraDevice, targets: List, config: CameraConfiguration): CaptureRequest { - val deviceDetails = cameraDeviceDetails ?: CameraDeviceDetails(cameraManager, device.id) - - val template = if (config.video.isEnabled) CameraDevice.TEMPLATE_RECORD else CameraDevice.TEMPLATE_PREVIEW - val captureRequest = device.createCaptureRequest(template) - - targets.forEach { t -> captureRequest.addTarget(t) } - - val format = config.format - - // Set FPS - val fps = config.fps - if (fps != null) { - if (format == null) throw PropRequiresFormatToBeNonNullError("fps") - if (format.maxFps < fps) throw InvalidFpsError(fps) - captureRequest.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(fps, fps)) - } - - // Set Video Stabilization - if (config.videoStabilizationMode != VideoStabilizationMode.OFF) { - if (format == null) throw PropRequiresFormatToBeNonNullError("videoStabilizationMode") - if (!format.videoStabilizationModes.contains( - config.videoStabilizationMode - ) - ) { - throw InvalidVideoStabilizationMode(config.videoStabilizationMode) - } - } - when (config.videoStabilizationMode) { - VideoStabilizationMode.OFF -> { - // do nothing - } - VideoStabilizationMode.STANDARD -> { - val mode = if (Build.VERSION.SDK_INT >= - Build.VERSION_CODES.TIRAMISU - ) { - CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION - } else { - CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON - } - captureRequest.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, mode) - } - VideoStabilizationMode.CINEMATIC, VideoStabilizationMode.CINEMATIC_EXTENDED -> { - captureRequest.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON) - } - } - - // Set HDR - val video = config.video as? CameraConfiguration.Output.Enabled - val videoHdr = video?.config?.enableHdr - if (videoHdr == true) { - if (format == null) throw PropRequiresFormatToBeNonNullError("videoHdr") - if (!format.supportsVideoHdr) throw InvalidVideoHdrError() - captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_HDR) - } else if (config.enableLowLightBoost) { - if (!deviceDetails.supportsLowLightBoost) throw LowLightBoostNotSupportedError() - captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_NIGHT) - } - - // Set Exposure Bias - val exposure = config.exposure?.toInt() - if (exposure != null) { - val clamped = deviceDetails.exposureRange.clamp(exposure) - captureRequest.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, clamped) - } - - // Set Zoom - // TODO: Cache camera characteristics? Check perf. - val cameraCharacteristics = cameraManager.getCameraCharacteristics(device.id) - captureRequest.setZoom(config.zoom, cameraCharacteristics) - - // Set Torch - if (config.torch == Torch.ON) { - if (!deviceDetails.hasFlash) throw FlashUnavailableError() - captureRequest.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH) - } - - // Start repeating request if the Camera is active - return captureRequest.build() - } - private fun configureCaptureRequest(config: CameraConfiguration) { - val captureSession = captureSession + val video = config.video as? CameraConfiguration.Output.Enabled + val enableVideo = video != null + val enableVideoHdr = video?.config?.enableHdr == true - if (!config.isActive) { - isRunning = false - try { - captureSession?.stopRepeating() - } catch (e: IllegalStateException) { - // ignore - captureSession is already closed. - } - return - } - if (captureSession == null) { - Log.i(TAG, "CameraSession hasn't configured the capture session, skipping CaptureRequest...") - return - } - - val preview = config.preview as? CameraConfiguration.Output.Enabled - val previewSurface = preview?.config?.surface - val targets = listOfNotNull(previewSurface, videoOutput?.surface, codeScannerOutput?.surface) - if (targets.isEmpty()) { - Log.i(TAG, "CameraSession has no repeating outputs (Preview, Video, CodeScanner), skipping CaptureRequest...") - return - } - - val request = createRepeatingRequest(captureSession.device, targets, config) - captureSession.setRepeatingRequest(request, null, null) - isRunning = true + captureSession.setRepeatingRequest( + RepeatingCaptureRequest( + enableVideo, + config.torch, + config.fps, + config.videoStabilizationMode, + enableVideoHdr, + config.enableLowLightBoost, + config.exposure, + config.zoom, + config.format + ) + ) } suspend fun takePhoto( qualityPrioritization: QualityPrioritization, - flashMode: Flash, + flash: Flash, enableShutterSound: Boolean, enableRedEyeReduction: Boolean, enableAutoStabilization: Boolean, outputOrientation: Orientation ): CapturedPhoto { - val captureSession = captureSession ?: throw CameraNotReadyError() val photoOutput = photoOutput ?: throw PhotoNotEnabledError() - Log.i(TAG, "Photo capture 0/3 - preparing capture request (${photoOutput.size.width}x${photoOutput.size.height})...") - - val zoom = configuration?.zoom ?: 1f - - val cameraCharacteristics = cameraManager.getCameraCharacteristics(captureSession.device.id) - val orientation = outputOrientation.toSensorRelativeOrientation(cameraCharacteristics) - val captureRequest = captureSession.device.createPhotoCaptureRequest( - cameraManager, - photoOutput.surface, - zoom, + Log.i(TAG, "Photo capture 1/3 - capturing ${photoOutput.size.width}x${photoOutput.size.height} image...") + val result = captureSession.capture( qualityPrioritization, - flashMode, + flash, enableRedEyeReduction, enableAutoStabilization, photoOutput.enableHdr, - orientation + outputOrientation, + enableShutterSound ) - Log.i(TAG, "Photo capture 1/3 - starting capture...") - val result = captureSession.capture(captureRequest, enableShutterSound) - val timestamp = result[CaptureResult.SENSOR_TIMESTAMP]!! - Log.i(TAG, "Photo capture 2/3 complete - received metadata with timestamp $timestamp") + try { + val timestamp = result[CaptureResult.SENSOR_TIMESTAMP]!! + Log.i(TAG, "Photo capture 2/3 - waiting for image with timestamp $timestamp now...") val image = photoOutputSynchronizer.await(timestamp) - val isMirrored = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT - - Log.i(TAG, "Photo capture 3/3 complete - received ${image.width} x ${image.height} image.") + Log.i(TAG, "Photo capture 3/3 - received ${image.width} x ${image.height} image, preparing result...") + val deviceDetails = captureSession.getActiveDeviceDetails() + val isMirrored = deviceDetails?.lensFacing == LensFacing.FRONT return CapturedPhoto(image, result, orientation, isMirrored, image.format) } catch (e: CancellationException) { throw CaptureAbortedError(false) @@ -620,13 +420,13 @@ class CameraSession(private val context: Context, private val cameraManager: Cam mutex.withLock { if (recording != null) throw RecordingInProgressError() val videoOutput = videoOutput ?: throw VideoNotEnabledError() - val cameraDevice = cameraDevice ?: throw CameraNotReadyError() + val cameraId = configuration?.cameraId ?: throw NoCameraDeviceError() val fps = configuration?.fps ?: 30 val recording = RecordingSession( context, - cameraDevice.id, + cameraId, videoOutput.size, enableAudio, fps, @@ -664,41 +464,13 @@ class CameraSession(private val context: Context, private val cameraManager: Cam } } + override fun onError(error: Throwable) { + callback.onError(error) + } + suspend fun focus(x: Int, y: Int): Unit = throw NotImplementedError("focus() is not yet implemented!") - private suspend fun focus(point: Point) { - mutex.withLock { - // TODO: Fix this method - val captureSession = captureSession ?: throw CameraNotReadyError() - val request = previewRequest ?: throw CameraNotReadyError() - - val weight = MeteringRectangle.METERING_WEIGHT_MAX - 1 - val focusAreaTouch = MeteringRectangle(point, Size(150, 150), weight) - - // Quickly pause preview - captureSession.stopRepeating() - - request.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL) - request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF) - captureSession.capture(request.build(), null, null) - - // Add AF trigger with focus region - val characteristics = cameraManager.getCameraCharacteristics(captureSession.device.id) - val maxSupportedFocusRegions = characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE) ?: 0 - if (maxSupportedFocusRegions >= 1) { - request.set(CaptureRequest.CONTROL_AF_REGIONS, arrayOf(focusAreaTouch)) - } - request.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO) - request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO) - request.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START) - - captureSession.capture(request.build(), false) - - // Resume preview - request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE) - captureSession.setRepeatingRequest(request.build(), null, null) - } - } + private suspend fun focus(point: Point): Unit = throw NotImplementedError() data class CapturedPhoto( val image: Image, diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt new file mode 100644 index 0000000..510392a --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt @@ -0,0 +1,266 @@ +package com.mrousavy.camera.core + +import android.hardware.camera2.CameraAccessException +import android.hardware.camera2.CameraCaptureSession +import android.hardware.camera2.CameraDevice +import android.hardware.camera2.CameraManager +import android.hardware.camera2.TotalCaptureResult +import android.util.Log +import com.mrousavy.camera.core.capture.PhotoCaptureRequest +import com.mrousavy.camera.core.capture.RepeatingCaptureRequest +import com.mrousavy.camera.core.outputs.SurfaceOutput +import com.mrousavy.camera.extensions.capture +import com.mrousavy.camera.extensions.createCaptureSession +import com.mrousavy.camera.extensions.isValid +import com.mrousavy.camera.extensions.openCamera +import com.mrousavy.camera.extensions.tryAbortCaptures +import com.mrousavy.camera.types.Flash +import com.mrousavy.camera.types.Orientation +import com.mrousavy.camera.types.QualityPrioritization +import java.io.Closeable +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +/** + * A [CameraCaptureSession] wrapper that safely handles interruptions and remains open whenever available. + * + * This class aims to be similar to Apple's `AVCaptureSession`. + */ +class PersistentCameraCaptureSession(private val cameraManager: CameraManager, private val callback: Callback) : Closeable { + companion object { + private const val TAG = "PersistentCameraCaptureSession" + } + + // Inputs/Dependencies + private var cameraId: String? = null + private var outputs: List = emptyList() + private var repeatingRequest: RepeatingCaptureRequest? = null + private var isActive = false + + // State/Dependants + private var device: CameraDevice? = null // depends on [cameraId] + private var session: CameraCaptureSession? = null // depends on [device, surfaceOutputs] + private var cameraDeviceDetails: CameraDeviceDetails? = null // depends on [device] + + private val mutex = Mutex() + private var didDestroyFromOutside = false + + val isRunning: Boolean + get() = isActive && session != null && device != null && !didDestroyFromOutside + + override fun close() { + session?.tryAbortCaptures() + device?.close() + } + + private fun assertLocked(method: String) { + if (!mutex.isLocked) { + throw SessionIsNotLockedError("Failed to call $method, session is not locked! Call beginConfiguration() first.") + } + } + + suspend fun withConfiguration(block: suspend () -> Unit) { + mutex.withLock { + block() + configure() + } + } + + fun setInput(cameraId: String) { + Log.d(TAG, "--> setInput($cameraId)") + assertLocked("setInput") + if (this.cameraId != cameraId || device?.id != cameraId) { + this.cameraId = cameraId + + // Abort any captures in the session so we get the onCaptureFailed handler for any outstanding photos + session?.tryAbortCaptures() + session = null + // Closing the device will also close the session above - even faster than manually closing it. + device?.close() + device = null + } + } + + fun setOutputs(outputs: List) { + Log.d(TAG, "--> setOutputs($outputs)") + assertLocked("setOutputs") + if (this.outputs != outputs) { + this.outputs = outputs + + if (outputs.isNotEmpty()) { + // Outputs have changed to something else, we don't wanna destroy the session directly + // so the outputs can be kept warm. The session that gets created next will take over the outputs. + session?.tryAbortCaptures() + } else { + // Just stop it, we don't have any outputs + session?.close() + } + session = null + } + } + + fun setRepeatingRequest(request: RepeatingCaptureRequest) { + assertLocked("setRepeatingRequest") + Log.d(TAG, "--> setRepeatingRequest(...)") + if (this.repeatingRequest != request) { + this.repeatingRequest = request + } + } + + fun setIsActive(isActive: Boolean) { + assertLocked("setIsActive") + Log.d(TAG, "--> setIsActive($isActive)") + if (this.isActive != isActive) { + this.isActive = isActive + } + if (isActive && didDestroyFromOutside) { + didDestroyFromOutside = false + } + } + + suspend fun capture( + qualityPrioritization: QualityPrioritization, + flash: Flash, + enableRedEyeReduction: Boolean, + enableAutoStabilization: Boolean, + enablePhotoHdr: Boolean, + orientation: Orientation, + enableShutterSound: Boolean + ): TotalCaptureResult { + mutex.withLock { + val session = session ?: throw CameraNotReadyError() + val repeatingRequest = repeatingRequest ?: throw CameraNotReadyError() + val photoRequest = PhotoCaptureRequest( + repeatingRequest, + qualityPrioritization, + flash, + enableRedEyeReduction, + enableAutoStabilization, + enablePhotoHdr, + orientation + ) + val device = session.device + val deviceDetails = getOrCreateCameraDeviceDetails(device) + + // Submit a single high-res capture to photo output as well as all preview outputs + val outputs = outputs + val request = photoRequest.createCaptureRequest(device, deviceDetails, outputs) + return session.capture(request.build(), enableShutterSound) + } + } + + fun getActiveDeviceDetails(): CameraDeviceDetails? { + val device = device ?: return null + return getOrCreateCameraDeviceDetails(device) + } + + private suspend fun configure() { + if (didDestroyFromOutside && !isActive) { + Log.d(TAG, "CameraCaptureSession has been destroyed by Android, skipping configuration until isActive is set to `true` again.") + return + } + Log.d(TAG, "Configure() with isActive: $isActive, ID: $cameraId, device: $device, session: $session") + val cameraId = cameraId ?: throw NoCameraDeviceError() + val repeatingRequest = repeatingRequest ?: throw CameraNotReadyError() + val outputs = outputs + + try { + didDestroyFromOutside = false + + val device = getOrCreateDevice(cameraId) + if (didDestroyFromOutside) return + + if (outputs.isEmpty()) return + val session = getOrCreateSession(device, outputs) + if (didDestroyFromOutside) return + + if (isActive) { + Log.d(TAG, "Updating repeating request...") + val details = getOrCreateCameraDeviceDetails(device) + val repeatingOutputs = outputs.filter { it.isRepeating } + val builder = repeatingRequest.createCaptureRequest(device, details, repeatingOutputs) + session.setRepeatingRequest(builder.build(), null, null) + } else { + session.stopRepeating() + Log.d(TAG, "Stopping repeating request...") + } + Log.d(TAG, "Configure() done! isActive: $isActive, ID: $cameraId, device: $device, session: $session") + } catch (e: CameraAccessException) { + if (didDestroyFromOutside) { + // Camera device has been destroyed in the meantime, that's fine. + Log.d(TAG, "Configure() canceled, session has been destroyed in the meantime!") + } else { + // Camera should still be active, so not sure what went wrong. Rethrow + throw e + } + } + } + + private suspend fun getOrCreateDevice(cameraId: String): CameraDevice { + val currentDevice = device + if (currentDevice?.id == cameraId && currentDevice.isValid) { + return currentDevice + } + + this.session?.tryAbortCaptures() + this.device?.close() + this.device = null + this.session = null + + Log.i(TAG, "Creating new device...") + val newDevice = cameraManager.openCamera(cameraId, { device, error -> + Log.i(TAG, "Camera $device closed!") + if (this.device == device) { + this.didDestroyFromOutside = true + this.session?.tryAbortCaptures() + this.session = null + this.device = null + this.isActive = false + } + if (error != null) { + callback.onError(error) + } + }, CameraQueues.videoQueue) + this.device = newDevice + return newDevice + } + + private suspend fun getOrCreateSession(device: CameraDevice, outputs: List): CameraCaptureSession { + val currentSession = session + if (currentSession?.device == device) { + return currentSession + } + + if (outputs.isEmpty()) throw NoOutputsError() + + Log.i(TAG, "Creating new session...") + val newSession = device.createCaptureSession(cameraManager, outputs, { session -> + Log.i(TAG, "Session $session closed!") + if (this.session == session) { + this.didDestroyFromOutside = true + this.session?.tryAbortCaptures() + this.session = null + this.isActive = false + } + }, CameraQueues.videoQueue) + session = newSession + return newSession + } + + private fun getOrCreateCameraDeviceDetails(device: CameraDevice): CameraDeviceDetails { + val currentDetails = cameraDeviceDetails + if (currentDetails?.cameraId == device.id) { + return currentDetails + } + + val newDetails = CameraDeviceDetails(cameraManager, device.id) + cameraDeviceDetails = newDetails + return newDetails + } + + interface Callback { + fun onError(error: Throwable) + } + + class SessionIsNotLockedError(message: String) : Error(message) +} diff --git a/package/android/src/main/java/com/mrousavy/camera/core/VideoPipeline.kt b/package/android/src/main/java/com/mrousavy/camera/core/VideoPipeline.kt index f6df554..f864642 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/VideoPipeline.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/VideoPipeline.kt @@ -125,8 +125,11 @@ class VideoPipeline( isActive = false imageWriter?.close() imageReader?.close() + removeRecordingSessionOutputSurface() recordingSession = null + surfaceTexture.setOnFrameAvailableListener(null, null) surfaceTexture.release() + surface.release() } } @@ -170,7 +173,7 @@ class VideoPipeline( synchronized(this) { if (recordingSession != null) { // Configure OpenGL pipeline to stream Frames into the Recording Session's surface - Log.i(TAG, "Setting $width x $height RecordingSession Output...") + Log.i(TAG, "Setting ${recordingSession.size} RecordingSession Output...") setRecordingSessionOutputSurface(recordingSession.surface) this.recordingSession = recordingSession } else { diff --git a/package/android/src/main/java/com/mrousavy/camera/core/capture/CameraCaptureRequest.kt b/package/android/src/main/java/com/mrousavy/camera/core/capture/CameraCaptureRequest.kt new file mode 100644 index 0000000..5a73913 --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/core/capture/CameraCaptureRequest.kt @@ -0,0 +1,86 @@ +package com.mrousavy.camera.core.capture + +import android.hardware.camera2.CameraDevice +import android.hardware.camera2.CaptureRequest +import com.mrousavy.camera.core.CameraDeviceDetails +import com.mrousavy.camera.core.FlashUnavailableError +import com.mrousavy.camera.core.InvalidVideoHdrError +import com.mrousavy.camera.core.LowLightBoostNotSupportedError +import com.mrousavy.camera.core.PropRequiresFormatToBeNonNullError +import com.mrousavy.camera.core.outputs.SurfaceOutput +import com.mrousavy.camera.extensions.setZoom +import com.mrousavy.camera.types.CameraDeviceFormat +import com.mrousavy.camera.types.Torch + +abstract class CameraCaptureRequest( + private val torch: Torch = Torch.OFF, + private val enableVideoHdr: Boolean = false, + val enableLowLightBoost: Boolean = false, + val exposureBias: Double? = null, + val zoom: Float = 1.0f, + val format: CameraDeviceFormat? = null +) { + enum class Template { + RECORD, + PHOTO, + PHOTO_ZSL, + PHOTO_SNAPSHOT, + PREVIEW; + + fun toRequestTemplate(): Int = + when (this) { + RECORD -> CameraDevice.TEMPLATE_RECORD + PHOTO -> CameraDevice.TEMPLATE_STILL_CAPTURE + PHOTO_ZSL -> CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG + PHOTO_SNAPSHOT -> CameraDevice.TEMPLATE_VIDEO_SNAPSHOT + PREVIEW -> CameraDevice.TEMPLATE_PREVIEW + } + } + + abstract fun createCaptureRequest( + device: CameraDevice, + deviceDetails: CameraDeviceDetails, + outputs: List + ): CaptureRequest.Builder + + protected open fun createCaptureRequest( + template: Template, + device: CameraDevice, + deviceDetails: CameraDeviceDetails, + outputs: List + ): CaptureRequest.Builder { + val builder = device.createCaptureRequest(template.toRequestTemplate()) + + // Add all repeating output surfaces + outputs.forEach { output -> + builder.addTarget(output.surface) + } + + // Set HDR + if (enableVideoHdr) { + if (format == null) throw PropRequiresFormatToBeNonNullError("videoHdr") + if (!format.supportsVideoHdr) throw InvalidVideoHdrError() + builder.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_HDR) + } else if (enableLowLightBoost) { + if (!deviceDetails.supportsLowLightBoost) throw LowLightBoostNotSupportedError() + builder.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_NIGHT) + } + + // Set Exposure Bias + if (exposureBias != null) { + val clamped = deviceDetails.exposureRange.clamp(exposureBias.toInt()) + builder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, clamped) + } + + // Set Zoom + builder.setZoom(zoom, deviceDetails.characteristics) + + // Set Torch + if (torch == Torch.ON) { + if (!deviceDetails.hasFlash) throw FlashUnavailableError() + builder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH) + } + + return builder + } +} diff --git a/package/android/src/main/java/com/mrousavy/camera/core/capture/PhotoCaptureRequest.kt b/package/android/src/main/java/com/mrousavy/camera/core/capture/PhotoCaptureRequest.kt new file mode 100644 index 0000000..82b8dc0 --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/core/capture/PhotoCaptureRequest.kt @@ -0,0 +1,113 @@ +package com.mrousavy.camera.core.capture + +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraDevice +import android.hardware.camera2.CaptureRequest +import android.util.Log +import com.mrousavy.camera.core.CameraDeviceDetails +import com.mrousavy.camera.core.outputs.SurfaceOutput +import com.mrousavy.camera.types.Flash +import com.mrousavy.camera.types.Orientation +import com.mrousavy.camera.types.QualityPrioritization +import com.mrousavy.camera.types.Torch + +class PhotoCaptureRequest( + repeatingRequest: RepeatingCaptureRequest, + private val qualityPrioritization: QualityPrioritization, + private val flash: Flash, + private val enableRedEyeReduction: Boolean, + private val enableAutoStabilization: Boolean, + enablePhotoHdr: Boolean, + private val outputOrientation: Orientation +) : CameraCaptureRequest( + Torch.OFF, + enablePhotoHdr, + repeatingRequest.enableLowLightBoost, + repeatingRequest.exposureBias, + repeatingRequest.zoom, + repeatingRequest.format +) { + companion object { + private const val TAG = "PhotoCaptureRequest" + } + + override fun createCaptureRequest( + device: CameraDevice, + deviceDetails: CameraDeviceDetails, + outputs: List + ): CaptureRequest.Builder { + val template = when (qualityPrioritization) { + QualityPrioritization.QUALITY -> Template.PHOTO + QualityPrioritization.BALANCED -> { + if (deviceDetails.supportsZsl) { + Template.PHOTO_ZSL + } else { + Template.PHOTO + } + } + QualityPrioritization.SPEED -> { + if (deviceDetails.supportsSnapshotCapture) { + Template.PHOTO_SNAPSHOT + } else if (deviceDetails.supportsZsl) { + Template.PHOTO_ZSL + } else { + Template.PHOTO + } + } + } + Log.i(TAG, "Using CaptureRequest Template $template...") + return this.createCaptureRequest(template, device, deviceDetails, outputs) + } + + override fun createCaptureRequest( + template: Template, + device: CameraDevice, + deviceDetails: CameraDeviceDetails, + outputs: List + ): CaptureRequest.Builder { + val builder = super.createCaptureRequest(template, device, deviceDetails, outputs) + + // Set JPEG quality + val jpegQuality = when (qualityPrioritization) { + QualityPrioritization.SPEED -> 85 + QualityPrioritization.BALANCED -> 92 + QualityPrioritization.QUALITY -> 100 + } + builder.set(CaptureRequest.JPEG_QUALITY, jpegQuality.toByte()) + + // Set JPEG Orientation + val targetOrientation = outputOrientation.toSensorRelativeOrientation(deviceDetails) + builder.set(CaptureRequest.JPEG_ORIENTATION, targetOrientation.toDegrees()) + + // TODO: Fix flash. + when (flash) { + // Set the Flash Mode + Flash.OFF -> { + builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON) + builder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF) + } + Flash.ON -> { + builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON) + builder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH) + } + Flash.AUTO -> { + if (enableRedEyeReduction) { + builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE) + } else { + builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH) + } + } + } + + // Set stabilization for this Frame + if (enableAutoStabilization) { + if (deviceDetails.opticalStabilizationModes.contains(CameraCharacteristics.LENS_OPTICAL_STABILIZATION_MODE_ON)) { + builder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CameraCharacteristics.LENS_OPTICAL_STABILIZATION_MODE_ON) + } else if (deviceDetails.digitalStabilizationModes.contains(CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_ON)) { + builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON) + } + } + + return builder + } +} diff --git a/package/android/src/main/java/com/mrousavy/camera/core/capture/RepeatingCaptureRequest.kt b/package/android/src/main/java/com/mrousavy/camera/core/capture/RepeatingCaptureRequest.kt new file mode 100644 index 0000000..3e790bb --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/core/capture/RepeatingCaptureRequest.kt @@ -0,0 +1,79 @@ +package com.mrousavy.camera.core.capture + +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraDevice +import android.hardware.camera2.CaptureRequest +import android.util.Range +import com.mrousavy.camera.core.CameraDeviceDetails +import com.mrousavy.camera.core.InvalidFpsError +import com.mrousavy.camera.core.InvalidVideoStabilizationMode +import com.mrousavy.camera.core.PropRequiresFormatToBeNonNullError +import com.mrousavy.camera.core.outputs.SurfaceOutput +import com.mrousavy.camera.types.CameraDeviceFormat +import com.mrousavy.camera.types.Torch +import com.mrousavy.camera.types.VideoStabilizationMode + +class RepeatingCaptureRequest( + private val enableVideoPipeline: Boolean, + torch: Torch = Torch.OFF, + private val fps: Int? = null, + private val videoStabilizationMode: VideoStabilizationMode = VideoStabilizationMode.OFF, + enableVideoHdr: Boolean = false, + enableLowLightBoost: Boolean = false, + exposureBias: Double? = null, + zoom: Float = 1.0f, + format: CameraDeviceFormat? = null +) : CameraCaptureRequest(torch, enableVideoHdr, enableLowLightBoost, exposureBias, zoom, format) { + override fun createCaptureRequest( + device: CameraDevice, + deviceDetails: CameraDeviceDetails, + outputs: List + ): CaptureRequest.Builder { + val template = if (enableVideoPipeline) Template.RECORD else Template.PREVIEW + return this.createCaptureRequest(template, device, deviceDetails, outputs) + } + + private fun getBestDigitalStabilizationMode(deviceDetails: CameraDeviceDetails): Int { + if (deviceDetails.digitalStabilizationModes.contains(CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION)) { + return CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION + } + return CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_ON + } + + override fun createCaptureRequest( + template: Template, + device: CameraDevice, + deviceDetails: CameraDeviceDetails, + outputs: List + ): CaptureRequest.Builder { + val builder = super.createCaptureRequest(template, device, deviceDetails, outputs) + + // Set FPS + if (fps != null) { + if (format == null) throw PropRequiresFormatToBeNonNullError("fps") + if (format.maxFps < fps) throw InvalidFpsError(fps) + builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(fps, fps)) + } + + // Set Video Stabilization + if (videoStabilizationMode != VideoStabilizationMode.OFF) { + if (format == null) throw PropRequiresFormatToBeNonNullError("videoStabilizationMode") + if (!format.videoStabilizationModes.contains(videoStabilizationMode)) { + throw InvalidVideoStabilizationMode(videoStabilizationMode) + } + } + when (videoStabilizationMode) { + VideoStabilizationMode.OFF -> { + // do nothing + } + VideoStabilizationMode.STANDARD -> { + builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, getBestDigitalStabilizationMode(deviceDetails)) + } + VideoStabilizationMode.CINEMATIC, VideoStabilizationMode.CINEMATIC_EXTENDED -> { + builder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON) + } + } + + return builder + } +} diff --git a/package/android/src/main/java/com/mrousavy/camera/core/outputs/SurfaceOutput.kt b/package/android/src/main/java/com/mrousavy/camera/core/outputs/SurfaceOutput.kt index 5ca1b15..6948a60 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/outputs/SurfaceOutput.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/outputs/SurfaceOutput.kt @@ -10,13 +10,7 @@ import android.view.Surface import androidx.annotation.RequiresApi import java.io.Closeable -open class SurfaceOutput( - val surface: Surface, - val size: Size, - val outputType: OutputType, - val enableHdr: Boolean = false, - private val closeSurfaceOnEnd: Boolean = false -) : Closeable { +open class SurfaceOutput(val surface: Surface, val size: Size, val outputType: OutputType, val enableHdr: Boolean = false) : Closeable { companion object { const val TAG = "SurfaceOutput" @@ -52,12 +46,18 @@ open class SurfaceOutput( return result } + val isRepeating: Boolean + get() { + return when (outputType) { + OutputType.VIDEO, OutputType.PREVIEW, OutputType.VIDEO_AND_PREVIEW -> true + OutputType.PHOTO -> false + } + } + override fun toString(): String = "$outputType (${size.width} x ${size.height})" override fun close() { - if (closeSurfaceOnEnd) { - surface.release() - } + // close() does nothing by default } enum class OutputType { diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+tryAbortCaptures.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+tryAbortCaptures.kt new file mode 100644 index 0000000..24c64ce --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+tryAbortCaptures.kt @@ -0,0 +1,9 @@ +package com.mrousavy.camera.extensions + +import android.hardware.camera2.CameraCaptureSession + +fun CameraCaptureSession.tryAbortCaptures() { + try { + abortCaptures() + } catch (_: Throwable) {} +} diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraDevice+createPhotoCaptureRequest.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraDevice+createPhotoCaptureRequest.kt deleted file mode 100644 index 0c425a8..0000000 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraDevice+createPhotoCaptureRequest.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.mrousavy.camera.extensions - -import android.hardware.camera2.CameraCharacteristics -import android.hardware.camera2.CameraDevice -import android.hardware.camera2.CameraManager -import android.hardware.camera2.CaptureRequest -import android.view.Surface -import com.mrousavy.camera.types.Flash -import com.mrousavy.camera.types.Orientation -import com.mrousavy.camera.types.QualityPrioritization - -private fun supportsSnapshotCapture(cameraCharacteristics: CameraCharacteristics): Boolean { - // As per CameraDevice.TEMPLATE_VIDEO_SNAPSHOT in documentation: - val hardwareLevel = cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)!! - if (hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) return false - - val capabilities = cameraCharacteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)!! - val hasDepth = capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT) - val isBackwardsCompatible = !capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) - if (hasDepth && !isBackwardsCompatible) return false - - return true -} - -fun CameraDevice.createPhotoCaptureRequest( - cameraManager: CameraManager, - surface: Surface, - zoom: Float, - qualityPrioritization: QualityPrioritization, - flashMode: Flash, - enableRedEyeReduction: Boolean, - enableAutoStabilization: Boolean, - enableHdr: Boolean, - orientation: Orientation -): CaptureRequest { - val cameraCharacteristics = cameraManager.getCameraCharacteristics(this.id) - - val template = if (qualityPrioritization == QualityPrioritization.SPEED && supportsSnapshotCapture(cameraCharacteristics)) { - CameraDevice.TEMPLATE_VIDEO_SNAPSHOT - } else { - CameraDevice.TEMPLATE_STILL_CAPTURE - } - val captureRequest = this.createCaptureRequest(template) - captureRequest.addTarget(surface) - - // TODO: Maybe we can even expose that prop directly? - val jpegQuality = when (qualityPrioritization) { - QualityPrioritization.SPEED -> 85 - QualityPrioritization.BALANCED -> 92 - QualityPrioritization.QUALITY -> 100 - } - captureRequest.set(CaptureRequest.JPEG_QUALITY, jpegQuality.toByte()) - - captureRequest.set(CaptureRequest.JPEG_ORIENTATION, orientation.toDegrees()) - - // TODO: Use the same options as from the preview request. This is duplicate code! - - when (flashMode) { - // Set the Flash Mode - Flash.OFF -> { - captureRequest.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON) - captureRequest.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF) - } - Flash.ON -> { - captureRequest.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON) - captureRequest.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH) - } - Flash.AUTO -> { - if (enableRedEyeReduction) { - captureRequest.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE) - } else { - captureRequest.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH) - } - } - } - - if (enableAutoStabilization) { - // Enable optical or digital image stabilization - val digitalStabilization = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES) - val hasDigitalStabilization = digitalStabilization?.contains(CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_ON) ?: false - - val opticalStabilization = cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION) - val hasOpticalStabilization = opticalStabilization?.contains(CameraCharacteristics.LENS_OPTICAL_STABILIZATION_MODE_ON) ?: false - if (hasOpticalStabilization) { - captureRequest.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_OFF) - captureRequest.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON) - } else if (hasDigitalStabilization) { - captureRequest.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON) - } else { - // no stabilization is supported. ignore it - } - } - - // TODO: Check if that zoom value is even supported. - captureRequest.setZoom(zoom, cameraCharacteristics) - - // Set HDR - // TODO: Check if that value is even supported - if (enableHdr) { - captureRequest.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_HDR) - } - - return captureRequest.build() -} diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraDevice+isValid.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraDevice+isValid.kt new file mode 100644 index 0000000..4a991f4 --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraDevice+isValid.kt @@ -0,0 +1,13 @@ +package com.mrousavy.camera.extensions + +import android.hardware.camera2.CameraDevice + +val CameraDevice.isValid: Boolean + get() { + try { + this.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) + return true + } catch (e: Throwable) { + return false + } + } diff --git a/package/android/src/main/java/com/mrousavy/camera/types/Orientation.kt b/package/android/src/main/java/com/mrousavy/camera/types/Orientation.kt index 926941b..c228ba0 100644 --- a/package/android/src/main/java/com/mrousavy/camera/types/Orientation.kt +++ b/package/android/src/main/java/com/mrousavy/camera/types/Orientation.kt @@ -1,6 +1,6 @@ package com.mrousavy.camera.types -import android.hardware.camera2.CameraCharacteristics +import com.mrousavy.camera.core.CameraDeviceDetails enum class Orientation(override val unionValue: String) : JSUnionValue { PORTRAIT("portrait"), @@ -16,18 +16,17 @@ enum class Orientation(override val unionValue: String) : JSUnionValue { LANDSCAPE_LEFT -> 270 } - fun toSensorRelativeOrientation(cameraCharacteristics: CameraCharacteristics): Orientation { - val sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!! - + fun toSensorRelativeOrientation(deviceDetails: CameraDeviceDetails): Orientation { // Convert target orientation to rotation degrees (0, 90, 180, 270) var rotationDegrees = this.toDegrees() // Reverse device orientation for front-facing cameras - val facingFront = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT - if (facingFront) rotationDegrees = -rotationDegrees + if (deviceDetails.lensFacing == LensFacing.FRONT) { + rotationDegrees = -rotationDegrees + } // Rotate sensor rotation by target rotation - val newRotationDegrees = (sensorOrientation + rotationDegrees + 360) % 360 + val newRotationDegrees = (deviceDetails.sensorOrientation + rotationDegrees + 360) % 360 return fromRotationDegrees(newRotationDegrees) } diff --git a/package/android/src/main/java/com/mrousavy/camera/types/VideoStabilizationMode.kt b/package/android/src/main/java/com/mrousavy/camera/types/VideoStabilizationMode.kt index 7d1ff25..87de458 100644 --- a/package/android/src/main/java/com/mrousavy/camera/types/VideoStabilizationMode.kt +++ b/package/android/src/main/java/com/mrousavy/camera/types/VideoStabilizationMode.kt @@ -13,21 +13,6 @@ enum class VideoStabilizationMode(override val unionValue: String) : JSUnionValu CINEMATIC("cinematic"), CINEMATIC_EXTENDED("cinematic-extended"); - fun toDigitalStabilizationMode(): Int = - when (this) { - OFF -> CONTROL_VIDEO_STABILIZATION_MODE_OFF - STANDARD -> CONTROL_VIDEO_STABILIZATION_MODE_ON - CINEMATIC -> 2 // TODO: CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION - else -> CONTROL_VIDEO_STABILIZATION_MODE_OFF - } - - fun toOpticalStabilizationMode(): Int = - when (this) { - OFF -> LENS_OPTICAL_STABILIZATION_MODE_OFF - CINEMATIC_EXTENDED -> LENS_OPTICAL_STABILIZATION_MODE_ON - else -> LENS_OPTICAL_STABILIZATION_MODE_OFF - } - companion object : JSUnionValue.Companion { override fun fromUnionValue(unionValue: String?): VideoStabilizationMode = when (unionValue) { diff --git a/package/example/ios/Podfile.lock b/package/example/ios/Podfile.lock index c6e827a..dc284e6 100644 --- a/package/example/ios/Podfile.lock +++ b/package/example/ios/Podfile.lock @@ -484,7 +484,7 @@ PODS: - libwebp (~> 1.0) - SDWebImage/Core (~> 5.10) - SocketRocket (0.6.1) - - VisionCamera (3.8.2): + - VisionCamera (3.9.0-beta.0): - React - React-callinvoker - React-Core @@ -724,7 +724,7 @@ SPEC CHECKSUMS: SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - VisionCamera: edbcd00e27a438b2228f67823e2b8d15a189065f + VisionCamera: f2f2fa58be438670ef5d5aa88846ffe59a78f7a8 Yoga: 4c3aa327e4a6a23eeacd71f61c81df1bcdf677d5 PODFILE CHECKSUM: 27f53791141a3303d814e09b55770336416ff4eb diff --git a/package/example/src/CameraPage.tsx b/package/example/src/CameraPage.tsx index c928b2b..0cf99a6 100644 --- a/package/example/src/CameraPage.tsx +++ b/package/example/src/CameraPage.tsx @@ -172,17 +172,19 @@ export function CameraPage({ navigation }: Props): React.ReactElement { 'Camera started!'} + onStopped={() => 'Camera stopped!'} format={format} fps={fps} photoHdr={enableHdr} videoHdr={enableHdr} lowLightBoost={device.supportsLowLightBoost && enableNightMode} - isActive={isActive} - onInitialized={onInitialized} - onError={onError} enableZoomGesture={false} animatedProps={cameraAnimatedProps} exposure={0} diff --git a/package/src/CameraError.ts b/package/src/CameraError.ts index baa02ce..e39beb5 100644 --- a/package/src/CameraError.ts +++ b/package/src/CameraError.ts @@ -25,6 +25,7 @@ export type SessionError = | 'session/camera-cannot-be-opened' | 'session/camera-has-been-disconnected' | 'session/audio-in-use-by-other-app' + | 'session/no-outputs' | 'session/audio-session-failed-to-activate' export type CodeScannerError = | 'code-scanner/not-compatible-with-outputs' From dbb7f80dc94ab725f0e660fca51102b6267ffe44 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 6 Feb 2024 14:23:57 +0100 Subject: [PATCH 09/71] chore: release 3.9.0-beta.1 --- package/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/package.json b/package/package.json index 133a504..15fea91 100644 --- a/package/package.json +++ b/package/package.json @@ -1,6 +1,6 @@ { "name": "react-native-vision-camera", - "version": "3.9.0-beta.0", + "version": "3.9.0-beta.1", "description": "A powerful, high-performance React Native Camera library.", "main": "lib/commonjs/index", "module": "lib/module/index", From 3b892c209e83d5f405312ff6a40133be91136c66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 14:32:48 +0100 Subject: [PATCH 10/71] chore(deps): bump actions/cache from 3 to 4 (#2510) Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-ios.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml index 9c4d499..8067991 100644 --- a/.github/workflows/build-ios.yml +++ b/.github/workflows/build-ios.yml @@ -54,7 +54,7 @@ jobs: working-directory: package/example/ios - name: Restore Pods cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: package/example/ios/Pods key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} @@ -113,7 +113,7 @@ jobs: working-directory: package/example/ios - name: Restore Pods cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: package/example/ios/Pods key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} From 395ee7af89589263c4a570786131390d314db28b Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 6 Feb 2024 18:40:52 +0100 Subject: [PATCH 11/71] fix: Fix `zoom` not resetting in example app --- package/example/src/CameraPage.tsx | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/package/example/src/CameraPage.tsx b/package/example/src/CameraPage.tsx index 0cf99a6..bef7fcf 100644 --- a/package/example/src/CameraPage.tsx +++ b/package/example/src/CameraPage.tsx @@ -2,7 +2,15 @@ import * as React from 'react' import { useRef, useState, useCallback, useMemo } from 'react' import { StyleSheet, Text, View } from 'react-native' import { PinchGestureHandler, PinchGestureHandlerGestureEvent, TapGestureHandler } from 'react-native-gesture-handler' -import { CameraRuntimeError, PhotoFile, useCameraDevice, useCameraFormat, useFrameProcessor, VideoFile } from 'react-native-vision-camera' +import { + CameraProps, + CameraRuntimeError, + PhotoFile, + useCameraDevice, + useCameraFormat, + useFrameProcessor, + VideoFile, +} from 'react-native-vision-camera' import { Camera } from 'react-native-vision-camera' import { CONTENT_SPACING, CONTROL_BUTTON_SIZE, MAX_ZOOM_FACTOR, SAFE_AREA_PADDING, SCREEN_HEIGHT, SCREEN_WIDTH } from './Constants' import Reanimated, { Extrapolate, interpolate, useAnimatedGestureHandler, useAnimatedProps, useSharedValue } from 'react-native-reanimated' @@ -32,7 +40,7 @@ export function CameraPage({ navigation }: Props): React.ReactElement { const camera = useRef(null) const [isCameraInitialized, setIsCameraInitialized] = useState(false) const hasMicrophonePermission = useMemo(() => Camera.getMicrophonePermissionStatus() === 'granted', []) - const zoom = useSharedValue(0) + const zoom = useSharedValue(1) const isPressingButton = useSharedValue(false) // check if camera page is active @@ -73,12 +81,10 @@ export function CameraPage({ navigation }: Props): React.ReactElement { const canToggleNightMode = device?.supportsLowLightBoost ?? false //#region Animated Zoom - // This just maps the zoom factor to a percentage value. - // so e.g. for [min, neutr., max] values [1, 2, 128] this would result in [0, 0.0081, 1] const minZoom = device?.minZoom ?? 1 const maxZoom = Math.min(device?.maxZoom ?? 1, MAX_ZOOM_FACTOR) - const cameraAnimatedProps = useAnimatedProps(() => { + const cameraAnimatedProps = useAnimatedProps(() => { const z = Math.max(Math.min(zoom.value, maxZoom), minZoom) return { zoom: z, @@ -126,11 +132,10 @@ export function CameraPage({ navigation }: Props): React.ReactElement { //#endregion //#region Effects - const neutralZoom = device?.neutralZoom ?? 1 useEffect(() => { - // Run everytime the neutralZoomScaled value changes. (reset zoom when device changes) - zoom.value = neutralZoom - }, [neutralZoom, zoom]) + // Reset zoom to it's default everytime the `device` changes. + zoom.value = device?.neutralZoom ?? 1 + }, [zoom, device]) //#endregion //#region Pinch to Zoom Gesture From b20d0fc5f7c3c484944a28f41698c776137ac38b Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 7 Feb 2024 10:52:07 +0100 Subject: [PATCH 12/71] chore: Update npm keywords --- package/package.json | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/package/package.json b/package/package.json index 15fea91..53d3050 100644 --- a/package/package.json +++ b/package/package.json @@ -49,26 +49,33 @@ "postpack": "rm ./README.md" }, "keywords": [ - "react-native", - "ios", - "android", - "camera", - "vision", - "native", - "module", "react", + "native", + "camera", + "react-native", + "react-native-camera", + "vision", + "javascript", + "typescript", + "android", + "ios", + "library", + "instagram", + "snapchat", "ai", - "ar", - "qr", - "qr-code", - "barcode", "scanner", + "qrcode", + "barcode", + "qr-code", + "jsi", + "worklet", + "module", "frame", "processing", "realtime" ], "repository": "https://github.com/mrousavy/react-native-vision-camera", - "author": "Marc Rousavy (https://github.com/mrousavy)", + "author": "Marc Rousavy (https://github.com/mrousavy)", "license": "MIT", "bugs": { "url": "https://github.com/mrousavy/react-native-vision-camera/issues" From 3192f5e939a5facd293143a49cf8787c6437c9db Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 7 Feb 2024 11:50:33 +0100 Subject: [PATCH 13/71] fix: Fix PreviewView being stretched (#2519) * fix: Fix Preview stretching * feat: Keep screen on on Android * Add test code for race condition * fix: Fix preview stretching by awaiting SurfaceHolder resizing (`setFixedSize`) before configuring Camera * Format * Update SurfaceHolder+resize.kt * Update CameraPage.tsx --- .../com/mrousavy/camera/core/CameraSession.kt | 4 +-- .../com/mrousavy/camera/core/PreviewView.kt | 28 ++++++++++------ .../CameraCharacteristics+getPreviewSize.kt | 2 +- .../camera/extensions/SurfaceHolder+resize.kt | 32 +++++++++++++++++++ package/example/src/CameraPage.tsx | 1 - 5 files changed, 53 insertions(+), 14 deletions(-) create mode 100644 package/android/src/main/java/com/mrousavy/camera/extensions/SurfaceHolder+resize.kt diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt index 12f4b77..41d21e6 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt @@ -232,7 +232,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam /** * Set up the `CaptureSession` with all outputs (preview, photo, video, codeScanner) and their HDR/Format settings. */ - private fun configureOutputs(configuration: CameraConfiguration) { + private suspend fun configureOutputs(configuration: CameraConfiguration) { val cameraId = configuration.cameraId ?: throw NoCameraDeviceError() // Destroy previous outputs @@ -313,7 +313,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam ) outputs.add(output) // Size is usually landscape, so we flip it here - previewView?.size = Size(size.height, size.width) + previewView?.setSurfaceSize(size.width, size.height) } // CodeScanner Output diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt b/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt index cfda57a..8af1e58 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt @@ -10,25 +10,21 @@ import android.view.SurfaceView import android.widget.FrameLayout import com.facebook.react.bridge.UiThreadUtil import com.mrousavy.camera.extensions.getMaximumPreviewSize +import com.mrousavy.camera.extensions.resize import com.mrousavy.camera.types.ResizeMode import kotlin.math.roundToInt +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext @SuppressLint("ViewConstructor") class PreviewView(context: Context, callback: SurfaceHolder.Callback) : SurfaceView(context) { var size: Size = getMaximumPreviewSize() - set(value) { - field = value - UiThreadUtil.runOnUiThread { - Log.i(TAG, "Setting PreviewView Surface Size to $width x $height...") - holder.setFixedSize(value.height, value.width) - requestLayout() - invalidate() - } - } + private set var resizeMode: ResizeMode = ResizeMode.COVER set(value) { field = value UiThreadUtil.runOnUiThread { + Log.i(TAG, "Setting PreviewView ResizeMode to $value...") requestLayout() invalidate() } @@ -41,11 +37,23 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : SurfaceV FrameLayout.LayoutParams.MATCH_PARENT, Gravity.CENTER ) + holder.setKeepScreenOn(true) holder.addCallback(callback) } + suspend fun setSurfaceSize(width: Int, height: Int) { + withContext(Dispatchers.Main) { + size = Size(width, height) + Log.i(TAG, "Setting PreviewView Surface Size to $size...") + requestLayout() + invalidate() + holder.resize(width, height) + } + } + private fun getSize(contentSize: Size, containerSize: Size, resizeMode: ResizeMode): Size { - val contentAspectRatio = contentSize.width.toDouble() / contentSize.height + // TODO: Take sensor orientation into account here + val contentAspectRatio = contentSize.height.toDouble() / contentSize.width val containerAspectRatio = containerSize.width.toDouble() / containerSize.height val widthOverHeight = when (resizeMode) { diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCharacteristics+getPreviewSize.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCharacteristics+getPreviewSize.kt index e758c45..ecd525c 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCharacteristics+getPreviewSize.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCharacteristics+getPreviewSize.kt @@ -9,7 +9,7 @@ fun getMaximumPreviewSize(): Size { // See https://developer.android.com/reference/android/hardware/camera2/params/StreamConfigurationMap // According to the Android Developer documentation, PREVIEW streams can have a resolution // of up to the phone's display's resolution, with a maximum of 1920x1080. - val display1080p = Size(1080, 1920) + val display1080p = Size(1920, 1080) val displaySize = Size( Resources.getSystem().displayMetrics.widthPixels, Resources.getSystem().displayMetrics.heightPixels diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/SurfaceHolder+resize.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/SurfaceHolder+resize.kt new file mode 100644 index 0000000..35f00b7 --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/SurfaceHolder+resize.kt @@ -0,0 +1,32 @@ +package com.mrousavy.camera.extensions + +import android.view.SurfaceHolder +import androidx.annotation.UiThread +import kotlin.coroutines.resume +import kotlinx.coroutines.suspendCancellableCoroutine + +@UiThread +suspend fun SurfaceHolder.resize(width: Int, height: Int) { + return suspendCancellableCoroutine { continuation -> + val currentSize = this.surfaceFrame + if (currentSize.width() == width && currentSize.height() == height) { + // Already in target size + continuation.resume(Unit) + return@suspendCancellableCoroutine + } + + val callback = object : SurfaceHolder.Callback { + override fun surfaceCreated(holder: SurfaceHolder) = Unit + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + holder.removeCallback(this) + continuation.resume(Unit) + } + override fun surfaceDestroyed(holder: SurfaceHolder) { + holder.removeCallback(this) + continuation.cancel(Error("Tried to resize SurfaceView, but Surface has been destroyed!")) + } + } + this.addCallback(callback) + this.setFixedSize(width, height) + } +} diff --git a/package/example/src/CameraPage.tsx b/package/example/src/CameraPage.tsx index bef7fcf..8b5683f 100644 --- a/package/example/src/CameraPage.tsx +++ b/package/example/src/CameraPage.tsx @@ -99,7 +99,6 @@ export function CameraPage({ navigation }: Props): React.ReactElement { }, [isPressingButton], ) - // Camera callbacks const onError = useCallback((error: CameraRuntimeError) => { console.error(error) }, []) From a4e241a431076fc85574a6e12f332e6fac91de4a Mon Sep 17 00:00:00 2001 From: Menardi Date: Wed, 7 Feb 2024 10:51:24 +0000 Subject: [PATCH 14/71] fix: Fix photo not saving in example app on Android 13+ (#2522) On Android 13+, requesting the WRITE_EXTERNAL_STORAGE permission immediately denies, without asking the user. The @react-native-camera-roll/camera-roll plugin being used already supports using scoped storage for saving images on Android 13+, so this commit skips the permission check in that case, since no permissions are needed. --- package/example/src/MediaPage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package/example/src/MediaPage.tsx b/package/example/src/MediaPage.tsx index 93e3a14..1ddb760 100644 --- a/package/example/src/MediaPage.tsx +++ b/package/example/src/MediaPage.tsx @@ -14,7 +14,8 @@ import { useIsFocused } from '@react-navigation/core' import FastImage, { OnLoadEvent } from 'react-native-fast-image' const requestSavePermission = async (): Promise => { - if (Platform.OS !== 'android') return true + // On Android 13 and above, scoped storage is used instead and no permission is needed + if (Platform.OS !== 'android' || Platform.Version >= 33) return true const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE if (permission == null) return false From fce6616964c4535036598cab5276d054d96c3a38 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 7 Feb 2024 13:15:32 +0100 Subject: [PATCH 15/71] chore: release 3.9.0-beta.2 --- package/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/package.json b/package/package.json index 53d3050..a4df37c 100644 --- a/package/package.json +++ b/package/package.json @@ -1,6 +1,6 @@ { "name": "react-native-vision-camera", - "version": "3.9.0-beta.1", + "version": "3.9.0-beta.2", "description": "A powerful, high-performance React Native Camera library.", "main": "lib/commonjs/index", "module": "lib/module/index", From fb1d82ad9af4f4fbe6f1bce96deef71027438bea Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 8 Feb 2024 15:16:58 +0100 Subject: [PATCH 16/71] feat: Implement `focus()` on Android (#2523) * feat: Implement `focus()` on Android * Throw if not supported * Do focus in example * Format * fix: Properly convert layer point to camera coordinates * Fix * Set trigger back to IDLE * Fix rotation maybe? * Rotated by * fix: Fix display point calculation * Try other * Invoke `capture` callback on same thread * Center metering rectangle * Reset AF Trigger to IDLE * Reset it to it's default AF mode again, i dont even know anymore * Update CameraPage.tsx * Format * Apply options to repeating * Set * Use scene mode * Update CameraPage.tsx * Update CameraDeviceDetails.kt * It fucking works * Update PersistentCameraCaptureSession.kt * Update PersistentCameraCaptureSession.kt * Update PersistentCameraCaptureSession.kt * Create CameraCaptureSession+setRepeatingRequestAndWait.kt * Oh my god it works * Also focus AE * Cancel reset request * Rename to AF * Format * Update PersistentCameraCaptureSession.kt --- .../camera/core/CameraDeviceDetails.kt | 12 ++- .../com/mrousavy/camera/core/CameraError.kt | 2 + .../com/mrousavy/camera/core/CameraSession.kt | 8 +- .../core/PersistentCameraCaptureSession.kt | 98 +++++++++++++++++++ .../com/mrousavy/camera/core/PreviewView.kt | 20 ++++ .../core/capture/CameraCaptureRequest.kt | 2 + .../core/capture/RepeatingCaptureRequest.kt | 28 +++++- .../CameraCaptureSession+capture.kt | 30 +++--- ...Session+setRepeatingRequestAndWaitForAF.kt | 47 +++++++++ .../camera/extensions/Size+Extensions.kt | 13 +-- .../camera/extensions/SurfaceHolder+resize.kt | 5 + .../com/mrousavy/camera/types/Orientation.kt | 36 ++++++- package/example/src/CameraPage.tsx | 18 +++- package/ios/Core/CameraError.swift | 2 +- 14 files changed, 288 insertions(+), 33 deletions(-) create mode 100644 package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForAF.kt diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt index 25856f8..dfa94af 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt @@ -44,6 +44,8 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(35f) } val sensorSize by lazy { characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!! } + val activeSize + get() = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!! val sensorOrientation by lazy { characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0 } val minFocusDistance by lazy { getMinFocusDistanceCm() } val name by lazy { @@ -91,6 +93,14 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId val isBackwardsCompatible by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) } val supportsSnapshotCapture by lazy { supportsSnapshotCapture() } + val supportsTapToFocus by lazy { (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF) ?: 0) > 0 } + val supportsTapToExposure by lazy { (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE) ?: 0) > 0 } + val supportsTapToWhiteBalance by lazy { (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB) ?: 0) > 0 } + + val afModes by lazy { characteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)?.toList() ?: emptyList() } + val aeModes by lazy { characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES)?.toList() ?: emptyList() } + val awbModes by lazy { characteristics.get(CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES)?.toList() ?: emptyList() } + // TODO: Also add 10-bit YUV here? val videoFormat = ImageFormat.YUV_420_888 @@ -244,7 +254,7 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId map.putBoolean("isMultiCam", isMultiCam) map.putBoolean("supportsRawCapture", supportsRawCapture) map.putBoolean("supportsLowLightBoost", supportsLowLightBoost) - map.putBoolean("supportsFocus", true) // I believe every device here supports focussing + map.putBoolean("supportsFocus", supportsTapToFocus) map.putDouble("minZoom", minZoom) map.putDouble("maxZoom", maxZoom) map.putDouble("neutralZoom", 1.0) // Zoom is always relative to 1.0 on Android diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt index 182477b..838ee7b 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt @@ -62,6 +62,8 @@ class FlashUnavailableError : "flash-unavailable", "The Camera Device does not have a flash unit! Make sure you select a device where `device.hasFlash`/`device.hasTorch` is true." ) +class FocusNotSupportedError : + CameraError("device", "focus-not-supported", "The currently selected camera device does not support focusing!") class CameraNotReadyError : CameraError("session", "camera-not-ready", "The Camera is not ready yet! Wait for the onInitialized() callback!") diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt index 41d21e6..fef6998 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt @@ -468,9 +468,13 @@ class CameraSession(private val context: Context, private val cameraManager: Cam callback.onError(error) } - suspend fun focus(x: Int, y: Int): Unit = throw NotImplementedError("focus() is not yet implemented!") + suspend fun focus(x: Int, y: Int) { + val previewView = previewView ?: throw CameraNotReadyError() + val deviceDetails = captureSession.getActiveDeviceDetails() ?: throw CameraNotReadyError() - private suspend fun focus(point: Point): Unit = throw NotImplementedError() + val cameraPoint = previewView.convertLayerPointToCameraCoordinates(Point(x, y), deviceDetails) + captureSession.focus(cameraPoint) + } data class CapturedPhoto( val image: Image, diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt index 510392a..d8d2ac9 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt @@ -1,11 +1,15 @@ package com.mrousavy.camera.core +import android.graphics.Point import android.hardware.camera2.CameraAccessException import android.hardware.camera2.CameraCaptureSession import android.hardware.camera2.CameraDevice import android.hardware.camera2.CameraManager +import android.hardware.camera2.CaptureRequest import android.hardware.camera2.TotalCaptureResult +import android.hardware.camera2.params.MeteringRectangle import android.util.Log +import android.util.Size import com.mrousavy.camera.core.capture.PhotoCaptureRequest import com.mrousavy.camera.core.capture.RepeatingCaptureRequest import com.mrousavy.camera.core.outputs.SurfaceOutput @@ -13,11 +17,18 @@ import com.mrousavy.camera.extensions.capture import com.mrousavy.camera.extensions.createCaptureSession import com.mrousavy.camera.extensions.isValid import com.mrousavy.camera.extensions.openCamera +import com.mrousavy.camera.extensions.setRepeatingRequestAndWaitForAF import com.mrousavy.camera.extensions.tryAbortCaptures import com.mrousavy.camera.types.Flash import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.QualityPrioritization import java.io.Closeable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -29,6 +40,8 @@ import kotlinx.coroutines.sync.withLock class PersistentCameraCaptureSession(private val cameraManager: CameraManager, private val callback: Callback) : Closeable { companion object { private const val TAG = "PersistentCameraCaptureSession" + private val DEFAULT_METERING_SIZE = Size(100, 100) + private const val FOCUS_RESET_TIMEOUT = 3000L } // Inputs/Dependencies @@ -44,6 +57,8 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p private val mutex = Mutex() private var didDestroyFromOutside = false + private var focusResetJob: Job? = null + private val coroutineScope = CoroutineScope(CameraQueues.cameraQueue.coroutineDispatcher) val isRunning: Boolean get() = isActive && session != null && device != null && !didDestroyFromOutside @@ -128,6 +143,7 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p enableShutterSound: Boolean ): TotalCaptureResult { mutex.withLock { + Log.i(TAG, "Capturing photo...") val session = session ?: throw CameraNotReadyError() val repeatingRequest = repeatingRequest ?: throw CameraNotReadyError() val photoRequest = PhotoCaptureRequest( @@ -149,6 +165,88 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p } } + suspend fun focus(point: Point) { + mutex.withLock { + Log.i(TAG, "Focusing to $point...") + val session = session ?: throw CameraNotReadyError() + val repeatingRequest = repeatingRequest ?: throw CameraNotReadyError() + val device = session.device + val deviceDetails = getOrCreateCameraDeviceDetails(device) + if (!deviceDetails.supportsTapToFocus) { + throw FocusNotSupportedError() + } + val outputs = outputs.filter { it.isRepeating } + val meteringRectangle = MeteringRectangle(point, DEFAULT_METERING_SIZE, MeteringRectangle.METERING_WEIGHT_MAX - 1) + + // 0. Cancel the 3 second focus reset task + focusResetJob?.cancelAndJoin() + focusResetJob = null + + // 1. Cancel any ongoing AF/AE/AWB request + repeatingRequest.createCaptureRequest(device, deviceDetails, outputs).also { request -> + if (deviceDetails.supportsTapToFocus) { + request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL) + } + if (deviceDetails.supportsTapToExposure) { + request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL) + } + session.capture(request.build(), null, null) + } + + // 2. After previous AF/AE/AWB requests have been canceled, start a new AF/AE/AWB request + repeatingRequest.createCaptureRequest(device, deviceDetails, outputs).also { request -> + request.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO) + if (deviceDetails.supportsTapToFocus) { + request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO) + request.set(CaptureRequest.CONTROL_AF_REGIONS, arrayOf(meteringRectangle)) + request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START) + } + if (deviceDetails.supportsTapToExposure) { + request.set(CaptureRequest.CONTROL_AE_REGIONS, arrayOf(meteringRectangle)) + request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) + } + if (deviceDetails.supportsTapToWhiteBalance) { + request.set(CaptureRequest.CONTROL_AWB_REGIONS, arrayOf(meteringRectangle)) + } + session.capture(request.build(), null, null) + + // 3. Start a repeating request without the trigger and wait until AF/AE/AWB locks + request.set(CaptureRequest.CONTROL_AF_TRIGGER, null) + request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, null) + session.setRepeatingRequestAndWaitForAF(request.build()) + } + + // 4. After the Camera has successfully found the AF/AE/AWB lock-point, we set it to idle and keep the point metered + repeatingRequest.createCaptureRequest(device, deviceDetails, outputs).also { request -> + request.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO) + if (deviceDetails.supportsTapToFocus) { + request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO) + request.set(CaptureRequest.CONTROL_AF_REGIONS, arrayOf(meteringRectangle)) + request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE) + } + if (deviceDetails.supportsTapToExposure) { + request.set(CaptureRequest.CONTROL_AE_REGIONS, arrayOf(meteringRectangle)) + request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE) + } + session.setRepeatingRequest(request.build(), null, null) + } + + // 5. Wait 3 seconds + focusResetJob = coroutineScope.launch { + delay(FOCUS_RESET_TIMEOUT) + if (!this.isActive) { + // this job got canceled from the outside + return@launch + } + Log.i(TAG, "Resetting focus to auto-focus...") + // 6. Reset AF/AE/AWB to continuous auto-focus again, which is the default here. + repeatingRequest.createCaptureRequest(device, deviceDetails, outputs).also { request -> + session.setRepeatingRequest(request.build(), null, null) + } + } + } + } + fun getActiveDeviceDetails(): CameraDeviceDetails? { val device = device ?: return null return getOrCreateCameraDeviceDetails(device) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt b/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt index 8af1e58..c862be7 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt @@ -2,6 +2,7 @@ package com.mrousavy.camera.core import android.annotation.SuppressLint import android.content.Context +import android.graphics.Point import android.util.Log import android.util.Size import android.view.Gravity @@ -11,6 +12,7 @@ import android.widget.FrameLayout import com.facebook.react.bridge.UiThreadUtil import com.mrousavy.camera.extensions.getMaximumPreviewSize import com.mrousavy.camera.extensions.resize +import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.ResizeMode import kotlin.math.roundToInt import kotlinx.coroutines.Dispatchers @@ -51,6 +53,24 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : SurfaceV } } + private val viewSize: Size + get() { + val displayMetrics = context.resources.displayMetrics + val dpX = width / displayMetrics.density + val dpY = height / displayMetrics.density + return Size(dpX.toInt(), dpY.toInt()) + } + + fun convertLayerPointToCameraCoordinates(point: Point, cameraDeviceDetails: CameraDeviceDetails): Point { + val sensorOrientation = Orientation.fromRotationDegrees(cameraDeviceDetails.sensorOrientation) + val cameraSize = Size(cameraDeviceDetails.activeSize.width(), cameraDeviceDetails.activeSize.height()) + val viewOrientation = Orientation.PORTRAIT + + val rotated = Orientation.rotatePoint(point, viewSize, cameraSize, viewOrientation, sensorOrientation) + Log.i(TAG, "$point -> $sensorOrientation (in $cameraSize -> $viewSize) -> $rotated") + return rotated + } + private fun getSize(contentSize: Size, containerSize: Size, resizeMode: ResizeMode): Size { // TODO: Take sensor orientation into account here val contentAspectRatio = contentSize.height.toDouble() / contentSize.width diff --git a/package/android/src/main/java/com/mrousavy/camera/core/capture/CameraCaptureRequest.kt b/package/android/src/main/java/com/mrousavy/camera/core/capture/CameraCaptureRequest.kt index 5a73913..fbf26cd 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/capture/CameraCaptureRequest.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/capture/CameraCaptureRequest.kt @@ -61,9 +61,11 @@ abstract class CameraCaptureRequest( if (format == null) throw PropRequiresFormatToBeNonNullError("videoHdr") if (!format.supportsVideoHdr) throw InvalidVideoHdrError() builder.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_HDR) + builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_USE_SCENE_MODE) } else if (enableLowLightBoost) { if (!deviceDetails.supportsLowLightBoost) throw LowLightBoostNotSupportedError() builder.set(CaptureRequest.CONTROL_SCENE_MODE, CaptureRequest.CONTROL_SCENE_MODE_NIGHT) + builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_USE_SCENE_MODE) } // Set Exposure Bias diff --git a/package/android/src/main/java/com/mrousavy/camera/core/capture/RepeatingCaptureRequest.kt b/package/android/src/main/java/com/mrousavy/camera/core/capture/RepeatingCaptureRequest.kt index 3e790bb..6eccf02 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/capture/RepeatingCaptureRequest.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/capture/RepeatingCaptureRequest.kt @@ -14,7 +14,7 @@ import com.mrousavy.camera.types.Torch import com.mrousavy.camera.types.VideoStabilizationMode class RepeatingCaptureRequest( - private val enableVideoPipeline: Boolean, + val enableVideoPipeline: Boolean, torch: Torch = Torch.OFF, private val fps: Int? = null, private val videoStabilizationMode: VideoStabilizationMode = VideoStabilizationMode.OFF, @@ -48,6 +48,32 @@ class RepeatingCaptureRequest( ): CaptureRequest.Builder { val builder = super.createCaptureRequest(template, device, deviceDetails, outputs) + builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO) + + // Set AF + if (enableVideoPipeline && deviceDetails.afModes.contains(CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_VIDEO)) { + builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO) + } else if (deviceDetails.afModes.contains(CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_PICTURE)) { + builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) + } else if (deviceDetails.afModes.contains(CameraCharacteristics.CONTROL_AF_MODE_AUTO)) { + builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO) + } else if (deviceDetails.afModes.contains(CameraCharacteristics.CONTROL_AF_MODE_OFF)) { + builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF) + builder.set(CaptureRequest.LENS_FOCUS_DISTANCE, 0f) + } + + // Set AE + if (deviceDetails.aeModes.contains(CameraCharacteristics.CONTROL_AE_MODE_ON)) { + builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON) + } else if (deviceDetails.aeModes.contains(CameraCharacteristics.CONTROL_AE_MODE_OFF)) { + builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF) + } + + // Set AWB + if (deviceDetails.awbModes.contains(CameraCharacteristics.CONTROL_AWB_MODE_AUTO)) { + builder.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO) + } + // Set FPS if (fps != null) { if (format == null) throw PropRequiresFormatToBeNonNullError("fps") diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+capture.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+capture.kt index 4a6a323..a3dce45 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+capture.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+capture.kt @@ -5,7 +5,6 @@ import android.hardware.camera2.CaptureFailure import android.hardware.camera2.CaptureRequest import android.hardware.camera2.TotalCaptureResult import android.media.MediaActionSound -import com.mrousavy.camera.core.CameraQueues import com.mrousavy.camera.core.CaptureAbortedError import com.mrousavy.camera.core.UnknownCaptureError import kotlin.coroutines.resume @@ -23,29 +22,36 @@ suspend fun CameraCaptureSession.capture(captureRequest: CaptureRequest, enableS override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) { super.onCaptureCompleted(session, request, result) - continuation.resume(result) - shutterSound?.release() + if (request == captureRequest) { + continuation.resume(result) + shutterSound?.release() + } } override fun onCaptureStarted(session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long) { super.onCaptureStarted(session, request, timestamp, frameNumber) - if (enableShutterSound) { - shutterSound?.play(MediaActionSound.SHUTTER_CLICK) + if (request == captureRequest) { + if (enableShutterSound) { + shutterSound?.play(MediaActionSound.SHUTTER_CLICK) + } } } override fun onCaptureFailed(session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure) { super.onCaptureFailed(session, request, failure) - val wasImageCaptured = failure.wasImageCaptured() - val error = when (failure.reason) { - CaptureFailure.REASON_ERROR -> UnknownCaptureError(wasImageCaptured) - CaptureFailure.REASON_FLUSHED -> CaptureAbortedError(wasImageCaptured) - else -> UnknownCaptureError(wasImageCaptured) + + if (request == captureRequest) { + val wasImageCaptured = failure.wasImageCaptured() + val error = when (failure.reason) { + CaptureFailure.REASON_ERROR -> UnknownCaptureError(wasImageCaptured) + CaptureFailure.REASON_FLUSHED -> CaptureAbortedError(wasImageCaptured) + else -> UnknownCaptureError(wasImageCaptured) + } + continuation.resumeWithException(error) } - continuation.resumeWithException(error) } }, - CameraQueues.cameraQueue.handler + null ) } diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForAF.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForAF.kt new file mode 100644 index 0000000..e085a02 --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForAF.kt @@ -0,0 +1,47 @@ +package com.mrousavy.camera.extensions + +import android.hardware.camera2.CameraCaptureSession +import android.hardware.camera2.CaptureFailure +import android.hardware.camera2.CaptureRequest +import android.hardware.camera2.CaptureResult +import android.hardware.camera2.TotalCaptureResult +import android.util.Log +import com.mrousavy.camera.core.CaptureAbortedError +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlinx.coroutines.suspendCancellableCoroutine + +private const val TAG = "CameraCaptureSession" + +/** + * Set a new repeating request for the [CameraCaptureSession] that contains an AF trigger, and wait until AF has locked. + */ +suspend fun CameraCaptureSession.setRepeatingRequestAndWaitForAF(request: CaptureRequest) = + suspendCancellableCoroutine { continuation -> + this.setRepeatingRequest( + request, + object : CameraCaptureSession.CaptureCallback() { + override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) { + super.onCaptureCompleted(session, request, result) + + if (continuation.isActive) { + val afState = result.get(CaptureResult.CONTROL_AF_STATE) + Log.i(TAG, "AF State: $afState") + if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + continuation.resume(Unit) + session.setRepeatingRequest(request, null, null) + } + } + } + override fun onCaptureFailed(session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure) { + super.onCaptureFailed(session, request, failure) + + if (continuation.isActive) { + continuation.resumeWithException(CaptureAbortedError(failure.wasImageCaptured())) + session.setRepeatingRequest(request, null, null) + } + } + }, + null + ) + } diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/Size+Extensions.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/Size+Extensions.kt index b9664c5..b0699e9 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/Size+Extensions.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/Size+Extensions.kt @@ -2,7 +2,7 @@ package com.mrousavy.camera.extensions import android.util.Size import android.util.SizeF -import android.view.Surface +import com.mrousavy.camera.types.Orientation import kotlin.math.abs import kotlin.math.max import kotlin.math.min @@ -14,13 +14,10 @@ fun List.closestToOrMax(size: Size?): Size = this.maxBy { it.width * it.height } } -fun Size.rotated(surfaceRotation: Int): Size = - when (surfaceRotation) { - Surface.ROTATION_0 -> Size(width, height) - Surface.ROTATION_90 -> Size(height, width) - Surface.ROTATION_180 -> Size(width, height) - Surface.ROTATION_270 -> Size(height, width) - else -> Size(width, height) +fun Size.rotatedBy(orientation: Orientation): Size = + when (orientation) { + Orientation.PORTRAIT, Orientation.PORTRAIT_UPSIDE_DOWN -> this + Orientation.LANDSCAPE_LEFT, Orientation.LANDSCAPE_RIGHT -> Size(height, width) } val Size.bigger: Int diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/SurfaceHolder+resize.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/SurfaceHolder+resize.kt index 35f00b7..cc0fe9c 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/SurfaceHolder+resize.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/SurfaceHolder+resize.kt @@ -1,5 +1,6 @@ package com.mrousavy.camera.extensions +import android.util.Log import android.view.SurfaceHolder import androidx.annotation.UiThread import kotlin.coroutines.resume @@ -15,14 +16,18 @@ suspend fun SurfaceHolder.resize(width: Int, height: Int) { return@suspendCancellableCoroutine } + Log.i("SurfaceHolder", "Resizing SurfaceHolder to $width x $height...") + val callback = object : SurfaceHolder.Callback { override fun surfaceCreated(holder: SurfaceHolder) = Unit override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { holder.removeCallback(this) + Log.i("SurfaceHolder", "Resized SurfaceHolder to $width x $height!") continuation.resume(Unit) } override fun surfaceDestroyed(holder: SurfaceHolder) { holder.removeCallback(this) + Log.e("SurfaceHolder", "Failed to resize SurfaceHolder to $width x $height!") continuation.cancel(Error("Tried to resize SurfaceView, but Surface has been destroyed!")) } } diff --git a/package/android/src/main/java/com/mrousavy/camera/types/Orientation.kt b/package/android/src/main/java/com/mrousavy/camera/types/Orientation.kt index c228ba0..a19f7a7 100644 --- a/package/android/src/main/java/com/mrousavy/camera/types/Orientation.kt +++ b/package/android/src/main/java/com/mrousavy/camera/types/Orientation.kt @@ -1,5 +1,9 @@ package com.mrousavy.camera.types +import android.graphics.Point +import android.graphics.PointF +import android.util.Log +import android.util.Size import com.mrousavy.camera.core.CameraDeviceDetails enum class Orientation(override val unionValue: String) : JSUnionValue { @@ -11,9 +15,9 @@ enum class Orientation(override val unionValue: String) : JSUnionValue { fun toDegrees(): Int = when (this) { PORTRAIT -> 0 - LANDSCAPE_RIGHT -> 90 + LANDSCAPE_LEFT -> 90 PORTRAIT_UPSIDE_DOWN -> 180 - LANDSCAPE_LEFT -> 270 + LANDSCAPE_RIGHT -> 270 } fun toSensorRelativeOrientation(deviceDetails: CameraDeviceDetails): Orientation { @@ -43,10 +47,34 @@ enum class Orientation(override val unionValue: String) : JSUnionValue { fun fromRotationDegrees(rotationDegrees: Int): Orientation = when (rotationDegrees) { - in 45..135 -> LANDSCAPE_RIGHT + in 45..135 -> LANDSCAPE_LEFT in 135..225 -> PORTRAIT_UPSIDE_DOWN - in 225..315 -> LANDSCAPE_LEFT + in 225..315 -> LANDSCAPE_RIGHT else -> PORTRAIT } + + fun rotatePoint( + point: Point, + fromSize: Size, + toSize: Size, + fromOrientation: Orientation, + toOrientation: Orientation + ): Point { + val differenceDegrees = (fromOrientation.toDegrees() + toOrientation.toDegrees()) % 360 + val difference = Orientation.fromRotationDegrees(differenceDegrees) + val normalizedPoint = PointF(point.x / fromSize.width.toFloat(), point.y / fromSize.height.toFloat()) + + val rotatedNormalizedPoint = when (difference) { + PORTRAIT -> normalizedPoint + PORTRAIT_UPSIDE_DOWN -> PointF(1 - normalizedPoint.x, 1 - normalizedPoint.y) + LANDSCAPE_LEFT -> PointF(normalizedPoint.y, 1 - normalizedPoint.x) + LANDSCAPE_RIGHT -> PointF(1 - normalizedPoint.y, normalizedPoint.x) + } + + val rotatedX = rotatedNormalizedPoint.x * toSize.width + val rotatedY = rotatedNormalizedPoint.y * toSize.height + Log.i("ROTATE", "$point -> $normalizedPoint -> $difference -> $rotatedX, $rotatedY") + return Point(rotatedX.toInt(), rotatedY.toInt()) + } } } diff --git a/package/example/src/CameraPage.tsx b/package/example/src/CameraPage.tsx index 8b5683f..6da1c3c 100644 --- a/package/example/src/CameraPage.tsx +++ b/package/example/src/CameraPage.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useRef, useState, useCallback, useMemo } from 'react' -import { StyleSheet, Text, View } from 'react-native' +import { GestureResponderEvent, StyleSheet, Text, View } from 'react-native' import { PinchGestureHandler, PinchGestureHandlerGestureEvent, TapGestureHandler } from 'react-native-gesture-handler' import { CameraProps, @@ -125,6 +125,16 @@ export function CameraPage({ navigation }: Props): React.ReactElement { //#endregion //#region Tap Gesture + const onFocusTap = useCallback( + ({ nativeEvent: event }: GestureResponderEvent) => { + if (!device?.supportsFocus) return + camera.current?.focus({ + x: event.locationX, + y: event.locationY, + }) + }, + [device?.supportsFocus], + ) const onDoubleTap = useCallback(() => { onFlipCameraPressed() }, [onFlipCameraPressed]) @@ -173,7 +183,7 @@ export function CameraPage({ navigation }: Props): React.ReactElement { {device != null && ( - + 'Camera stopped!'} format={format} fps={fps} - photoHdr={enableHdr} - videoHdr={enableHdr} + photoHdr={format?.supportsPhotoHdr && enableHdr} + videoHdr={format?.supportsVideoHdr && enableHdr} lowLightBoost={device.supportsLowLightBoost && enableNightMode} enableZoomGesture={false} animatedProps={cameraAnimatedProps} diff --git a/package/ios/Core/CameraError.swift b/package/ios/Core/CameraError.swift index e460c32..ec96658 100644 --- a/package/ios/Core/CameraError.swift +++ b/package/ios/Core/CameraError.swift @@ -93,7 +93,7 @@ enum DeviceError: String { case .lowLightBoostNotSupported: return "The currently selected camera device does not support low-light boost! Select a device where `device.supportsLowLightBoost` is true." case .focusNotSupported: - return "The currently selected camera device does not support focussing!" + return "The currently selected camera device does not support focusing!" case .microphoneUnavailable: return "The microphone was unavailable." case .notAvailableOnSimulator: From 919e6c9fe833f7d581442c5b2cac59a4b2bd598b Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 8 Feb 2024 15:17:29 +0100 Subject: [PATCH 17/71] chore: release 3.9.0-beta.3 --- package/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/package.json b/package/package.json index a4df37c..8030f3a 100644 --- a/package/package.json +++ b/package/package.json @@ -1,6 +1,6 @@ { "name": "react-native-vision-camera", - "version": "3.9.0-beta.2", + "version": "3.9.0-beta.3", "description": "A powerful, high-performance React Native Camera library.", "main": "lib/commonjs/index", "module": "lib/module/index", From b3a88278de55236e56306d1df5bbf743e95e8ea3 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 13 Feb 2024 13:32:11 +0100 Subject: [PATCH 18/71] perf: Fix double configuration flicker on fast device change (#2537) * fix: Fix double configuration on device change Fixes a situation that happened on every device change (or initial mount) where the device was configured after the session, separately, instead of just all at once causing an additonal delay/flicker of the prevjew. * Try? * Format * Update CameraSession.swift * Use `defer` * Throw `.device` --- package/ios/Core/CameraSession.swift | 132 ++++++++++++--------------- 1 file changed, 56 insertions(+), 76 deletions(-) diff --git a/package/ios/Core/CameraSession.swift b/package/ios/Core/CameraSession.swift index 92caaac..1af5dca 100644 --- a/package/ios/Core/CameraSession.swift +++ b/package/ios/Core/CameraSession.swift @@ -117,51 +117,64 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC do { // If needed, configure the AVCaptureSession (inputs, outputs) if difference.isSessionConfigurationDirty { - try self.withSessionLock { - // 1. Update input device - if difference.inputChanged { - try self.configureDevice(configuration: config) - } - // 2. Update outputs - if difference.outputsChanged { - try self.configureOutputs(configuration: config) - } - // 3. Update Video Stabilization - if difference.videoStabilizationChanged { - self.configureVideoStabilization(configuration: config) - } - // 4. Update output orientation - if difference.orientationChanged { - self.configureOrientation(configuration: config) - } + self.captureSession.beginConfiguration() + + // 1. Update input device + if difference.inputChanged { + try self.configureDevice(configuration: config) } + // 2. Update outputs + if difference.outputsChanged { + try self.configureOutputs(configuration: config) + } + // 3. Update Video Stabilization + if difference.videoStabilizationChanged { + self.configureVideoStabilization(configuration: config) + } + // 4. Update output orientation + if difference.orientationChanged { + self.configureOrientation(configuration: config) + } + } + + guard let device = self.videoDeviceInput?.device else { + throw CameraError.device(.noDevice) } // If needed, configure the AVCaptureDevice (format, zoom, low-light-boost, ..) if difference.isDeviceConfigurationDirty { - try self.withDeviceLock { device in - // 4. Configure format - if difference.formatChanged { - try self.configureFormat(configuration: config, device: device) - } - // 5. After step 2. and 4., we also need to configure the PixelFormat. - // This needs to be done AFTER we updated the `format`, as this controls the supported PixelFormats. - if difference.outputsChanged || difference.formatChanged { - try self.configurePixelFormat(configuration: config) - } - // 6. Configure side-props (fps, lowLightBoost) - if difference.sidePropsChanged { - try self.configureSideProps(configuration: config, device: device) - } - // 7. Configure zoom - if difference.zoomChanged { - self.configureZoom(configuration: config, device: device) - } - // 8. Configure exposure bias - if difference.exposureChanged { - self.configureExposure(configuration: config, device: device) - } + try device.lockForConfiguration() + defer { + device.unlockForConfiguration() } + + // 4. Configure format + if difference.formatChanged { + try self.configureFormat(configuration: config, device: device) + } + // 5. After step 2. and 4., we also need to configure the PixelFormat. + // This needs to be done AFTER we updated the `format`, as this controls the supported PixelFormats. + if difference.outputsChanged || difference.formatChanged { + try self.configurePixelFormat(configuration: config) + } + // 6. Configure side-props (fps, lowLightBoost) + if difference.sidePropsChanged { + try self.configureSideProps(configuration: config, device: device) + } + // 7. Configure zoom + if difference.zoomChanged { + self.configureZoom(configuration: config, device: device) + } + // 8. Configure exposure bias + if difference.exposureChanged { + self.configureExposure(configuration: config, device: device) + } + } + + if difference.isSessionConfigurationDirty { + // We commit the session config updates AFTER the device config, + // that way we can also batch those changes into one update instead of doing two updates. + self.captureSession.commitConfiguration() } // 9. Start or stop the session if needed @@ -169,9 +182,11 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC // 10. Enable or disable the Torch if needed (requires session to be running) if difference.torchChanged { - try self.withDeviceLock { device in - try self.configureTorch(configuration: config, device: device) + try device.lockForConfiguration() + defer { + device.unlockForConfiguration() } + try self.configureTorch(configuration: config, device: device) } // Notify about Camera initialization @@ -206,41 +221,6 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC } } - /** - Runs the given [lambda] under an AVCaptureSession configuration lock (`beginConfiguration()`) - */ - private func withSessionLock(_ lambda: () throws -> Void) throws { - // Lock Capture Session for configuration - ReactLogger.log(level: .info, message: "Beginning CameraSession configuration...") - captureSession.beginConfiguration() - defer { - // Unlock Capture Session again and submit configuration to Hardware - self.captureSession.commitConfiguration() - ReactLogger.log(level: .info, message: "Committed CameraSession configuration!") - } - - // Call lambda - try lambda() - } - - /** - Runs the given [lambda] under an AVCaptureDevice configuration lock (`lockForConfiguration()`) - */ - private func withDeviceLock(_ lambda: (_ device: AVCaptureDevice) throws -> Void) throws { - guard let device = videoDeviceInput?.device else { - throw CameraError.session(.cameraNotReady) - } - ReactLogger.log(level: .info, message: "Beginning CaptureDevice configuration...") - try device.lockForConfiguration() - defer { - device.unlockForConfiguration() - ReactLogger.log(level: .info, message: "Committed CaptureDevice configuration!") - } - - // Call lambda with Device - try lambda(device) - } - /** Starts or stops the CaptureSession if needed (`isActive`) */ From 0ed3aed48a674a4cafea57c5b230275748fcac75 Mon Sep 17 00:00:00 2001 From: Luiz Carlos Date: Tue, 13 Feb 2024 09:35:36 -0300 Subject: [PATCH 19/71] docs: Add `react-native-vision-camera-face-detector` to the FP plugin list (#2534) Also fixed link to this file --- docs/docs/guides/FRAME_PROCESSOR_PLUGINS.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/guides/FRAME_PROCESSOR_PLUGINS.mdx b/docs/docs/guides/FRAME_PROCESSOR_PLUGINS.mdx index 9a777bd..27506a4 100644 --- a/docs/docs/guides/FRAME_PROCESSOR_PLUGINS.mdx +++ b/docs/docs/guides/FRAME_PROCESSOR_PLUGINS.mdx @@ -40,7 +40,7 @@ cd ios && pod install - +* [nonam4/**react-native-vision-camera-face-detector**](https://github.com/nonam4/react-native-vision-camera-face-detector): A V3 frame processor plugin to detect faces using MLKit Vision Face Detector. @@ -50,7 +50,7 @@ cd ios && pod install

-Click here to add your Frame Processor Plugin to this list! +Click here to add your Frame Processor Plugin to this list!


From 24f43efa352fc272a1a50ab18682c84caf5be519 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 13 Feb 2024 13:46:07 +0100 Subject: [PATCH 20/71] feat: Add `context` to `VisionCameraProxy` (#2545) --- .../com/mrousavy/camera/frameprocessor/VisionCameraProxy.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/frameprocessor/VisionCameraProxy.kt b/package/android/src/main/java/com/mrousavy/camera/frameprocessor/VisionCameraProxy.kt index db41e55..81bd1af 100644 --- a/package/android/src/main/java/com/mrousavy/camera/frameprocessor/VisionCameraProxy.kt +++ b/package/android/src/main/java/com/mrousavy/camera/frameprocessor/VisionCameraProxy.kt @@ -14,7 +14,7 @@ import com.mrousavy.camera.core.ViewNotFoundError import java.lang.ref.WeakReference @Suppress("KotlinJniMissingFunction") // we use fbjni. -class VisionCameraProxy(context: ReactApplicationContext) { +class VisionCameraProxy(private val reactContext: ReactApplicationContext) { companion object { const val TAG = "VisionCameraProxy" } @@ -24,6 +24,8 @@ class VisionCameraProxy(context: ReactApplicationContext) { private var mHybridData: HybridData private var mContext: WeakReference private var mScheduler: VisionCameraScheduler + val context: ReactApplicationContext + get() = reactContext init { val jsCallInvokerHolder = context.catalystInstance.jsCallInvokerHolder as CallInvokerHolderImpl From 1011c3f03920a5521ec44cc40ecc2222ad48b648 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 13 Feb 2024 13:46:17 +0100 Subject: [PATCH 21/71] fix: Fix `java.lang.NoSuchMethodError` in `FrameProcessor` initializer (#2546) --- .../java/com/mrousavy/camera/frameprocessor/FrameProcessor.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessor.java b/package/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessor.java index 09e1b29..80c3a8d 100644 --- a/package/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessor.java +++ b/package/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessor.java @@ -21,6 +21,8 @@ public final class FrameProcessor { @Keep private final HybridData mHybridData; + @DoNotStrip + @Keep public FrameProcessor(HybridData hybridData) { mHybridData = hybridData; } From a2a2e94865fa8fdf7e140cd15358f4c6cbe31e72 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 13 Feb 2024 13:52:21 +0100 Subject: [PATCH 22/71] fix: Fix "Invalid pixel format 35" HardwareBuffer crash (#2547) --- .../src/main/java/com/mrousavy/camera/core/VideoPipeline.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/VideoPipeline.kt b/package/android/src/main/java/com/mrousavy/camera/core/VideoPipeline.kt index f864642..99374cc 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/VideoPipeline.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/VideoPipeline.kt @@ -228,7 +228,11 @@ class VideoPipeline( @RequiresApi(Build.VERSION_CODES.Q) private fun supportsHardwareBufferFlags(flags: Long): Boolean { val hardwareBufferFormat = format.toHardwareBufferFormat() - return HardwareBuffer.isSupported(width, height, hardwareBufferFormat, 1, flags) + try { + return HardwareBuffer.isSupported(width, height, hardwareBufferFormat, 1, flags) + } catch (_: Throwable) { + return false + } } private external fun getInputTextureId(): Int From b7e24c444e35a894611fd45eabaea4e3988137e8 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 13 Feb 2024 15:15:19 +0100 Subject: [PATCH 23/71] fix: Check if session is still valid before resetting AF after focus (#2550) * fix: Check if session is still valid before resetting AF after focus * Update PersistentCameraCaptureSession.kt --- .../mrousavy/camera/core/PersistentCameraCaptureSession.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt index d8d2ac9..3b6e056 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt @@ -238,6 +238,10 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p // this job got canceled from the outside return@launch } + if (!isRunning || this@PersistentCameraCaptureSession.session != session) { + // the view/session has already been destroyed in the meantime + return@launch + } Log.i(TAG, "Resetting focus to auto-focus...") // 6. Reset AF/AE/AWB to continuous auto-focus again, which is the default here. repeatingRequest.createCaptureRequest(device, deviceDetails, outputs).also { request -> From ec7ce36d25cc31668fc0cb0fc34585747399ff71 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 13 Feb 2024 15:15:29 +0100 Subject: [PATCH 24/71] fix: Use `acquireLatestImage` instead of `acquireNextImage` for CodeScanner (#2549) --- .../main/java/com/mrousavy/camera/core/CodeScannerPipeline.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CodeScannerPipeline.kt b/package/android/src/main/java/com/mrousavy/camera/core/CodeScannerPipeline.kt index 6a22f59..b5d147c 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CodeScannerPipeline.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CodeScannerPipeline.kt @@ -39,7 +39,7 @@ class CodeScannerPipeline( var isBusy = false imageReader = ImageReader.newInstance(size.width, size.height, format, MAX_IMAGES) imageReader.setOnImageAvailableListener({ reader -> - val image = reader.acquireNextImage() ?: return@setOnImageAvailableListener + val image = reader.acquireLatestImage() ?: return@setOnImageAvailableListener if (isBusy) { // We're currently executing on a previous Frame, so we skip this one. From 01300853763828a4228c74a6c4bcc6d637470502 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 14 Feb 2024 10:58:11 +0100 Subject: [PATCH 25/71] chore: Update lockfile --- package/example/ios/Podfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package/example/ios/Podfile.lock b/package/example/ios/Podfile.lock index dc284e6..cc03551 100644 --- a/package/example/ios/Podfile.lock +++ b/package/example/ios/Podfile.lock @@ -484,7 +484,7 @@ PODS: - libwebp (~> 1.0) - SDWebImage/Core (~> 5.10) - SocketRocket (0.6.1) - - VisionCamera (3.9.0-beta.0): + - VisionCamera (3.9.0-beta.3): - React - React-callinvoker - React-Core @@ -724,7 +724,7 @@ SPEC CHECKSUMS: SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - VisionCamera: f2f2fa58be438670ef5d5aa88846ffe59a78f7a8 + VisionCamera: 666b0baa70060c0a472b30fc6d3a37f77e40fa9c Yoga: 4c3aa327e4a6a23eeacd71f61c81df1bcdf677d5 PODFILE CHECKSUM: 27f53791141a3303d814e09b55770336416ff4eb From ad33dd91b1c5d13f9c6bab6589417513810409c0 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 14 Feb 2024 11:51:24 +0100 Subject: [PATCH 26/71] fix: Fix VideoPipeline crash on Samsung (Disable `USAGE_GPU_SAMPLED_IMAGE` ImageReader) (#2555) * fix: Fix VideoPipeline crash on Samsung (`USAGE_GPU_SAMPLED_IMAGE` not supported) * Format code --- .../com/mrousavy/camera/core/VideoPipeline.kt | 60 +------------------ .../com/mrousavy/camera/types/PixelFormat.kt | 6 -- .../camera/utils/HardwareBufferUtils.kt | 36 ----------- 3 files changed, 3 insertions(+), 99 deletions(-) delete mode 100644 package/android/src/main/java/com/mrousavy/camera/utils/HardwareBufferUtils.kt diff --git a/package/android/src/main/java/com/mrousavy/camera/core/VideoPipeline.kt b/package/android/src/main/java/com/mrousavy/camera/core/VideoPipeline.kt index 99374cc..f45520c 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/VideoPipeline.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/VideoPipeline.kt @@ -9,7 +9,6 @@ import android.os.Build import android.util.Log import android.view.Surface import androidx.annotation.Keep -import androidx.annotation.RequiresApi import com.facebook.jni.HybridData import com.facebook.proguard.annotations.DoNotStrip import com.mrousavy.camera.frameprocessor.Frame @@ -79,15 +78,12 @@ class VideoPipeline( val format = getImageReaderFormat() Log.i(TAG, "Using ImageReader round-trip (format: #$format)") + imageReader = ImageReader.newInstance(width, height, format, MAX_IMAGES) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - Log.i(TAG, "Using API 29 for GPU ImageReader...") - val usageFlags = getRecommendedHardwareBufferFlags() - Log.i(TAG, "Using ImageReader flags: $usageFlags") - imageReader = ImageReader.newInstance(width, height, format, MAX_IMAGES, usageFlags) + Log.i(TAG, "Using ImageWriter with custom format (#$format)...") imageWriter = ImageWriter.newInstance(glSurface, MAX_IMAGES, format) } else { - Log.i(TAG, "Using legacy API for CPU ImageReader...") - imageReader = ImageReader.newInstance(width, height, format, MAX_IMAGES) + Log.i(TAG, "Using ImageWriter with default format...") imageWriter = ImageWriter.newInstance(glSurface, MAX_IMAGES) } imageReader!!.setOnImageAvailableListener({ reader -> @@ -185,56 +181,6 @@ class VideoPipeline( } } - /** - * Get the recommended HardwareBuffer flags for creating ImageReader instances with. - * - * Tries to use [HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE] if possible, [HardwareBuffer.USAGE_CPU_READ_OFTEN] - * or a combination of both flags if CPU access is needed ([enableFrameProcessor]), and [0] otherwise. - */ - @RequiresApi(Build.VERSION_CODES.Q) - @Suppress("LiftReturnOrAssignment") - private fun getRecommendedHardwareBufferFlags(): Long { - val cpuFlag = HardwareBuffer.USAGE_CPU_READ_OFTEN - val gpuFlag = HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE - val bothFlags = gpuFlag or cpuFlag - - if (format == PixelFormat.NATIVE) { - // We don't need CPU access, so we can use GPU optimized buffers - if (supportsHardwareBufferFlags(gpuFlag)) { - // We support GPU Buffers directly and - Log.i(TAG, "GPU HardwareBuffers are supported!") - return gpuFlag - } else { - // no flags are supported - fall back to default - return 0 - } - } else { - // We are using YUV or RGB formats, so we need CPU access on the Frame - if (supportsHardwareBufferFlags(bothFlags)) { - // We support both CPU and GPU flags! - Log.i(TAG, "GPU + CPU HardwareBuffers are supported!") - return bothFlags - } else if (supportsHardwareBufferFlags(cpuFlag)) { - // We only support a CPU read flag, that's fine - Log.i(TAG, "CPU HardwareBuffers are supported!") - return cpuFlag - } else { - // no flags are supported - fall back to default - return 0 - } - } - } - - @RequiresApi(Build.VERSION_CODES.Q) - private fun supportsHardwareBufferFlags(flags: Long): Boolean { - val hardwareBufferFormat = format.toHardwareBufferFormat() - try { - return HardwareBuffer.isSupported(width, height, hardwareBufferFormat, 1, flags) - } catch (_: Throwable) { - return false - } - } - private external fun getInputTextureId(): Int private external fun onBeforeFrame() private external fun onFrame(transformMatrix: FloatArray) diff --git a/package/android/src/main/java/com/mrousavy/camera/types/PixelFormat.kt b/package/android/src/main/java/com/mrousavy/camera/types/PixelFormat.kt index 697c53b..c4a7a3c 100644 --- a/package/android/src/main/java/com/mrousavy/camera/types/PixelFormat.kt +++ b/package/android/src/main/java/com/mrousavy/camera/types/PixelFormat.kt @@ -4,7 +4,6 @@ import android.graphics.ImageFormat import android.util.Log import com.mrousavy.camera.core.InvalidTypeScriptUnionError import com.mrousavy.camera.core.PixelFormatNotSupportedError -import com.mrousavy.camera.utils.HardwareBufferUtils import com.mrousavy.camera.utils.ImageFormatUtils enum class PixelFormat(override val unionValue: String) : JSUnionValue { @@ -20,11 +19,6 @@ enum class PixelFormat(override val unionValue: String) : JSUnionValue { else -> throw PixelFormatNotSupportedError(this.unionValue) } - fun toHardwareBufferFormat(): Int { - val imageFormat = toImageFormat() - return HardwareBufferUtils.getHardwareBufferFormat(imageFormat) - } - companion object : JSUnionValue.Companion { private const val TAG = "PixelFormat" fun fromImageFormat(imageFormat: Int): PixelFormat = diff --git a/package/android/src/main/java/com/mrousavy/camera/utils/HardwareBufferUtils.kt b/package/android/src/main/java/com/mrousavy/camera/utils/HardwareBufferUtils.kt deleted file mode 100644 index c1d5106..0000000 --- a/package/android/src/main/java/com/mrousavy/camera/utils/HardwareBufferUtils.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.mrousavy.camera.utils - -import android.graphics.ImageFormat -import android.hardware.HardwareBuffer -import android.media.ImageReader -import android.os.Build - -class HardwareBufferUtils { - companion object { - fun getHardwareBufferFormat(imageFormat: Int): Int { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - // Dynamically create an ImageReader with the target ImageFormat, and then - // get it's HardwareBuffer format to see what it uses underneath. - val imageReader = ImageReader.newInstance(1, 1, imageFormat, 1, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE) - val format = imageReader.hardwareBufferFormat - imageReader.close() - return format - } - - if (imageFormat == ImageFormat.PRIVATE) { - // PRIVATE formats are opaque, their actual equivalent HardwareBuffer format is unknown. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // We can assume that YUV 4:2:0 or RGB is used. - return HardwareBuffer.YCBCR_420_888 - } else { - // Maybe assume we are on RGB if we're not on API R or above... - return HardwareBuffer.RGB_888 - } - } - - // According to PublicFormat.cpp in Android's codebase, the formats map 1:1 anyways.. - // https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/ui/PublicFormat.cpp - return imageFormat - } - } -} From 3699ccde949dd131ed0e734db10f4d3362e13b12 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 14 Feb 2024 11:53:17 +0100 Subject: [PATCH 27/71] chore: Organize code --- .../core/capture/RepeatingCaptureRequest.kt | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/capture/RepeatingCaptureRequest.kt b/package/android/src/main/java/com/mrousavy/camera/core/capture/RepeatingCaptureRequest.kt index 6eccf02..b9a2f08 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/capture/RepeatingCaptureRequest.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/capture/RepeatingCaptureRequest.kt @@ -3,6 +3,7 @@ package com.mrousavy.camera.core.capture import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraDevice import android.hardware.camera2.CaptureRequest +import android.os.Build import android.util.Range import com.mrousavy.camera.core.CameraDeviceDetails import com.mrousavy.camera.core.InvalidFpsError @@ -14,7 +15,7 @@ import com.mrousavy.camera.types.Torch import com.mrousavy.camera.types.VideoStabilizationMode class RepeatingCaptureRequest( - val enableVideoPipeline: Boolean, + private val enableVideoPipeline: Boolean, torch: Torch = Torch.OFF, private val fps: Int? = null, private val videoStabilizationMode: VideoStabilizationMode = VideoStabilizationMode.OFF, @@ -34,8 +35,10 @@ class RepeatingCaptureRequest( } private fun getBestDigitalStabilizationMode(deviceDetails: CameraDeviceDetails): Int { - if (deviceDetails.digitalStabilizationModes.contains(CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION)) { - return CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (deviceDetails.digitalStabilizationModes.contains(CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION)) { + return CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION + } } return CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_ON } @@ -87,16 +90,14 @@ class RepeatingCaptureRequest( if (!format.videoStabilizationModes.contains(videoStabilizationMode)) { throw InvalidVideoStabilizationMode(videoStabilizationMode) } - } - when (videoStabilizationMode) { - VideoStabilizationMode.OFF -> { - // do nothing - } - VideoStabilizationMode.STANDARD -> { - builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, getBestDigitalStabilizationMode(deviceDetails)) - } - VideoStabilizationMode.CINEMATIC, VideoStabilizationMode.CINEMATIC_EXTENDED -> { - builder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON) + when (videoStabilizationMode) { + VideoStabilizationMode.STANDARD -> { + builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, getBestDigitalStabilizationMode(deviceDetails)) + } + VideoStabilizationMode.CINEMATIC, VideoStabilizationMode.CINEMATIC_EXTENDED -> { + builder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON) + } + else -> throw InvalidVideoStabilizationMode(videoStabilizationMode) } } From 478688529b425c3125bd769955b6bd66242e9945 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 14 Feb 2024 12:47:03 +0100 Subject: [PATCH 28/71] fix: Fix 60 FPS crashing on some Samsungs (#2556) * fix: Fix 60 FPS crash on Samsung by checking `CamcorderProfile.maxFps` * Log FPS clamp * Update CameraDeviceDetails.kt * Format code --- .../camera/core/CameraDeviceDetails.kt | 18 +++- .../CameraCharacteristics+getOutputSizes.kt | 30 +----- .../RecordingSession+getRecommendedBitRate.kt | 40 +------ .../camera/utils/CamcorderProfileUtils.kt | 101 ++++++++++++++++++ 4 files changed, 120 insertions(+), 69 deletions(-) create mode 100644 package/android/src/main/java/com/mrousavy/camera/utils/CamcorderProfileUtils.kt diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt index dfa94af..43a09db 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt @@ -1,12 +1,12 @@ package com.mrousavy.camera.core -import android.annotation.SuppressLint import android.graphics.ImageFormat import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraExtensionCharacteristics import android.hardware.camera2.CameraManager import android.hardware.camera2.CameraMetadata import android.os.Build +import android.util.Log import android.util.Range import android.util.Size import com.facebook.react.bridge.Arguments @@ -22,11 +22,15 @@ import com.mrousavy.camera.types.LensFacing import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.PixelFormat import com.mrousavy.camera.types.VideoStabilizationMode +import com.mrousavy.camera.utils.CamcorderProfileUtils import kotlin.math.atan2 import kotlin.math.sqrt -@SuppressLint("InlinedApi") class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId: String) { + companion object { + private const val TAG = "CameraDeviceDetails" + } + val characteristics by lazy { cameraManager.getCameraCharacteristics(cameraId) } val hardwareLevel by lazy { HardwareLevel.fromCameraCharacteristics(characteristics) } val capabilities by lazy { characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) ?: IntArray(0) } @@ -200,7 +204,15 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId videoSizes.forEach { videoSize -> val frameDuration = cameraConfig.getOutputMinFrameDuration(videoFormat, videoSize) - val maxFps = (1.0 / (frameDuration.toDouble() / 1_000_000_000)).toInt() + var maxFps = (1.0 / (frameDuration.toDouble() / 1_000_000_000)).toInt() + val maxEncoderFps = CamcorderProfileUtils.getMaximumFps(cameraId, videoSize) + if (maxEncoderFps != null && maxEncoderFps < maxFps) { + Log.i( + TAG, + "Camera could do $maxFps FPS at $videoSize, but Media Encoder can only do $maxEncoderFps FPS. Clamping to $maxEncoderFps FPS..." + ) + maxFps = maxEncoderFps + } photoSizes.forEach { photoSize -> val map = buildFormatMap(photoSize, videoSize, Range(1, maxFps)) diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCharacteristics+getOutputSizes.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCharacteristics+getOutputSizes.kt index 168dabe..883e4c6 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCharacteristics+getOutputSizes.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCharacteristics+getOutputSizes.kt @@ -1,39 +1,13 @@ package com.mrousavy.camera.extensions import android.hardware.camera2.CameraCharacteristics -import android.media.CamcorderProfile -import android.os.Build import android.util.Size - -private fun getMaximumVideoSize(cameraId: String): Size? { - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - val profiles = CamcorderProfile.getAll(cameraId, CamcorderProfile.QUALITY_HIGH) - if (profiles != null) { - val largestProfile = profiles.videoProfiles.filterNotNull().maxByOrNull { it.width * it.height } - if (largestProfile != null) { - return Size(largestProfile.width, largestProfile.height) - } - } - } - - val cameraIdInt = cameraId.toIntOrNull() - if (cameraIdInt != null) { - val profile = CamcorderProfile.get(cameraIdInt, CamcorderProfile.QUALITY_HIGH) - return Size(profile.videoFrameWidth, profile.videoFrameHeight) - } - - return null - } catch (e: Throwable) { - // some Samsung phones just crash when trying to get the CamcorderProfile. Only god knows why. - return null - } -} +import com.mrousavy.camera.utils.CamcorderProfileUtils fun CameraCharacteristics.getVideoSizes(cameraId: String, format: Int): List { val config = this.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! val sizes = config.getOutputSizes(format) ?: emptyArray() - val maxVideoSize = getMaximumVideoSize(cameraId) + val maxVideoSize = CamcorderProfileUtils.getMaximumVideoSize(cameraId) if (maxVideoSize != null) { return sizes.filter { it.bigger <= maxVideoSize.bigger } } diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/RecordingSession+getRecommendedBitRate.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/RecordingSession+getRecommendedBitRate.kt index ed6cee2..d616a6e 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/RecordingSession+getRecommendedBitRate.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/RecordingSession+getRecommendedBitRate.kt @@ -4,9 +4,9 @@ import android.media.CamcorderProfile import android.media.MediaRecorder.VideoEncoder import android.os.Build import android.util.Log -import android.util.Size import com.mrousavy.camera.core.RecordingSession import com.mrousavy.camera.types.VideoCodec +import com.mrousavy.camera.utils.CamcorderProfileUtils import kotlin.math.abs data class RecommendedProfile( @@ -23,7 +23,7 @@ fun RecordingSession.getRecommendedBitRate(fps: Int, codec: VideoCodec, hdr: Boo val targetResolution = size val encoder = codec.toVideoEncoder() val bitDepth = if (hdr) 10 else 8 - val quality = findClosestCamcorderProfileQuality(cameraId, targetResolution) + val quality = CamcorderProfileUtils.findClosestCamcorderProfileQuality(cameraId, targetResolution, true) Log.i("CamcorderProfile", "Closest matching CamcorderProfile: $quality") var recommendedProfile: RecommendedProfile? = null @@ -75,39 +75,3 @@ fun RecordingSession.getRecommendedBitRate(fps: Int, codec: VideoCodec, hdr: Boo } return bitRate.toInt() } - -private fun getResolutionForCamcorderProfileQuality(camcorderProfile: Int): Int = - when (camcorderProfile) { - CamcorderProfile.QUALITY_QCIF -> 176 * 144 - CamcorderProfile.QUALITY_QVGA -> 320 * 240 - CamcorderProfile.QUALITY_CIF -> 352 * 288 - CamcorderProfile.QUALITY_VGA -> 640 * 480 - CamcorderProfile.QUALITY_480P -> 720 * 480 - CamcorderProfile.QUALITY_720P -> 1280 * 720 - CamcorderProfile.QUALITY_1080P -> 1920 * 1080 - CamcorderProfile.QUALITY_2K -> 2048 * 1080 - CamcorderProfile.QUALITY_QHD -> 2560 * 1440 - CamcorderProfile.QUALITY_2160P -> 3840 * 2160 - CamcorderProfile.QUALITY_4KDCI -> 4096 * 2160 - CamcorderProfile.QUALITY_8KUHD -> 7680 * 4320 - else -> throw Error("Invalid CamcorderProfile \"$camcorderProfile\"!") - } - -private fun findClosestCamcorderProfileQuality(cameraId: String, resolution: Size): Int { - // Iterate through all available CamcorderProfiles and find the one that matches the closest - val targetResolution = resolution.width * resolution.height - val cameraIdInt = cameraId.toIntOrNull() - - val profiles = (CamcorderProfile.QUALITY_QCIF..CamcorderProfile.QUALITY_8KUHD).filter { profile -> - if (cameraIdInt != null) { - return@filter CamcorderProfile.hasProfile(cameraIdInt, profile) - } else { - return@filter CamcorderProfile.hasProfile(profile) - } - } - val closestProfile = profiles.minBy { profile -> - val currentResolution = getResolutionForCamcorderProfileQuality(profile) - return@minBy abs(currentResolution - targetResolution) - } - return closestProfile -} diff --git a/package/android/src/main/java/com/mrousavy/camera/utils/CamcorderProfileUtils.kt b/package/android/src/main/java/com/mrousavy/camera/utils/CamcorderProfileUtils.kt new file mode 100644 index 0000000..f1dc64e --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/utils/CamcorderProfileUtils.kt @@ -0,0 +1,101 @@ +package com.mrousavy.camera.utils + +import android.media.CamcorderProfile +import android.os.Build +import android.util.Size +import kotlin.math.abs + +class CamcorderProfileUtils { + companion object { + private fun getResolutionForCamcorderProfileQuality(camcorderProfile: Int): Int = + when (camcorderProfile) { + CamcorderProfile.QUALITY_QCIF -> 176 * 144 + CamcorderProfile.QUALITY_QVGA -> 320 * 240 + CamcorderProfile.QUALITY_CIF -> 352 * 288 + CamcorderProfile.QUALITY_VGA -> 640 * 480 + CamcorderProfile.QUALITY_480P -> 720 * 480 + CamcorderProfile.QUALITY_720P -> 1280 * 720 + CamcorderProfile.QUALITY_1080P -> 1920 * 1080 + CamcorderProfile.QUALITY_2K -> 2048 * 1080 + CamcorderProfile.QUALITY_QHD -> 2560 * 1440 + CamcorderProfile.QUALITY_2160P -> 3840 * 2160 + CamcorderProfile.QUALITY_4KDCI -> 4096 * 2160 + CamcorderProfile.QUALITY_8KUHD -> 7680 * 4320 + else -> throw Error("Invalid CamcorderProfile \"$camcorderProfile\"!") + } + + fun findClosestCamcorderProfileQuality(cameraId: String, resolution: Size, allowLargerSize: Boolean): Int { + // Iterate through all available CamcorderProfiles and find the one that matches the closest + val targetResolution = resolution.width * resolution.height + val cameraIdInt = cameraId.toIntOrNull() + + var profiles = (CamcorderProfile.QUALITY_QCIF..CamcorderProfile.QUALITY_8KUHD).filter { profile -> + if (cameraIdInt != null) { + return@filter CamcorderProfile.hasProfile(cameraIdInt, profile) + } else { + return@filter CamcorderProfile.hasProfile(profile) + } + } + if (!allowLargerSize) { + profiles = profiles.filter { profile -> + val currentResolution = getResolutionForCamcorderProfileQuality(profile) + return@filter currentResolution <= targetResolution + } + } + val closestProfile = profiles.minBy { profile -> + val currentResolution = getResolutionForCamcorderProfileQuality(profile) + return@minBy abs(currentResolution - targetResolution) + } + return closestProfile + } + + fun getMaximumVideoSize(cameraId: String): Size? { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val profiles = CamcorderProfile.getAll(cameraId, CamcorderProfile.QUALITY_HIGH) + if (profiles != null) { + val largestProfile = profiles.videoProfiles.filterNotNull().maxByOrNull { it.width * it.height } + if (largestProfile != null) { + return Size(largestProfile.width, largestProfile.height) + } + } + } + + val cameraIdInt = cameraId.toIntOrNull() + if (cameraIdInt != null) { + val profile = CamcorderProfile.get(cameraIdInt, CamcorderProfile.QUALITY_HIGH) + return Size(profile.videoFrameWidth, profile.videoFrameHeight) + } + + return null + } catch (e: Throwable) { + // some Samsung phones just crash when trying to get the CamcorderProfile. Only god knows why. + return null + } + } + + fun getMaximumFps(cameraId: String, size: Size): Int? { + try { + val quality = findClosestCamcorderProfileQuality(cameraId, size, false) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val profiles = CamcorderProfile.getAll(cameraId, quality) + if (profiles != null) { + return profiles.videoProfiles.maxOf { profile -> profile.frameRate } + } + } + + val cameraIdInt = cameraId.toIntOrNull() + if (cameraIdInt != null) { + val profile = CamcorderProfile.get(cameraIdInt, quality) + return profile.videoFrameRate + } + + return null + } catch (e: Throwable) { + // some Samsung phones just crash when trying to get the CamcorderProfile. Only god knows why. + return null + } + } + } +} From 1a0bd8f7c287c878f0c50fe9ba74129cbb2b3da7 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 14 Feb 2024 12:47:40 +0100 Subject: [PATCH 29/71] feat: Add `enableGpuBuffers` property (#2557) * Revert "fix: Fix VideoPipeline crash on Samsung (Disable `USAGE_GPU_SAMPLED_IMAGE` ImageReader) (#2555)" This reverts commit ad33dd91b1c5d13f9c6bab6589417513810409c0. * feat: Add `enableGpuBuffers` prop * Create ImageWriter separately --- docs/docs/guides/PERFORMANCE.mdx | 7 ++ docs/docs/guides/TROUBLESHOOTING.mdx | 2 +- .../java/com/mrousavy/camera/CameraView.kt | 4 +- .../com/mrousavy/camera/CameraViewManager.kt | 5 ++ .../camera/core/CameraConfiguration.kt | 2 +- .../com/mrousavy/camera/core/CameraSession.kt | 1 + .../com/mrousavy/camera/core/VideoPipeline.kt | 69 ++++++++++++++++++- .../com/mrousavy/camera/types/PixelFormat.kt | 6 ++ .../camera/utils/HardwareBufferUtils.kt | 36 ++++++++++ package/src/CameraProps.ts | 21 +++++- 10 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 package/android/src/main/java/com/mrousavy/camera/utils/HardwareBufferUtils.kt diff --git a/docs/docs/guides/PERFORMANCE.mdx b/docs/docs/guides/PERFORMANCE.mdx index c943f97..8e2434f 100644 --- a/docs/docs/guides/PERFORMANCE.mdx +++ b/docs/docs/guides/PERFORMANCE.mdx @@ -70,6 +70,13 @@ Enable Buffer Compression ([`enableBufferCompression`](/docs/api/interfaces/Came Note: When not using a `frameProcessor`, buffer compression is automatically enabled. +### GPU buffers + +Enable GPU Buffer flags ([`enableGpuBuffers`](/docs/api/interfaces/CameraProps#enablegpubuffers)) to optimize the Video Pipeline for zero-copy buffer forwarding. +If this is enabled, the Video Pipeline can avoid an additional CPU -> GPU copy, resulting in better performance and more efficiency. + +Note: This only takes effect when using a `frameProcessor`. + ### Video Stabilization Video Stabilization requires additional overhead to start the algorithm, so disabling [`videoStabilizationMode`](/docs/api/interfaces/CameraProps#videostabilizationmode) can significantly speed up the Camera initialization time. diff --git a/docs/docs/guides/TROUBLESHOOTING.mdx b/docs/docs/guides/TROUBLESHOOTING.mdx index 18398c5..c068c3f 100644 --- a/docs/docs/guides/TROUBLESHOOTING.mdx +++ b/docs/docs/guides/TROUBLESHOOTING.mdx @@ -112,7 +112,7 @@ If you're experiencing build issues or runtime issues in VisionCamera, make sure 2. If a camera device is not being returned by [`Camera.getAvailableCameraDevices()`](/docs/api/classes/Camera#getavailablecameradevices), make sure it is a Camera2 compatible device. See [this section in the Android docs](https://developer.android.com/reference/android/hardware/camera2/CameraDevice#reprocessing) for more information. 3. If your Frame Processor is not running, make sure you check the native Android Studio/Logcat logs. There is useful information about the Frame Processor Runtime that will tell you if something goes wrong. 4. If your Frame Processor is not running, make sure you are not using a remote JS debugger such as Google Chrome, since those don't work with JSI. -5. If you are experiencing black-screens, try removing all properties such as `fps`, `videoHdr` or `format` on the `` component except for the required ones: +5. If you are experiencing black-screens, try removing all properties such as `fps`, `videoHdr`, `enableGpuBuffers` or `format` on the `` component except for the required ones: ```tsx ``` diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt index 1f24448..7f0f7b4 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt @@ -64,6 +64,7 @@ class CameraView(context: Context) : var videoHdr = false var photoHdr = false var lowLightBoost: Boolean? = null // nullable bool + var enableGpuBuffers: Boolean = false // other props var isActive = false @@ -160,7 +161,8 @@ class CameraView(context: Context) : CameraConfiguration.Video( videoHdr, pixelFormat, - enableFrameProcessor + enableFrameProcessor, + enableGpuBuffers ) ) } else { diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt b/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt index 54f7fce..ab047d7 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt @@ -84,6 +84,11 @@ class CameraViewManager : ViewGroupManager() { view.enableFpsGraph = enableFpsGraph } + @ReactProp(name = "enableGpuBuffers") + fun setEnableGpuBuffers(view: CameraView, enableGpuBuffers: Boolean) { + view.enableGpuBuffers = enableGpuBuffers + } + @ReactProp(name = "videoStabilizationMode") fun setVideoStabilizationMode(view: CameraView, videoStabilizationMode: String?) { val newMode = VideoStabilizationMode.fromUnionValue(videoStabilizationMode) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt index f1dca59..593d40a 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt @@ -44,7 +44,7 @@ data class CameraConfiguration( // Output types, those need to be comparable data class CodeScanner(val codeTypes: List) data class Photo(val enableHdr: Boolean) - data class Video(val enableHdr: Boolean, val pixelFormat: PixelFormat, val enableFrameProcessor: Boolean) + data class Video(val enableHdr: Boolean, val pixelFormat: PixelFormat, val enableFrameProcessor: Boolean, val enableGpuBuffers: Boolean) data class Audio(val nothing: Unit) data class Preview(val surface: Surface) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt index fef6998..4d52117 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt @@ -288,6 +288,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam video.config.pixelFormat, isSelfie, video.config.enableFrameProcessor, + video.config.enableGpuBuffers, callback ) val output = VideoPipelineOutput(videoPipeline, video.config.enableHdr) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/VideoPipeline.kt b/package/android/src/main/java/com/mrousavy/camera/core/VideoPipeline.kt index f45520c..ae2c107 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/VideoPipeline.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/VideoPipeline.kt @@ -9,6 +9,7 @@ import android.os.Build import android.util.Log import android.view.Surface import androidx.annotation.Keep +import androidx.annotation.RequiresApi import com.facebook.jni.HybridData import com.facebook.proguard.annotations.DoNotStrip import com.mrousavy.camera.frameprocessor.Frame @@ -32,6 +33,7 @@ class VideoPipeline( val format: PixelFormat = PixelFormat.NATIVE, private val isMirrored: Boolean = false, private val enableFrameProcessor: Boolean = false, + private val enableGpuBuffers: Boolean = false, private val callback: CameraSession.Callback ) : SurfaceTexture.OnFrameAvailableListener, Closeable { @@ -78,14 +80,25 @@ class VideoPipeline( val format = getImageReaderFormat() Log.i(TAG, "Using ImageReader round-trip (format: #$format)") - imageReader = ImageReader.newInstance(width, height, format, MAX_IMAGES) + // Create ImageReader + if (enableGpuBuffers && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val usageFlags = getRecommendedHardwareBufferFlags() + Log.i(TAG, "Creating ImageReader with GPU-optimized usage flags: $usageFlags") + imageReader = ImageReader.newInstance(width, height, format, MAX_IMAGES, usageFlags) + } else { + Log.i(TAG, "Creating ImageReader with default usage flags...") + imageReader = ImageReader.newInstance(width, height, format, MAX_IMAGES) + } + + // Create ImageWriter if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - Log.i(TAG, "Using ImageWriter with custom format (#$format)...") + Log.i(TAG, "Creating ImageWriter with format #$format...") imageWriter = ImageWriter.newInstance(glSurface, MAX_IMAGES, format) } else { - Log.i(TAG, "Using ImageWriter with default format...") + Log.i(TAG, "Creating ImageWriter with default format...") imageWriter = ImageWriter.newInstance(glSurface, MAX_IMAGES) } + imageReader!!.setOnImageAvailableListener({ reader -> Log.i(TAG, "ImageReader::onImageAvailable!") val image = reader.acquireNextImage() ?: return@setOnImageAvailableListener @@ -181,6 +194,56 @@ class VideoPipeline( } } + /** + * Get the recommended HardwareBuffer flags for creating ImageReader instances with. + * + * Tries to use [HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE] if possible, [HardwareBuffer.USAGE_CPU_READ_OFTEN] + * or a combination of both flags if CPU access is needed ([enableFrameProcessor]), and [0] otherwise. + */ + @RequiresApi(Build.VERSION_CODES.Q) + @Suppress("LiftReturnOrAssignment") + private fun getRecommendedHardwareBufferFlags(): Long { + val cpuFlag = HardwareBuffer.USAGE_CPU_READ_OFTEN + val gpuFlag = HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE + val bothFlags = gpuFlag or cpuFlag + + if (format == PixelFormat.NATIVE) { + // We don't need CPU access, so we can use GPU optimized buffers + if (supportsHardwareBufferFlags(gpuFlag)) { + // We support GPU Buffers directly and + Log.i(TAG, "GPU HardwareBuffers are supported!") + return gpuFlag + } else { + // no flags are supported - fall back to default + return 0 + } + } else { + // We are using YUV or RGB formats, so we need CPU access on the Frame + if (supportsHardwareBufferFlags(bothFlags)) { + // We support both CPU and GPU flags! + Log.i(TAG, "GPU + CPU HardwareBuffers are supported!") + return bothFlags + } else if (supportsHardwareBufferFlags(cpuFlag)) { + // We only support a CPU read flag, that's fine + Log.i(TAG, "CPU HardwareBuffers are supported!") + return cpuFlag + } else { + // no flags are supported - fall back to default + return 0 + } + } + } + + @RequiresApi(Build.VERSION_CODES.Q) + private fun supportsHardwareBufferFlags(flags: Long): Boolean { + val hardwareBufferFormat = format.toHardwareBufferFormat() + try { + return HardwareBuffer.isSupported(width, height, hardwareBufferFormat, 1, flags) + } catch (_: Throwable) { + return false + } + } + private external fun getInputTextureId(): Int private external fun onBeforeFrame() private external fun onFrame(transformMatrix: FloatArray) diff --git a/package/android/src/main/java/com/mrousavy/camera/types/PixelFormat.kt b/package/android/src/main/java/com/mrousavy/camera/types/PixelFormat.kt index c4a7a3c..697c53b 100644 --- a/package/android/src/main/java/com/mrousavy/camera/types/PixelFormat.kt +++ b/package/android/src/main/java/com/mrousavy/camera/types/PixelFormat.kt @@ -4,6 +4,7 @@ import android.graphics.ImageFormat import android.util.Log import com.mrousavy.camera.core.InvalidTypeScriptUnionError import com.mrousavy.camera.core.PixelFormatNotSupportedError +import com.mrousavy.camera.utils.HardwareBufferUtils import com.mrousavy.camera.utils.ImageFormatUtils enum class PixelFormat(override val unionValue: String) : JSUnionValue { @@ -19,6 +20,11 @@ enum class PixelFormat(override val unionValue: String) : JSUnionValue { else -> throw PixelFormatNotSupportedError(this.unionValue) } + fun toHardwareBufferFormat(): Int { + val imageFormat = toImageFormat() + return HardwareBufferUtils.getHardwareBufferFormat(imageFormat) + } + companion object : JSUnionValue.Companion { private const val TAG = "PixelFormat" fun fromImageFormat(imageFormat: Int): PixelFormat = diff --git a/package/android/src/main/java/com/mrousavy/camera/utils/HardwareBufferUtils.kt b/package/android/src/main/java/com/mrousavy/camera/utils/HardwareBufferUtils.kt new file mode 100644 index 0000000..c1d5106 --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/utils/HardwareBufferUtils.kt @@ -0,0 +1,36 @@ +package com.mrousavy.camera.utils + +import android.graphics.ImageFormat +import android.hardware.HardwareBuffer +import android.media.ImageReader +import android.os.Build + +class HardwareBufferUtils { + companion object { + fun getHardwareBufferFormat(imageFormat: Int): Int { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // Dynamically create an ImageReader with the target ImageFormat, and then + // get it's HardwareBuffer format to see what it uses underneath. + val imageReader = ImageReader.newInstance(1, 1, imageFormat, 1, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE) + val format = imageReader.hardwareBufferFormat + imageReader.close() + return format + } + + if (imageFormat == ImageFormat.PRIVATE) { + // PRIVATE formats are opaque, their actual equivalent HardwareBuffer format is unknown. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // We can assume that YUV 4:2:0 or RGB is used. + return HardwareBuffer.YCBCR_420_888 + } else { + // Maybe assume we are on RGB if we're not on API R or above... + return HardwareBuffer.RGB_888 + } + } + + // According to PublicFormat.cpp in Android's codebase, the formats map 1:1 anyways.. + // https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/ui/PublicFormat.cpp + return imageFormat + } + } +} diff --git a/package/src/CameraProps.ts b/package/src/CameraProps.ts index 83ed1f6..e78e26e 100644 --- a/package/src/CameraProps.ts +++ b/package/src/CameraProps.ts @@ -183,10 +183,29 @@ export interface CameraProps extends ViewProps { * * @platform iOS * @default - * - true // if video={true} and frameProcessor={undefined} + * - true // if frameProcessor={undefined} * - false // otherwise */ enableBufferCompression?: boolean + /** + * Enables or disables GPU-sampled buffers for the video stream. This only takes effect when using a {@linkcode frameProcessor}. + * + * When recording a Video ({@linkcode video}) while a Frame Processor is running ({@linkcode frameProcessor}), + * the {@linkcode Frame | Frames} will need to be forwarded to the Media Encoder. + * + * - When `enableGpuBuffers` is `false`, the Video Pipeline will use CPU buffers causing an additional copy + * from the Frame Processor to the Media Encoder, which potentially results in increased latency. + * - When `enableGpuBuffers` is `true`, the Video Pipeline will use shared GPU buffers which greatly increases + * it's efficiency as an additional buffer copy is avoided. + * (See [`USAGE_GPU_SAMPLED_IMAGE`](https://developer.android.com/reference/android/hardware/HardwareBuffer#USAGE_GPU_SAMPLED_IMAGE)) + * + * In general, it is recommended to set this to `true` if possible, as this can increase performance and efficiency of the Video Pipeline. + * This is an experimental feature flag however, so use at your own risk. + * + * @platform Android + * @default false + */ + enableGpuBuffers?: boolean /** * Enables or disables low-light boost on this camera device. * From 14daaaaf9d2c9474bf150303f896d2fe5a7173a1 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 14 Feb 2024 13:47:18 +0100 Subject: [PATCH 30/71] fix: Return after configure error --- package/ios/Core/CameraSession.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/package/ios/Core/CameraSession.swift b/package/ios/Core/CameraSession.swift index 1af5dca..7952a0e 100644 --- a/package/ios/Core/CameraSession.swift +++ b/package/ios/Core/CameraSession.swift @@ -109,6 +109,7 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC try lambda(config) } catch { self.onConfigureError(error) + return } let difference = CameraConfiguration.Difference(between: self.configuration, and: config) From 61b2f7dd4a94c5bb4825c5e2be32804b689b487a Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 14 Feb 2024 13:52:02 +0100 Subject: [PATCH 31/71] chore: Improve `enableGpuBuffers` docs --- package/src/CameraProps.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package/src/CameraProps.ts b/package/src/CameraProps.ts index e78e26e..e264cb3 100644 --- a/package/src/CameraProps.ts +++ b/package/src/CameraProps.ts @@ -200,9 +200,9 @@ export interface CameraProps extends ViewProps { * (See [`USAGE_GPU_SAMPLED_IMAGE`](https://developer.android.com/reference/android/hardware/HardwareBuffer#USAGE_GPU_SAMPLED_IMAGE)) * * In general, it is recommended to set this to `true` if possible, as this can increase performance and efficiency of the Video Pipeline. - * This is an experimental feature flag however, so use at your own risk. * - * @platform Android + * @experimental This is an experimental feature flag, use at your own risk. Some devices (especially Samsungs) may crash when trying to use GPU buffers. + * @platform Android (API 29+) * @default false */ enableGpuBuffers?: boolean From 37398cc909aa4d7754279c96e0b21d4ebe746b3d Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 14 Feb 2024 16:43:30 +0100 Subject: [PATCH 32/71] feat: Flash with AE Pre-capture trigger for Android (#2558) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Flash (`flash`) Adds `flash` functionality with a fully custom pre-capture AE/AF/AWB trigger sequence for Android. πŸŽ‰ ```ts camera.current.takePhoto({ flash: 'on' // or 'auto' }) ``` ### Better photos (`qualityPrioritization`) We now also run the AE/AF/AWB precapture sequence on every photo (unless `qualityPrioritization` is `speed`), meaning photos are now less blurry, properly exposed, and properly white-balanced - so in short: **photo quality is now better!**. The fast path still exists when using `qualityPrioritization: speed`, as that will skip the precapture sequence and metering actions and just grab an Image from the Camera as quickly as possible. Additionally, `qualityPrioritization` now controls these options: - [COLOR_CORRECTION_MODE](https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#COLOR_CORRECTION_MODE) - [EDGE_MODE](https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#EDGE_MODE) - [COLOR_CORRECTION_ABERRATION_MODE](https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#COLOR_CORRECTION_ABERRATION_MODE) - [HOT_PIXEL_MODE](https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#HOT_PIXEL_MODE) - [DISTORTION_CORRECTION_MODE](https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#DISTORTION_CORRECTION_MODE) - [NOISE_REDUCTION_MODE](https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#NOISE_REDUCTION_MODE) - [SHADING_MODE](https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#SHADING_MODE) - [TONEMAP_MODE](https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#TONEMAP_MODE) ..by setting them to `_FAST` or `_HIGH_QUALITY`, which was previously left untouched. This now means: - `takePhoto({ qualityPrioritization: 'speed' })` got FASTER πŸš€ - `takePhoto({ qualityPrioritization: 'quality' })` got BETTER QUALITY πŸ“Έ - `takePhoto({ qualityPrioritization: 'balanced' })` is left unchanged βœ… --- .../camera/core/CameraDeviceDetails.kt | 31 ++- .../com/mrousavy/camera/core/CameraError.kt | 1 + .../core/PersistentCameraCaptureSession.kt | 96 ++++------ .../core/capture/PhotoCaptureRequest.kt | 71 ++++++- .../CameraCaptureSession+capture.kt | 21 ++- .../CameraCaptureSession+precapture.kt | 99 ++++++++++ ...Session+setRepeatingRequestAndWaitForAF.kt | 47 ----- ...setRepeatingRequestAndWaitForPrecapture.kt | 178 ++++++++++++++++++ .../mrousavy/camera/types/HardwareLevel.kt | 13 ++ package/example/src/views/CaptureButton.tsx | 21 +-- package/src/CameraError.ts | 1 + 11 files changed, 448 insertions(+), 131 deletions(-) create mode 100644 package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt delete mode 100644 package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForAF.kt create mode 100644 package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt index 43a09db..5dbae98 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt @@ -50,7 +50,7 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId val sensorSize by lazy { characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!! } val activeSize get() = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!! - val sensorOrientation by lazy { characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0 } + val sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0 val minFocusDistance by lazy { getMinFocusDistanceCm() } val name by lazy { val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) characteristics.get(CameraCharacteristics.INFO_VERSION) else null @@ -97,14 +97,28 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId val isBackwardsCompatible by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) } val supportsSnapshotCapture by lazy { supportsSnapshotCapture() } - val supportsTapToFocus by lazy { (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF) ?: 0) > 0 } - val supportsTapToExposure by lazy { (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE) ?: 0) > 0 } - val supportsTapToWhiteBalance by lazy { (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB) ?: 0) > 0 } + val supportsFocusRegions by lazy { (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF) ?: 0) > 0 } + val supportsExposureRegions by lazy { (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE) ?: 0) > 0 } + val supportsWhiteBalanceRegions by lazy { (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB) ?: 0) > 0 } val afModes by lazy { characteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)?.toList() ?: emptyList() } val aeModes by lazy { characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES)?.toList() ?: emptyList() } val awbModes by lazy { characteristics.get(CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES)?.toList() ?: emptyList() } + val availableAberrationModes by lazy { + characteristics.get(CameraCharacteristics.COLOR_CORRECTION_AVAILABLE_ABERRATION_MODES) + ?: intArrayOf() + } + val availableHotPixelModes by lazy { characteristics.get(CameraCharacteristics.HOT_PIXEL_AVAILABLE_HOT_PIXEL_MODES) ?: intArrayOf() } + val availableEdgeModes by lazy { characteristics.get(CameraCharacteristics.EDGE_AVAILABLE_EDGE_MODES) ?: intArrayOf() } + val availableDistortionCorrectionModes by lazy { getAvailableDistortionCorrectionModesOrEmptyArray() } + val availableShadingModes by lazy { characteristics.get(CameraCharacteristics.SHADING_AVAILABLE_MODES) ?: intArrayOf() } + val availableToneMapModes by lazy { characteristics.get(CameraCharacteristics.TONEMAP_AVAILABLE_TONE_MAP_MODES) ?: intArrayOf() } + val availableNoiseReductionModes by lazy { + characteristics.get(CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES) + ?: intArrayOf() + } + // TODO: Also add 10-bit YUV here? val videoFormat = ImageFormat.YUV_420_888 @@ -117,6 +131,13 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId emptyList() } + private fun getAvailableDistortionCorrectionModesOrEmptyArray(): IntArray = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + characteristics.get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES) ?: intArrayOf() + } else { + intArrayOf() + } + private fun getHasVideoHdr(): Boolean { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (capabilities.contains(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT)) { @@ -266,7 +287,7 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId map.putBoolean("isMultiCam", isMultiCam) map.putBoolean("supportsRawCapture", supportsRawCapture) map.putBoolean("supportsLowLightBoost", supportsLowLightBoost) - map.putBoolean("supportsFocus", supportsTapToFocus) + map.putBoolean("supportsFocus", supportsFocusRegions) map.putDouble("minZoom", minZoom) map.putDouble("maxZoom", maxZoom) map.putDouble("neutralZoom", 1.0) // Zoom is always relative to 1.0 on Android diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt index 838ee7b..6e7c8c6 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt @@ -104,6 +104,7 @@ class PhotoNotEnabledError : CameraError("capture", "photo-not-enabled", "Photo capture is disabled! Pass `photo={true}` to enable photo capture.") class CaptureAbortedError(wasImageCaptured: Boolean) : CameraError("capture", "aborted", "The image capture was aborted! Was Image captured: $wasImageCaptured") +class CaptureTimedOutError : CameraError("capture", "timed-out", "The image capture was aborted because it timed out.") class UnknownCaptureError(wasImageCaptured: Boolean) : CameraError("capture", "unknown", "An unknown error occurred while trying to capture an Image! Was Image captured: $wasImageCaptured") class RecorderError(name: String, extra: Int) : diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt index 3b6e056..f953b23 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt @@ -7,17 +7,17 @@ import android.hardware.camera2.CameraDevice import android.hardware.camera2.CameraManager import android.hardware.camera2.CaptureRequest import android.hardware.camera2.TotalCaptureResult -import android.hardware.camera2.params.MeteringRectangle import android.util.Log -import android.util.Size import com.mrousavy.camera.core.capture.PhotoCaptureRequest import com.mrousavy.camera.core.capture.RepeatingCaptureRequest import com.mrousavy.camera.core.outputs.SurfaceOutput +import com.mrousavy.camera.extensions.PrecaptureOptions +import com.mrousavy.camera.extensions.PrecaptureTrigger import com.mrousavy.camera.extensions.capture import com.mrousavy.camera.extensions.createCaptureSession import com.mrousavy.camera.extensions.isValid import com.mrousavy.camera.extensions.openCamera -import com.mrousavy.camera.extensions.setRepeatingRequestAndWaitForAF +import com.mrousavy.camera.extensions.precapture import com.mrousavy.camera.extensions.tryAbortCaptures import com.mrousavy.camera.types.Flash import com.mrousavy.camera.types.Orientation @@ -40,7 +40,6 @@ import kotlinx.coroutines.sync.withLock class PersistentCameraCaptureSession(private val cameraManager: CameraManager, private val callback: Callback) : Closeable { companion object { private const val TAG = "PersistentCameraCaptureSession" - private val DEFAULT_METERING_SIZE = Size(100, 100) private const val FOCUS_RESET_TIMEOUT = 3000L } @@ -160,8 +159,34 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p // Submit a single high-res capture to photo output as well as all preview outputs val outputs = outputs - val request = photoRequest.createCaptureRequest(device, deviceDetails, outputs) - return session.capture(request.build(), enableShutterSound) + val repeatingOutputs = outputs.filter { it.isRepeating } + + if (qualityPrioritization == QualityPrioritization.SPEED && flash == Flash.OFF) { + // 0. We want to take a picture as fast as possible, so skip any precapture sequence and just capture one Frame. + Log.i(TAG, "Using fast capture path without pre-capture sequence...") + val singleRequest = photoRequest.createCaptureRequest(device, deviceDetails, outputs) + return session.capture(singleRequest.build(), enableShutterSound) + } + + Log.i(TAG, "Locking AF/AE/AWB...") + + // 1. Run precapture sequence + val precaptureRequest = repeatingRequest.createCaptureRequest(device, deviceDetails, repeatingOutputs) + val options = PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE, PrecaptureTrigger.AWB), flash, emptyList()) + val result = session.precapture(precaptureRequest, deviceDetails, options) + + try { + // 2. Once precapture AF/AE/AWB successfully locked, capture the actual photo + val singleRequest = photoRequest.createCaptureRequest(device, deviceDetails, outputs) + if (result.needsFlash) { + singleRequest.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_SINGLE) + } + return session.capture(singleRequest.build(), enableShutterSound) + } finally { + // 5. After taking a photo we set the repeating request back to idle to remove the AE/AF/AWB locks again + val idleRequest = repeatingRequest.createCaptureRequest(device, deviceDetails, repeatingOutputs) + session.setRepeatingRequest(idleRequest.build(), null, null) + } } } @@ -172,66 +197,21 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p val repeatingRequest = repeatingRequest ?: throw CameraNotReadyError() val device = session.device val deviceDetails = getOrCreateCameraDeviceDetails(device) - if (!deviceDetails.supportsTapToFocus) { + if (!deviceDetails.supportsFocusRegions) { throw FocusNotSupportedError() } val outputs = outputs.filter { it.isRepeating } - val meteringRectangle = MeteringRectangle(point, DEFAULT_METERING_SIZE, MeteringRectangle.METERING_WEIGHT_MAX - 1) // 0. Cancel the 3 second focus reset task focusResetJob?.cancelAndJoin() focusResetJob = null - // 1. Cancel any ongoing AF/AE/AWB request - repeatingRequest.createCaptureRequest(device, deviceDetails, outputs).also { request -> - if (deviceDetails.supportsTapToFocus) { - request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL) - } - if (deviceDetails.supportsTapToExposure) { - request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL) - } - session.capture(request.build(), null, null) - } + // 1. Run a precapture sequence for AF, AE and AWB. + val request = repeatingRequest.createCaptureRequest(device, deviceDetails, outputs) + val options = PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point)) + session.precapture(request, deviceDetails, options) - // 2. After previous AF/AE/AWB requests have been canceled, start a new AF/AE/AWB request - repeatingRequest.createCaptureRequest(device, deviceDetails, outputs).also { request -> - request.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO) - if (deviceDetails.supportsTapToFocus) { - request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO) - request.set(CaptureRequest.CONTROL_AF_REGIONS, arrayOf(meteringRectangle)) - request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START) - } - if (deviceDetails.supportsTapToExposure) { - request.set(CaptureRequest.CONTROL_AE_REGIONS, arrayOf(meteringRectangle)) - request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) - } - if (deviceDetails.supportsTapToWhiteBalance) { - request.set(CaptureRequest.CONTROL_AWB_REGIONS, arrayOf(meteringRectangle)) - } - session.capture(request.build(), null, null) - - // 3. Start a repeating request without the trigger and wait until AF/AE/AWB locks - request.set(CaptureRequest.CONTROL_AF_TRIGGER, null) - request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, null) - session.setRepeatingRequestAndWaitForAF(request.build()) - } - - // 4. After the Camera has successfully found the AF/AE/AWB lock-point, we set it to idle and keep the point metered - repeatingRequest.createCaptureRequest(device, deviceDetails, outputs).also { request -> - request.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO) - if (deviceDetails.supportsTapToFocus) { - request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO) - request.set(CaptureRequest.CONTROL_AF_REGIONS, arrayOf(meteringRectangle)) - request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE) - } - if (deviceDetails.supportsTapToExposure) { - request.set(CaptureRequest.CONTROL_AE_REGIONS, arrayOf(meteringRectangle)) - request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE) - } - session.setRepeatingRequest(request.build(), null, null) - } - - // 5. Wait 3 seconds + // 2. Wait 3 seconds focusResetJob = coroutineScope.launch { delay(FOCUS_RESET_TIMEOUT) if (!this.isActive) { @@ -243,7 +223,7 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p return@launch } Log.i(TAG, "Resetting focus to auto-focus...") - // 6. Reset AF/AE/AWB to continuous auto-focus again, which is the default here. + // 3. Reset AF/AE/AWB to continuous auto-focus again, which is the default here. repeatingRequest.createCaptureRequest(device, deviceDetails, outputs).also { request -> session.setRepeatingRequest(request.build(), null, null) } diff --git a/package/android/src/main/java/com/mrousavy/camera/core/capture/PhotoCaptureRequest.kt b/package/android/src/main/java/com/mrousavy/camera/core/capture/PhotoCaptureRequest.kt index 82b8dc0..cfb5ae0 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/capture/PhotoCaptureRequest.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/capture/PhotoCaptureRequest.kt @@ -3,10 +3,12 @@ package com.mrousavy.camera.core.capture import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraDevice import android.hardware.camera2.CaptureRequest +import android.os.Build import android.util.Log import com.mrousavy.camera.core.CameraDeviceDetails import com.mrousavy.camera.core.outputs.SurfaceOutput import com.mrousavy.camera.types.Flash +import com.mrousavy.camera.types.HardwareLevel import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.QualityPrioritization import com.mrousavy.camera.types.Torch @@ -67,13 +69,70 @@ class PhotoCaptureRequest( ): CaptureRequest.Builder { val builder = super.createCaptureRequest(template, device, deviceDetails, outputs) - // Set JPEG quality - val jpegQuality = when (qualityPrioritization) { - QualityPrioritization.SPEED -> 85 - QualityPrioritization.BALANCED -> 92 - QualityPrioritization.QUALITY -> 100 + // Set various speed vs quality optimization flags + when (qualityPrioritization) { + QualityPrioritization.SPEED -> { + if (deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.FULL)) { + builder.set(CaptureRequest.COLOR_CORRECTION_MODE, CaptureRequest.COLOR_CORRECTION_MODE_FAST) + if (deviceDetails.availableEdgeModes.contains(CaptureRequest.EDGE_MODE_FAST)) { + builder.set(CaptureRequest.EDGE_MODE, CaptureRequest.EDGE_MODE_FAST) + } + } + if (deviceDetails.availableAberrationModes.contains(CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE_FAST)) { + builder.set(CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE, CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE_FAST) + } + if (deviceDetails.availableHotPixelModes.contains(CaptureRequest.HOT_PIXEL_MODE_FAST)) { + builder.set(CaptureRequest.HOT_PIXEL_MODE, CaptureRequest.HOT_PIXEL_MODE_FAST) + } + if (deviceDetails.availableDistortionCorrectionModes.contains(CaptureRequest.DISTORTION_CORRECTION_MODE_FAST) && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.P + ) { + builder.set(CaptureRequest.DISTORTION_CORRECTION_MODE, CaptureRequest.DISTORTION_CORRECTION_MODE_FAST) + } + if (deviceDetails.availableNoiseReductionModes.contains(CaptureRequest.NOISE_REDUCTION_MODE_FAST)) { + builder.set(CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_FAST) + } + if (deviceDetails.availableShadingModes.contains(CaptureRequest.SHADING_MODE_FAST)) { + builder.set(CaptureRequest.SHADING_MODE, CaptureRequest.SHADING_MODE_FAST) + } + if (deviceDetails.availableToneMapModes.contains(CaptureRequest.TONEMAP_MODE_FAST)) { + builder.set(CaptureRequest.TONEMAP_MODE, CaptureRequest.TONEMAP_MODE_FAST) + } + builder.set(CaptureRequest.JPEG_QUALITY, 85) + } + QualityPrioritization.BALANCED -> { + builder.set(CaptureRequest.JPEG_QUALITY, 92) + } + QualityPrioritization.QUALITY -> { + if (deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.FULL)) { + builder.set(CaptureRequest.COLOR_CORRECTION_MODE, CaptureRequest.COLOR_CORRECTION_MODE_HIGH_QUALITY) + if (deviceDetails.availableEdgeModes.contains(CaptureRequest.EDGE_MODE_HIGH_QUALITY)) { + builder.set(CaptureRequest.EDGE_MODE, CaptureRequest.EDGE_MODE_HIGH_QUALITY) + } + } + if (deviceDetails.availableAberrationModes.contains(CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE_HIGH_QUALITY)) { + builder.set(CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE, CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE_HIGH_QUALITY) + } + if (deviceDetails.availableHotPixelModes.contains(CaptureRequest.HOT_PIXEL_MODE_HIGH_QUALITY)) { + builder.set(CaptureRequest.HOT_PIXEL_MODE, CaptureRequest.HOT_PIXEL_MODE_HIGH_QUALITY) + } + if (deviceDetails.availableDistortionCorrectionModes.contains(CaptureRequest.DISTORTION_CORRECTION_MODE_HIGH_QUALITY) && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.P + ) { + builder.set(CaptureRequest.DISTORTION_CORRECTION_MODE, CaptureRequest.DISTORTION_CORRECTION_MODE_HIGH_QUALITY) + } + if (deviceDetails.availableNoiseReductionModes.contains(CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY)) { + builder.set(CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY) + } + if (deviceDetails.availableShadingModes.contains(CaptureRequest.SHADING_MODE_HIGH_QUALITY)) { + builder.set(CaptureRequest.SHADING_MODE, CaptureRequest.SHADING_MODE_HIGH_QUALITY) + } + if (deviceDetails.availableToneMapModes.contains(CaptureRequest.TONEMAP_MODE_HIGH_QUALITY)) { + builder.set(CaptureRequest.TONEMAP_MODE, CaptureRequest.TONEMAP_MODE_HIGH_QUALITY) + } + builder.set(CaptureRequest.JPEG_QUALITY, 100) + } } - builder.set(CaptureRequest.JPEG_QUALITY, jpegQuality.toByte()) // Set JPEG Orientation val targetOrientation = outputOrientation.toSensorRelativeOrientation(deviceDetails) diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+capture.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+capture.kt index a3dce45..4ff3e3f 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+capture.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+capture.kt @@ -5,17 +5,34 @@ import android.hardware.camera2.CaptureFailure import android.hardware.camera2.CaptureRequest import android.hardware.camera2.TotalCaptureResult import android.media.MediaActionSound +import android.util.Log import com.mrousavy.camera.core.CaptureAbortedError +import com.mrousavy.camera.core.CaptureTimedOutError import com.mrousavy.camera.core.UnknownCaptureError import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine + +private const val TAG = "CameraCaptureSession" suspend fun CameraCaptureSession.capture(captureRequest: CaptureRequest, enableShutterSound: Boolean): TotalCaptureResult = - suspendCoroutine { continuation -> + suspendCancellableCoroutine { continuation -> val shutterSound = if (enableShutterSound) MediaActionSound() else null shutterSound?.load(MediaActionSound.SHUTTER_CLICK) + CoroutineScope(Dispatchers.Default).launch { + delay(5000) // after 5s, cancel capture + if (continuation.isActive) { + Log.e(TAG, "Capture timed out after 5 seconds!") + continuation.resumeWithException(CaptureTimedOutError()) + tryAbortCaptures() + } + } + this.capture( captureRequest, object : CameraCaptureSession.CaptureCallback() { diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt new file mode 100644 index 0000000..0372e05 --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt @@ -0,0 +1,99 @@ +package com.mrousavy.camera.extensions + +import android.graphics.Point +import android.hardware.camera2.CameraCaptureSession +import android.hardware.camera2.CaptureRequest +import android.hardware.camera2.CaptureResult +import android.hardware.camera2.params.MeteringRectangle +import android.util.Log +import android.util.Size +import com.mrousavy.camera.core.CameraDeviceDetails +import com.mrousavy.camera.types.Flash +import com.mrousavy.camera.types.HardwareLevel + +data class PrecaptureOptions(val modes: List, val flash: Flash = Flash.OFF, val pointsOfInterest: List) + +data class PrecaptureResult(val needsFlash: Boolean) + +private const val TAG = "Precapture" +private val DEFAULT_METERING_SIZE = Size(100, 100) + +/** + * Run a precapture sequence to trigger an AF, AE or AWB scan and lock to the optimal values. + * After this function completes, you can capture high quality photos as AF/AE/AWB are in focused state. + * + * To reset to auto-focus again, create a new `RepeatingRequest` with a fresh set of CONTROL_MODEs set. + */ +suspend fun CameraCaptureSession.precapture( + request: CaptureRequest.Builder, + deviceDetails: CameraDeviceDetails, + options: PrecaptureOptions +): PrecaptureResult { + Log.i(TAG, "Running precapture sequence... ($options)") + request.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO) + + var enableFlash = options.flash == Flash.ON + + // 1. Cancel any ongoing precapture sequences + request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL) + request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL) + if (options.flash == Flash.AUTO) { + // we want Auto-Flash, so check the current lighting conditions if we need it. + val result = this.capture(request.build(), false) + val aeState = result.get(CaptureResult.CONTROL_AE_STATE) + if (aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { + Log.i(TAG, "Auto-Flash: Flash is required for photo capture, enabling flash...") + enableFlash = true + } else { + Log.i(TAG, "Auto-Flash: Flash is not required for photo capture.") + } + } else { + // we either want Flash ON or OFF, so we don't care about lighting conditions - do a fast capture. + this.capture(request.build(), null, null) + } + + val meteringWeight = MeteringRectangle.METERING_WEIGHT_MAX - 1 + val meteringRectangles = options.pointsOfInterest.map { point -> + MeteringRectangle(point, DEFAULT_METERING_SIZE, meteringWeight) + }.toTypedArray() + + // 2. Submit a precapture start sequence + if (enableFlash && deviceDetails.hasFlash) { + request.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH) + } + if (options.modes.contains(PrecaptureTrigger.AF)) { + // AF Precapture + if (deviceDetails.afModes.contains(CaptureRequest.CONTROL_AF_MODE_AUTO)) { + request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO) + } + if (meteringRectangles.isNotEmpty() && deviceDetails.supportsFocusRegions) { + request.set(CaptureRequest.CONTROL_AF_REGIONS, meteringRectangles) + } + request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START) + } + if (options.modes.contains(PrecaptureTrigger.AE) && deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED)) { + // AE Precapture + if (meteringRectangles.isNotEmpty() && deviceDetails.supportsExposureRegions) { + request.set(CaptureRequest.CONTROL_AE_REGIONS, meteringRectangles) + } + request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) + } + if (options.modes.contains(PrecaptureTrigger.AWB)) { + // AWB Precapture + if (meteringRectangles.isNotEmpty() && deviceDetails.supportsWhiteBalanceRegions) { + request.set(CaptureRequest.CONTROL_AWB_REGIONS, meteringRectangles) + } + } + this.capture(request.build(), null, null) + + // 3. Start a repeating request without the trigger and wait until AF/AE/AWB locks + request.set(CaptureRequest.CONTROL_AF_TRIGGER, null) + request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, null) + val result = this.setRepeatingRequestAndWaitForPrecapture(request.build(), *options.modes.toTypedArray()) + + Log.i(TAG, "AF/AE/AWB successfully locked!") + // TODO: Set to idle again? + + val needsFlash = result.exposureState == ExposureState.FlashRequired + return PrecaptureResult(needsFlash) +} diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForAF.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForAF.kt deleted file mode 100644 index e085a02..0000000 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForAF.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.mrousavy.camera.extensions - -import android.hardware.camera2.CameraCaptureSession -import android.hardware.camera2.CaptureFailure -import android.hardware.camera2.CaptureRequest -import android.hardware.camera2.CaptureResult -import android.hardware.camera2.TotalCaptureResult -import android.util.Log -import com.mrousavy.camera.core.CaptureAbortedError -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlinx.coroutines.suspendCancellableCoroutine - -private const val TAG = "CameraCaptureSession" - -/** - * Set a new repeating request for the [CameraCaptureSession] that contains an AF trigger, and wait until AF has locked. - */ -suspend fun CameraCaptureSession.setRepeatingRequestAndWaitForAF(request: CaptureRequest) = - suspendCancellableCoroutine { continuation -> - this.setRepeatingRequest( - request, - object : CameraCaptureSession.CaptureCallback() { - override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) { - super.onCaptureCompleted(session, request, result) - - if (continuation.isActive) { - val afState = result.get(CaptureResult.CONTROL_AF_STATE) - Log.i(TAG, "AF State: $afState") - if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { - continuation.resume(Unit) - session.setRepeatingRequest(request, null, null) - } - } - } - override fun onCaptureFailed(session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure) { - super.onCaptureFailed(session, request, failure) - - if (continuation.isActive) { - continuation.resumeWithException(CaptureAbortedError(failure.wasImageCaptured())) - session.setRepeatingRequest(request, null, null) - } - } - }, - null - ) - } diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt new file mode 100644 index 0000000..a655657 --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt @@ -0,0 +1,178 @@ +package com.mrousavy.camera.extensions + +import android.hardware.camera2.CameraCaptureSession +import android.hardware.camera2.CaptureFailure +import android.hardware.camera2.CaptureRequest +import android.hardware.camera2.CaptureResult +import android.hardware.camera2.TotalCaptureResult +import android.util.Log +import com.mrousavy.camera.core.CaptureAbortedError +import com.mrousavy.camera.core.CaptureTimedOutError +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine + +private const val TAG = "CameraCaptureSession" + +enum class PrecaptureTrigger { + AE, + AF, + AWB +} + +enum class FocusState { + Inactive, + Scanning, + Focused, + Unfocused, + PassiveScanning, + PassiveFocused, + PassiveUnfocused; + + val isCompleted: Boolean + get() = this == Focused || this == Unfocused + + companion object { + fun fromAFState(afState: Int): FocusState = + when (afState) { + CaptureResult.CONTROL_AF_STATE_INACTIVE -> Inactive + CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN -> Scanning + CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED -> Focused + CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED -> Unfocused + CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN -> PassiveScanning + CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED -> PassiveFocused + CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED -> PassiveUnfocused + else -> throw Error("Invalid CONTROL_AF_STATE! $afState") + } + } +} +enum class ExposureState { + Locked, + Inactive, + Precapture, + Searching, + Converged, + FlashRequired; + + val isCompleted: Boolean + get() = this == Converged || this == FlashRequired + + companion object { + fun fromAEState(aeState: Int): ExposureState = + when (aeState) { + CaptureResult.CONTROL_AE_STATE_INACTIVE -> Inactive + CaptureResult.CONTROL_AE_STATE_SEARCHING -> Searching + CaptureResult.CONTROL_AE_STATE_PRECAPTURE -> Precapture + CaptureResult.CONTROL_AE_STATE_CONVERGED -> Converged + CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED -> FlashRequired + CaptureResult.CONTROL_AE_STATE_LOCKED -> Locked + else -> throw Error("Invalid CONTROL_AE_STATE! $aeState") + } + } +} + +enum class WhiteBalanceState { + Inactive, + Locked, + Searching, + Converged; + + val isCompleted: Boolean + get() = this == Converged + + companion object { + fun fromAWBState(awbState: Int): WhiteBalanceState = + when (awbState) { + CaptureResult.CONTROL_AWB_STATE_INACTIVE -> Inactive + CaptureResult.CONTROL_AWB_STATE_SEARCHING -> Searching + CaptureResult.CONTROL_AWB_STATE_CONVERGED -> Converged + CaptureResult.CONTROL_AWB_STATE_LOCKED -> Locked + else -> throw Error("Invalid CONTROL_AWB_STATE! $awbState") + } + } +} + +data class ResultState(val focusState: FocusState, val exposureState: ExposureState, val whiteBalanceState: WhiteBalanceState) + +/** + * Set a new repeating request for the [CameraCaptureSession] that contains a precapture trigger, and wait until the given precaptures have locked. + */ +suspend fun CameraCaptureSession.setRepeatingRequestAndWaitForPrecapture( + request: CaptureRequest, + vararg precaptureTriggers: PrecaptureTrigger +): ResultState = + suspendCancellableCoroutine { continuation -> + // Map of all completed precaptures + val completed = precaptureTriggers.associateWith { false }.toMutableMap() + + CoroutineScope(Dispatchers.Default).launch { + delay(5000) // after 5s, cancel capture + if (continuation.isActive) { + Log.e(TAG, "Precapture timed out after 5 seconds!") + continuation.resumeWithException(CaptureTimedOutError()) + try { + setRepeatingRequest(request, null, null) + } catch (e: Throwable) { + // session might have already been closed + Log.e(TAG, "Error resetting session repeating request..", e) + } + } + } + + this.setRepeatingRequest( + request, + object : CameraCaptureSession.CaptureCallback() { + override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) { + super.onCaptureCompleted(session, request, result) + + if (continuation.isActive) { + // AF Precapture + val afState = FocusState.fromAFState(result.get(CaptureResult.CONTROL_AF_STATE) ?: CaptureResult.CONTROL_AF_STATE_INACTIVE) + val aeState = ExposureState.fromAEState(result.get(CaptureResult.CONTROL_AE_STATE) ?: CaptureResult.CONTROL_AE_STATE_INACTIVE) + val awbState = WhiteBalanceState.fromAWBState( + result.get(CaptureResult.CONTROL_AWB_STATE) ?: CaptureResult.CONTROL_AWB_STATE_INACTIVE + ) + + if (precaptureTriggers.contains(PrecaptureTrigger.AF)) { + Log.i(TAG, "AF State: $afState (isCompleted: ${afState.isCompleted})") + completed[PrecaptureTrigger.AF] = afState.isCompleted + } + // AE Precapture + if (precaptureTriggers.contains(PrecaptureTrigger.AE)) { + Log.i(TAG, "AE State: $aeState (isCompleted: ${aeState.isCompleted})") + completed[PrecaptureTrigger.AE] = aeState.isCompleted + } + // AWB Precapture + if (precaptureTriggers.contains(PrecaptureTrigger.AWB)) { + Log.i(TAG, "AWB State: $awbState (isCompleted: ${awbState.isCompleted})") + completed[PrecaptureTrigger.AWB] = awbState.isCompleted + } + + if (completed.values.all { it == true }) { + // All precaptures did complete! + continuation.resume(ResultState(afState, aeState, awbState)) + session.setRepeatingRequest(request, null, null) + } + } + } + override fun onCaptureFailed(session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure) { + super.onCaptureFailed(session, request, failure) + + if (continuation.isActive) { + // Capture failed or session closed. + continuation.resumeWithException(CaptureAbortedError(failure.wasImageCaptured())) + try { + session.setRepeatingRequest(request, null, null) + } catch (e: Throwable) { + Log.e(TAG, "Failed to continue repeating request!", e) + } + } + } + }, + null + ) + } diff --git a/package/android/src/main/java/com/mrousavy/camera/types/HardwareLevel.kt b/package/android/src/main/java/com/mrousavy/camera/types/HardwareLevel.kt index 2df2049..ca34ea6 100644 --- a/package/android/src/main/java/com/mrousavy/camera/types/HardwareLevel.kt +++ b/package/android/src/main/java/com/mrousavy/camera/types/HardwareLevel.kt @@ -9,6 +9,19 @@ enum class HardwareLevel(override val unionValue: String) : JSUnionValue { FULL("full"), LEVEL_3("full"); + private val rank: Int + get() { + return when (this) { + LEGACY -> 0 + LIMITED -> 1 + EXTERNAL -> 1 + FULL -> 2 + LEVEL_3 -> 3 + } + } + + fun isAtLeast(level: HardwareLevel): Boolean = this.rank >= level.rank + companion object { fun fromCameraCharacteristics(cameraCharacteristics: CameraCharacteristics): HardwareLevel = when (cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)) { diff --git a/package/example/src/views/CaptureButton.tsx b/package/example/src/views/CaptureButton.tsx index 16da7bf..e4a91c8 100644 --- a/package/example/src/views/CaptureButton.tsx +++ b/package/example/src/views/CaptureButton.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useRef } from 'react' +import React, { useCallback, useRef } from 'react' import { StyleSheet, View, ViewProps } from 'react-native' import { PanGestureHandler, @@ -19,7 +19,7 @@ import Reanimated, { useSharedValue, withRepeat, } from 'react-native-reanimated' -import type { Camera, PhotoFile, TakePhotoOptions, VideoFile } from 'react-native-vision-camera' +import type { Camera, PhotoFile, VideoFile } from 'react-native-vision-camera' import { CAPTURE_BUTTON_SIZE, SCREEN_HEIGHT, SCREEN_WIDTH } from './../Constants' const PAN_GESTURE_HANDLER_FAIL_X = [-SCREEN_WIDTH, SCREEN_WIDTH] @@ -58,15 +58,6 @@ const _CaptureButton: React.FC = ({ const pressDownDate = useRef(undefined) const isRecording = useRef(false) const recordingProgress = useSharedValue(0) - const takePhotoOptions = useMemo( - () => ({ - qualityPrioritization: 'speed', - flash: flash, - quality: 90, - enableShutterSound: false, - }), - [flash], - ) const isPressingButton = useSharedValue(false) //#region Camera Capture @@ -75,12 +66,16 @@ const _CaptureButton: React.FC = ({ if (camera.current == null) throw new Error('Camera ref is null!') console.log('Taking photo...') - const photo = await camera.current.takePhoto(takePhotoOptions) + const photo = await camera.current.takePhoto({ + qualityPrioritization: 'quality', + flash: flash, + enableShutterSound: false, + }) onMediaCaptured(photo, 'photo') } catch (e) { console.error('Failed to take photo!', e) } - }, [camera, onMediaCaptured, takePhotoOptions]) + }, [camera, flash, onMediaCaptured]) const onStoppedRecording = useCallback(() => { isRecording.current = false diff --git a/package/src/CameraError.ts b/package/src/CameraError.ts index e39beb5..f47f61f 100644 --- a/package/src/CameraError.ts +++ b/package/src/CameraError.ts @@ -43,6 +43,7 @@ export type CaptureError = | 'capture/photo-not-enabled' | 'capture/frame-invalid' | 'capture/aborted' + | 'capture/timed-out' | 'capture/unknown' export type SystemError = | 'system/camera-module-not-found' From a7701c8c9c2b5eaefb75190ca09ed28f143994f8 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 14 Feb 2024 17:04:25 +0100 Subject: [PATCH 33/71] perf: Allow skipping pre-capture sequence if already focused (#2561) This PR speeds up photo capture on Android by skipping pre-capture sequences on modes that are already focused (either AF, AE or AWB) --- .../core/PersistentCameraCaptureSession.kt | 11 ++++- .../CameraCaptureSession+precapture.kt | 47 +++++++++++++++---- ...setRepeatingRequestAndWaitForPrecapture.kt | 23 ++++++--- 3 files changed, 64 insertions(+), 17 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt index f953b23..df580b5 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt @@ -172,7 +172,14 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p // 1. Run precapture sequence val precaptureRequest = repeatingRequest.createCaptureRequest(device, deviceDetails, repeatingOutputs) - val options = PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE, PrecaptureTrigger.AWB), flash, emptyList()) + val skipIfPassivelyFocused = flash == Flash.OFF + val options = + PrecaptureOptions( + listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE, PrecaptureTrigger.AWB), + flash, + emptyList(), + skipIfPassivelyFocused + ) val result = session.precapture(precaptureRequest, deviceDetails, options) try { @@ -208,7 +215,7 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p // 1. Run a precapture sequence for AF, AE and AWB. val request = repeatingRequest.createCaptureRequest(device, deviceDetails, outputs) - val options = PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point)) + val options = PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point), false) session.precapture(request, deviceDetails, options) // 2. Wait 3 seconds diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt index 0372e05..89af52c 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt @@ -11,7 +11,12 @@ import com.mrousavy.camera.core.CameraDeviceDetails import com.mrousavy.camera.types.Flash import com.mrousavy.camera.types.HardwareLevel -data class PrecaptureOptions(val modes: List, val flash: Flash = Flash.OFF, val pointsOfInterest: List) +data class PrecaptureOptions( + val modes: List, + val flash: Flash = Flash.OFF, + val pointsOfInterest: List, + val skipIfPassivelyFocused: Boolean +) data class PrecaptureResult(val needsFlash: Boolean) @@ -33,15 +38,23 @@ suspend fun CameraCaptureSession.precapture( request.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO) var enableFlash = options.flash == Flash.ON + var afState = FocusState.Inactive + var aeState = ExposureState.Inactive + var awbState = WhiteBalanceState.Inactive + val precaptureModes = options.modes.toMutableList() // 1. Cancel any ongoing precapture sequences request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL) request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL) - if (options.flash == Flash.AUTO) { - // we want Auto-Flash, so check the current lighting conditions if we need it. + if (options.flash == Flash.AUTO || options.skipIfPassivelyFocused) { + // We want to read the current AE/AF/AWB values to determine if we need flash or can skip AF/AE/AWB precapture val result = this.capture(request.build(), false) - val aeState = result.get(CaptureResult.CONTROL_AE_STATE) - if (aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { + + afState = FocusState.fromAFState(result.get(CaptureResult.CONTROL_AF_STATE) ?: CaptureResult.CONTROL_AF_STATE_INACTIVE) + aeState = ExposureState.fromAEState(result.get(CaptureResult.CONTROL_AE_STATE) ?: CaptureResult.CONTROL_AE_STATE_INACTIVE) + awbState = WhiteBalanceState.fromAWBState(result.get(CaptureResult.CONTROL_AWB_STATE) ?: CaptureResult.CONTROL_AWB_STATE_INACTIVE) + + if (aeState == ExposureState.FlashRequired) { Log.i(TAG, "Auto-Flash: Flash is required for photo capture, enabling flash...") enableFlash = true } else { @@ -57,11 +70,27 @@ suspend fun CameraCaptureSession.precapture( MeteringRectangle(point, DEFAULT_METERING_SIZE, meteringWeight) }.toTypedArray() + if (options.skipIfPassivelyFocused) { + // If user allows us to skip precapture for values that are already focused, remove them from the precapture modes. + if (afState.isPassivelyFocused) { + Log.i(TAG, "AF is already focused, skipping...") + precaptureModes.remove(PrecaptureTrigger.AF) + } + if (aeState.isPassivelyFocused) { + Log.i(TAG, "AE is already focused, skipping...") + precaptureModes.remove(PrecaptureTrigger.AE) + } + if (awbState.isPassivelyFocused) { + Log.i(TAG, "AWB is already focused, skipping...") + precaptureModes.remove(PrecaptureTrigger.AWB) + } + } + // 2. Submit a precapture start sequence if (enableFlash && deviceDetails.hasFlash) { request.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH) } - if (options.modes.contains(PrecaptureTrigger.AF)) { + if (precaptureModes.contains(PrecaptureTrigger.AF)) { // AF Precapture if (deviceDetails.afModes.contains(CaptureRequest.CONTROL_AF_MODE_AUTO)) { request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO) @@ -71,14 +100,14 @@ suspend fun CameraCaptureSession.precapture( } request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START) } - if (options.modes.contains(PrecaptureTrigger.AE) && deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED)) { + if (precaptureModes.contains(PrecaptureTrigger.AE) && deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED)) { // AE Precapture if (meteringRectangles.isNotEmpty() && deviceDetails.supportsExposureRegions) { request.set(CaptureRequest.CONTROL_AE_REGIONS, meteringRectangles) } request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) } - if (options.modes.contains(PrecaptureTrigger.AWB)) { + if (precaptureModes.contains(PrecaptureTrigger.AWB)) { // AWB Precapture if (meteringRectangles.isNotEmpty() && deviceDetails.supportsWhiteBalanceRegions) { request.set(CaptureRequest.CONTROL_AWB_REGIONS, meteringRectangles) @@ -89,7 +118,7 @@ suspend fun CameraCaptureSession.precapture( // 3. Start a repeating request without the trigger and wait until AF/AE/AWB locks request.set(CaptureRequest.CONTROL_AF_TRIGGER, null) request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, null) - val result = this.setRepeatingRequestAndWaitForPrecapture(request.build(), *options.modes.toTypedArray()) + val result = this.setRepeatingRequestAndWaitForPrecapture(request.build(), *precaptureModes.toTypedArray()) Log.i(TAG, "AF/AE/AWB successfully locked!") // TODO: Set to idle again? diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt index a655657..70b3fb7 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt @@ -24,7 +24,12 @@ enum class PrecaptureTrigger { AWB } -enum class FocusState { +interface AutoState { + val isCompleted: Boolean + val isPassivelyFocused: Boolean +} + +enum class FocusState : AutoState { Inactive, Scanning, Focused, @@ -33,8 +38,10 @@ enum class FocusState { PassiveFocused, PassiveUnfocused; - val isCompleted: Boolean + override val isCompleted: Boolean get() = this == Focused || this == Unfocused + override val isPassivelyFocused: Boolean + get() = this == PassiveFocused companion object { fun fromAFState(afState: Int): FocusState = @@ -50,7 +57,7 @@ enum class FocusState { } } } -enum class ExposureState { +enum class ExposureState : AutoState { Locked, Inactive, Precapture, @@ -58,8 +65,10 @@ enum class ExposureState { Converged, FlashRequired; - val isCompleted: Boolean + override val isCompleted: Boolean get() = this == Converged || this == FlashRequired + override val isPassivelyFocused: Boolean + get() = this == Converged companion object { fun fromAEState(aeState: Int): ExposureState = @@ -75,13 +84,15 @@ enum class ExposureState { } } -enum class WhiteBalanceState { +enum class WhiteBalanceState : AutoState { Inactive, Locked, Searching, Converged; - val isCompleted: Boolean + override val isCompleted: Boolean + get() = this == Converged + override val isPassivelyFocused: Boolean get() = this == Converged companion object { From 21042048aee6a7b81f5819c8b3ba0af1a0f7ddbc Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 14 Feb 2024 17:05:05 +0100 Subject: [PATCH 34/71] chore: release 3.9.0-beta.4 --- package/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/package.json b/package/package.json index 8030f3a..21e949f 100644 --- a/package/package.json +++ b/package/package.json @@ -1,6 +1,6 @@ { "name": "react-native-vision-camera", - "version": "3.9.0-beta.3", + "version": "3.9.0-beta.4", "description": "A powerful, high-performance React Native Camera library.", "main": "lib/commonjs/index", "module": "lib/module/index", From 5df5ca9adfcba280cd7e01f05bfb9b799247db1c Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 15 Feb 2024 13:09:16 +0100 Subject: [PATCH 35/71] fix: Fix `PreviewView` stretching on Android (now finally a real fix) (#2564) * fix: Only resolve once SurfaceHolder actually resized * fix: Fix onMeasure not being called for `PreviewView` * fix: Auto-trigger layout computation on Surface Change * fix: Add proper LayoutParams to `PreviewView` * Format --- .../java/com/mrousavy/camera/CameraView.kt | 6 ++ .../com/mrousavy/camera/core/PreviewView.kt | 57 +++++++++++-------- .../camera/extensions/SurfaceHolder+resize.kt | 20 ++++--- 3 files changed, 52 insertions(+), 31 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt index 7f0f7b4..12bffc7 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.Context import android.hardware.camera2.CameraManager import android.util.Log +import android.view.Gravity import android.view.ScaleGestureDetector import android.widget.FrameLayout import com.facebook.react.bridge.ReadableMap @@ -110,6 +111,11 @@ class CameraView(context: Context) : clipToOutline = true cameraSession = CameraSession(context, cameraManager, this) previewView = cameraSession.createPreviewView(context) + previewView.layoutParams = LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT, + Gravity.CENTER + ) addView(previewView) } diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt b/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt index c862be7..df33b83 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt @@ -11,6 +11,7 @@ import android.view.SurfaceView import android.widget.FrameLayout import com.facebook.react.bridge.UiThreadUtil import com.mrousavy.camera.extensions.getMaximumPreviewSize +import com.mrousavy.camera.extensions.installHierarchyFitter import com.mrousavy.camera.extensions.resize import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.ResizeMode @@ -19,7 +20,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @SuppressLint("ViewConstructor") -class PreviewView(context: Context, callback: SurfaceHolder.Callback) : SurfaceView(context) { +class PreviewView(context: Context, callback: SurfaceHolder.Callback) : + FrameLayout(context), + SurfaceHolder.Callback { var size: Size = getMaximumPreviewSize() private set var resizeMode: ResizeMode = ResizeMode.COVER @@ -31,28 +34,6 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : SurfaceV invalidate() } } - - init { - Log.i(TAG, "Creating PreviewView...") - layoutParams = FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.MATCH_PARENT, - Gravity.CENTER - ) - holder.setKeepScreenOn(true) - holder.addCallback(callback) - } - - suspend fun setSurfaceSize(width: Int, height: Int) { - withContext(Dispatchers.Main) { - size = Size(width, height) - Log.i(TAG, "Setting PreviewView Surface Size to $size...") - requestLayout() - invalidate() - holder.resize(width, height) - } - } - private val viewSize: Size get() { val displayMetrics = context.resources.displayMetrics @@ -60,6 +41,36 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : SurfaceV val dpY = height / displayMetrics.density return Size(dpX.toInt(), dpY.toInt()) } + private val surfaceView = SurfaceView(context) + + init { + Log.i(TAG, "Creating PreviewView...") + this.installHierarchyFitter() + surfaceView.layoutParams = LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT, + Gravity.CENTER + ) + surfaceView.holder.setKeepScreenOn(true) + surfaceView.holder.addCallback(this) + surfaceView.holder.addCallback(callback) + addView(surfaceView) + } + + override fun surfaceCreated(holder: SurfaceHolder) = Unit + override fun surfaceDestroyed(holder: SurfaceHolder) = Unit + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + Log.i(TAG, "PreviewView Surface size changed: $size -> ${width}x$height, re-computing layout...") + size = Size(width, height) + requestLayout() + invalidate() + } + + suspend fun setSurfaceSize(width: Int, height: Int) { + withContext(Dispatchers.Main) { + surfaceView.holder.resize(width, height) + } + } fun convertLayerPointToCameraCoordinates(point: Point, cameraDeviceDetails: CameraDeviceDetails): Point { val sensorOrientation = Orientation.fromRotationDegrees(cameraDeviceDetails.sensorOrientation) diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/SurfaceHolder+resize.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/SurfaceHolder+resize.kt index cc0fe9c..561f6fc 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/SurfaceHolder+resize.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/SurfaceHolder+resize.kt @@ -6,32 +6,36 @@ import androidx.annotation.UiThread import kotlin.coroutines.resume import kotlinx.coroutines.suspendCancellableCoroutine +private const val TAG = "SurfaceHolder" + @UiThread -suspend fun SurfaceHolder.resize(width: Int, height: Int) { +suspend fun SurfaceHolder.resize(targetWidth: Int, targetHeight: Int) { return suspendCancellableCoroutine { continuation -> val currentSize = this.surfaceFrame - if (currentSize.width() == width && currentSize.height() == height) { + if (currentSize.width() == targetWidth && currentSize.height() == targetHeight) { // Already in target size continuation.resume(Unit) return@suspendCancellableCoroutine } - Log.i("SurfaceHolder", "Resizing SurfaceHolder to $width x $height...") + Log.i(TAG, "Resizing SurfaceHolder to $targetWidth x $targetHeight...") val callback = object : SurfaceHolder.Callback { override fun surfaceCreated(holder: SurfaceHolder) = Unit override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { - holder.removeCallback(this) - Log.i("SurfaceHolder", "Resized SurfaceHolder to $width x $height!") - continuation.resume(Unit) + if (width == targetWidth && height == targetHeight) { + holder.removeCallback(this) + Log.i(TAG, "Resized SurfaceHolder to $width x $height!") + continuation.resume(Unit) + } } override fun surfaceDestroyed(holder: SurfaceHolder) { holder.removeCallback(this) - Log.e("SurfaceHolder", "Failed to resize SurfaceHolder to $width x $height!") + Log.e(TAG, "Failed to resize SurfaceHolder to $targetWidth x $targetHeight!") continuation.cancel(Error("Tried to resize SurfaceView, but Surface has been destroyed!")) } } this.addCallback(callback) - this.setFixedSize(width, height) + this.setFixedSize(targetWidth, targetHeight) } } From 83c0184796288076b50b11c488491a380a09de43 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 15 Feb 2024 13:30:14 +0100 Subject: [PATCH 36/71] fix: Take Orientation into account for `PreviewView` (#2565) * fix: Take Orientation into account for `PreviewView` * Log * Take aspect ratio into account * Reorganize code a bit * Set LANDSCAPE_LEFT as default * chore: Format --- .../camera/core/CameraDeviceDetails.kt | 37 ++++++++++++++++--- .../com/mrousavy/camera/core/CameraSession.kt | 20 +++++----- .../com/mrousavy/camera/core/PreviewView.kt | 29 ++++++++++----- .../CameraCharacteristics+getPreviewSize.kt | 29 --------------- .../camera/extensions/Point+rotatedBy.kt | 25 +++++++++++++ .../com/mrousavy/camera/types/Orientation.kt | 30 +-------------- 6 files changed, 87 insertions(+), 83 deletions(-) delete mode 100644 package/android/src/main/java/com/mrousavy/camera/extensions/CameraCharacteristics+getPreviewSize.kt create mode 100644 package/android/src/main/java/com/mrousavy/camera/extensions/Point+rotatedBy.kt diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt index 5dbae98..3d20edb 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt @@ -1,5 +1,6 @@ package com.mrousavy.camera.core +import android.content.res.Resources import android.graphics.ImageFormat import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraExtensionCharacteristics @@ -9,11 +10,14 @@ import android.os.Build import android.util.Log import android.util.Range import android.util.Size +import android.view.SurfaceHolder import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap +import com.mrousavy.camera.extensions.bigger import com.mrousavy.camera.extensions.getPhotoSizes import com.mrousavy.camera.extensions.getVideoSizes +import com.mrousavy.camera.extensions.smaller import com.mrousavy.camera.extensions.toJSValue import com.mrousavy.camera.types.AutoFocusSystem import com.mrousavy.camera.types.DeviceType @@ -29,6 +33,20 @@ import kotlin.math.sqrt class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId: String) { companion object { private const val TAG = "CameraDeviceDetails" + + fun getMaximumPreviewSize(): Size { + // See https://developer.android.com/reference/android/hardware/camera2/params/StreamConfigurationMap + // According to the Android Developer documentation, PREVIEW streams can have a resolution + // of up to the phone's display's resolution, with a maximum of 1920x1080. + val display1080p = Size(1920, 1080) + val displaySize = Size( + Resources.getSystem().displayMetrics.widthPixels, + Resources.getSystem().displayMetrics.heightPixels + ) + val isHighResScreen = displaySize.bigger >= display1080p.bigger || displaySize.smaller >= display1080p.smaller + + return if (isHighResScreen) display1080p else displaySize + } } val characteristics by lazy { cameraManager.getCameraCharacteristics(cameraId) } @@ -50,7 +68,10 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId val sensorSize by lazy { characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!! } val activeSize get() = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!! - val sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0 + val sensorOrientation by lazy { + val degrees = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0 + return@lazy Orientation.fromRotationDegrees(degrees) + } val minFocusDistance by lazy { getMinFocusDistanceCm() } val name by lazy { val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) characteristics.get(CameraCharacteristics.INFO_VERSION) else null @@ -121,6 +142,7 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId // TODO: Also add 10-bit YUV here? val videoFormat = ImageFormat.YUV_420_888 + val photoFormat = ImageFormat.JPEG // get extensions (HDR, Night Mode, ..) private fun getSupportedExtensions(): List = @@ -214,13 +236,18 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId return getFieldOfView(smallestFocalLength) } - private fun getVideoSizes(): List = characteristics.getVideoSizes(cameraId, videoFormat) - private fun getPhotoSizes(): List = characteristics.getPhotoSizes(ImageFormat.JPEG) + fun getVideoSizes(format: Int): List = characteristics.getVideoSizes(cameraId, format) + fun getPhotoSizes(): List = characteristics.getPhotoSizes(photoFormat) + fun getPreviewSizes(): List { + val maximumPreviewSize = getMaximumPreviewSize() + return cameraConfig.getOutputSizes(SurfaceHolder::class.java) + .filter { it.bigger <= maximumPreviewSize.bigger && it.smaller <= maximumPreviewSize.smaller } + } private fun getFormats(): ReadableArray { val array = Arguments.createArray() - val videoSizes = getVideoSizes() + val videoSizes = getVideoSizes(videoFormat) val photoSizes = getPhotoSizes() videoSizes.forEach { videoSize -> @@ -294,7 +321,7 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId map.putDouble("minExposure", exposureRange.lower.toDouble()) map.putDouble("maxExposure", exposureRange.upper.toDouble()) map.putString("hardwareLevel", hardwareLevel.unionValue) - map.putString("sensorOrientation", Orientation.fromRotationDegrees(sensorOrientation).unionValue) + map.putString("sensorOrientation", sensorOrientation.unionValue) map.putArray("formats", getFormats()) return map } diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt index 4d52117..ee9b2e5 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt @@ -23,9 +23,6 @@ import com.mrousavy.camera.core.outputs.PhotoOutput import com.mrousavy.camera.core.outputs.SurfaceOutput import com.mrousavy.camera.core.outputs.VideoPipelineOutput import com.mrousavy.camera.extensions.closestToOrMax -import com.mrousavy.camera.extensions.getPhotoSizes -import com.mrousavy.camera.extensions.getPreviewTargetSize -import com.mrousavy.camera.extensions.getVideoSizes import com.mrousavy.camera.frameprocessor.Frame import com.mrousavy.camera.types.Flash import com.mrousavy.camera.types.LensFacing @@ -245,20 +242,20 @@ class CameraSession(private val context: Context, private val cameraManager: Cam codeScannerOutput = null isRunning = false - val characteristics = cameraManager.getCameraCharacteristics(cameraId) + val deviceDetails = CameraDeviceDetails(cameraManager, cameraId) val format = configuration.format Log.i(TAG, "Creating outputs for Camera #$cameraId...") - val isSelfie = characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT + val isSelfie = deviceDetails.lensFacing == LensFacing.FRONT val outputs = mutableListOf() // Photo Output val photo = configuration.photo as? CameraConfiguration.Output.Enabled if (photo != null) { - val imageFormat = ImageFormat.JPEG - val sizes = characteristics.getPhotoSizes(imageFormat) + val imageFormat = deviceDetails.photoFormat + val sizes = deviceDetails.getPhotoSizes() val size = sizes.closestToOrMax(format?.photoSize) val maxImages = 10 @@ -278,7 +275,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam val video = configuration.video as? CameraConfiguration.Output.Enabled if (video != null) { val imageFormat = video.config.pixelFormat.toImageFormat() - val sizes = characteristics.getVideoSizes(cameraId, imageFormat) + val sizes = deviceDetails.getVideoSizes(imageFormat) val size = sizes.closestToOrMax(format?.videoSize) Log.i(TAG, "Adding ${size.width}x${size.height} Video Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...") @@ -301,7 +298,8 @@ class CameraSession(private val context: Context, private val cameraManager: Cam if (preview != null) { // Compute Preview Size based on chosen video size val videoSize = videoOutput?.size ?: format?.videoSize - val size = characteristics.getPreviewTargetSize(videoSize) + val sizes = deviceDetails.getPreviewSizes() + val size = sizes.closestToOrMax(videoSize) val enableHdr = video?.config?.enableHdr ?: false @@ -314,7 +312,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam ) outputs.add(output) // Size is usually landscape, so we flip it here - previewView?.setSurfaceSize(size.width, size.height) + previewView?.setSurfaceSize(size.width, size.height, deviceDetails.sensorOrientation) } // CodeScanner Output @@ -327,7 +325,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam } val imageFormat = ImageFormat.YUV_420_888 - val sizes = characteristics.getVideoSizes(cameraId, imageFormat) + val sizes = deviceDetails.getVideoSizes(imageFormat) val size = sizes.closestToOrMax(Size(1280, 720)) Log.i(TAG, "Adding ${size.width}x${size.height} CodeScanner Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...") diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt b/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt index df33b83..02ad602 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt @@ -10,9 +10,9 @@ import android.view.SurfaceHolder import android.view.SurfaceView import android.widget.FrameLayout import com.facebook.react.bridge.UiThreadUtil -import com.mrousavy.camera.extensions.getMaximumPreviewSize import com.mrousavy.camera.extensions.installHierarchyFitter import com.mrousavy.camera.extensions.resize +import com.mrousavy.camera.extensions.rotatedBy import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.ResizeMode import kotlin.math.roundToInt @@ -23,7 +23,7 @@ import kotlinx.coroutines.withContext class PreviewView(context: Context, callback: SurfaceHolder.Callback) : FrameLayout(context), SurfaceHolder.Callback { - var size: Size = getMaximumPreviewSize() + var size: Size = CameraDeviceDetails.getMaximumPreviewSize() private set var resizeMode: ResizeMode = ResizeMode.COVER set(value) { @@ -34,6 +34,15 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : invalidate() } } + private var inputOrientation: Orientation = Orientation.LANDSCAPE_LEFT + set(value) { + field = value + UiThreadUtil.runOnUiThread { + Log.i(TAG, "Camera Input Orientation changed to $value!") + requestLayout() + invalidate() + } + } private val viewSize: Size get() { val displayMetrics = context.resources.displayMetrics @@ -66,25 +75,26 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : invalidate() } - suspend fun setSurfaceSize(width: Int, height: Int) { + suspend fun setSurfaceSize(width: Int, height: Int, cameraSensorOrientation: Orientation) { withContext(Dispatchers.Main) { + inputOrientation = cameraSensorOrientation surfaceView.holder.resize(width, height) } } fun convertLayerPointToCameraCoordinates(point: Point, cameraDeviceDetails: CameraDeviceDetails): Point { - val sensorOrientation = Orientation.fromRotationDegrees(cameraDeviceDetails.sensorOrientation) + val sensorOrientation = cameraDeviceDetails.sensorOrientation val cameraSize = Size(cameraDeviceDetails.activeSize.width(), cameraDeviceDetails.activeSize.height()) val viewOrientation = Orientation.PORTRAIT - val rotated = Orientation.rotatePoint(point, viewSize, cameraSize, viewOrientation, sensorOrientation) - Log.i(TAG, "$point -> $sensorOrientation (in $cameraSize -> $viewSize) -> $rotated") + val rotated = point.rotatedBy(viewSize, cameraSize, viewOrientation, sensorOrientation) + Log.i(TAG, "Converted layer point $point to camera point $rotated! ($sensorOrientation, $cameraSize -> $viewSize)") return rotated } private fun getSize(contentSize: Size, containerSize: Size, resizeMode: ResizeMode): Size { // TODO: Take sensor orientation into account here - val contentAspectRatio = contentSize.height.toDouble() / contentSize.width + val contentAspectRatio = contentSize.width.toDouble() / contentSize.height val containerAspectRatio = containerSize.width.toDouble() / containerSize.height val widthOverHeight = when (resizeMode) { @@ -108,9 +118,10 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : super.onMeasure(widthMeasureSpec, heightMeasureSpec) val viewSize = Size(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)) - val fittedSize = getSize(size, viewSize, resizeMode) + val surfaceSize = size.rotatedBy(inputOrientation) + val fittedSize = getSize(surfaceSize, viewSize, resizeMode) - Log.i(TAG, "PreviewView is $viewSize, rendering $size content. Resizing to: $fittedSize ($resizeMode)") + Log.i(TAG, "PreviewView is $viewSize, rendering $surfaceSize content ($inputOrientation). Resizing to: $fittedSize ($resizeMode)") setMeasuredDimension(fittedSize.width, fittedSize.height) } diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCharacteristics+getPreviewSize.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCharacteristics+getPreviewSize.kt deleted file mode 100644 index ecd525c..0000000 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCharacteristics+getPreviewSize.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.mrousavy.camera.extensions - -import android.content.res.Resources -import android.hardware.camera2.CameraCharacteristics -import android.util.Size -import android.view.SurfaceHolder - -fun getMaximumPreviewSize(): Size { - // See https://developer.android.com/reference/android/hardware/camera2/params/StreamConfigurationMap - // According to the Android Developer documentation, PREVIEW streams can have a resolution - // of up to the phone's display's resolution, with a maximum of 1920x1080. - val display1080p = Size(1920, 1080) - val displaySize = Size( - Resources.getSystem().displayMetrics.widthPixels, - Resources.getSystem().displayMetrics.heightPixels - ) - val isHighResScreen = displaySize.bigger >= display1080p.bigger || displaySize.smaller >= display1080p.smaller - - return if (isHighResScreen) display1080p else displaySize -} - -fun CameraCharacteristics.getPreviewTargetSize(targetSize: Size?): Size { - val config = this.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! - val maximumPreviewSize = getMaximumPreviewSize() - val outputSizes = config.getOutputSizes(SurfaceHolder::class.java) - .filter { it.bigger <= maximumPreviewSize.bigger && it.smaller <= maximumPreviewSize.smaller } - - return outputSizes.closestToOrMax(targetSize) -} diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/Point+rotatedBy.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/Point+rotatedBy.kt new file mode 100644 index 0000000..7e84e3f --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/Point+rotatedBy.kt @@ -0,0 +1,25 @@ +package com.mrousavy.camera.extensions + +import android.graphics.Point +import android.graphics.PointF +import android.util.Log +import android.util.Size +import com.mrousavy.camera.types.Orientation + +fun Point.rotatedBy(fromSize: Size, toSize: Size, fromOrientation: Orientation, toOrientation: Orientation): Point { + val differenceDegrees = (fromOrientation.toDegrees() + toOrientation.toDegrees()) % 360 + val difference = Orientation.fromRotationDegrees(differenceDegrees) + val normalizedPoint = PointF(this.x / fromSize.width.toFloat(), this.y / fromSize.height.toFloat()) + + val rotatedNormalizedPoint = when (difference) { + Orientation.PORTRAIT -> normalizedPoint + Orientation.PORTRAIT_UPSIDE_DOWN -> PointF(1 - normalizedPoint.x, 1 - normalizedPoint.y) + Orientation.LANDSCAPE_LEFT -> PointF(normalizedPoint.y, 1 - normalizedPoint.x) + Orientation.LANDSCAPE_RIGHT -> PointF(1 - normalizedPoint.y, normalizedPoint.x) + } + + val rotatedX = rotatedNormalizedPoint.x * toSize.width + val rotatedY = rotatedNormalizedPoint.y * toSize.height + Log.i("ROTATE", "$this -> $normalizedPoint -> $difference -> $rotatedX, $rotatedY") + return Point(rotatedX.toInt(), rotatedY.toInt()) +} diff --git a/package/android/src/main/java/com/mrousavy/camera/types/Orientation.kt b/package/android/src/main/java/com/mrousavy/camera/types/Orientation.kt index a19f7a7..64dc5bc 100644 --- a/package/android/src/main/java/com/mrousavy/camera/types/Orientation.kt +++ b/package/android/src/main/java/com/mrousavy/camera/types/Orientation.kt @@ -1,9 +1,5 @@ package com.mrousavy.camera.types -import android.graphics.Point -import android.graphics.PointF -import android.util.Log -import android.util.Size import com.mrousavy.camera.core.CameraDeviceDetails enum class Orientation(override val unionValue: String) : JSUnionValue { @@ -30,7 +26,7 @@ enum class Orientation(override val unionValue: String) : JSUnionValue { } // Rotate sensor rotation by target rotation - val newRotationDegrees = (deviceDetails.sensorOrientation + rotationDegrees + 360) % 360 + val newRotationDegrees = (deviceDetails.sensorOrientation.toDegrees() + rotationDegrees + 360) % 360 return fromRotationDegrees(newRotationDegrees) } @@ -52,29 +48,5 @@ enum class Orientation(override val unionValue: String) : JSUnionValue { in 225..315 -> LANDSCAPE_RIGHT else -> PORTRAIT } - - fun rotatePoint( - point: Point, - fromSize: Size, - toSize: Size, - fromOrientation: Orientation, - toOrientation: Orientation - ): Point { - val differenceDegrees = (fromOrientation.toDegrees() + toOrientation.toDegrees()) % 360 - val difference = Orientation.fromRotationDegrees(differenceDegrees) - val normalizedPoint = PointF(point.x / fromSize.width.toFloat(), point.y / fromSize.height.toFloat()) - - val rotatedNormalizedPoint = when (difference) { - PORTRAIT -> normalizedPoint - PORTRAIT_UPSIDE_DOWN -> PointF(1 - normalizedPoint.x, 1 - normalizedPoint.y) - LANDSCAPE_LEFT -> PointF(normalizedPoint.y, 1 - normalizedPoint.x) - LANDSCAPE_RIGHT -> PointF(1 - normalizedPoint.y, normalizedPoint.x) - } - - val rotatedX = rotatedNormalizedPoint.x * toSize.width - val rotatedY = rotatedNormalizedPoint.y * toSize.height - Log.i("ROTATE", "$point -> $normalizedPoint -> $difference -> $rotatedX, $rotatedY") - return Point(rotatedX.toInt(), rotatedY.toInt()) - } } } From c5646ca1e278b6c2afc6c259cb2e98980cc4f96d Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 15 Feb 2024 13:30:40 +0100 Subject: [PATCH 37/71] chore: release 3.9.0-beta.5 --- package/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/package.json b/package/package.json index 21e949f..9af7e1c 100644 --- a/package/package.json +++ b/package/package.json @@ -1,6 +1,6 @@ { "name": "react-native-vision-camera", - "version": "3.9.0-beta.4", + "version": "3.9.0-beta.5", "description": "A powerful, high-performance React Native Camera library.", "main": "lib/commonjs/index", "module": "lib/module/index", From bcd12649e2c8297c39bba07626a9b4ae12bda7d4 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 15 Feb 2024 17:07:57 +0100 Subject: [PATCH 38/71] fix: Move PreviewView into SurfaceView to make it simpler (#2566) * hmmm * Set initial fixed size * fix: Repair PreviewView again * Update PreviewView.kt --- .../com/mrousavy/camera/core/PreviewView.kt | 59 +++++++++---------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt b/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt index 02ad602..ba3c94b 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt @@ -5,12 +5,9 @@ import android.content.Context import android.graphics.Point import android.util.Log import android.util.Size -import android.view.Gravity import android.view.SurfaceHolder import android.view.SurfaceView -import android.widget.FrameLayout import com.facebook.react.bridge.UiThreadUtil -import com.mrousavy.camera.extensions.installHierarchyFitter import com.mrousavy.camera.extensions.resize import com.mrousavy.camera.extensions.rotatedBy import com.mrousavy.camera.types.Orientation @@ -21,26 +18,30 @@ import kotlinx.coroutines.withContext @SuppressLint("ViewConstructor") class PreviewView(context: Context, callback: SurfaceHolder.Callback) : - FrameLayout(context), + SurfaceView(context), SurfaceHolder.Callback { var size: Size = CameraDeviceDetails.getMaximumPreviewSize() - private set + set(value) { + if (field != value) { + Log.i(TAG, "Surface Size changed: $field -> $value") + field = value + updateLayout() + } + } var resizeMode: ResizeMode = ResizeMode.COVER set(value) { - field = value - UiThreadUtil.runOnUiThread { - Log.i(TAG, "Setting PreviewView ResizeMode to $value...") - requestLayout() - invalidate() + if (field != value) { + Log.i(TAG, "Resize Mode changed: $field -> $value") + field = value + updateLayout() } } private var inputOrientation: Orientation = Orientation.LANDSCAPE_LEFT set(value) { - field = value - UiThreadUtil.runOnUiThread { - Log.i(TAG, "Camera Input Orientation changed to $value!") - requestLayout() - invalidate() + if (field != value) { + Log.i(TAG, "Input Orientation changed: $field -> $value") + field = value + updateLayout() } } private val viewSize: Size @@ -50,35 +51,25 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : val dpY = height / displayMetrics.density return Size(dpX.toInt(), dpY.toInt()) } - private val surfaceView = SurfaceView(context) init { Log.i(TAG, "Creating PreviewView...") - this.installHierarchyFitter() - surfaceView.layoutParams = LayoutParams( - LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT, - Gravity.CENTER - ) - surfaceView.holder.setKeepScreenOn(true) - surfaceView.holder.addCallback(this) - surfaceView.holder.addCallback(callback) - addView(surfaceView) + holder.setKeepScreenOn(true) + holder.addCallback(this) + holder.addCallback(callback) + holder.setFixedSize(size.width, size.height) } override fun surfaceCreated(holder: SurfaceHolder) = Unit override fun surfaceDestroyed(holder: SurfaceHolder) = Unit override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { - Log.i(TAG, "PreviewView Surface size changed: $size -> ${width}x$height, re-computing layout...") size = Size(width, height) - requestLayout() - invalidate() } suspend fun setSurfaceSize(width: Int, height: Int, cameraSensorOrientation: Orientation) { withContext(Dispatchers.Main) { inputOrientation = cameraSensorOrientation - surfaceView.holder.resize(width, height) + holder.resize(width, height) } } @@ -92,8 +83,14 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : return rotated } + private fun updateLayout() { + UiThreadUtil.runOnUiThread { + requestLayout() + invalidate() + } + } + private fun getSize(contentSize: Size, containerSize: Size, resizeMode: ResizeMode): Size { - // TODO: Take sensor orientation into account here val contentAspectRatio = contentSize.width.toDouble() / contentSize.height val containerAspectRatio = containerSize.width.toDouble() / containerSize.height From 4168d8f752550658fa8a1b5b7f8f2c2555eae5b1 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 15 Feb 2024 17:33:19 +0100 Subject: [PATCH 39/71] feat: Allow focus calls to be cancelable (#2567) * feat: Allow focus calls to be cancelable * Cancelable --- .../com/mrousavy/camera/core/CameraError.kt | 1 + .../core/PersistentCameraCaptureSession.kt | 34 ++++++++++++------- .../CameraCaptureSession+precapture.kt | 10 +++++- package/src/CameraError.ts | 1 + 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt index 6e7c8c6..834c190 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt @@ -104,6 +104,7 @@ class PhotoNotEnabledError : CameraError("capture", "photo-not-enabled", "Photo capture is disabled! Pass `photo={true}` to enable photo capture.") class CaptureAbortedError(wasImageCaptured: Boolean) : CameraError("capture", "aborted", "The image capture was aborted! Was Image captured: $wasImageCaptured") +class FocusCanceledError : CameraError("capture", "focus-canceled", "The focus operation was canceled.") class CaptureTimedOutError : CameraError("capture", "timed-out", "The image capture was aborted because it timed out.") class UnknownCaptureError(wasImageCaptured: Boolean) : CameraError("capture", "unknown", "An unknown error occurred while trying to capture an Image! Was Image captured: $wasImageCaptured") diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt index df580b5..707c7b3 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt @@ -25,7 +25,7 @@ import com.mrousavy.camera.types.QualityPrioritization import java.io.Closeable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -56,7 +56,7 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p private val mutex = Mutex() private var didDestroyFromOutside = false - private var focusResetJob: Job? = null + private var focusJob: Job? = null private val coroutineScope = CoroutineScope(CameraQueues.cameraQueue.coroutineDispatcher) val isRunning: Boolean @@ -74,6 +74,10 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p } suspend fun withConfiguration(block: suspend () -> Unit) { + // Cancel any ongoing focus jobs + focusJob?.cancel() + focusJob = null + mutex.withLock { block() configure() @@ -141,6 +145,10 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p orientation: Orientation, enableShutterSound: Boolean ): TotalCaptureResult { + // Cancel any ongoing focus jobs + focusJob?.cancel() + focusJob = null + mutex.withLock { Log.i(TAG, "Capturing photo...") val session = session ?: throw CameraNotReadyError() @@ -198,6 +206,10 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p } suspend fun focus(point: Point) { + // Cancel any previous focus jobs + focusJob?.cancel() + focusJob = null + mutex.withLock { Log.i(TAG, "Focusing to $point...") val session = session ?: throw CameraNotReadyError() @@ -209,17 +221,16 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p } val outputs = outputs.filter { it.isRepeating } - // 0. Cancel the 3 second focus reset task - focusResetJob?.cancelAndJoin() - focusResetJob = null - // 1. Run a precapture sequence for AF, AE and AWB. - val request = repeatingRequest.createCaptureRequest(device, deviceDetails, outputs) - val options = PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point), false) - session.precapture(request, deviceDetails, options) + focusJob = coroutineScope.launch { + val request = repeatingRequest.createCaptureRequest(device, deviceDetails, outputs) + val options = PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point), false) + session.precapture(request, deviceDetails, options) + } + focusJob?.join() - // 2. Wait 3 seconds - focusResetJob = coroutineScope.launch { + // 2. Reset AF/AE/AWB again after 3 seconds timeout + focusJob = coroutineScope.launch { delay(FOCUS_RESET_TIMEOUT) if (!this.isActive) { // this job got canceled from the outside @@ -230,7 +241,6 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p return@launch } Log.i(TAG, "Resetting focus to auto-focus...") - // 3. Reset AF/AE/AWB to continuous auto-focus again, which is the default here. repeatingRequest.createCaptureRequest(device, deviceDetails, outputs).also { request -> session.setRepeatingRequest(request.build(), null, null) } diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt index 89af52c..ea9178d 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt @@ -8,8 +8,11 @@ import android.hardware.camera2.params.MeteringRectangle import android.util.Log import android.util.Size import com.mrousavy.camera.core.CameraDeviceDetails +import com.mrousavy.camera.core.FocusCanceledError import com.mrousavy.camera.types.Flash import com.mrousavy.camera.types.HardwareLevel +import kotlin.coroutines.coroutineContext +import kotlinx.coroutines.isActive data class PrecaptureOptions( val modes: List, @@ -65,6 +68,8 @@ suspend fun CameraCaptureSession.precapture( this.capture(request.build(), null, null) } + if (!coroutineContext.isActive) throw FocusCanceledError() + val meteringWeight = MeteringRectangle.METERING_WEIGHT_MAX - 1 val meteringRectangles = options.pointsOfInterest.map { point -> MeteringRectangle(point, DEFAULT_METERING_SIZE, meteringWeight) @@ -115,13 +120,16 @@ suspend fun CameraCaptureSession.precapture( } this.capture(request.build(), null, null) + if (!coroutineContext.isActive) throw FocusCanceledError() + // 3. Start a repeating request without the trigger and wait until AF/AE/AWB locks request.set(CaptureRequest.CONTROL_AF_TRIGGER, null) request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, null) val result = this.setRepeatingRequestAndWaitForPrecapture(request.build(), *precaptureModes.toTypedArray()) + if (!coroutineContext.isActive) throw FocusCanceledError() + Log.i(TAG, "AF/AE/AWB successfully locked!") - // TODO: Set to idle again? val needsFlash = result.exposureState == ExposureState.FlashRequired return PrecaptureResult(needsFlash) diff --git a/package/src/CameraError.ts b/package/src/CameraError.ts index f47f61f..cca7c75 100644 --- a/package/src/CameraError.ts +++ b/package/src/CameraError.ts @@ -43,6 +43,7 @@ export type CaptureError = | 'capture/photo-not-enabled' | 'capture/frame-invalid' | 'capture/aborted' + | 'capture/focus-canceled' | 'capture/timed-out' | 'capture/unknown' export type SystemError = From 129e21f14ee6f7b17cc68b9e50a9991311945c7d Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 15 Feb 2024 17:34:55 +0100 Subject: [PATCH 40/71] fix: Fix `Session has been closed; further changes are illegal` error by using `tryStopRepeating()` (#2568) * feat: Allow focus calls to be cancelable * Cancelable * fix: Use `tryStopRepeating` --- .../camera/core/PersistentCameraCaptureSession.kt | 3 ++- .../extensions/CameraCaptureSession+tryStopRepeating.kt | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+tryStopRepeating.kt diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt index 707c7b3..fc3276d 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt @@ -19,6 +19,7 @@ import com.mrousavy.camera.extensions.isValid import com.mrousavy.camera.extensions.openCamera import com.mrousavy.camera.extensions.precapture import com.mrousavy.camera.extensions.tryAbortCaptures +import com.mrousavy.camera.extensions.tryStopRepeating import com.mrousavy.camera.types.Flash import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.QualityPrioritization @@ -280,8 +281,8 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p val builder = repeatingRequest.createCaptureRequest(device, details, repeatingOutputs) session.setRepeatingRequest(builder.build(), null, null) } else { - session.stopRepeating() Log.d(TAG, "Stopping repeating request...") + session.tryStopRepeating() } Log.d(TAG, "Configure() done! isActive: $isActive, ID: $cameraId, device: $device, session: $session") } catch (e: CameraAccessException) { diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+tryStopRepeating.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+tryStopRepeating.kt new file mode 100644 index 0000000..0810978 --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+tryStopRepeating.kt @@ -0,0 +1,9 @@ +package com.mrousavy.camera.extensions + +import android.hardware.camera2.CameraCaptureSession + +fun CameraCaptureSession.tryStopRepeating() { + try { + stopRepeating() + } catch (_: Throwable) {} +} From 31754eb74c9a719dca705edbae3dbced996b1111 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 15 Feb 2024 17:37:19 +0100 Subject: [PATCH 41/71] chore: release 3.9.0-beta.6 --- package/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/package.json b/package/package.json index 9af7e1c..3899618 100644 --- a/package/package.json +++ b/package/package.json @@ -1,6 +1,6 @@ { "name": "react-native-vision-camera", - "version": "3.9.0-beta.5", + "version": "3.9.0-beta.6", "description": "A powerful, high-performance React Native Camera library.", "main": "lib/commonjs/index", "module": "lib/module/index", From e6c12e2ed2819bcc915170fc42d9349438511c12 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 15 Feb 2024 20:34:49 +0100 Subject: [PATCH 42/71] chore: Update ShadowLens links for Android release --- README.md | 9 +++++++++ docs/docs/guides/SHADOW_LENS.mdx | 2 +- docs/docusaurus.config.js | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2ae50a2..a64c3fc 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,15 @@ cd ios && pod install To see VisionCamera in action, check out [ShadowLens](https://mrousavy.com/projects/shadowlens)! + + ### Example ```tsx diff --git a/docs/docs/guides/SHADOW_LENS.mdx b/docs/docs/guides/SHADOW_LENS.mdx index 47e6a0a..4c91ebd 100644 --- a/docs/docs/guides/SHADOW_LENS.mdx +++ b/docs/docs/guides/SHADOW_LENS.mdx @@ -21,7 +21,7 @@ import useBaseUrl from '@docusaurus/useBaseUrl' **Download now:**
- + diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 89d3ef2..66fc859 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -25,7 +25,7 @@ module.exports = { }, announcementBar: { id: 'shadowlens', - content: 'ShadowLens is out!!! πŸ₯³πŸ₯³ Download the iOS app to see VisionCamera in action: apps.apple.com/shadowlens', + content: 'ShadowLens is out!!! πŸ₯³πŸ₯³ See VisionCamera in action: Download ShadowLens for iOS or Download ShadowLens for Android', backgroundColor: '#e39600', textColor: '#ffffff', isCloseable: false, From aad7d2b8b3ad6c985d987704fc6327e66b055be4 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 15 Feb 2024 20:37:26 +0100 Subject: [PATCH 43/71] docs: Use SVG PlayStore Button --- README.md | 4 ++-- docs/docs/guides/SHADOW_LENS.mdx | 2 +- docs/static/img/googleplay.svg | 2 ++ docs/static/img/playstore.png | Bin 4904 -> 0 bytes 4 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 docs/static/img/googleplay.svg delete mode 100644 docs/static/img/playstore.png diff --git a/README.md b/README.md index a64c3fc..39dceed 100644 --- a/README.md +++ b/README.md @@ -47,12 +47,12 @@ cd ios && pod install To see VisionCamera in action, check out [ShadowLens](https://mrousavy.com/projects/shadowlens)! -
+ diff --git a/docs/docs/guides/SHADOW_LENS.mdx b/docs/docs/guides/SHADOW_LENS.mdx index 4c91ebd..bcc0d1c 100644 --- a/docs/docs/guides/SHADOW_LENS.mdx +++ b/docs/docs/guides/SHADOW_LENS.mdx @@ -25,6 +25,6 @@ import useBaseUrl from '@docusaurus/useBaseUrl' - +
diff --git a/docs/static/img/googleplay.svg b/docs/static/img/googleplay.svg new file mode 100644 index 0000000..68b707a --- /dev/null +++ b/docs/static/img/googleplay.svg @@ -0,0 +1,2 @@ + +image/svg+xml diff --git a/docs/static/img/playstore.png b/docs/static/img/playstore.png deleted file mode 100644 index 131f3acaa252a863c3b694d0f522ea750aebd81c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4904 zcmZu#X*kqj*B?vC^fxJqAzKJz8HNyL8$|XkODM@MO?I+mn~{BA$1;lSlw{2|q>(kd zNMqlPoovtaK40Ds&$+JqKIi(~zjLm$eY)T3YCoU@vw%S$5S_Z3vOWkzi6+~BFHw>2 z%YvR`AP~iruBM^N1C*w*v9YD4rLC>4i;Ihum6f-*cT!SPK|w)ubhMkBo4L7pR#sMM zXlO!0g1x=Hy1F_H24iGoB*)?4;2;yQSgeGEgulN(27{54lQS?dFf}!GbaVuR!DNse znUj-KK|uk9LebOHzj^cK>C>m8qN2LGx-^VnriVOiPvCq3ay;&LSqx$HcUY>bsumU& zR#sNd&(CLOX5!=Hr>3U1x3@bwI@Z?K4i67!XJgwwD_V)Jo_gh+8mY0`(eSLR# zcdyHH{Q2{TL?UrHNTjBw@>p2U&(AkBG$5YW%0yReZEeYTj3|Ze-cP9M>FJSvuT7|6 za512i$=4pKWjJo6p3J9~hf~NFXqN?3aWGTd|DYXUNl6cY);>~OKA@B@(*7DlmE}pL zP^7IROA+Hp85b9qo15E{PE}c1+27xPuh0w8dyslD8GibTPTcb_T*4wKq!--YlsEE~=_}VEV1t$aCU!*Vi@x$yAJQcK#)|bwBR>jI92jI4+!@YWtUy zw}pN}U0Ko4n__Lu=-H|Qy-I7mx|CQ~u;^M~Vk{InTTy%m8?-@5j`_buJ~zFG=}C+0 zNK)eC@`)4b-rmvq&2S znYL+>MWNEpl?(mg_OiwMBs(k}nb*mWUPg0tC|{`x2!y zr?doYs^bD|j#gy5wbbyZbAtK7y`XdA7sF3#h<(zNhKNzC%(*dqpvPW0-X_8SiViAy z?#oc+!=HO3>QVI#$FGD2_Usus0-LVjF`>XvRKs>} zj!S2hKo1QD;<>*6S2fyAH>H@3;zVPnvyb6cq3I zpsU<=E?!Zh?1*wx^rDVE7s0-2YicvaW~$bZ8)jROv;lCag16 za=qp5L-`1v7FG`xG;1fQ1iIxSFeazO$`Nzj;*meZ<-?^I9ceLvQkscm8a7I{Wd1m z;b-2)IVa%Y^gjxAAjs%&;juCau?yHeURI8*YB%+!ZYd4#>%83`ee+Rb8HEYAR72CV z0n2h!f7LM*B>jcTHHED&E|vJm?8ahj_zbmz)a^_&!mZ+1tE86d{1FOgYyhO67Iot_ z8VmFdZksK(8_~3Iy4vVR13RbM*wZ1EGp;=XDvd(b2OR}?{j4w><4p;r5YZu&siNa4 zByAryabA(XC}GH;&?i1;U5nLTF4h7psMNw^ttyMWb$1_sWz1!92nHXn0&%7)z=%?XN?tKZSt zTuk^6i(t%ho6zuMs{VYt@5J^Wqpep&KOfia?QC_t8Pwpsn$5+4UtD~MS@z3Z>5lp| zrZAu`3{0J3pX6Y;|FuC5olV@)Fw=92SuepU#-Fh^v*=5JdY_STt7^QvQ|huE-Z=pm zFWI_VWo5ZZFB%bxvol#2m7lgg(lQOWF7+m{d0lG$UOvSxGt?J73|T*7A6!LST|D{w z%Tme@&OS%!!|aQm_NsO3^@{jOf$9l}n1M)N>|YZn56lfSEbJ_vCD3P1sY=Qg;A?~M zJKO%TUV=W7{k-@ZkaFgq1fjZL;}=||#s2JGlb$6%1x#Bx7409RXT0O&BmZu&LLP3g zHl+Zypq3x~;-#MUaJx~T3bWW!;1&Ap?1IX>DLuUUN zcb%TRrL*d!n3Fzjmu4j1ok)|vX&~7PijaB}m5#Gpl9`j6X3+np52K091A@L7s zi26+ukITIe=bsjO**D+K{6E$7Ky*lU&A~tRX`#C`i%vTt826j#zTgwUX?qpegW$v3 zRP=--|Cx#DH3B`MQ6a=OoGLM|k^U1H4aUrO%a(#eo&SN_#Jmi6m5QjLw6pDRQy;i@?48gM7z zAQ^?d)+fBSCCi_y#FR0ON^%F~ueVg^=e!W^?yPx#<~ruZ%u5Bo7_cJ0;gq=+`rM-> zT6f;oP&}LMyS5~=<@fXHk!R1hIJa;x?-7-7G>hL|#o^w{GRP7ue z?bnZ5cA9GJgHsmh-!4EtL=VR)&u2BH{HxyqG{JZBenSMgDTzj43U!lKqrRICNKEkP zn%qauN0>FIC~Ai45K2SUjz29DK z*WJXf4BrnB7cSL@MrL0?Fw_aDs-1f2b5uz z+U(1|4vFtIA7}CiSPqTrB*;+u2;q;|__%GpH_}r5>WvP_w$yp8XK?em>nY$9b`7&V z%jzO|yJSGz!QlqxJ{NOlzD5B1V_Os76Fs<%y$DCNlimHqrQ615)?0iOM)KrTTW}|$ znG34Pp(1a)K9?<8AL8OK_r(RC=m}eo9Y)$0SZt2-U@Dx?_67WY?};mixni2n1=R=F zpv5c(T`D1Sm!TKHi`AsmH4t~C=&gbIt z;-mUHpobeGas(*4`=LE$!f(Ip6TZ;AGx&6D3NjgOe6HOWS>`AGp-3y8=>9NOD68YP zpL@2I826~tm(+LV8S?1ZC5cy;lish|T>@W#5F1Bl#U-RKAG}*Yk2LPUCA>y$e|L)t za;8|dbmmOog4;6Rk@xo6>2kY2Q?D$>meY1t?TRkcUC<5qdk3#7Y?F33OuDw%0iJr) zsa1vG_)*$!il&nbINDQ##uQ6q4E*hl=VKK48i&Mk1vL2)nzE7W%M}qC%-=Rh5%uD?L!C=QIPG|sWIDb za}gZ9oOLeJmS3BpeEWV^7D(c-TRQe#B(HCx?&7t@Isw=CY?;dnPJ!gNiPLy0#OIxg ze$Y#;Ve}(Wk?T{hbUGE zA(KyqXrcPSqd$bqif#DyE*NQ3+A4J9@6L zLi@t!6XSCl>HDj*yByp`UG&$x+|DVcI4jX{HPOmuC>$U6O~@E8k3lvxjS6VE{rb|7 zZ&`cg!F)FiGxc0lbQob?P9k#GTjlvFO}(o?APXbLJNwrV60T^b!@L|Gli<>p{=@s} z9_K!YJ0r>e*jo(OR7GL2&pz(o5Zl%4S&sl%;ZF;7e}is2J+Cz<4%)fzEVZZ7rug{y zmnz9*e>m{?tWy<@3${^z5N8CvxUlTz z-?(Q7W${jQwBdhrxXoC`j0M$wQhM)N%CU26`hqwhkm6XMWSL{t+04{Xanbf|HNsEW z@NnU78EZ1+beNW{fY*36MH1wQ_d)~srb;^H*pH@JR(LsqCzGvl?$tmTn4>v&c#v&u z+M%@w`QTC%d^h5gQpM-mevO`-tGhX0Ez_-j$O$|xOuJ8Sgt!hfche7tbwz5w;C8bN zC|bKC41b+<7Wv3*KVuPs`xVn81Ef15y!8WqHdq^$0XE>x=DgWvfi3~ z(MZCITSkaIg93x6Q#N-ME$5LxV3hGvspX|&GbPgd@vRzh3aBJILN_0net2EZPy0dS zUc#KiRm6tJr!ZkX5w4em>Dg1)!Z>EfSe?-B_8Nwf9E}biqzEOVL$oUmQDPxEp_~*? zjPe0qa&7Od3pBDXPcxNQD-NkeIBNh7q>d=x3uu}gBjW3uMcU|VVVgMvuVz)7 zUDq$w{8;P&_V^9Fsn9ZD=*;J~5rS?m>5N7uAd2ZESxeKc3Xk;7;DOty$y{eF$H#4M zV_L){ysHb8&>YPP$!=`&#X?-rBfW>Vn36Sq-iAYd-j&PK!5+-pkOw`8AzI4-e?Z>9 zTIuenFFix7?vE=eZj-I9w*+BA_e>zhQ?-Q_@m$MRb_^UpbdPolSD1s9Oz)0vy;~QR zN|S{>sCoDPr%F1%K)lOEKXWYxG^BU3J@fmDCEn+z70){9>bIEizo--?J(LH~Oatzi zH}BLL+$Rc!e2jspRQuCo|E^95NoSh;I_oyWbT-;t!y4T5Es5Eik~n!q=uVp)>Li`Z z2lB!fWjdA|&oqOA0vaUIPnr@^-Ljsu)UpIbh8U$VBK&yBgdm9!k@YsmrIgOL{M8la zpGb0=w8}LD$4x_>hJ(-y7_^C>>}F`LJ!u>C<%|5{qDL~YtW}3yO-dM>QA~xUv$VMc z$v~TYyU8?@D7%~W)c)PV5X<`VBrbpNf1qJyURPnHczrJQj3><*r&}4YJnr9wT{Uc5 zL#DCCWEU?34HL}IEE~tmuj}u3jF}Z&k6*D4KO$``pB}?=P+=08c=Mj9ys|_tTa9u< zuIz()u~K<7+_H4xr-zPL5b*|6+DwO|46;HJy@wHCzsXX}nRPcZUlxyw+~}r1OLWc% zta)rq8$%^VuOR#9mDZRTfIo%Tb!%qjA3$>9>PDyMtt`*z{(H`7N{KVSM(JH(WyF*5 zNo1cCQH)S};#@sPMv z^CwKdmU^#-)~Z&*a{W2wZX34m;pzQu?=^Xf`jUHt~g4A>3^F^`QwuC zwS7#v>^`Zx-vjfi>vDBl7VCIM9b@5<5gGJs-tJL6%sq9 g6#x5Mf{gz51&^3@tr*$xbpPY1t7t2iD?JVV5121yGXMYp From 57c64313536120ecd53c8c60a1e3014162761625 Mon Sep 17 00:00:00 2001 From: farwayer Date: Fri, 16 Feb 2024 11:27:00 +0100 Subject: [PATCH 44/71] fix: Replace deprecated `RCTEventEmitter` with `UIManager` (#2569) use new events --- .../com/mrousavy/camera/CameraView+Events.kt | 66 ++++++++++++------- .../java/com/mrousavy/camera/types/Events.kt | 36 ++++++++++ 2 files changed, 77 insertions(+), 25 deletions(-) create mode 100644 package/android/src/main/java/com/mrousavy/camera/types/Events.kt diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView+Events.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView+Events.kt index 4b18754..1ad33e1 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView+Events.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView+Events.kt @@ -4,57 +4,64 @@ import android.util.Log import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.WritableMap -import com.facebook.react.uimanager.events.RCTEventEmitter +import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.events.Event import com.google.mlkit.vision.barcode.common.Barcode import com.mrousavy.camera.core.CameraError import com.mrousavy.camera.core.CodeScannerFrame import com.mrousavy.camera.core.UnknownCameraError import com.mrousavy.camera.core.code -import com.mrousavy.camera.types.CodeType +import com.mrousavy.camera.types.* fun CameraView.invokeOnInitialized() { Log.i(CameraView.TAG, "invokeOnInitialized()") - val reactContext = context as ReactContext - reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraInitialized", null) + val surfaceId = UIManagerHelper.getSurfaceId(this) + val event = CameraInitializedEvent(surfaceId, id) + this.sendEvent(event) } fun CameraView.invokeOnStarted() { Log.i(CameraView.TAG, "invokeOnStarted()") - val reactContext = context as ReactContext - reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraStarted", null) + val surfaceId = UIManagerHelper.getSurfaceId(this) + val event = CameraStartedEvent(surfaceId, id) + this.sendEvent(event) } fun CameraView.invokeOnStopped() { Log.i(CameraView.TAG, "invokeOnStopped()") - val reactContext = context as ReactContext - reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraStopped", null) + val surfaceId = UIManagerHelper.getSurfaceId(this) + val event = CameraStoppedEvent(surfaceId, id) + this.sendEvent(event) } fun CameraView.invokeOnError(error: Throwable) { Log.e(CameraView.TAG, "invokeOnError(...):") error.printStackTrace() - val cameraError = when (error) { - is CameraError -> error - else -> UnknownCameraError(error) - } - val event = Arguments.createMap() - event.putString("code", cameraError.code) - event.putString("message", cameraError.message) + val cameraError = + when (error) { + is CameraError -> error + else -> UnknownCameraError(error) + } + val data = Arguments.createMap() + data.putString("code", cameraError.code) + data.putString("message", cameraError.message) cameraError.cause?.let { cause -> - event.putMap("cause", errorToMap(cause)) + data.putMap("cause", errorToMap(cause)) } - val reactContext = context as ReactContext - reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraError", event) + + val surfaceId = UIManagerHelper.getSurfaceId(this) + val event = CameraErrorEvent(surfaceId, id, data) + this.sendEvent(event) } fun CameraView.invokeOnViewReady() { - val event = Arguments.createMap() - val reactContext = context as ReactContext - reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraViewReady", event) + val surfaceId = UIManagerHelper.getSurfaceId(this) + val event = CameraViewReadyEvent(surfaceId, id) + this.sendEvent(event) } fun CameraView.invokeOnCodeScanned(barcodes: List, scannerFrame: CodeScannerFrame) { @@ -87,14 +94,23 @@ fun CameraView.invokeOnCodeScanned(barcodes: List, scannerFrame: CodeSc codes.pushMap(code) } - val event = Arguments.createMap() - event.putArray("codes", codes) + val data = Arguments.createMap() + data.putArray("codes", codes) val codeScannerFrame = Arguments.createMap() codeScannerFrame.putInt("width", scannerFrame.width) codeScannerFrame.putInt("height", scannerFrame.height) - event.putMap("frame", codeScannerFrame) + data.putMap("frame", codeScannerFrame) + + val surfaceId = UIManagerHelper.getSurfaceId(this) + val event = CameraCodeScannedEvent(surfaceId, id, data) + this.sendEvent(event) +} + +private fun CameraView.sendEvent(event: Event<*>) { val reactContext = context as ReactContext - reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraCodeScanned", event) + val dispatcher = + UIManagerHelper.getEventDispatcherForReactTag(reactContext, id) + dispatcher?.dispatchEvent(event) } private fun errorToMap(error: Throwable): WritableMap { diff --git a/package/android/src/main/java/com/mrousavy/camera/types/Events.kt b/package/android/src/main/java/com/mrousavy/camera/types/Events.kt new file mode 100644 index 0000000..e34c9f5 --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/types/Events.kt @@ -0,0 +1,36 @@ +package com.mrousavy.camera.types + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event + +class CameraInitializedEvent(surfaceId: Int, viewId: Int) : Event(surfaceId, viewId) { + override fun getEventName() = "cameraInitialized" + override fun getEventData(): WritableMap = Arguments.createMap() +} + +class CameraStartedEvent(surfaceId: Int, viewId: Int) : Event(surfaceId, viewId) { + override fun getEventName() = "cameraStarted" + override fun getEventData(): WritableMap = Arguments.createMap() +} + +class CameraStoppedEvent(surfaceId: Int, viewId: Int) : Event(surfaceId, viewId) { + override fun getEventName() = "cameraStopped" + override fun getEventData(): WritableMap = Arguments.createMap() +} + +class CameraErrorEvent(surfaceId: Int, viewId: Int, private val data: WritableMap) : Event(surfaceId, viewId) { + override fun getEventName() = "cameraError" + override fun getEventData() = data +} + +class CameraViewReadyEvent(surfaceId: Int, viewId: Int) : Event(surfaceId, viewId) { + override fun getEventName() = "cameraViewReady" + override fun getEventData(): WritableMap = Arguments.createMap() +} + +class CameraCodeScannedEvent(surfaceId: Int, viewId: Int, private val data: WritableMap) : + Event(surfaceId, viewId) { + override fun getEventName() = "cameraCodeScanned" + override fun getEventData() = data +} From b105de0194c949745a85be8d4612a6b0903a44d3 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 16 Feb 2024 13:13:56 +0100 Subject: [PATCH 45/71] fix: Add extra sanity-checks for `CaptureRequest` (#2571) * fix: Add extra sanity-checks for `CaptureRequest` * Update CameraDeviceDetails.kt * Refactor zoom a bit * fix: Remove unneeded flash * Move to AE instead of FLASH control * Revert "Move to AE instead of FLASH control" This reverts commit 755689411535803d156a4e84f143d0c9d08c858f. * Set AE Mode to ON for manual flash control --- .../mrousavy/camera/CameraView+TakePhoto.kt | 5 ++-- .../camera/core/CameraDeviceDetails.kt | 6 ++++- .../com/mrousavy/camera/core/CameraSession.kt | 2 -- .../core/PersistentCameraCaptureSession.kt | 4 +--- .../core/capture/CameraCaptureRequest.kt | 2 +- .../core/capture/PhotoCaptureRequest.kt | 23 ------------------- .../core/capture/RepeatingCaptureRequest.kt | 11 +++++++-- .../extensions/CaptureRequest+setZoom.kt | 18 +++++++-------- 8 files changed, 27 insertions(+), 44 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView+TakePhoto.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView+TakePhoto.kt index 7ba6775..9098074 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView+TakePhoto.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView+TakePhoto.kt @@ -30,10 +30,12 @@ suspend fun CameraView.takePhoto(optionsMap: ReadableMap): WritableMap { val qualityPrioritization = options["qualityPrioritization"] as? String ?: "balanced" val flash = options["flash"] as? String ?: "off" - val enableAutoRedEyeReduction = options["enableAutoRedEyeReduction"] == true val enableAutoStabilization = options["enableAutoStabilization"] == true val enableShutterSound = options["enableShutterSound"] as? Boolean ?: true + // TODO: Implement Red Eye Reduction + options["enableAutoRedEyeReduction"] + val flashMode = Flash.fromUnionValue(flash) val qualityPrioritizationMode = QualityPrioritization.fromUnionValue(qualityPrioritization) @@ -41,7 +43,6 @@ suspend fun CameraView.takePhoto(optionsMap: ReadableMap): WritableMap { qualityPrioritizationMode, flashMode, enableShutterSound, - enableAutoRedEyeReduction, enableAutoStabilization, orientation ) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt index 3d20edb..a589c26 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt @@ -58,7 +58,10 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId val isMultiCam by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA) } val supportsDepthCapture by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT) } val supportsRawCapture by lazy { capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW) } - val supportsLowLightBoost by lazy { extensions.contains(CameraExtensionCharacteristics.EXTENSION_NIGHT) } + val supportsLowLightBoost by lazy { + extensions.contains(CameraExtensionCharacteristics.EXTENSION_NIGHT) && + modes.contains(CameraCharacteristics.CONTROL_MODE_USE_SCENE_MODE) + } val lensFacing by lazy { LensFacing.fromCameraCharacteristics(characteristics) } val hasFlash by lazy { characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false } val focalLengths by lazy { @@ -122,6 +125,7 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId val supportsExposureRegions by lazy { (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE) ?: 0) > 0 } val supportsWhiteBalanceRegions by lazy { (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB) ?: 0) > 0 } + val modes by lazy { characteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_MODES)?.toList() ?: emptyList() } val afModes by lazy { characteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)?.toList() ?: emptyList() } val aeModes by lazy { characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES)?.toList() ?: emptyList() } val awbModes by lazy { characteristics.get(CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES)?.toList() ?: emptyList() } diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt index ee9b2e5..893199e 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt @@ -368,7 +368,6 @@ class CameraSession(private val context: Context, private val cameraManager: Cam qualityPrioritization: QualityPrioritization, flash: Flash, enableShutterSound: Boolean, - enableRedEyeReduction: Boolean, enableAutoStabilization: Boolean, outputOrientation: Orientation ): CapturedPhoto { @@ -378,7 +377,6 @@ class CameraSession(private val context: Context, private val cameraManager: Cam val result = captureSession.capture( qualityPrioritization, flash, - enableRedEyeReduction, enableAutoStabilization, photoOutput.enableHdr, outputOrientation, diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt index fc3276d..bca088d 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt @@ -140,7 +140,6 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p suspend fun capture( qualityPrioritization: QualityPrioritization, flash: Flash, - enableRedEyeReduction: Boolean, enableAutoStabilization: Boolean, enablePhotoHdr: Boolean, orientation: Orientation, @@ -157,8 +156,6 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p val photoRequest = PhotoCaptureRequest( repeatingRequest, qualityPrioritization, - flash, - enableRedEyeReduction, enableAutoStabilization, enablePhotoHdr, orientation @@ -195,6 +192,7 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p // 2. Once precapture AF/AE/AWB successfully locked, capture the actual photo val singleRequest = photoRequest.createCaptureRequest(device, deviceDetails, outputs) if (result.needsFlash) { + singleRequest.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON) singleRequest.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_SINGLE) } return session.capture(singleRequest.build(), enableShutterSound) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/capture/CameraCaptureRequest.kt b/package/android/src/main/java/com/mrousavy/camera/core/capture/CameraCaptureRequest.kt index fbf26cd..eeb5276 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/capture/CameraCaptureRequest.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/capture/CameraCaptureRequest.kt @@ -75,7 +75,7 @@ abstract class CameraCaptureRequest( } // Set Zoom - builder.setZoom(zoom, deviceDetails.characteristics) + builder.setZoom(zoom, deviceDetails) // Set Torch if (torch == Torch.ON) { diff --git a/package/android/src/main/java/com/mrousavy/camera/core/capture/PhotoCaptureRequest.kt b/package/android/src/main/java/com/mrousavy/camera/core/capture/PhotoCaptureRequest.kt index cfb5ae0..03ca3de 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/capture/PhotoCaptureRequest.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/capture/PhotoCaptureRequest.kt @@ -7,7 +7,6 @@ import android.os.Build import android.util.Log import com.mrousavy.camera.core.CameraDeviceDetails import com.mrousavy.camera.core.outputs.SurfaceOutput -import com.mrousavy.camera.types.Flash import com.mrousavy.camera.types.HardwareLevel import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.QualityPrioritization @@ -16,8 +15,6 @@ import com.mrousavy.camera.types.Torch class PhotoCaptureRequest( repeatingRequest: RepeatingCaptureRequest, private val qualityPrioritization: QualityPrioritization, - private val flash: Flash, - private val enableRedEyeReduction: Boolean, private val enableAutoStabilization: Boolean, enablePhotoHdr: Boolean, private val outputOrientation: Orientation @@ -138,26 +135,6 @@ class PhotoCaptureRequest( val targetOrientation = outputOrientation.toSensorRelativeOrientation(deviceDetails) builder.set(CaptureRequest.JPEG_ORIENTATION, targetOrientation.toDegrees()) - // TODO: Fix flash. - when (flash) { - // Set the Flash Mode - Flash.OFF -> { - builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON) - builder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF) - } - Flash.ON -> { - builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON) - builder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH) - } - Flash.AUTO -> { - if (enableRedEyeReduction) { - builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE) - } else { - builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH) - } - } - } - // Set stabilization for this Frame if (enableAutoStabilization) { if (deviceDetails.opticalStabilizationModes.contains(CameraCharacteristics.LENS_OPTICAL_STABILIZATION_MODE_ON)) { diff --git a/package/android/src/main/java/com/mrousavy/camera/core/capture/RepeatingCaptureRequest.kt b/package/android/src/main/java/com/mrousavy/camera/core/capture/RepeatingCaptureRequest.kt index b9a2f08..a95ca29 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/capture/RepeatingCaptureRequest.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/capture/RepeatingCaptureRequest.kt @@ -11,6 +11,7 @@ import com.mrousavy.camera.core.InvalidVideoStabilizationMode import com.mrousavy.camera.core.PropRequiresFormatToBeNonNullError import com.mrousavy.camera.core.outputs.SurfaceOutput import com.mrousavy.camera.types.CameraDeviceFormat +import com.mrousavy.camera.types.HardwareLevel import com.mrousavy.camera.types.Torch import com.mrousavy.camera.types.VideoStabilizationMode @@ -51,7 +52,9 @@ class RepeatingCaptureRequest( ): CaptureRequest.Builder { val builder = super.createCaptureRequest(template, device, deviceDetails, outputs) - builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO) + if (deviceDetails.modes.contains(CameraCharacteristics.CONTROL_MODE_AUTO)) { + builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO) + } // Set AF if (enableVideoPipeline && deviceDetails.afModes.contains(CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_VIDEO)) { @@ -95,7 +98,11 @@ class RepeatingCaptureRequest( builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, getBestDigitalStabilizationMode(deviceDetails)) } VideoStabilizationMode.CINEMATIC, VideoStabilizationMode.CINEMATIC_EXTENDED -> { - builder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON) + if (deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED)) { + builder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON) + } else { + builder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, getBestDigitalStabilizationMode(deviceDetails)) + } } else -> throw InvalidVideoStabilizationMode(videoStabilizationMode) } diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CaptureRequest+setZoom.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CaptureRequest+setZoom.kt index 657886e..d097cbc 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CaptureRequest+setZoom.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CaptureRequest+setZoom.kt @@ -1,20 +1,18 @@ package com.mrousavy.camera.extensions -import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CaptureRequest import android.os.Build -import android.util.Range +import com.mrousavy.camera.core.CameraDeviceDetails +import com.mrousavy.camera.types.HardwareLevel -fun CaptureRequest.Builder.setZoom(zoom: Float, cameraCharacteristics: CameraCharacteristics) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val zoomRange = cameraCharacteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE) ?: Range(1f, 1f) - val zoomClamped = zoomRange.clamp(zoom) +fun CaptureRequest.Builder.setZoom(zoom: Float, deviceDetails: CameraDeviceDetails) { + val zoomRange = deviceDetails.zoomRange + val zoomClamped = zoomRange.clamp(zoom) + + if (deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { this.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoomClamped) } else { - val maxZoom = cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) - val zoomRange = Range(1f, maxZoom ?: 1f) - val size = cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!! - val zoomClamped = zoomRange.clamp(zoom) + val size = deviceDetails.activeSize this.set(CaptureRequest.SCALER_CROP_REGION, size.zoomed(zoomClamped)) } } From bda43d39846f2a874233e8882f133c4be417ef36 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 16 Feb 2024 13:46:13 +0100 Subject: [PATCH 46/71] fix: Fix nullable props (#2573) * fix: Fix nullable props * Update CameraView.kt * Pass error to JS --- .../java/com/mrousavy/camera/CameraView.kt | 31 +++---- .../com/mrousavy/camera/CameraViewManager.kt | 81 ++++++++++++------- .../com/mrousavy/camera/core/VideoPipeline.kt | 4 +- .../camera/types/CodeScannerOptions.kt | 20 ++--- .../com/mrousavy/camera/types/ResizeMode.kt | 4 +- package/example/ios/Podfile.lock | 6 +- package/src/CameraProps.ts | 1 + 7 files changed, 80 insertions(+), 67 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt index 12bffc7..bbff079 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt @@ -7,7 +7,6 @@ import android.util.Log import android.view.Gravity import android.view.ScaleGestureDetector import android.widget.FrameLayout -import com.facebook.react.bridge.ReadableMap import com.google.mlkit.vision.barcode.common.Barcode import com.mrousavy.camera.core.CameraConfiguration import com.mrousavy.camera.core.CameraQueues @@ -48,24 +47,23 @@ class CameraView(context: Context) : // props that require reconfiguring var cameraId: String? = null var enableDepthData = false - var enableHighQualityPhotos: Boolean? = null var enablePortraitEffectsMatteDelivery = false // use-cases - var photo: Boolean? = null - var video: Boolean? = null - var audio: Boolean? = null + var photo = false + var video = false + var audio = false var enableFrameProcessor = false var pixelFormat: PixelFormat = PixelFormat.NATIVE // props that require format reconfiguring - var format: ReadableMap? = null + var format: CameraDeviceFormat? = null var fps: Int? = null var videoStabilizationMode: VideoStabilizationMode? = null var videoHdr = false var photoHdr = false - var lowLightBoost: Boolean? = null // nullable bool - var enableGpuBuffers: Boolean = false + var lowLightBoost = false + var enableGpuBuffers = false // other props var isActive = false @@ -73,7 +71,7 @@ class CameraView(context: Context) : var zoom: Float = 1f // in "factor" var exposure: Double = 1.0 var orientation: Orientation = Orientation.PORTRAIT - var enableZoomGesture: Boolean = false + var enableZoomGesture = false set(value) { field = value updateZoomGesture() @@ -83,7 +81,7 @@ class CameraView(context: Context) : previewView.resizeMode = value field = value } - var enableFpsGraph: Boolean = false + var enableFpsGraph = false set(value) { field = value updateFpsGraph() @@ -155,14 +153,14 @@ class CameraView(context: Context) : config.cameraId = cameraId // Photo - if (photo == true) { + if (photo) { config.photo = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Photo(photoHdr)) } else { config.photo = CameraConfiguration.Output.Disabled.create() } // Video/Frame Processor - if (video == true || enableFrameProcessor) { + if (video || enableFrameProcessor) { config.video = CameraConfiguration.Output.Enabled.create( CameraConfiguration.Video( videoHdr, @@ -176,7 +174,7 @@ class CameraView(context: Context) : } // Audio - if (audio == true) { + if (audio) { config.audio = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Audio(Unit)) } else { config.audio = CameraConfiguration.Output.Disabled.create() @@ -196,12 +194,7 @@ class CameraView(context: Context) : config.orientation = orientation // Format - val format = format - if (format != null) { - config.format = CameraDeviceFormat.fromJSValue(format) - } else { - config.format = null - } + config.format = format // Side-Props config.fps = fps diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt b/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt index ab047d7..9d67dfd 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt @@ -5,6 +5,7 @@ import com.facebook.react.common.MapBuilder import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ViewGroupManager import com.facebook.react.uimanager.annotations.ReactProp +import com.mrousavy.camera.types.CameraDeviceFormat import com.mrousavy.camera.types.CodeScannerOptions import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.PixelFormat @@ -44,17 +45,17 @@ class CameraViewManager : ViewGroupManager() { } @ReactProp(name = "photo") - fun setPhoto(view: CameraView, photo: Boolean?) { + fun setPhoto(view: CameraView, photo: Boolean) { view.photo = photo } @ReactProp(name = "video") - fun setVideo(view: CameraView, video: Boolean?) { + fun setVideo(view: CameraView, video: Boolean) { view.video = video } @ReactProp(name = "audio") - fun setAudio(view: CameraView, audio: Boolean?) { + fun setAudio(view: CameraView, audio: Boolean) { view.audio = audio } @@ -65,8 +66,12 @@ class CameraViewManager : ViewGroupManager() { @ReactProp(name = "pixelFormat") fun setPixelFormat(view: CameraView, pixelFormat: String?) { - val newPixelFormat = PixelFormat.fromUnionValue(pixelFormat) - view.pixelFormat = newPixelFormat + if (pixelFormat != null) { + val newPixelFormat = PixelFormat.fromUnionValue(pixelFormat) + view.pixelFormat = newPixelFormat + } else { + view.pixelFormat = PixelFormat.NATIVE + } } @ReactProp(name = "enableDepthData") @@ -91,13 +96,12 @@ class CameraViewManager : ViewGroupManager() { @ReactProp(name = "videoStabilizationMode") fun setVideoStabilizationMode(view: CameraView, videoStabilizationMode: String?) { - val newMode = VideoStabilizationMode.fromUnionValue(videoStabilizationMode) - view.videoStabilizationMode = newMode - } - - @ReactProp(name = "enableHighQualityPhotos") - fun setEnableHighQualityPhotos(view: CameraView, enableHighQualityPhotos: Boolean?) { - view.enableHighQualityPhotos = enableHighQualityPhotos + if (videoStabilizationMode != null) { + val newMode = VideoStabilizationMode.fromUnionValue(videoStabilizationMode) + view.videoStabilizationMode = newMode + } else { + view.videoStabilizationMode = null + } } @ReactProp(name = "enablePortraitEffectsMatteDelivery") @@ -107,13 +111,22 @@ class CameraViewManager : ViewGroupManager() { @ReactProp(name = "format") fun setFormat(view: CameraView, format: ReadableMap?) { - view.format = format + if (format != null) { + val newFormat = CameraDeviceFormat.fromJSValue(format) + view.format = newFormat + } else { + view.format = null + } } @ReactProp(name = "resizeMode") - fun setResizeMode(view: CameraView, resizeMode: String) { - val newMode = ResizeMode.fromUnionValue(resizeMode) - view.resizeMode = newMode + fun setResizeMode(view: CameraView, resizeMode: String?) { + if (resizeMode != null) { + val newMode = ResizeMode.fromUnionValue(resizeMode) + view.resizeMode = newMode + } else { + view.resizeMode = ResizeMode.COVER + } } // TODO: Change when TurboModules release. @@ -124,30 +137,34 @@ class CameraViewManager : ViewGroupManager() { view.fps = if (fps > 0) fps else null } - @ReactProp(name = "photoHdr", defaultBoolean = false) + @ReactProp(name = "photoHdr") fun setPhotoHdr(view: CameraView, photoHdr: Boolean) { view.photoHdr = photoHdr } - @ReactProp(name = "videoHdr", defaultBoolean = false) + @ReactProp(name = "videoHdr") fun setVideoHdr(view: CameraView, videoHdr: Boolean) { view.videoHdr = videoHdr } @ReactProp(name = "lowLightBoost") - fun setLowLightBoost(view: CameraView, lowLightBoost: Boolean?) { + fun setLowLightBoost(view: CameraView, lowLightBoost: Boolean) { view.lowLightBoost = lowLightBoost } - @ReactProp(name = "isActive", defaultBoolean = false) + @ReactProp(name = "isActive") fun setIsActive(view: CameraView, isActive: Boolean) { view.isActive = isActive } @ReactProp(name = "torch") - fun setTorch(view: CameraView, torch: String) { - val newMode = Torch.fromUnionValue(torch) - view.torch = newMode + fun setTorch(view: CameraView, torch: String?) { + if (torch != null) { + val newMode = Torch.fromUnionValue(torch) + view.torch = newMode + } else { + view.torch = Torch.OFF + } } @ReactProp(name = "zoom") @@ -162,14 +179,22 @@ class CameraViewManager : ViewGroupManager() { @ReactProp(name = "orientation") fun setOrientation(view: CameraView, orientation: String?) { - val newMode = Orientation.fromUnionValue(orientation) - view.orientation = newMode + if (orientation != null) { + val newMode = Orientation.fromUnionValue(orientation) + view.orientation = newMode + } else { + view.orientation = Orientation.PORTRAIT + } } @ReactProp(name = "codeScannerOptions") - fun setCodeScanner(view: CameraView, codeScannerOptions: ReadableMap) { - val newCodeScannerOptions = CodeScannerOptions(codeScannerOptions) - view.codeScannerOptions = newCodeScannerOptions + fun setCodeScanner(view: CameraView, codeScannerOptions: ReadableMap?) { + if (codeScannerOptions != null) { + val newCodeScannerOptions = CodeScannerOptions.fromJSValue(codeScannerOptions) + view.codeScannerOptions = newCodeScannerOptions + } else { + view.codeScannerOptions = null + } } companion object { diff --git a/package/android/src/main/java/com/mrousavy/camera/core/VideoPipeline.kt b/package/android/src/main/java/com/mrousavy/camera/core/VideoPipeline.kt index ae2c107..f46ad2b 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/VideoPipeline.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/VideoPipeline.kt @@ -33,7 +33,7 @@ class VideoPipeline( val format: PixelFormat = PixelFormat.NATIVE, private val isMirrored: Boolean = false, private val enableFrameProcessor: Boolean = false, - private val enableGpuBuffers: Boolean = false, + enableGpuBuffers: Boolean = false, private val callback: CameraSession.Callback ) : SurfaceTexture.OnFrameAvailableListener, Closeable { @@ -116,7 +116,7 @@ class VideoPipeline( } } catch (e: Throwable) { Log.e(TAG, "FrameProcessor/ImageReader pipeline threw an error!", e) - throw e + callback.onError(e) } finally { frame.decrementRefCount() } diff --git a/package/android/src/main/java/com/mrousavy/camera/types/CodeScannerOptions.kt b/package/android/src/main/java/com/mrousavy/camera/types/CodeScannerOptions.kt index ee47657..618f4aa 100644 --- a/package/android/src/main/java/com/mrousavy/camera/types/CodeScannerOptions.kt +++ b/package/android/src/main/java/com/mrousavy/camera/types/CodeScannerOptions.kt @@ -3,20 +3,12 @@ package com.mrousavy.camera.types import com.facebook.react.bridge.ReadableMap import com.mrousavy.camera.core.InvalidTypeScriptUnionError -class CodeScannerOptions(map: ReadableMap) { - val codeTypes: List - - init { - val codeTypes = map.getArray("codeTypes")?.toArrayList() ?: throw InvalidTypeScriptUnionError("codeScanner", map.toString()) - this.codeTypes = codeTypes.map { - return@map CodeType.fromUnionValue(it as String) +data class CodeScannerOptions(val codeTypes: List) { + companion object { + fun fromJSValue(value: ReadableMap): CodeScannerOptions { + val jsCodeTypes = value.getArray("codeTypes") ?: throw InvalidTypeScriptUnionError("codeScanner", value.toString()) + val codeTypes = jsCodeTypes.toArrayList().map { CodeType.fromUnionValue(it as String) } + return CodeScannerOptions(codeTypes) } } - - override fun equals(other: Any?): Boolean { - if (other !is CodeScannerOptions) return false - return codeTypes.size == other.codeTypes.size && codeTypes.containsAll(other.codeTypes) - } - - override fun hashCode(): Int = codeTypes.hashCode() } diff --git a/package/android/src/main/java/com/mrousavy/camera/types/ResizeMode.kt b/package/android/src/main/java/com/mrousavy/camera/types/ResizeMode.kt index b3fa76d..8d03d17 100644 --- a/package/android/src/main/java/com/mrousavy/camera/types/ResizeMode.kt +++ b/package/android/src/main/java/com/mrousavy/camera/types/ResizeMode.kt @@ -1,5 +1,7 @@ package com.mrousavy.camera.types +import com.mrousavy.camera.core.InvalidTypeScriptUnionError + enum class ResizeMode(override val unionValue: String) : JSUnionValue { COVER("cover"), CONTAIN("contain"); @@ -9,7 +11,7 @@ enum class ResizeMode(override val unionValue: String) : JSUnionValue { when (unionValue) { "cover" -> COVER "contain" -> CONTAIN - else -> COVER + else -> throw InvalidTypeScriptUnionError("resizeMode", unionValue) } } } diff --git a/package/example/ios/Podfile.lock b/package/example/ios/Podfile.lock index cc03551..34be125 100644 --- a/package/example/ios/Podfile.lock +++ b/package/example/ios/Podfile.lock @@ -484,7 +484,7 @@ PODS: - libwebp (~> 1.0) - SDWebImage/Core (~> 5.10) - SocketRocket (0.6.1) - - VisionCamera (3.9.0-beta.3): + - VisionCamera (3.9.0-beta.6): - React - React-callinvoker - React-Core @@ -724,9 +724,9 @@ SPEC CHECKSUMS: SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - VisionCamera: 666b0baa70060c0a472b30fc6d3a37f77e40fa9c + VisionCamera: 33c90675adf75528199f840f81dfbe74a2fe6c3f Yoga: 4c3aa327e4a6a23eeacd71f61c81df1bcdf677d5 PODFILE CHECKSUM: 27f53791141a3303d814e09b55770336416ff4eb -COCOAPODS: 1.11.3 +COCOAPODS: 1.14.3 diff --git a/package/src/CameraProps.ts b/package/src/CameraProps.ts index e264cb3..eea1fdd 100644 --- a/package/src/CameraProps.ts +++ b/package/src/CameraProps.ts @@ -246,6 +246,7 @@ export interface CameraProps extends ViewProps { * * Dual Device fusion for greater detail ([`isDualCameraDualPhotoDeliveryEnabled`](https://developer.apple.com/documentation/avfoundation/avcapturephotosettings/2873917-isdualcameradualphotodeliveryena)) * * Sets the maximum quality prioritization to `.quality` ([`maxPhotoQualityPrioritization`](https://developer.apple.com/documentation/avfoundation/avcapturephotooutput/3182995-maxphotoqualityprioritization)) * + * @platform iOS * @default false */ enableHighQualityPhotos?: boolean From 147aff8683b6500ede825c4c06d27110af7a0654 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 16 Feb 2024 13:52:12 +0100 Subject: [PATCH 47/71] chore: release 3.9.0 --- package/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/package.json b/package/package.json index 3899618..85d2e64 100644 --- a/package/package.json +++ b/package/package.json @@ -1,6 +1,6 @@ { "name": "react-native-vision-camera", - "version": "3.9.0-beta.6", + "version": "3.9.0", "description": "A powerful, high-performance React Native Camera library.", "main": "lib/commonjs/index", "module": "lib/module/index", From 2c976d8ccde764301d7d8a5ea695c47f86d03c12 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 16 Feb 2024 17:55:35 +0100 Subject: [PATCH 48/71] feat: Upload .apk as a GitHub artifact (#2575) * feat: Upload .apk as a GitHub artifact * Update build-android.yml * Update build-android.yml * fix path * Update build-android.yml --- .github/workflows/build-android.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index ca207b4..7bc9ecb 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -64,6 +64,12 @@ jobs: ${{ runner.os }}-gradle- - name: Run Gradle Build for example/android/ run: cd example/android && ./gradlew assembleDebug --build-cache && cd ../.. + - name: Upload .apk to GitHub + uses: actions/upload-artifact@v4 + with: + name: VisionCamera Example (.apk) + path: package/example/android/app/build/outputs/apk/debug/app-debug.apk + if-no-files-found: error build-no-frame-processors: name: Build Android Example App (without Frame Processors) From f6b7163660679599e48d1b75ab444ba8b5564809 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 16 Feb 2024 18:01:52 +0100 Subject: [PATCH 49/71] chore: Fix some C++ compiler warnings (#2576) * chore: Fix some C++ compiler warnings * Update MutableJByteBuffer.h * Update MutableJByteBuffer.h * Remove noexcept * Explicit --- package/android/src/main/cpp/MutableJByteBuffer.cpp | 2 +- .../src/main/cpp/frameprocessor/java-bindings/JSharedArray.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package/android/src/main/cpp/MutableJByteBuffer.cpp b/package/android/src/main/cpp/MutableJByteBuffer.cpp index b4d5695..e472da3 100644 --- a/package/android/src/main/cpp/MutableJByteBuffer.cpp +++ b/package/android/src/main/cpp/MutableJByteBuffer.cpp @@ -14,7 +14,7 @@ MutableJByteBuffer::MutableJByteBuffer(jni::alias_ref byteBuff _byteBuffer = jni::make_global(byteBuffer); } -MutableJByteBuffer::~MutableJByteBuffer() noexcept { +MutableJByteBuffer::~MutableJByteBuffer() { // Hermes GC might destroy HostObjects on an arbitrary Thread which might not be // connected to the JNI environment. To make sure fbjni can properly destroy // the Java method, we connect to a JNI environment first. diff --git a/package/android/src/main/cpp/frameprocessor/java-bindings/JSharedArray.cpp b/package/android/src/main/cpp/frameprocessor/java-bindings/JSharedArray.cpp index 474d0dd..46f94f8 100644 --- a/package/android/src/main/cpp/frameprocessor/java-bindings/JSharedArray.cpp +++ b/package/android/src/main/cpp/frameprocessor/java-bindings/JSharedArray.cpp @@ -34,7 +34,7 @@ JSharedArray::JSharedArray(const jni::alias_ref& javaThis, const #else jsi::Runtime& runtime = *proxy->cthis()->getJSRuntime(); #endif - __android_log_print(ANDROID_LOG_INFO, TAG, "Wrapping Java ByteBuffer with size %i...", byteBuffer->getDirectSize()); + __android_log_print(ANDROID_LOG_INFO, TAG, "Wrapping Java ByteBuffer with size %zu...", byteBuffer->getDirectSize()); _byteBuffer = jni::make_global(byteBuffer); _size = _byteBuffer->getDirectSize(); From 2c52fb01e4686d5ca87bb1982165ed644a0211d3 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Mon, 19 Feb 2024 13:41:23 +0100 Subject: [PATCH 50/71] chore: Update comments --- .../com/mrousavy/camera/core/PersistentCameraCaptureSession.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt index bca088d..2a7bd8c 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt @@ -197,7 +197,7 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p } return session.capture(singleRequest.build(), enableShutterSound) } finally { - // 5. After taking a photo we set the repeating request back to idle to remove the AE/AF/AWB locks again + // 3. After taking a photo we set the repeating request back to idle to remove the AE/AF/AWB locks again val idleRequest = repeatingRequest.createCaptureRequest(device, deviceDetails, repeatingOutputs) session.setRepeatingRequest(idleRequest.build(), null, null) } From 9af6e61dc8293d68f77a2e3e9650e6f5a58ac24d Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Mon, 19 Feb 2024 14:11:14 +0100 Subject: [PATCH 51/71] fix: Cancel focus on session close (#2578) --- .../com/mrousavy/camera/core/PersistentCameraCaptureSession.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt index 2a7bd8c..db31fcf 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt @@ -64,6 +64,7 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p get() = isActive && session != null && device != null && !didDestroyFromOutside override fun close() { + focusJob?.cancel() session?.tryAbortCaptures() device?.close() } From 7ac6f4d008af15e16c55e8bb23998e676c7fb931 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Mon, 19 Feb 2024 14:54:13 +0100 Subject: [PATCH 52/71] fix: Trigger `measure` and `layout` manually in PreviewView (#2588) * fix: Trigger `measure` and `layout` manually to fix Preview stretching * fix: Check for `0`/`NaN` --- .../java/com/mrousavy/camera/core/PreviewView.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt b/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt index ba3c94b..0727a1f 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt @@ -90,9 +90,23 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : } } + override fun requestLayout() { + super.requestLayout() + // Manually trigger measure & layout, as RN on Android skips those. + // See this issue: https://github.com/facebook/react-native/issues/17968#issuecomment-721958427 + post { + measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)) + layout(left, top, right, bottom) + } + } + private fun getSize(contentSize: Size, containerSize: Size, resizeMode: ResizeMode): Size { val contentAspectRatio = contentSize.width.toDouble() / contentSize.height val containerAspectRatio = containerSize.width.toDouble() / containerSize.height + if (!(contentAspectRatio > 0 && containerAspectRatio > 0)) { + // One of the aspect ratios is 0 or NaN, maybe the view hasn't been laid out yet. + return contentSize + } val widthOverHeight = when (resizeMode) { ResizeMode.COVER -> contentAspectRatio > containerAspectRatio From 55992bb954acde1aba8d85f92e59271809355607 Mon Sep 17 00:00:00 2001 From: bglgwyng Date: Tue, 20 Feb 2024 00:01:21 +0900 Subject: [PATCH 53/71] fix: Fix Frame Processor SIGSEV crash in `VisionCameraScheduler::trigger` by locking mutex in `dispatchAsync` (#2591) Add missing lock. --- .../cpp/frameprocessor/java-bindings/JVisionCameraScheduler.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/package/android/src/main/cpp/frameprocessor/java-bindings/JVisionCameraScheduler.cpp b/package/android/src/main/cpp/frameprocessor/java-bindings/JVisionCameraScheduler.cpp index 77a28a6..816b0af 100644 --- a/package/android/src/main/cpp/frameprocessor/java-bindings/JVisionCameraScheduler.cpp +++ b/package/android/src/main/cpp/frameprocessor/java-bindings/JVisionCameraScheduler.cpp @@ -15,6 +15,7 @@ TSelf JVisionCameraScheduler::initHybrid(jni::alias_ref jThis) { } void JVisionCameraScheduler::dispatchAsync(const std::function& job) { + std::unique_lock lock(_mutex); // 1. add job to queue _jobs.push(job); scheduleTrigger(); From 4c159aff61e7fd16114dc9dcd9dc438cc7d12520 Mon Sep 17 00:00:00 2001 From: Lia/Leo Treloar <79512656+ltrel@users.noreply.github.com> Date: Tue, 20 Feb 2024 18:54:08 +1100 Subject: [PATCH 54/71] docs: Fix `toArrayBuffer` example in Frame Processor Guide (#2595) --- docs/docs/guides/FRAME_PROCESSORS.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/docs/guides/FRAME_PROCESSORS.mdx b/docs/docs/guides/FRAME_PROCESSORS.mdx index 31b36a1..755c510 100644 --- a/docs/docs/guides/FRAME_PROCESSORS.mdx +++ b/docs/docs/guides/FRAME_PROCESSORS.mdx @@ -89,7 +89,8 @@ Additionally, you can also directly access the Frame's pixel data using [`toArra const frameProcessor = useFrameProcessor((frame) => { 'worklet' if (frame.pixelFormat === 'rgb') { - const data = frame.toArrayBuffer() + const buffer = frame.toArrayBuffer() + const data = new Uint8Array(buffer) console.log(`Pixel at 0,0: RGB(${data[0]}, ${data[1]}, ${data[2]})`) } }, []) From fabf019f664fdcb8e4509c835c45b9d0f136b54e Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 20 Feb 2024 08:54:26 +0100 Subject: [PATCH 55/71] fix: Fix "Unknown AF/AE/AWB State" error (#2592) --- ...ureSession+setRepeatingRequestAndWaitForPrecapture.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt index 70b3fb7..916debb 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt @@ -30,6 +30,7 @@ interface AutoState { } enum class FocusState : AutoState { + Unknown, Inactive, Scanning, Focused, @@ -53,11 +54,12 @@ enum class FocusState : AutoState { CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN -> PassiveScanning CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED -> PassiveFocused CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED -> PassiveUnfocused - else -> throw Error("Invalid CONTROL_AF_STATE! $afState") + else -> Unknown } } } enum class ExposureState : AutoState { + Unknown, Locked, Inactive, Precapture, @@ -79,12 +81,13 @@ enum class ExposureState : AutoState { CaptureResult.CONTROL_AE_STATE_CONVERGED -> Converged CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED -> FlashRequired CaptureResult.CONTROL_AE_STATE_LOCKED -> Locked - else -> throw Error("Invalid CONTROL_AE_STATE! $aeState") + else -> Unknown } } } enum class WhiteBalanceState : AutoState { + Unknown, Inactive, Locked, Searching, @@ -102,7 +105,7 @@ enum class WhiteBalanceState : AutoState { CaptureResult.CONTROL_AWB_STATE_SEARCHING -> Searching CaptureResult.CONTROL_AWB_STATE_CONVERGED -> Converged CaptureResult.CONTROL_AWB_STATE_LOCKED -> Locked - else -> throw Error("Invalid CONTROL_AWB_STATE! $awbState") + else -> Unknown } } } From 369cb4a043aef601aa092fe08f807c7376da647f Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 20 Feb 2024 18:35:55 +0100 Subject: [PATCH 56/71] fix: Fix `Precapture timed out after 5 seconds` error (#2586) * fix: Fix precapture timeout error on capture * fix: Catch timeout errors * Update PersistentCameraCaptureSession.kt * Update PersistentCameraCaptureSession.kt * fix: Remove unsupported AE/AF/AWB triggers * fix: Only enable flash if it is really AUTO * Update CameraCaptureSession+precapture.kt * Update CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt * Update PersistentCameraCaptureSession.kt --- .../core/PersistentCameraCaptureSession.kt | 27 +++++++--- .../CameraCaptureSession+precapture.kt | 51 ++++++++++++------- ...setRepeatingRequestAndWaitForPrecapture.kt | 15 +++--- 3 files changed, 60 insertions(+), 33 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt index db31fcf..6162dc2 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt @@ -42,6 +42,7 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p companion object { private const val TAG = "PersistentCameraCaptureSession" private const val FOCUS_RESET_TIMEOUT = 3000L + private const val PRECAPTURE_LOCK_TIMEOUT = 5000L } // Inputs/Dependencies @@ -178,21 +179,30 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p Log.i(TAG, "Locking AF/AE/AWB...") // 1. Run precapture sequence - val precaptureRequest = repeatingRequest.createCaptureRequest(device, deviceDetails, repeatingOutputs) - val skipIfPassivelyFocused = flash == Flash.OFF - val options = - PrecaptureOptions( + var needsFlash: Boolean + try { + val precaptureRequest = repeatingRequest.createCaptureRequest(device, deviceDetails, repeatingOutputs) + val skipIfPassivelyFocused = flash == Flash.OFF + val options = PrecaptureOptions( listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE, PrecaptureTrigger.AWB), flash, emptyList(), - skipIfPassivelyFocused + skipIfPassivelyFocused, + PRECAPTURE_LOCK_TIMEOUT ) - val result = session.precapture(precaptureRequest, deviceDetails, options) + val result = session.precapture(precaptureRequest, deviceDetails, options) + needsFlash = result.needsFlash + } catch (e: CaptureTimedOutError) { + // the precapture just timed out after 5 seconds, take picture anyways without focus. + needsFlash = false + } catch (e: FocusCanceledError) { + throw CaptureAbortedError(false) + } try { // 2. Once precapture AF/AE/AWB successfully locked, capture the actual photo val singleRequest = photoRequest.createCaptureRequest(device, deviceDetails, outputs) - if (result.needsFlash) { + if (needsFlash) { singleRequest.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON) singleRequest.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_SINGLE) } @@ -224,7 +234,8 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p // 1. Run a precapture sequence for AF, AE and AWB. focusJob = coroutineScope.launch { val request = repeatingRequest.createCaptureRequest(device, deviceDetails, outputs) - val options = PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point), false) + val options = + PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point), false, FOCUS_RESET_TIMEOUT) session.precapture(request, deviceDetails, options) } focusJob?.join() diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt index ea9178d..e52ad2c 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt @@ -18,7 +18,8 @@ data class PrecaptureOptions( val modes: List, val flash: Flash = Flash.OFF, val pointsOfInterest: List, - val skipIfPassivelyFocused: Boolean + val skipIfPassivelyFocused: Boolean, + val timeoutMs: Long ) data class PrecaptureResult(val needsFlash: Boolean) @@ -57,12 +58,8 @@ suspend fun CameraCaptureSession.precapture( aeState = ExposureState.fromAEState(result.get(CaptureResult.CONTROL_AE_STATE) ?: CaptureResult.CONTROL_AE_STATE_INACTIVE) awbState = WhiteBalanceState.fromAWBState(result.get(CaptureResult.CONTROL_AWB_STATE) ?: CaptureResult.CONTROL_AWB_STATE_INACTIVE) - if (aeState == ExposureState.FlashRequired) { - Log.i(TAG, "Auto-Flash: Flash is required for photo capture, enabling flash...") - enableFlash = true - } else { - Log.i(TAG, "Auto-Flash: Flash is not required for photo capture.") - } + Log.i(TAG, "Precapture current states: AF: $afState, AE: $aeState, AWB: $awbState") + enableFlash = aeState == ExposureState.FlashRequired && options.flash == Flash.AUTO } else { // we either want Flash ON or OFF, so we don't care about lighting conditions - do a fast capture. this.capture(request.build(), null, null) @@ -99,23 +96,41 @@ suspend fun CameraCaptureSession.precapture( // AF Precapture if (deviceDetails.afModes.contains(CaptureRequest.CONTROL_AF_MODE_AUTO)) { request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO) + request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START) + if (meteringRectangles.isNotEmpty() && deviceDetails.supportsFocusRegions) { + request.set(CaptureRequest.CONTROL_AF_REGIONS, meteringRectangles) + } + } else { + // AF is not supported on this device. + precaptureModes.remove(PrecaptureTrigger.AF) } - if (meteringRectangles.isNotEmpty() && deviceDetails.supportsFocusRegions) { - request.set(CaptureRequest.CONTROL_AF_REGIONS, meteringRectangles) - } - request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START) } - if (precaptureModes.contains(PrecaptureTrigger.AE) && deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED)) { + if (precaptureModes.contains(PrecaptureTrigger.AE)) { // AE Precapture - if (meteringRectangles.isNotEmpty() && deviceDetails.supportsExposureRegions) { - request.set(CaptureRequest.CONTROL_AE_REGIONS, meteringRectangles) + if (deviceDetails.aeModes.contains(CaptureRequest.CONTROL_AE_MODE_ON) && deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED)) { + request.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON) + request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) + if (meteringRectangles.isNotEmpty() && + deviceDetails.supportsExposureRegions && + deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED) + ) { + request.set(CaptureRequest.CONTROL_AE_REGIONS, meteringRectangles) + } + } else { + // AE is not supported on this device. + precaptureModes.remove(PrecaptureTrigger.AE) } - request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) } if (precaptureModes.contains(PrecaptureTrigger.AWB)) { // AWB Precapture - if (meteringRectangles.isNotEmpty() && deviceDetails.supportsWhiteBalanceRegions) { - request.set(CaptureRequest.CONTROL_AWB_REGIONS, meteringRectangles) + if (deviceDetails.awbModes.contains(CaptureRequest.CONTROL_AWB_MODE_AUTO)) { + request.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO) + if (meteringRectangles.isNotEmpty() && deviceDetails.supportsWhiteBalanceRegions) { + request.set(CaptureRequest.CONTROL_AWB_REGIONS, meteringRectangles) + } + } else { + // AWB is not supported on this device. + precaptureModes.remove(PrecaptureTrigger.AWB) } } this.capture(request.build(), null, null) @@ -125,7 +140,7 @@ suspend fun CameraCaptureSession.precapture( // 3. Start a repeating request without the trigger and wait until AF/AE/AWB locks request.set(CaptureRequest.CONTROL_AF_TRIGGER, null) request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, null) - val result = this.setRepeatingRequestAndWaitForPrecapture(request.build(), *precaptureModes.toTypedArray()) + val result = this.setRepeatingRequestAndWaitForPrecapture(request.build(), options.timeoutMs, *precaptureModes.toTypedArray()) if (!coroutineContext.isActive) throw FocusCanceledError() diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt index 916debb..48ca860 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt @@ -117,6 +117,7 @@ data class ResultState(val focusState: FocusState, val exposureState: ExposureSt */ suspend fun CameraCaptureSession.setRepeatingRequestAndWaitForPrecapture( request: CaptureRequest, + timeoutMs: Long, vararg precaptureTriggers: PrecaptureTrigger ): ResultState = suspendCancellableCoroutine { continuation -> @@ -124,9 +125,9 @@ suspend fun CameraCaptureSession.setRepeatingRequestAndWaitForPrecapture( val completed = precaptureTriggers.associateWith { false }.toMutableMap() CoroutineScope(Dispatchers.Default).launch { - delay(5000) // after 5s, cancel capture + delay(timeoutMs) // after timeout, cancel capture if (continuation.isActive) { - Log.e(TAG, "Precapture timed out after 5 seconds!") + Log.e(TAG, "Precapture timed out after ${timeoutMs / 1000} seconds!") continuation.resumeWithException(CaptureTimedOutError()) try { setRepeatingRequest(request, null, null) @@ -144,25 +145,25 @@ suspend fun CameraCaptureSession.setRepeatingRequestAndWaitForPrecapture( super.onCaptureCompleted(session, request, result) if (continuation.isActive) { - // AF Precapture val afState = FocusState.fromAFState(result.get(CaptureResult.CONTROL_AF_STATE) ?: CaptureResult.CONTROL_AF_STATE_INACTIVE) - val aeState = ExposureState.fromAEState(result.get(CaptureResult.CONTROL_AE_STATE) ?: CaptureResult.CONTROL_AE_STATE_INACTIVE) + val aeState = ExposureState.fromAEState( + result.get(CaptureResult.CONTROL_AE_STATE) ?: CaptureResult.CONTROL_AE_STATE_INACTIVE + ) val awbState = WhiteBalanceState.fromAWBState( result.get(CaptureResult.CONTROL_AWB_STATE) ?: CaptureResult.CONTROL_AWB_STATE_INACTIVE ) + Log.i(TAG, "Precapture state: AF: $afState, AE: $aeState, AWB: $awbState") + // AF Precapture if (precaptureTriggers.contains(PrecaptureTrigger.AF)) { - Log.i(TAG, "AF State: $afState (isCompleted: ${afState.isCompleted})") completed[PrecaptureTrigger.AF] = afState.isCompleted } // AE Precapture if (precaptureTriggers.contains(PrecaptureTrigger.AE)) { - Log.i(TAG, "AE State: $aeState (isCompleted: ${aeState.isCompleted})") completed[PrecaptureTrigger.AE] = aeState.isCompleted } // AWB Precapture if (precaptureTriggers.contains(PrecaptureTrigger.AWB)) { - Log.i(TAG, "AWB State: $awbState (isCompleted: ${awbState.isCompleted})") completed[PrecaptureTrigger.AWB] = awbState.isCompleted } From eb57b3877cc4a23ddc4a1fb0c0c08bb8dfe3bb3a Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 20 Feb 2024 20:34:34 +0100 Subject: [PATCH 57/71] chore: Update actions --- .github/workflows/build-android.yml | 14 ++++---------- package/scripts/clang-format.sh | 3 ++- package/scripts/ktlint.sh | 3 ++- package/scripts/swiftformat.sh | 3 ++- package/scripts/swiftlint.sh | 3 ++- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 7bc9ecb..e6d39ea 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -30,11 +30,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup JDK 11 + - name: Setup JDK 17 uses: actions/setup-java@v4 with: distribution: 'zulu' - java-version: 11 + java-version: 17 java-package: jdk - name: Get yarn cache directory path @@ -64,12 +64,6 @@ jobs: ${{ runner.os }}-gradle- - name: Run Gradle Build for example/android/ run: cd example/android && ./gradlew assembleDebug --build-cache && cd ../.. - - name: Upload .apk to GitHub - uses: actions/upload-artifact@v4 - with: - name: VisionCamera Example (.apk) - path: package/example/android/app/build/outputs/apk/debug/app-debug.apk - if-no-files-found: error build-no-frame-processors: name: Build Android Example App (without Frame Processors) @@ -80,11 +74,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup JDK 11 + - name: Setup JDK 17 uses: actions/setup-java@v4 with: distribution: 'zulu' - java-version: 11 + java-version: 17 java-package: jdk - name: Get yarn cache directory path diff --git a/package/scripts/clang-format.sh b/package/scripts/clang-format.sh index 9c98c17..1d6cea4 100755 --- a/package/scripts/clang-format.sh +++ b/package/scripts/clang-format.sh @@ -5,5 +5,6 @@ if which clang-format >/dev/null; then clang-format -style=file:./cpp/.clang-format -i "$file" done else - echo "warning: clang-format not installed, install with 'brew install clang-format' (or manually from https://clang.llvm.org/docs/ClangFormat.html)" + echo "error: clang-format not installed, install with 'brew install clang-format' (or manually from https://clang.llvm.org/docs/ClangFormat.html)" + exit 1 fi diff --git a/package/scripts/ktlint.sh b/package/scripts/ktlint.sh index 3236451..7a8a263 100755 --- a/package/scripts/ktlint.sh +++ b/package/scripts/ktlint.sh @@ -3,5 +3,6 @@ if which ktlint >/dev/null; then cd android && ktlint --color --relative --editorconfig=./.editorconfig -F ./**/*.kt* else - echo "warning: KTLint not installed, install with 'brew install ktlint' (or manually from https://github.com/pinterest/ktlint)" + echo "error: KTLint not installed, install with 'brew install ktlint' (or manually from https://github.com/pinterest/ktlint)" + exit 1 fi diff --git a/package/scripts/swiftformat.sh b/package/scripts/swiftformat.sh index a7b628f..a06751d 100755 --- a/package/scripts/swiftformat.sh +++ b/package/scripts/swiftformat.sh @@ -3,5 +3,6 @@ if which swiftformat >/dev/null; then cd ios && swiftformat --quiet . else - echo "warning: SwiftFormat not installed, install with 'brew install swiftformat' (or manually from https://github.com/nicklockwood/SwiftFormat)" + echo "error: SwiftFormat not installed, install with 'brew install swiftformat' (or manually from https://github.com/nicklockwood/SwiftFormat)" + exit 1 fi diff --git a/package/scripts/swiftlint.sh b/package/scripts/swiftlint.sh index e355098..42efed7 100755 --- a/package/scripts/swiftlint.sh +++ b/package/scripts/swiftlint.sh @@ -3,5 +3,6 @@ if which swiftlint >/dev/null; then cd ios && swiftlint --quiet --fix && swiftlint --quiet else - echo "warning: SwiftLint not installed, install with 'brew install swiftlint' (or manually from https://github.com/realm/SwiftLint)" + echo "error: SwiftLint not installed, install with 'brew install swiftlint' (or manually from https://github.com/realm/SwiftLint)" + exit 1 fi From 5ab7b291dbb802819c0fdf6415b2e90f3e58bd00 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 20 Feb 2024 21:07:41 +0100 Subject: [PATCH 58/71] Update gradle.properties --- package/example/android/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/example/android/gradle.properties b/package/example/android/gradle.properties index 7f078ce..251094b 100644 --- a/package/example/android/gradle.properties +++ b/package/example/android/gradle.properties @@ -10,7 +10,7 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx10248m -XX:MaxPermSize=256m -org.gradle.jvmargs=-Xms512M -Xmx4g -XX:MaxPermSize=1024m -XX:MaxMetaspaceSize=1g -Dkotlin.daemon.jvm.options="-Xmx1g" +org.gradle.jvmargs=-Xms512M -Xmx4g -XX:MaxMetaspaceSize=1g -Dkotlin.daemon.jvm.options="-Xmx1g" org.gradle.parallel=true org.gradle.daemon=true org.gradle.configureondemand=true From 83b852acd038586abe6b4b2b3b43a7ee56d63022 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 23 Feb 2024 10:43:50 +0100 Subject: [PATCH 59/71] chore: Use `CameraRuntimeError` if `device` is null --- package/src/Camera.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package/src/Camera.tsx b/package/src/Camera.tsx index be99be6..26eb395 100644 --- a/package/src/Camera.tsx +++ b/package/src/Camera.tsx @@ -464,7 +464,8 @@ export class Camera extends React.PureComponent { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (device == null) { - throw new Error( + throw new CameraRuntimeError( + 'device/no-device', 'Camera: `device` is null! Select a valid Camera device. See: https://mrousavy.com/react-native-vision-camera/docs/guides/devices', ) } From 8225ac1afff765d9513fca695a327d501abd7ede Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 23 Feb 2024 15:36:43 +0100 Subject: [PATCH 60/71] Create tea.yaml --- tea.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 tea.yaml diff --git a/tea.yaml b/tea.yaml new file mode 100644 index 0000000..570c683 --- /dev/null +++ b/tea.yaml @@ -0,0 +1,6 @@ +# https://tea.xyz/what-is-this-file +--- +version: 1.0.0 +codeOwners: + - '0xcF3c286e7cDED19D87f61E85B3370283C885bA88' +quorum: 1 From 86637ac1129621e12420df85d0d834493ba710d5 Mon Sep 17 00:00:00 2001 From: mbghsource Date: Fri, 23 Feb 2024 10:37:44 -0800 Subject: [PATCH 61/71] chore: Update package README (#2601) Update README.md yarn bootstrap needs package.json which is in package directory. --- package/example/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/example/README.md b/package/example/README.md index 1f87c4c..413a28f 100644 --- a/package/example/README.md +++ b/package/example/README.md @@ -30,7 +30,7 @@ To try the playground out for yourself, run the following commands: ```sh git clone https://github.com/mrousavy/react-native-vision-camera -cd react-native-vision-camera +cd react-native-vision-camera/package yarn bootstrap ``` From 79ebae56f3802eedabd993a051d09dd7270f1a74 Mon Sep 17 00:00:00 2001 From: Lihang Xu Date: Sat, 24 Feb 2024 02:38:37 +0800 Subject: [PATCH 62/71] docs: Add `vision-camera-cropper` to the plugins list (#2600) --- docs/docs/guides/FRAME_PROCESSOR_PLUGINS.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/docs/guides/FRAME_PROCESSOR_PLUGINS.mdx b/docs/docs/guides/FRAME_PROCESSOR_PLUGINS.mdx index 27506a4..a425609 100644 --- a/docs/docs/guides/FRAME_PROCESSOR_PLUGINS.mdx +++ b/docs/docs/guides/FRAME_PROCESSOR_PLUGINS.mdx @@ -33,6 +33,7 @@ cd ios && pod install * [xulihang/**vision-camera-dynamsoft-barcode-reader**](https://github.com/xulihang/vision-camera-dynamsoft-barcode-reader): A plugin to read barcodes using Dynamsoft Barcode Reader. * [xulihang/**vision-camera-dynamsoft-label-recognizer**](https://github.com/xulihang/vision-camera-dynamsoft-label-recognizer): A plugin to recognize text on labels, MRZ passports, etc. using Dynamsoft Label Recognizer. * [tony-xlh/**vision-camera-dynamsoft-document-normalizer**](https://github.com/tony-xlh/vision-camera-dynamsoft-document-normalizer): A plugin to scan documents using Dynamsoft Document Normalizer with features like document border detection and perspective transformation. +* [tony-xlh/**vision-camera-cropper**](https://github.com/tony-xlh/vision-camera-cropper): A plugin to crop frames and save frames to files or as base64. * [aarongrider/**vision-camera-ocr**](https://github.com/aarongrider/vision-camera-ocr): A plugin to detect text in real time using MLKit Text Detector (OCR). * [yasintorun/**vision-camera-base64**](https://github.com/yasintorun/vision-camera-base64): A plugin to convert a Frame to a base64 string. From bca9472ab8d973d8079da89144f7b58a0321f5ec Mon Sep 17 00:00:00 2001 From: Jonas Thuresson Date: Mon, 26 Feb 2024 09:16:54 +0100 Subject: [PATCH 63/71] fix: Fix Boolean.parse string values from gradle properties (#2599) * parse string booleans from gradle properties * Define separate safeExtGetBool function --------- Co-authored-by: Jonas Thuresson --- package/android/build.gradle | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/package/android/build.gradle b/package/android/build.gradle index 8a79120..88ea6a5 100644 --- a/package/android/build.gradle +++ b/package/android/build.gradle @@ -46,6 +46,10 @@ def safeExtGet(prop, fallback) { rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback } +def safeExtGetBool(prop, fallback) { + Boolean.parseBoolean("${safeExtGet(prop, fallback)}") +} + def reactNativeArchitectures() { def value = project.getProperties().get("reactNativeArchitectures") return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] @@ -68,10 +72,10 @@ static def findNodeModules(baseDir) { def nodeModules = findNodeModules(projectDir) -def hasWorklets = !safeExtGet("VisionCamera_disableFrameProcessors", false) && findProject(":react-native-worklets-core") != null +def hasWorklets = !safeExtGetBool('VisionCamera_disableFrameProcessors', false) && findProject(":react-native-worklets-core") != null logger.warn("[VisionCamera] react-native-worklets-core ${hasWorklets ? "found" : "not found"}, Frame Processors ${hasWorklets ? "enabled" : "disabled"}!") -def enableCodeScanner = safeExtGet("VisionCamera_enableCodeScanner", false) +def enableCodeScanner = safeExtGetBool('VisionCamera_enableCodeScanner', false) repositories { google() From e8dd1e0b2e321a1c75ad148a6f44076fdb61eead Mon Sep 17 00:00:00 2001 From: Guoting Lei <141813170+BrainLei@users.noreply.github.com> Date: Thu, 29 Feb 2024 21:53:36 +0800 Subject: [PATCH 64/71] fix: Use `0x0` as a fallback Size if `SENSOR_INFO_PHYSICAL_SIZE` is null (e.g. on USB cameras) (#2608) 2602:There is a NullPointerException here because CameraCharacteristics cannot get the SENSOR_INFO_PHYSICAL_SIZE value of the camera device --- .../java/com/mrousavy/camera/core/CameraDeviceDetails.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt index a589c26..028cbf6 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt @@ -10,6 +10,7 @@ import android.os.Build import android.util.Log import android.util.Range import android.util.Size +import android.util.SizeF import android.view.SurfaceHolder import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReadableArray @@ -68,7 +69,7 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId // 35mm is the film standard sensor size characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(35f) } - val sensorSize by lazy { characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!! } + val sensorSize by lazy { characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE) ?: SizeF(0f, 0f) } val activeSize get() = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!! val sensorOrientation by lazy { @@ -230,6 +231,9 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId } private fun getFieldOfView(focalLength: Float): Double { + if ((sensorSize.width == 0f) || (sensorSize.height == 0f)) { + return 0.0 + } val sensorDiagonal = sqrt((sensorSize.width * sensorSize.width + sensorSize.height * sensorSize.height).toDouble()) val fovRadians = 2.0 * atan2(sensorDiagonal, (2.0 * focalLength)) return Math.toDegrees(fovRadians) From 3f1a7c9e32e0a9c30298a3718e9a5b610b33f553 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Mon, 4 Mar 2024 12:49:03 +0100 Subject: [PATCH 65/71] fix: Disable precapture sequence by default (#2629) --- .../main/java/com/mrousavy/camera/CameraView+TakePhoto.kt | 2 ++ .../main/java/com/mrousavy/camera/core/CameraSession.kt | 4 +++- .../camera/core/PersistentCameraCaptureSession.kt | 6 ++++-- package/src/PhotoFile.ts | 8 ++++++++ 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView+TakePhoto.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView+TakePhoto.kt index 9098074..c09ae3f 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView+TakePhoto.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView+TakePhoto.kt @@ -32,6 +32,7 @@ suspend fun CameraView.takePhoto(optionsMap: ReadableMap): WritableMap { val flash = options["flash"] as? String ?: "off" val enableAutoStabilization = options["enableAutoStabilization"] == true val enableShutterSound = options["enableShutterSound"] as? Boolean ?: true + val enablePrecapture = options["enablePrecapture"] as? Boolean ?: false // TODO: Implement Red Eye Reduction options["enableAutoRedEyeReduction"] @@ -44,6 +45,7 @@ suspend fun CameraView.takePhoto(optionsMap: ReadableMap): WritableMap { flashMode, enableShutterSound, enableAutoStabilization, + enablePrecapture, orientation ) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt index 893199e..747270b 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt @@ -369,6 +369,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam flash: Flash, enableShutterSound: Boolean, enableAutoStabilization: Boolean, + enablePrecapture: Boolean, outputOrientation: Orientation ): CapturedPhoto { val photoOutput = photoOutput ?: throw PhotoNotEnabledError() @@ -380,7 +381,8 @@ class CameraSession(private val context: Context, private val cameraManager: Cam enableAutoStabilization, photoOutput.enableHdr, outputOrientation, - enableShutterSound + enableShutterSound, + enablePrecapture ) try { diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt index 6162dc2..3bb1c15 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt @@ -145,7 +145,8 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p enableAutoStabilization: Boolean, enablePhotoHdr: Boolean, orientation: Orientation, - enableShutterSound: Boolean + enableShutterSound: Boolean, + enablePrecapture: Boolean ): TotalCaptureResult { // Cancel any ongoing focus jobs focusJob?.cancel() @@ -169,7 +170,8 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p val outputs = outputs val repeatingOutputs = outputs.filter { it.isRepeating } - if (qualityPrioritization == QualityPrioritization.SPEED && flash == Flash.OFF) { + val skipPrecapture = !enablePrecapture || qualityPrioritization == QualityPrioritization.SPEED + if (skipPrecapture && flash == Flash.OFF) { // 0. We want to take a picture as fast as possible, so skip any precapture sequence and just capture one Frame. Log.i(TAG, "Using fast capture path without pre-capture sequence...") val singleRequest = photoRequest.createCaptureRequest(device, deviceDetails, outputs) diff --git a/package/src/PhotoFile.ts b/package/src/PhotoFile.ts index 4c7e132..10d6f5b 100644 --- a/package/src/PhotoFile.ts +++ b/package/src/PhotoFile.ts @@ -44,6 +44,14 @@ export interface TakePhotoOptions { * @default true */ enableShutterSound?: boolean + /** + * Whether to run the pre-capture sequence to properly lock AF, AE and AWB values. + * Enabling this results in greater photos, but might not work on some devices. + * + * @platform Android + * @default false + */ + enablePrecapture?: boolean } /** From 8e1f03907bfa7ae49ca9dc2e74ed149fc8f67388 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Mon, 4 Mar 2024 12:49:46 +0100 Subject: [PATCH 66/71] chore: release 3.9.1 --- package/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/package.json b/package/package.json index 85d2e64..834096f 100644 --- a/package/package.json +++ b/package/package.json @@ -1,6 +1,6 @@ { "name": "react-native-vision-camera", - "version": "3.9.0", + "version": "3.9.1", "description": "A powerful, high-performance React Native Camera library.", "main": "lib/commonjs/index", "module": "lib/module/index", From 97941a919fda4adf910c57c01b3afa206aa737c8 Mon Sep 17 00:00:00 2001 From: Keaton Roux Date: Mon, 18 Mar 2024 16:42:57 +0200 Subject: [PATCH 67/71] feat: Add UPC-A support (#2563) * Add UPC_A support * Add documentation for UPC-A on iOS * Doc adjustments --------- Co-authored-by: Keaton Roux --- docs/docs/guides/CODE_SCANNING.mdx | 19 +++++++++++++++++++ .../com/mrousavy/camera/types/CodeType.kt | 4 ++++ ...MetadataObject.ObjectType+descriptor.swift | 3 +++ package/src/CodeScanner.ts | 1 + 4 files changed, 27 insertions(+) diff --git a/docs/docs/guides/CODE_SCANNING.mdx b/docs/docs/guides/CODE_SCANNING.mdx index 01f8c96..d59ef8f 100644 --- a/docs/docs/guides/CODE_SCANNING.mdx +++ b/docs/docs/guides/CODE_SCANNING.mdx @@ -130,4 +130,23 @@ The Code Scanner will call your [`onCodeScanned`](/docs/api/interfaces/CodeScann
+## UPC-A vs EAN-13 codes + +UPC-A is a special case to handle if you need to cater for it. Android's SDK officially supports UPC-A but iOS does not, instead they handle the code as EAN-13. Since EAN-13 is a superset of UPC-A, with an extra 0 digit at the front. + +This means, the `upc-a` types are reported under the `ean-13` umbrella type on iOS: + +```jsx +const codeScanner = useCodeScanner({ + codeTypes: ['upc-a'], // <-- βœ… We configure for 'upc-a' types + onCodeScanned: (codes) => { + for (const code of codes) { + console.log(code.type); // <-- ❌ On iOS, we receive 'ean-13' + } + } +}) +``` + +You will need to keep this in mind and do the conversion from EAN-13 to UPC-A yourself. This can be done by removing the front `0` digit from the code to get a UPC-A code. + #### πŸš€ Next section: [Frame Processors](frame-processors) diff --git a/package/android/src/main/java/com/mrousavy/camera/types/CodeType.kt b/package/android/src/main/java/com/mrousavy/camera/types/CodeType.kt index 11d137d..c279087 100644 --- a/package/android/src/main/java/com/mrousavy/camera/types/CodeType.kt +++ b/package/android/src/main/java/com/mrousavy/camera/types/CodeType.kt @@ -13,6 +13,7 @@ enum class CodeType(override val unionValue: String) : JSUnionValue { EAN_8("ean-8"), ITF("itf"), UPC_E("upc-e"), + UPC_A("upc-a"), QR("qr"), PDF_417("pdf-417"), AZTEC("aztec"), @@ -29,6 +30,7 @@ enum class CodeType(override val unionValue: String) : JSUnionValue { EAN_8 -> Barcode.FORMAT_EAN_8 ITF -> Barcode.FORMAT_ITF UPC_E -> Barcode.FORMAT_UPC_E + UPC_A -> Barcode.FORMAT_UPC_A QR -> Barcode.FORMAT_QR_CODE PDF_417 -> Barcode.FORMAT_PDF417 AZTEC -> Barcode.FORMAT_AZTEC @@ -47,6 +49,7 @@ enum class CodeType(override val unionValue: String) : JSUnionValue { Barcode.FORMAT_EAN_8 -> EAN_8 Barcode.FORMAT_ITF -> ITF Barcode.FORMAT_UPC_E -> UPC_E + Barcode.FORMAT_UPC_A -> UPC_A Barcode.FORMAT_QR_CODE -> QR Barcode.FORMAT_PDF417 -> PDF_417 Barcode.FORMAT_AZTEC -> AZTEC @@ -64,6 +67,7 @@ enum class CodeType(override val unionValue: String) : JSUnionValue { "ean-8" -> EAN_8 "itf" -> ITF "upc-e" -> UPC_E + "upc-a" -> UPC_A "qr" -> QR "pdf-417" -> PDF_417 "aztec" -> AZTEC diff --git a/package/ios/Parsers/AVMetadataObject.ObjectType+descriptor.swift b/package/ios/Parsers/AVMetadataObject.ObjectType+descriptor.swift index e74838b..0e22bb7 100644 --- a/package/ios/Parsers/AVMetadataObject.ObjectType+descriptor.swift +++ b/package/ios/Parsers/AVMetadataObject.ObjectType+descriptor.swift @@ -40,6 +40,9 @@ extension AVMetadataObject.ObjectType { case "upc-e": self = .upce return + case "upc-a": + self = .ean13 + return case "qr": self = .qr return diff --git a/package/src/CodeScanner.ts b/package/src/CodeScanner.ts index 1ad965d..8511d5a 100644 --- a/package/src/CodeScanner.ts +++ b/package/src/CodeScanner.ts @@ -12,6 +12,7 @@ export type CodeType = | 'ean-8' | 'itf' | 'upc-e' + | 'upc-a' | 'qr' | 'pdf-417' | 'aztec' From ebe04b246c464b8a3ba579c253c0a78d8959a3e8 Mon Sep 17 00:00:00 2001 From: Danny Kim Date: Tue, 19 Mar 2024 19:14:20 +0900 Subject: [PATCH 68/71] fix: Changes relative import paths in `usePreferredCameraDevice.ts` (#2661) Update import paths in usePreferredCameraDevice The import paths for CameraDevice and useCameraDevices in the usePreferredCameraDevice file were updated. They now correctly reference 'react-native-vision-camera' instead of the non-existent relative path. This ensures correct and efficient module importation, enhancing code readability and maintainability. --- package/example/src/hooks/usePreferredCameraDevice.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package/example/src/hooks/usePreferredCameraDevice.ts b/package/example/src/hooks/usePreferredCameraDevice.ts index f5a7a27..46ba5f9 100644 --- a/package/example/src/hooks/usePreferredCameraDevice.ts +++ b/package/example/src/hooks/usePreferredCameraDevice.ts @@ -1,7 +1,6 @@ import { useMMKVString } from 'react-native-mmkv' -import { CameraDevice } from '../../../src/CameraDevice' import { useCallback, useMemo } from 'react' -import { useCameraDevices } from '../../../src/hooks/useCameraDevices' +import { CameraDevice, useCameraDevices } from 'react-native-vision-camera' export function usePreferredCameraDevice(): [CameraDevice | undefined, (device: CameraDevice) => void] { const [preferredDeviceId, setPreferredDeviceId] = useMMKVString('camera.preferredDeviceId') From d731fe491dc43d39c43d23200e0205b25a17be31 Mon Sep 17 00:00:00 2001 From: Kirill Zyusko Date: Wed, 20 Mar 2024 11:16:26 +0100 Subject: [PATCH 69/71] docs: Compress images (#2667) --- .github/workflows/compress-images.yml | 33 +++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/compress-images.yml diff --git a/.github/workflows/compress-images.yml b/.github/workflows/compress-images.yml new file mode 100644 index 0000000..c12d854 --- /dev/null +++ b/.github/workflows/compress-images.yml @@ -0,0 +1,33 @@ +name: Compress Images (docs) +on: + pull_request: + # Run Image Actions when JPG, JPEG, PNG or WebP files are added or changed. + # See https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#onpushpull_requestpaths for reference. + paths: + - ".github/workflows/compress-images.yml" + - "**.jpg" + - "**.jpeg" + - "**.png" + - "**.webp" + +jobs: + compress-images: + # Only run on Pull Requests within the same repository, and not from forks. + if: github.event.pull_request.head.repo.full_name == github.repository + name: πŸ—‚ Compress images + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Compress Images + uses: calibreapp/image-actions@main + with: + # The `GITHUB_TOKEN` is automatically generated by GitHub and scoped only to the repository that is currently running the action. By default, the action can’t update Pull Requests initiated from forked repositories. + # See https://docs.github.com/en/actions/reference/authentication-in-a-workflow and https://help.github.com/en/articles/virtual-environments-for-github-actions#token-permissions + githubToken: ${{ secrets.GITHUB_TOKEN }} + ignorePaths: "e2e/**" + jpegQuality: "80" + jpegProgressive: false + pngQuality: "80" + webpQuality: "80" From 60925fc816e459fec45f72c7233a1e80721efb57 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 27 Mar 2024 14:36:21 +0100 Subject: [PATCH 70/71] Update package.json --- package/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package/package.json b/package/package.json index 834096f..780498d 100644 --- a/package/package.json +++ b/package/package.json @@ -166,5 +166,6 @@ } ] ] - } + }, + "packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447" } From 83168044a66f18a5b36dc996a389ded7bb840a1c Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 27 Mar 2024 14:37:01 +0100 Subject: [PATCH 71/71] chore: release 3.9.2 --- package/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/package.json b/package/package.json index 780498d..e750ac2 100644 --- a/package/package.json +++ b/package/package.json @@ -1,6 +1,6 @@ { "name": "react-native-vision-camera", - "version": "3.9.1", + "version": "3.9.2", "description": "A powerful, high-performance React Native Camera library.", "main": "lib/commonjs/index", "module": "lib/module/index",