From 2fc8acc796f18d44c0341bc5025d6691a512b4b5 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Thu, 7 Mar 2024 14:00:33 +0000 Subject: [PATCH 01/32] (build) change: use local plugin version for demo --- demo/package.json | 2 +- demo/yarn.lock | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/demo/package.json b/demo/package.json index 615a52ea..b648d8ab 100644 --- a/demo/package.json +++ b/demo/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "private": true, "dependencies": { - "@capacitor-community/camera-preview": "^5.0.0", + "@capacitor-community/camera-preview": "file:..", "@capacitor/android": "^5.0.0", "@capacitor/core": "^5.0.0", "@capacitor/ios": "^5.0.0", diff --git a/demo/yarn.lock b/demo/yarn.lock index c1a84786..64d46a35 100644 --- a/demo/yarn.lock +++ b/demo/yarn.lock @@ -1154,10 +1154,8 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" -"@capacitor-community/camera-preview@^5.0.0": +"@capacitor-community/camera-preview@file:..": version "5.0.0" - resolved "https://registry.yarnpkg.com/@capacitor-community/camera-preview/-/camera-preview-5.0.0.tgz#11ef4b93198e3f33b003631649c2e81c53541519" - integrity sha512-pnoWs8DpKpjoJGVanJ/tRzplp5kZL6iEpKUsRUwyf5BUH77SutZaLo1kF32CaHUaMGawNzvx/rep5Wa5TnxScg== "@capacitor/android@^5.0.0": version "5.0.4" From b46053d7ad13f645a90e09c739396c2818711381 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Thu, 7 Mar 2024 11:02:05 +0000 Subject: [PATCH 02/32] feat: implement getMaxZoom(), getZoom(), setZoom() for Android and iOS --- README.md | 27 +++++++ .../camera/preview/CameraPreview.java | 72 +++++++++++++++++++ demo/src/pages/Home.tsx | 31 ++++++++ ios/Plugin/CameraController.swift | 64 +++++++++++++++++ ios/Plugin/Plugin.m | 3 + ios/Plugin/Plugin.swift | 31 ++++++++ src/definitions.ts | 3 + src/web.ts | 12 ++++ 8 files changed, 243 insertions(+) diff --git a/README.md b/README.md index 18bff45f..3b9510b9 100644 --- a/README.md +++ b/README.md @@ -295,6 +295,33 @@ const myCamera = CameraPreview.start({enableOpacity: true}); myCamera.setOpacity({opacity: 0.4}); ``` +### getMaxZoom(): Promise<{value: number}>; ---- ANDROID and iOS only + +Get the maximum zoom level for the camera device currently started.
+ +```javascript +const myCamera = CameraPreview.start(); +const maxZoom = await CameraPreview.getMaxZoom(); +``` + +### getZoom(): Promise<{value: number}>; ---- ANDROID and iOS only + +Get the current zoom level for the camera device currently started.
+ +```javascript +const myCamera = CameraPreview.start(); +const zoom = await CameraPreview.getZoom(); +``` + +### setZoom({zoom: number}): Promise; ---- ANDROID and iOS only + +Set the zoom level for the camera device currently started.
+ +```javascript +const myCamera = CameraPreview.start(); +await CameraPreview.setZoom({zoom: 2}); +``` + # Settings diff --git a/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java b/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java index 784a15b6..425e93fc 100644 --- a/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +++ b/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java @@ -79,6 +79,78 @@ public void setOpacity(PluginCall call) { fragment.setOpacity(opacity); } + @PluginMethod + public void setZoom(PluginCall call) { + if (this.hasCamera(call) == false) { + call.error("Camera is not running"); + return; + } + + try { + int zoom = call.getInt("zoom", 0); + Camera camera = fragment.getCamera(); + Camera.Parameters params = camera.getParameters(); + if(params.isZoomSupported()) { + params.setZoom(zoom); + fragment.setCameraParameters(params); + call.resolve(); + } else { + call.reject("Zoom not supported"); + } + } catch (Exception e) { + Logger.debug(getLogTag(), "Set camera zoom exception: " + e); + call.reject("failed to zoom camera"); + } + } + + @PluginMethod + public void getZoom(PluginCall call) { + if (this.hasCamera(call) == false) { + call.error("Camera is not running"); + return; + } + + try { + Camera camera = fragment.getCamera(); + Camera.Parameters params = camera.getParameters(); + if(params.isZoomSupported()) { + int currentZoom = params.getZoom(); + JSObject jsObject = new JSObject(); + jsObject.put("value", currentZoom); + call.resolve(jsObject); + } else { + call.reject("Zoom not supported"); + } + } catch (Exception e) { + Logger.debug(getLogTag(), "Get camera zoom exception: " + e); + call.reject("failed to get camera zoom"); + } + } + + @PluginMethod + public void getMaxZoom(PluginCall call) { + if (this.hasCamera(call) == false) { + call.error("Camera is not running"); + return; + } + + try { + Camera camera = fragment.getCamera(); + Camera.Parameters params = camera.getParameters(); + if(params.isZoomSupported()) { + int maxZoom = params.getMaxZoom(); + JSObject jsObject = new JSObject(); + jsObject.put("value", maxZoom); + call.resolve(jsObject); + } else { + call.reject("Zoom not supported"); + } + } catch (Exception e) { + Logger.debug(getLogTag(), "Get max camera zoom exception: " + e); + call.reject("failed to get max camera zoom"); + } + } + @PluginMethod public void capture(PluginCall call) { if (this.hasCamera(call) == false) { diff --git a/demo/src/pages/Home.tsx b/demo/src/pages/Home.tsx index 2624c545..c04b9cba 100644 --- a/demo/src/pages/Home.tsx +++ b/demo/src/pages/Home.tsx @@ -62,6 +62,37 @@ const Home: React.FC = () => { > Flip + { + CameraPreview.getMaxZoom().then((result:any) => { + console.log(`Max Zoom: ${result.value}`); + }); + }} + > + Get Max Zoom + + { + CameraPreview.getZoom().then((result:any) => { + console.log(`Zoom: ${result.value}`); + }); + }} + > + Get Zoom + + { + CameraPreview.getMaxZoom().then((result:any) => { + console.log(`Setting Zoom: ${result.value}`); + CameraPreview.setZoom({ zoom: result.value }); + }); + }} + > + Set Zoom + { diff --git a/ios/Plugin/CameraController.swift b/ios/Plugin/CameraController.swift index a09e72a0..e6af8fa5 100644 --- a/ios/Plugin/CameraController.swift +++ b/ios/Plugin/CameraController.swift @@ -395,6 +395,70 @@ extension CameraController { } } + + func getMaxZoom() throws -> CGFloat { + var currentCamera: AVCaptureDevice? + switch currentCameraPosition { + case .front: + currentCamera = self.frontCamera! + case .rear: + currentCamera = self.rearCamera! + default: break + } + + guard + let device = currentCamera + else { + throw CameraControllerError.invalidOperation + } + + return device.activeFormat.videoMaxZoomFactor + } + + func getZoom() throws -> CGFloat { + var currentCamera: AVCaptureDevice? + switch currentCameraPosition { + case .front: + currentCamera = self.frontCamera! + case .rear: + currentCamera = self.rearCamera! + default: break + } + + guard + let device = currentCamera + else { + throw CameraControllerError.invalidOperation + } + + return device.videoZoomFactor + } + + func setZoom(desiredZoomFactor: CGFloat) throws{ + var currentCamera: AVCaptureDevice? + switch currentCameraPosition { + case .front: + currentCamera = self.frontCamera! + case .rear: + currentCamera = self.rearCamera! + default: break + } + + guard + let device = currentCamera + else { + throw CameraControllerError.invalidOperation + } + + do { + try device.lockForConfiguration() + let videoZoomFactor = max(1.0, min(desiredZoomFactor, device.activeFormat.videoMaxZoomFactor)) + device.videoZoomFactor = videoZoomFactor + device.unlockForConfiguration() + } catch { + throw CameraControllerError.invalidOperation + } + } func captureVideo(completion: @escaping (URL?, Error?) -> Void) { guard let captureSession = self.captureSession, captureSession.isRunning else { diff --git a/ios/Plugin/Plugin.m b/ios/Plugin/Plugin.m index 06fb55e1..306b9911 100644 --- a/ios/Plugin/Plugin.m +++ b/ios/Plugin/Plugin.m @@ -13,4 +13,7 @@ CAP_PLUGIN_METHOD(setFlashMode, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(startRecordVideo, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(stopRecordVideo, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(getMaxZoom, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(getZoom, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(setZoom, CAPPluginReturnPromise); ) diff --git a/ios/Plugin/Plugin.swift b/ios/Plugin/Plugin.swift index 044a9e68..d71f9a61 100644 --- a/ios/Plugin/Plugin.swift +++ b/ios/Plugin/Plugin.swift @@ -287,5 +287,36 @@ public class CameraPreview: CAPPlugin { } } + + @objc func getMaxZoom(_ call: CAPPluginCall) { + do { + let maxZoom = try self.cameraController.getMaxZoom() + call.resolve(["value": maxZoom]) + } catch { + call.reject("failed to get max zoom") + } + } + + @objc func getZoom(_ call: CAPPluginCall) { + do { + let zoom = try self.cameraController.getZoom() + call.resolve(["value": zoom]) + } catch { + call.reject("failed to get zoom") + } + } + + @objc func setZoom(_ call: CAPPluginCall) { + do { + guard let zoom = call.getFloat("zoom") else { + call.reject("failed to set zoom. required parameter zoom is missing") + return + } + try self.cameraController.setZoom(desiredZoomFactor: CGFloat(zoom)) + call.resolve() + } catch { + call.reject("failed to set zoom") + } + } } diff --git a/src/definitions.ts b/src/definitions.ts index 1e4efe00..9d69c77d 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -69,6 +69,9 @@ export interface CameraPreviewPlugin { result: CameraPreviewFlashMode[]; }>; setFlashMode(options: { flashMode: CameraPreviewFlashMode | string }): Promise; + setZoom(options: { zoom: number }): Promise; + getZoom(): Promise<{ value: number }>; + getMaxZoom(): Promise<{ value: number }>; flip(): Promise; setOpacity(options: CameraOpacityOptions): Promise<{}>; } diff --git a/src/web.ts b/src/web.ts index 5f9bf913..6ea2f5d5 100644 --- a/src/web.ts +++ b/src/web.ts @@ -174,6 +174,18 @@ export class CameraPreviewWeb extends WebPlugin implements CameraPreviewPlugin { throw new Error('flip not supported under the web platform'); } + async getZoom(): Promise<{ value: number }> { + throw new Error('getZoom not supported under the web platform'); + } + + async setZoom(_options: { zoom: number }): Promise { + throw new Error('setZoom not supported under the web platform'); + } + + async getMaxZoom(): Promise<{ value: number }> { + throw new Error('getMaxZoom not supported under the web platform'); + } + async setOpacity(_options: CameraOpacityOptions): Promise { const video = document.getElementById('video'); if (!!video && !!_options['opacity']) { From 4f4e6a7647ab0ae6eadee2ebf2f1850931055858 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Sat, 10 Feb 2024 11:01:03 +0000 Subject: [PATCH 03/32] (android) rework: use Camera2 API instead of legacy Camera API --- .../camera/preview/CameraActivity.java | 1858 ++++++++++------- .../camera/preview/CameraPreview.java | 457 +++- .../camera/preview/CustomSurfaceView.java | 23 - .../camera/preview/CustomTextureView.java | 29 - .../ahm/capacitor/camera/preview/Preview.java | 386 ---- src/definitions.ts | 1 + src/web.ts | 4 + 7 files changed, 1527 insertions(+), 1231 deletions(-) delete mode 100644 android/src/main/java/com/ahm/capacitor/camera/preview/CustomSurfaceView.java delete mode 100644 android/src/main/java/com/ahm/capacitor/camera/preview/CustomTextureView.java delete mode 100644 android/src/main/java/com/ahm/capacitor/camera/preview/Preview.java diff --git a/android/src/main/java/com/ahm/capacitor/camera/preview/CameraActivity.java b/android/src/main/java/com/ahm/capacitor/camera/preview/CameraActivity.java index b0554fe1..3aa0f402 100644 --- a/android/src/main/java/com/ahm/capacitor/camera/preview/CameraActivity.java +++ b/android/src/main/java/com/ahm/capacitor/camera/preview/CameraActivity.java @@ -1,48 +1,73 @@ package com.ahm.capacitor.camera.preview; +import static androidx.core.math.MathUtils.clamp; + +import android.Manifest; +import android.annotation.SuppressLint; import android.app.Activity; -import android.app.Fragment; import android.content.Context; import android.content.pm.PackageManager; -import android.content.res.Configuration; import android.graphics.Bitmap; -import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.ImageFormat; import android.graphics.Matrix; import android.graphics.Rect; -import android.graphics.YuvImage; +import android.graphics.RectF; +import android.graphics.SurfaceTexture; import android.hardware.Camera; -import android.hardware.Camera.PictureCallback; -import android.hardware.Camera.ShutterCallback; +import android.hardware.SensorManager; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; +import android.hardware.camera2.params.MeteringRectangle; +import android.hardware.camera2.params.StreamConfigurationMap; import android.media.AudioManager; import android.media.CamcorderProfile; +import android.media.Image; +import android.media.ImageReader; import android.media.MediaRecorder; +import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; import android.util.Base64; -import android.util.DisplayMetrics; import android.util.Log; +import android.util.Size; +import android.util.SparseIntArray; import android.view.GestureDetector; -import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.Surface; -import android.view.Surface; -import android.view.SurfaceHolder; -import android.view.SurfaceView; +import android.view.TextureView; import android.view.View; import android.view.ViewGroup; -import android.view.ViewTreeObserver; +import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.RelativeLayout; -import androidx.exifinterface.media.ExifInterface; -import java.io.ByteArrayInputStream; + +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; + +import android.app.Fragment; + +import com.getcapacitor.Bridge; + import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.UUID; @@ -50,33 +75,62 @@ public class CameraActivity extends Fragment { public interface CameraPreviewListener { void onPictureTaken(String originalPicture); + void onPictureTakenError(String message); + void onSnapshotTaken(String originalPicture); + void onSnapshotTakenError(String message); + void onFocusSet(int pointX, int pointY); + void onFocusSetError(String message); + void onBackButton(); + void onCameraStarted(); + void onStartRecordVideo(); + void onStartRecordVideoError(String message); + void onStopRecordVideo(String file); + void onStopRecordVideoError(String error); } - private CameraPreviewListener eventListener; - private static final String TAG = "CameraActivity"; - public FrameLayout mainLayout; - public FrameLayout frameContainerLayout; + /** + * Private properties + */ + private static final String TAG = "CameraPreviewActivity"; + private static final int REQUEST_CAMERA_PERMISSION = 200; + + private static final float PINCH_ZOOM_SENSITIVITY = 0.01f; + private static final long PINCH_ZOOM_DEBOUNCE_DELAY_MS = 10; // milliseconds + + + private TextureView textureView; + private CameraDevice cameraDevice; + private CameraCaptureSession captureSession; + private CaptureRequest.Builder captureRequestBuilder; + + private CameraActivity.CameraPreviewListener eventListener; + + private Size[] mSupportedPreviewSizes; + private CameraCharacteristics mCameraCharacteristics; + Size mPreviewSize; + + private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); - private Preview mPreview; - private boolean canTakePicture = true; + static { + ORIENTATIONS.append(Surface.ROTATION_0, 90); + ORIENTATIONS.append(Surface.ROTATION_90, 0); + ORIENTATIONS.append(Surface.ROTATION_180, 270); + ORIENTATIONS.append(Surface.ROTATION_270, 180); + } - private View view; - private Camera.Parameters cameraParameters; - private Camera mCamera; - private int numberOfCameras; - private int cameraCurrentlyLocked; - private int currentQuality; + private Handler mBackgroundHandler; + private HandlerThread mBackgroundThread; private enum RecordingState { INITIALIZING, @@ -84,14 +138,22 @@ private enum RecordingState { STOPPED } - private RecordingState mRecordingState = RecordingState.INITIALIZING; + private final CameraActivity.RecordingState mRecordingState = CameraActivity.RecordingState.INITIALIZING; private MediaRecorder mRecorder = null; private String recordFilePath; - private float opacity; - // The first rear facing camera - private int defaultCameraId; - public String defaultCamera; + private String cameraId; + + /** + * Public properties + */ + public Bridge bridge; + public Activity activity; + public Context context; + public FrameLayout mainLayout; + public FrameLayout frameContainerLayout; + + public String position = "back"; public boolean tapToTakePicture; public boolean dragEnabled; public boolean tapToFocus; @@ -100,26 +162,19 @@ private enum RecordingState { public boolean toBack; public boolean enableOpacity = false; public boolean enableZoom = false; + public boolean cropToPreview = true; // whether to crop captured image to preview size public int width; public int height; public int x; public int y; - public void setEventListener(CameraPreviewListener listener) { - eventListener = listener; - } - - private String appResourcesPackage; - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - appResourcesPackage = getActivity().getPackageName(); - - // Inflate the layout for this fragment - view = inflater.inflate(getResources().getIdentifier("camera_activity", "layout", appResourcesPackage), container, false); - createCameraPreview(); - return view; + /** + * Public methods + */ + public void setEventListener(CameraActivity.CameraPreviewListener listener) { + eventListener = listener; } public void setRect(int x, int y, int width, int height) { @@ -129,872 +184,1169 @@ public void setRect(int x, int y, int width, int height) { this.height = height; } - private void createCameraPreview() { - if (mPreview == null) { - setDefaultCameraId(); - - //set box position and size - FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(width, height); - layoutParams.setMargins(x, y, 0, 0); - frameContainerLayout = - (FrameLayout) view.findViewById(getResources().getIdentifier("frame_container", "id", appResourcesPackage)); - frameContainerLayout.setLayoutParams(layoutParams); - - //video view - mPreview = new Preview(getActivity(), enableOpacity); - mainLayout = (FrameLayout) view.findViewById(getResources().getIdentifier("video_view", "id", appResourcesPackage)); - mainLayout.setLayoutParams( - new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) - ); - mainLayout.addView(mPreview); - mainLayout.setEnabled(false); - - if (enableZoom) { - this.setupTouchAndBackButton(); - } - } + public CameraDevice getCamera() { + return cameraDevice; } - private void setupTouchAndBackButton() { - final GestureDetector gestureDetector = new GestureDetector(getActivity().getApplicationContext(), new TapGestureDetector()); - - getActivity() - .runOnUiThread( - new Runnable() { - @Override - public void run() { - frameContainerLayout.setClickable(true); - frameContainerLayout.setOnTouchListener( - new View.OnTouchListener() { - private int mLastTouchX; - private int mLastTouchY; - private int mPosX = 0; - private int mPosY = 0; - - @Override - public boolean onTouch(View v, MotionEvent event) { - FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) frameContainerLayout.getLayoutParams(); - - boolean isSingleTapTouch = gestureDetector.onTouchEvent(event); - int action = event.getAction(); - int eventCount = event.getPointerCount(); - Log.d(TAG, "onTouch event, action, count: " + event + ", " + action + ", " + eventCount); - if (eventCount > 1) { - // handle multi-touch events - Camera.Parameters params = mCamera.getParameters(); - if (action == MotionEvent.ACTION_POINTER_DOWN) { - mDist = getFingerSpacing(event); - } else if (action == MotionEvent.ACTION_MOVE && params.isZoomSupported()) { - handleZoom(event, params); - } - } else { - if (action != MotionEvent.ACTION_MOVE && isSingleTapTouch) { - if (tapToTakePicture && tapToFocus) { - setFocusArea( - (int) event.getX(0), - (int) event.getY(0), - new Camera.AutoFocusCallback() { - public void onAutoFocus(boolean success, Camera camera) { - if (success) { - takePicture(0, 0, 85); - } else { - Log.d(TAG, "onTouch:" + " setFocusArea() did not suceed"); - } - } - } - ); - } else if (tapToTakePicture) { - takePicture(0, 0, 85); - } else if (tapToFocus) { - setFocusArea( - (int) event.getX(0), - (int) event.getY(0), - new Camera.AutoFocusCallback() { - public void onAutoFocus(boolean success, Camera camera) { - if (success) { - // A callback to JS might make sense here. - } else { - Log.d(TAG, "onTouch:" + " setFocusArea() did not suceed"); - } - } - } - ); - } - return true; - } else { - if (dragEnabled) { - int x; - int y; - - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - if (mLastTouchX == 0 || mLastTouchY == 0) { - mLastTouchX = (int) event.getRawX() - layoutParams.leftMargin; - mLastTouchY = (int) event.getRawY() - layoutParams.topMargin; - } else { - mLastTouchX = (int) event.getRawX(); - mLastTouchY = (int) event.getRawY(); - } - break; - case MotionEvent.ACTION_MOVE: - x = (int) event.getRawX(); - y = (int) event.getRawY(); - - final float dx = x - mLastTouchX; - final float dy = y - mLastTouchY; - - mPosX += dx; - mPosY += dy; + public CameraCharacteristics getCameraCharacteristics() { + return mCameraCharacteristics; + } - layoutParams.leftMargin = mPosX; - layoutParams.topMargin = mPosY; + public void switchCamera() { + closeCamera(); + position = position.equals("front") ? "back" : "front"; + logMessage("switchCamera to: " + position); + openCamera(); + } - frameContainerLayout.setLayoutParams(layoutParams); + public void setOpacity(final float opacity) { + logMessage("setOpacity: " + opacity); + if (enableOpacity && textureView != null) { + textureView.setAlpha(opacity); + } + } - // Remember this touch position for the next move event - mLastTouchX = x; - mLastTouchY = y; + public void takePicture(final int width, final int height, final int quality) throws Exception { + logMessage("takePicture"); + if (cameraDevice == null) { + return; + } + Size[] jpegSizes = null; + if (mCameraCharacteristics != null) { + jpegSizes = mCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) + .getOutputSizes(android.graphics.ImageFormat.JPEG); + } + int imageWidth = width; + int imageHeight = height; + if(cropToPreview){ + imageWidth = mPreviewSize.getWidth(); + imageHeight = mPreviewSize.getHeight(); + }else if (jpegSizes != null && jpegSizes.length > 0) { + imageWidth = jpegSizes[0].getWidth(); + imageHeight = jpegSizes[0].getHeight(); + } + final ImageReader reader = ImageReader.newInstance(imageWidth, imageHeight, android.graphics.ImageFormat.JPEG, 1); + List outputSurfaces = new ArrayList<>(2); + outputSurfaces.add(reader.getSurface()); + outputSurfaces.add(new Surface(textureView.getSurfaceTexture())); + final CaptureRequest.Builder captureBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + captureBuilder.addTarget(reader.getSurface()); + captureBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); + + if (!disableExifHeaderStripping) { + int deviceOrientation = activity.getWindowManager().getDefaultDisplay().getRotation(); + captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATIONS.get(deviceOrientation)); + }else{ + captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, 0); + } - break; - default: - break; - } - } - } - } - return true; - } - } - ); - frameContainerLayout.setFocusableInTouchMode(true); - frameContainerLayout.requestFocus(); - frameContainerLayout.setOnKeyListener( - new View.OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, android.view.KeyEvent event) { - if (keyCode == android.view.KeyEvent.KEYCODE_BACK) { - eventListener.onBackButton(); - return true; - } - return false; - } - } - ); + Rect zoomRect = getZoomRect(getCurrentZoomLevel()); + if (zoomRect != null) { + captureBuilder.set(CaptureRequest.SCALER_CROP_REGION, zoomRect); + } + ImageReader.OnImageAvailableListener readerListener = new ImageReader.OnImageAvailableListener() { + @Override + public void onImageAvailable(ImageReader reader) { + try (Image image = reader.acquireLatestImage()) { + ByteBuffer buffer = image.getPlanes()[0].getBuffer(); + byte[] bytes = new byte[buffer.capacity()]; + buffer.get(bytes); + + if (!storeToFile) { + String encodedImage = Base64.encodeToString(bytes, Base64.NO_WRAP); + eventListener.onPictureTaken(encodedImage); + } else { + String path = getTempFilePath(); + save(bytes, path, quality); + eventListener.onPictureTaken(path); } + } catch (Exception e) { + eventListener.onPictureTakenError(e.getMessage()); + logException(e); + } + } - private float mDist = 0F; - - private void handleZoom(MotionEvent event, Camera.Parameters params) { - if (mCamera != null) { - mCamera.cancelAutoFocus(); - int maxZoom = params.getMaxZoom(); - int zoom = params.getZoom(); - float newDist = getFingerSpacing(event); - if (newDist > mDist) { - //zoom in - if (zoom < maxZoom) zoom++; - } else if (newDist < mDist) { - //zoom out - if (zoom > 0) zoom--; - } - mDist = newDist; - params.setZoom(zoom); - mCamera.setParameters(params); - } - } + private void save(byte[] bytes, String filePath, int quality) throws IOException { + Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); + try (OutputStream output = new FileOutputStream(filePath)) { + bitmap.compress(Bitmap.CompressFormat.JPEG, quality, output); } - ); - } + } + }; + reader.setOnImageAvailableListener(readerListener, mBackgroundHandler); + CameraCaptureSession.CaptureCallback captureListener = new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { + super.onCaptureCompleted(session, request, result); + try { + startPreview(); + } catch (Exception e) { + eventListener.onPictureTakenError(e.getMessage()); + logException(e); + } + } + }; + cameraDevice.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + try { + session.capture(captureBuilder.build(), captureListener, mBackgroundHandler); + } catch (CameraAccessException e) { + eventListener.onPictureTakenError(e.getMessage()); + logException(e); + } + } - private void setDefaultCameraId() { - // Find the total number of cameras available - numberOfCameras = Camera.getNumberOfCameras(); + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession session) { + eventListener.onPictureTakenError("Configuration failed"); + } + }, mBackgroundHandler); + } - int facing = "front".equals(defaultCamera) ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK; + public void takeSnapshot(final int quality) throws Exception { + if (cameraDevice == null) { + return; + } + logMessage("takeSnapshot"); + final ImageReader reader = ImageReader.newInstance(width, height, android.graphics.ImageFormat.JPEG, 1); + List outputSurfaces = new ArrayList<>(2); + outputSurfaces.add(reader.getSurface()); + outputSurfaces.add(new Surface(textureView.getSurfaceTexture())); + final CaptureRequest.Builder captureBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + captureBuilder.addTarget(reader.getSurface()); + captureBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); + // Orientation + if (!disableExifHeaderStripping) { + int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); + captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATIONS.get(rotation)); + }else{ + captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, 0); + } + Rect zoomRect = getZoomRect(getCurrentZoomLevel()); + if (zoomRect != null) { + captureBuilder.set(CaptureRequest.SCALER_CROP_REGION, zoomRect); + } + ImageReader.OnImageAvailableListener readerListener = new ImageReader.OnImageAvailableListener() { + @Override + public void onImageAvailable(ImageReader reader) { + try (Image image = reader.acquireLatestImage()) { + ByteBuffer buffer = image.getPlanes()[0].getBuffer(); + byte[] bytes = new byte[buffer.capacity()]; + buffer.get(bytes); + + // Compress the image using the quality parameter + Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos); + byte[] compressedBytes = baos.toByteArray(); + + String encodedImage = Base64.encodeToString(compressedBytes, Base64.NO_WRAP); + eventListener.onSnapshotTaken(encodedImage); + } catch (Exception e) { + eventListener.onSnapshotTakenError(e.getMessage()); + logException(e); + } + } + }; + reader.setOnImageAvailableListener(readerListener, mBackgroundHandler); + CameraCaptureSession.CaptureCallback captureListener = new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { + super.onCaptureCompleted(session, request, result); + try { + startPreview(); + } catch (Exception e) { + eventListener.onSnapshotTakenError(e.getMessage()); + logException(e); + } + } + }; + cameraDevice.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + try { + session.capture(captureBuilder.build(), captureListener, mBackgroundHandler); + } catch (CameraAccessException e) { + eventListener.onSnapshotTakenError(e.getMessage()); + logException(e); + } + } - // Find the ID of the default camera - Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); - for (int i = 0; i < numberOfCameras; i++) { - Camera.getCameraInfo(i, cameraInfo); - if (cameraInfo.facing == facing) { - defaultCameraId = i; - break; + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession session) { } - } - } + }, mBackgroundHandler); - @Override - public void onResume() { - super.onResume(); + } - mCamera = Camera.open(defaultCameraId); + public void startRecord( + final String filePath, + final String camera, + final int width, + final int height, + final int quality, + final boolean withFlash, + final int maxDuration + ) throws Exception { + + logMessage("CameraPreview startRecord camera: " + camera + " width: " + width + ", height: " + height + ", quality: " + quality); + muteStream(true, activity); + if (this.mRecordingState == RecordingState.STARTED) { + logError("Recording already started"); + return; + } + this.recordFilePath = filePath; - if (cameraParameters != null) { - mCamera.setParameters(cameraParameters); + if (withFlash) { + // Turn on the flash + CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + manager.setTorchMode(cameraId, true); + } else { // for old devices + @SuppressWarnings("deprecation") + Camera oldCamera = Camera.open(); + Camera.Parameters parameters = oldCamera.getParameters(); + parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH); + oldCamera.setParameters(parameters); + oldCamera.startPreview(); + } } - cameraCurrentlyLocked = defaultCameraId; + mRecorder = new MediaRecorder(); - if (mPreview.mPreviewSize == null) { - mPreview.setCamera(mCamera, cameraCurrentlyLocked); - eventListener.onCameraStarted(); - } else { - mPreview.switchCamera(mCamera, cameraCurrentlyLocked); - mCamera.startPreview(); - } + try { + CamcorderProfile profile; + if (CamcorderProfile.hasProfile(getCameraToUse(), CamcorderProfile.QUALITY_HIGH)) { + profile = CamcorderProfile.get(getCameraToUse(), CamcorderProfile.QUALITY_HIGH); + } else { + if (CamcorderProfile.hasProfile(getCameraToUse(), CamcorderProfile.QUALITY_480P)) { + profile = CamcorderProfile.get(getCameraToUse(), CamcorderProfile.QUALITY_480P); + } else { + if (CamcorderProfile.hasProfile(getCameraToUse(), CamcorderProfile.QUALITY_720P)) { + profile = CamcorderProfile.get(getCameraToUse(), CamcorderProfile.QUALITY_720P); + } else { + if (CamcorderProfile.hasProfile(getCameraToUse(), CamcorderProfile.QUALITY_1080P)) { + profile = CamcorderProfile.get(getCameraToUse(), CamcorderProfile.QUALITY_1080P); + } else { + profile = CamcorderProfile.get(getCameraToUse(), CamcorderProfile.QUALITY_LOW); + } + } + } + } - Log.d(TAG, "cameraCurrentlyLocked:" + cameraCurrentlyLocked); + mRecorder.setAudioSource(MediaRecorder.AudioSource.VOICE_RECOGNITION); + mRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); + mRecorder.setProfile(profile); + mRecorder.setOutputFile(filePath); + mRecorder.setOrientationHint(getOrientationHint()); + mRecorder.setMaxDuration(maxDuration); - final FrameLayout frameContainerLayout = (FrameLayout) view.findViewById( - getResources().getIdentifier("frame_container", "id", appResourcesPackage) - ); + List surfaces = new ArrayList<>(); - ViewTreeObserver viewTreeObserver = frameContainerLayout.getViewTreeObserver(); - - if (viewTreeObserver.isAlive()) { - viewTreeObserver.addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - frameContainerLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this); - frameContainerLayout.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); - Activity activity = getActivity(); - if (isAdded() && activity != null) { - final RelativeLayout frameCamContainerLayout = (RelativeLayout) view.findViewById( - getResources().getIdentifier("frame_camera_cont", "id", appResourcesPackage) - ); - - FrameLayout.LayoutParams camViewLayout = new FrameLayout.LayoutParams( - frameContainerLayout.getWidth(), - frameContainerLayout.getHeight() - ); - camViewLayout.gravity = Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL; - frameCamContainerLayout.setLayoutParams(camViewLayout); - } + Surface recorderSurface = mRecorder.getSurface(); + surfaces.add(recorderSurface); + + surfaces.add(new Surface(textureView.getSurfaceTexture())); + + cameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + captureSession = session; + try { + // Build the capture request, and start the session + CaptureRequest.Builder requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); + requestBuilder.addTarget(recorderSurface); + session.setRepeatingRequest(requestBuilder.build(), null, null); + mRecorder.prepare(); + logMessage("Starting recording"); + mRecorder.start(); + } catch (Exception e) { + logException(e); } } - ); + + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession session) { + // Handle configuration failure + } + }, null); + + eventListener.onStartRecordVideo(); + } catch (Exception e) { + eventListener.onStartRecordVideoError(e.getMessage()); } + } - @Override - public void onPause() { - super.onPause(); + public void muteStream(boolean mute, Activity activity) { + AudioManager audioManager = ((AudioManager) activity.getApplicationContext().getSystemService(Context.AUDIO_SERVICE)); + int direction; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + direction = mute ? AudioManager.ADJUST_MUTE : AudioManager.ADJUST_UNMUTE; + } else { + direction = mute ? AudioManager.ADJUST_LOWER : AudioManager.ADJUST_RAISE; + } + audioManager.adjustStreamVolume(AudioManager.STREAM_SYSTEM, direction, 0); + } + + public void stopRecord() { + logMessage("stopRecord"); - // Because the Camera object is a shared resource, it's very important to release it when the activity is paused. - if (mCamera != null) { - setDefaultCameraId(); - mPreview.setCamera(null, -1); - mCamera.setPreviewCallback(null); - mCamera.release(); - mCamera = null; + try { + mRecorder.stop(); + mRecorder.reset(); // clear recorder configuration + mRecorder.release(); // release the recorder object + mRecorder = null; + startPreview(); + eventListener.onStopRecordVideo(this.recordFilePath); + } catch (Exception e) { + eventListener.onStopRecordVideoError(e.getMessage()); } } - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); + public boolean isZoomSupported() { + if (mCameraCharacteristics == null) return false; + return mCameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) > 1; + } - final FrameLayout frameContainerLayout = (FrameLayout) view.findViewById( - getResources().getIdentifier("frame_container", "id", appResourcesPackage) - ); - final int previousOrientation = frameContainerLayout.getHeight() > frameContainerLayout.getWidth() - ? Configuration.ORIENTATION_PORTRAIT - : Configuration.ORIENTATION_LANDSCAPE; - // Checks if the orientation of the screen has changed - if (newConfig.orientation != previousOrientation) { - final RelativeLayout frameCamContainerLayout = (RelativeLayout) view.findViewById( - getResources().getIdentifier("frame_camera_cont", "id", appResourcesPackage) - ); + public float getMaxZoomLevel() { + Float maxZoom = null; + if(mCameraCharacteristics != null){ + maxZoom = mCameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); + } + return (maxZoom != null) ? maxZoom : 1.0f; + } - frameContainerLayout.getLayoutParams().width = frameCamContainerLayout.getHeight(); - frameContainerLayout.getLayoutParams().height = frameCamContainerLayout.getWidth(); + public float getMinZoomLevel() { + return 1.0f; // The minimum zoom level is always 1.0 (no zoom) + } - frameCamContainerLayout.getLayoutParams().width = frameCamContainerLayout.getHeight(); - frameCamContainerLayout.getLayoutParams().height = frameCamContainerLayout.getWidth(); + public void setCurrentZoomLevel(float zoomLevel) throws Exception { + if (mCameraCharacteristics == null) return; + logMessage("setCurrentZoomLevel to: " + zoomLevel); + logMessage("currentZoomLevel (before setCurrentZoomLevel): " + getCurrentZoomLevel()); - frameContainerLayout.invalidate(); - frameContainerLayout.requestLayout(); + float maxZoom = getMaxZoomLevel(); + logMessage("maxZoom: " + maxZoom); + float minZoom = getMinZoomLevel(); + logMessage("minZoom: " + minZoom); - frameCamContainerLayout.forceLayout(); + float newLevel = Math.max(minZoom, Math.min(zoomLevel, maxZoom)); + logMessage("newLevel: " + newLevel); - mPreview.setCameraDisplayOrientation(); + Rect zoomRect = getZoomRect(newLevel); + if (zoomRect != null) { + captureRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, zoomRect); + captureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); } } - public Camera getCamera() { - return mCamera; - } - public void switchCamera() { - // check for availability of multiple cameras - if (numberOfCameras == 1) { - //There is only one camera available - } else { - Log.d(TAG, "numberOfCameras: " + numberOfCameras); - - // OK, we have multiple cameras. Release this camera -> cameraCurrentlyLocked - if (mCamera != null) { - mCamera.stopPreview(); - mPreview.setCamera(null, -1); - mCamera.release(); - mCamera = null; + public float getCurrentZoomLevel() { + if(mCameraCharacteristics != null){ + Rect activeArraySize = mCameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + if (activeArraySize != null) { + Rect currentCropRegion = captureRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION); + if (currentCropRegion != null) { + return (float) activeArraySize.width() / currentCropRegion.width(); + } } + } + return getMinZoomLevel(); // Default to minimum zoom if current crop region is unavailable + } - Log.d(TAG, "cameraCurrentlyLocked := " + Integer.toString(cameraCurrentlyLocked)); - try { - cameraCurrentlyLocked = (cameraCurrentlyLocked + 1) % numberOfCameras; - Log.d(TAG, "cameraCurrentlyLocked new: " + cameraCurrentlyLocked); - } catch (Exception exception) { - Log.d(TAG, exception.getMessage()); + // get supported flash modes + public String[] getSupportedFlashModes() { + if (mCameraCharacteristics != null) { + int[] flashModes = mCameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES); + String[] flashModesStr = new String[flashModes.length]; + for (int i = 0; i < flashModes.length; i++) { + flashModesStr[i] = flashModes[i] + ""; } + return flashModesStr; + } + return new String[0]; + } - // Acquire the next camera and request Preview to reconfigure parameters. - mCamera = Camera.open(cameraCurrentlyLocked); + public void setFlashMode(String flashMode) { + if (mCameraCharacteristics != null) { + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, Integer.parseInt(flashMode)); + logMessage("setFlashMode: " + flashMode); + } + } - if (cameraParameters != null) { - Log.d(TAG, "camera parameter not null"); + public void onOrientationChange(String orientation) { + try { + logMessage("onOrientationChanged: " + orientation); +// Log.d(TAG, "device orientation: " + getDeviceOrientation()); +// Log.d(TAG, "sensor orientation: " + getSensorOrientation()); + configureTransform(textureView.getWidth(), textureView.getHeight()); + } catch (Exception e) { + logException("onOrientationChanged error", e); + } + } - // Check for flashMode as well to prevent error on frontward facing camera. - List supportedFlashModesNewCamera = mCamera.getParameters().getSupportedFlashModes(); - String currentFlashModePreviousCamera = cameraParameters.getFlashMode(); - if (supportedFlashModesNewCamera != null && supportedFlashModesNewCamera.contains(currentFlashModePreviousCamera)) { - Log.d(TAG, "current flash mode supported on new camera. setting params"); - /* mCamera.setParameters(cameraParameters); - The line above is disabled because parameters that can actually be changed are different from one device to another. Makes less sense trying to reconfigure them when changing camera device while those settings gan be changed using plugin methods. - */ - } else { - Log.d(TAG, "current flash mode NOT supported on new camera"); - } - } else { - Log.d(TAG, "camera parameter NULL"); + /** + * Internal methods and listeners + */ + private Rect getZoomRect(float zoomLevel){ + if(mCameraCharacteristics != null){ + Rect activeArraySize = mCameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + if (activeArraySize != null) { + int cropWidth = (int) (activeArraySize.width() / zoomLevel); + int cropHeight = (int) (activeArraySize.height() / zoomLevel); + int cropLeft = (activeArraySize.width() - cropWidth) / 2; + int cropTop = (activeArraySize.height() - cropHeight) / 2; + return new Rect(cropLeft, cropTop, cropLeft + cropWidth, cropTop + cropHeight); } + } + return null; + } - mPreview.switchCamera(mCamera, cameraCurrentlyLocked); + private final TextureView.SurfaceTextureListener textureListener = new TextureView.SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) { + openCamera(); + } - mCamera.startPreview(); + @Override + public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int width, int height) { + configureTransform(width, height); } - } - public void setCameraParameters(Camera.Parameters params) { - cameraParameters = params; + @Override + public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) { + return true; + } - if (mCamera != null && cameraParameters != null) { - mCamera.setParameters(cameraParameters); + @Override + public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) { } - } + }; - public boolean hasFrontCamera() { - return getActivity().getApplicationContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT); - } + private final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() { + @Override + public void onOpened(@NonNull CameraDevice camera) { + cameraDevice = camera; + createCameraPreview(); + } - public static Bitmap applyMatrix(Bitmap source, Matrix matrix) { - return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, true); - } + @Override + public void onDisconnected(@NonNull CameraDevice camera) { + closeCamera(); + } - ShutterCallback shutterCallback = new ShutterCallback() { - public void onShutter() { - // do nothing, availabilty of this callback causes default system shutter sound to work + @Override + public void onError(@NonNull CameraDevice camera, int error) { + closeCamera(); } }; - private static int exifToDegrees(int exifOrientation) { - if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90) { - return 90; - } else if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_180) { - return 180; - } else if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) { - return 270; - } - return 0; - } + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + String appResourcesPackage = activity.getPackageName(); - private String getTempDirectoryPath() { - File cache = null; + // Inflate the layout for this fragment + View view = inflater.inflate(getResources().getIdentifier("camera_activity", "layout", appResourcesPackage), container, false); - // Use internal storage - cache = getActivity().getCacheDir(); + FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(width, height); + layoutParams.setMargins(x, y, 0, 0); + frameContainerLayout = + view.findViewById(getResources().getIdentifier("frame_container", "id", appResourcesPackage)); + frameContainerLayout.setLayoutParams(layoutParams); - // Create the cache directory if it doesn't exist - cache.mkdirs(); - return cache.getAbsolutePath(); - } - private String getTempFilePath() { - return getTempDirectoryPath() + "/cpcp_capture_" + UUID.randomUUID().toString().replace("-", "").substring(0, 8) + ".jpg"; + mainLayout = view.findViewById(getResources().getIdentifier("video_view", "id", appResourcesPackage)); + mainLayout.setLayoutParams( + new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) + ); + mainLayout.setEnabled(false); + + // create texture view and add it to mainLayout + textureView = new TextureView(getActivity()); + mainLayout.addView(textureView); + textureView.setSurfaceTextureListener(textureListener); + textureView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + + if (enableZoom) { + setupTouchAndBackButton(); + } + + return view; } - PictureCallback jpegPictureCallback = new PictureCallback() { - public void onPictureTaken(byte[] data, Camera arg1) { - Log.d(TAG, "CameraPreview jpegPictureCallback"); + private void setupTouchAndBackButton() { + final GestureDetector gestureDetector = new GestureDetector(activity.getApplicationContext(), new TapGestureDetector()); + + activity + .runOnUiThread( + new Runnable() { + @Override + public void run() { + frameContainerLayout.setClickable(true); + frameContainerLayout.setOnTouchListener( + new View.OnTouchListener() { + private int mLastTouchX; + private int mLastTouchY; + private int mPosX = 0; + private int mPosY = 0; + + @Override + public boolean onTouch(View v, MotionEvent event) { + try { + FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) frameContainerLayout.getLayoutParams(); + + boolean isSingleTapTouch = gestureDetector.onTouchEvent(event); + int action = event.getAction(); + int eventCount = event.getPointerCount(); +// Log.d(TAG, "onTouch event, action, count: " + event + ", " + action + ", " + eventCount); + if (eventCount > 1) { + // handle multi-touch events + if (action == MotionEvent.ACTION_POINTER_DOWN || action == MotionEvent.ACTION_POINTER_2_DOWN) { + mDist = getFingerSpacing(event); +// Log.d(TAG, "onTouch start: mDist=" + mDist); + } else if (action == MotionEvent.ACTION_MOVE && isZoomSupported()) { + handlePinchZoom(event); + } + } else { + if (action != MotionEvent.ACTION_MOVE && isSingleTapTouch) { + if (tapToTakePicture && tapToFocus) { + int tapX = (int) event.getX(0); + int tapY = (int) event.getY(0); + setFocusArea( + tapX, + tapY, + new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { + super.onCaptureCompleted(session, request, result); + try { + eventListener.onFocusSet(tapX, tapY); + takePicture(0, 0, 85); + } catch (Exception e) { + eventListener.onFocusSetError(e.getMessage()); + logException(e); + } + } + } + ); + } else if (tapToTakePicture) { + takePicture(0, 0, 85); + } else if (tapToFocus) { + int tapX = (int) event.getX(0); + int tapY = (int) event.getY(0); + + CameraCaptureSession.CaptureCallback captureCallback = new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { + super.onCaptureCompleted(session, request, result); + try { + logMessage("onTouch:" + " setFocusArea() succeeded"); + eventListener.onFocusSet(tapX, tapY); + } catch (Exception e) { + eventListener.onFocusSetError(e.getMessage()); + logException(e); + } + } + }; + + setFocusArea( + tapX, + tapY, + captureCallback + ); + } + return true; + } else { + if (dragEnabled) { + int x; + int y; + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (mLastTouchX == 0 || mLastTouchY == 0) { + mLastTouchX = (int) event.getRawX() - layoutParams.leftMargin; + mLastTouchY = (int) event.getRawY() - layoutParams.topMargin; + } else { + mLastTouchX = (int) event.getRawX(); + mLastTouchY = (int) event.getRawY(); + } + break; + case MotionEvent.ACTION_MOVE: + x = (int) event.getRawX(); + y = (int) event.getRawY(); + + final float dx = x - mLastTouchX; + final float dy = y - mLastTouchY; + + mPosX += (int) dx; + mPosY += (int) dy; + + layoutParams.leftMargin = mPosX; + layoutParams.topMargin = mPosY; + + frameContainerLayout.setLayoutParams(layoutParams); + + // Remember this touch position for the next move event + mLastTouchX = x; + mLastTouchY = y; + + break; + default: + break; + } + } + } + } + } catch (Exception e) { + logException("onTouch error: ", e); + } + return true; + } + } + ); + frameContainerLayout.setFocusableInTouchMode(true); + frameContainerLayout.requestFocus(); + frameContainerLayout.setOnKeyListener( + new View.OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, android.view.KeyEvent event) { + if (keyCode == android.view.KeyEvent.KEYCODE_BACK) { + eventListener.onBackButton(); + return true; + } + return false; + } + } + ); + } - try { - if (!disableExifHeaderStripping) { - Matrix matrix = new Matrix(); - if (cameraCurrentlyLocked == Camera.CameraInfo.CAMERA_FACING_FRONT) { - matrix.preScale(1.0f, -1.0f); - } + private float mDist = 0F; + private long lastZoomTime = 0; - ExifInterface exifInterface = new ExifInterface(new ByteArrayInputStream(data)); - int rotation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); - int rotationInDegrees = exifToDegrees(rotation); + private void handlePinchZoom(MotionEvent event){ + if (cameraDevice == null) return; + try { + float newDist = getFingerSpacing(event); - if (rotation != 0f) { - matrix.preRotate(rotationInDegrees); - } + float maxZoom = getMaxZoomLevel(); + float minZoom = getMinZoomLevel(); - // Check if matrix has changed. In that case, apply matrix and override data - if (!matrix.isIdentity()) { - Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); - bitmap = applyMatrix(bitmap, matrix); + float currentZoomLevel = getCurrentZoomLevel(); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - bitmap.compress(CompressFormat.JPEG, currentQuality, outputStream); - data = outputStream.toByteArray(); - } - } + float newZoomLevel = currentZoomLevel; - if (!storeToFile) { - String encodedImage = Base64.encodeToString(data, Base64.NO_WRAP); + if (newDist > mDist) { + // Zoom in + newZoomLevel = Math.min(currentZoomLevel + ((newDist - mDist) * PINCH_ZOOM_SENSITIVITY), maxZoom); + } else if (newDist < mDist) { + // Zoom out + newZoomLevel = Math.max(currentZoomLevel - ((mDist - newDist) * PINCH_ZOOM_SENSITIVITY), minZoom); + } + mDist = newDist; - eventListener.onPictureTaken(encodedImage); - } else { - String path = getTempFilePath(); - FileOutputStream out = new FileOutputStream(path); - out.write(data); - out.close(); - eventListener.onPictureTaken(path); - } - Log.d(TAG, "CameraPreview pictureTakenHandler called back"); - } catch (OutOfMemoryError e) { - // most likely failed to allocate memory for rotateBitmap - Log.d(TAG, "CameraPreview OutOfMemoryError"); - // failed to allocate memory - eventListener.onPictureTakenError("Picture too large (memory)"); - } catch (IOException e) { - Log.d(TAG, "CameraPreview IOException"); - eventListener.onPictureTakenError("IO Error when extracting exif"); - } catch (Exception e) { - Log.d(TAG, "CameraPreview onPictureTaken general exception"); - } finally { - canTakePicture = true; - mCamera.startPreview(); - } - } - }; + long currentTime = System.currentTimeMillis(); + if (newZoomLevel != currentZoomLevel && (currentTime - lastZoomTime) > PINCH_ZOOM_DEBOUNCE_DELAY_MS) { + setCurrentZoomLevel(newZoomLevel); + lastZoomTime = currentTime; + } + } catch (Exception e) { + logException(e); + } + } + } + ); + } - private Camera.Size getOptimalPictureSize( - final int width, - final int height, - final Camera.Size previewSize, - final List supportedSizes - ) { - /* - get the supportedPictureSize that: - - matches exactly width and height - - has the closest aspect ratio to the preview aspect ratio - - has picture.width and picture.height closest to width and height - - has the highest supported picture width and height up to 2 Megapixel if width == 0 || height == 0 - */ - Camera.Size size = mCamera.new Size(width, height); + private Handler focusHandler = null; + private final int MAX_FOCUS_RETRY_COUNT = 3; + private final int FOCUS_RETRY_INTERVAL_MS = 100; + private int focusRetryCount = 0; - // convert to landscape if necessary - if (size.width < size.height) { - int temp = size.width; - size.width = size.height; - size.height = temp; - } + public void setFocusArea(final int pointX, final int pointY, final CameraCaptureSession.CaptureCallback callback) throws Exception { + if (cameraDevice == null) return; - Camera.Size requestedSize = mCamera.new Size(size.width, size.height); + captureRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + captureSession.capture(captureRequestBuilder.build(), new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { + triggerAutofocus(pointX, pointY, callback); + } + }, mBackgroundHandler); + } - double previewAspectRatio = (double) previewSize.width / (double) previewSize.height; + private void triggerAutofocus(final int pointX, final int pointY, final CameraCaptureSession.CaptureCallback callback) { + try { + // Calculate focus and metering areas + Rect focusRect = calculateTapArea(pointX, pointY, 1f); + Rect meteringRect = calculateTapArea(pointX, pointY, 1.5f); - if (previewAspectRatio < 1.0) { - // reset ratio to landscape - previewAspectRatio = 1.0 / previewAspectRatio; - } + if (focusRect == null || meteringRect == null) { + logError("Invalid focus or metering area dimensions"); + return; + } - Log.d(TAG, "CameraPreview previewAspectRatio " + previewAspectRatio); + // Set AF, AE, and AWB regions + captureRequestBuilder.set(CaptureRequest.CONTROL_AF_REGIONS, new MeteringRectangle[]{new MeteringRectangle(focusRect, 1000)}); + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_REGIONS, new MeteringRectangle[]{new MeteringRectangle(meteringRect, 1000)}); + captureRequestBuilder.set(CaptureRequest.CONTROL_AWB_REGIONS, new MeteringRectangle[]{new MeteringRectangle(meteringRect, 1000)}); - double aspectTolerance = 0.1; - double bestDifference = Double.MAX_VALUE; + // Set AF mode to auto + captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); - for (int i = 0; i < supportedSizes.size(); i++) { - Camera.Size supportedSize = supportedSizes.get(i); + // Start autofocus + captureRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START); + captureSession.capture(captureRequestBuilder.build(), new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { + clearFocusRetry(); + focusRetryCount = 0; + handleFocusResult(result, pointX, pointY, callback); + } + }, mBackgroundHandler); + } catch (CameraAccessException e) { + logException(e); + } + } - // Perfect match - if (supportedSize.equals(requestedSize)) { - Log.d(TAG, "CameraPreview optimalPictureSize " + supportedSize.width + 'x' + supportedSize.height); - return supportedSize; - } - double difference = Math.abs(previewAspectRatio - ((double) supportedSize.width / (double) supportedSize.height)); + private void clearFocusRetry() { + if(focusHandler != null) focusHandler.removeCallbacksAndMessages(null); + } - if (difference < bestDifference - aspectTolerance) { - // better aspectRatio found - if ((width != 0 && height != 0) || (supportedSize.width * supportedSize.height < 2048 * 1024)) { - size.width = supportedSize.width; - size.height = supportedSize.height; - bestDifference = difference; + private void handleFocusResult(TotalCaptureResult result, final int pointX, final int pointY, final CameraCaptureSession.CaptureCallback callback) { + Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); + if (afState == null) return; + switch (afState) { + case CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED: + case CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED: + // Focus is complete, reset AF trigger + clearFocusRetry(); + captureRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + try { + captureSession.setRepeatingRequest(captureRequestBuilder.build(), null, mBackgroundHandler); + callback.onCaptureCompleted(captureSession, captureRequestBuilder.build(), result); + } catch (CameraAccessException e) { + logException(e); } - } else if (difference < bestDifference + aspectTolerance) { - // same aspectRatio found (within tolerance) - if (width == 0 || height == 0) { - // set highest supported resolution below 2 Megapixel - if ((size.width < supportedSize.width) && (supportedSize.width * supportedSize.height < 2048 * 1024)) { - size.width = supportedSize.width; - size.height = supportedSize.height; - } - } else { - // check if this pictureSize closer to requested width and height - if ( - Math.abs(width * height - supportedSize.width * supportedSize.height) < - Math.abs(width * height - size.width * size.height) - ) { - size.width = supportedSize.width; - size.height = supportedSize.height; + break; + + case CaptureResult.CONTROL_AF_STATE_INACTIVE: + case CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN: + case CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED: + case CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED: + // Retry autofocus if necessary + clearFocusRetry(); + focusRetryCount++; + if (focusRetryCount >= MAX_FOCUS_RETRY_COUNT) { + Log.d(TAG,"Max focus retry count reached"); + return; + } + focusHandler = new Handler(Looper.getMainLooper()); + focusHandler.postDelayed(() -> { + try { + triggerAutofocus(pointX, pointY, callback); + } catch (Exception e) { + logException(e); } + }, FOCUS_RETRY_INTERVAL_MS); + break; + + case CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN: + // Autofocus is still in progress, wait and check again + clearFocusRetry(); + focusRetryCount++; + if (focusRetryCount >= MAX_FOCUS_RETRY_COUNT) { + Log.d(TAG,"Max focus retry count reached"); + return; } - } + focusHandler = new Handler(Looper.getMainLooper()); + focusHandler.postDelayed(() -> { + try { + handleFocusResult(result, pointX, pointY, callback); + } catch (Exception e) { + logException(e); + } + }, FOCUS_RETRY_INTERVAL_MS); + break; } - Log.d(TAG, "CameraPreview optimalPictureSize " + size.width + 'x' + size.height); - return size; } - static byte[] rotateNV21(final byte[] yuv, final int width, final int height, final int rotation) { - if (rotation == 0) return yuv; - if (rotation % 90 != 0 || rotation < 0 || rotation > 270) { - throw new IllegalArgumentException("0 <= rotation < 360, rotation % 90 == 0"); - } + private Rect calculateTapArea(float x, float y, float coefficient) { + int areaSize = Math.round(200 * coefficient); - final byte[] output = new byte[yuv.length]; - final int frameSize = width * height; - final boolean swap = rotation % 180 != 0; - final boolean xflip = rotation % 270 != 0; - final boolean yflip = rotation >= 180; + int left = clamp((int) x - areaSize / 2, 0, textureView.getWidth() - areaSize); + int top = clamp((int) y - areaSize / 2, 0, textureView.getHeight() - areaSize); - for (int j = 0; j < height; j++) { - for (int i = 0; i < width; i++) { - final int yIn = j * width + i; - final int uIn = frameSize + (j >> 1) * width + (i & ~1); - final int vIn = uIn + 1; + int right = left + areaSize; + int bottom = top + areaSize; - final int wOut = swap ? height : width; - final int hOut = swap ? width : height; - final int iSwapped = swap ? j : i; - final int jSwapped = swap ? i : j; - final int iOut = xflip ? wOut - iSwapped - 1 : iSwapped; - final int jOut = yflip ? hOut - jSwapped - 1 : jSwapped; + if (left >= right || top >= bottom) { + logError("Calculated tap area has invalid dimensions"); + return null; // Return null for invalid dimensions + } - final int yOut = jOut * wOut + iOut; - final int uOut = frameSize + (jOut >> 1) * wOut + (iOut & ~1); - final int vOut = uOut + 1; + return new Rect(left, top, right, bottom); + } - output[yOut] = (byte) (0xff & yuv[yIn]); - output[uOut] = (byte) (0xff & yuv[uIn]); - output[vOut] = (byte) (0xff & yuv[vIn]); + /** + * Determine the space between the first two fingers + */ + private static float getFingerSpacing(MotionEvent event) { + float x = event.getX(0) - event.getX(1); + float y = event.getY(0) - event.getY(1); + return (float) Math.sqrt(x * x + y * y); + } + + @SuppressLint("MissingPermission") + private void openCamera() { + CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + try { + cameraId = findCameraIdForPosition(); + mCameraCharacteristics = manager.getCameraCharacteristics(cameraId); + mSupportedPreviewSizes = getSupportedPreviewSizes(cameraId); + if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION); + return; } + + manager.openCamera(cameraId, stateCallback, null); + } catch (CameraAccessException e) { + logException(e); } - return output; } - public void setOpacity(final float opacity) { - Log.d(TAG, "set opacity:" + opacity); - this.opacity = opacity; - mPreview.setOpacity(opacity); + private String findCameraIdForPosition() throws CameraAccessException { + CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + for (String cameraId : manager.getCameraIdList()) { + CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); + int cOrientation = characteristics.get(CameraCharacteristics.LENS_FACING); + if (position.equals("front") && cOrientation == CameraCharacteristics.LENS_FACING_FRONT) { + return cameraId; + } else if (position.equals("back") && cOrientation == CameraCharacteristics.LENS_FACING_BACK) { + return cameraId; + } + } + return "0"; } - public void takeSnapshot(final int quality) { - mCamera.setPreviewCallback( - new Camera.PreviewCallback() { + private void createCameraPreview() { + try { + SurfaceTexture texture = textureView.getSurfaceTexture(); + mPreviewSize = getOptimalPreviewSize(mSupportedPreviewSizes, textureView.getWidth(), textureView.getHeight()); + assert texture != null; + texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); + configureTransform(textureView.getWidth(), textureView.getHeight()); + Surface surface = new Surface(texture); + captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); + captureRequestBuilder.addTarget(surface); + + cameraDevice.createCaptureSession(Collections.singletonList(surface), new CameraCaptureSession.StateCallback() { @Override - public void onPreviewFrame(byte[] bytes, Camera camera) { - try { - Camera.Parameters parameters = camera.getParameters(); - Camera.Size size = parameters.getPreviewSize(); - int orientation = mPreview.getDisplayOrientation(); - if (mPreview.getCameraFacing() == Camera.CameraInfo.CAMERA_FACING_FRONT) { - bytes = rotateNV21(bytes, size.width, size.height, (360 - orientation) % 360); - } else { - bytes = rotateNV21(bytes, size.width, size.height, orientation); - } - // switch width/height when rotating 90/270 deg - Rect rect = orientation == 90 || orientation == 270 - ? new Rect(0, 0, size.height, size.width) - : new Rect(0, 0, size.width, size.height); - YuvImage yuvImage = new YuvImage(bytes, parameters.getPreviewFormat(), rect.width(), rect.height(), null); - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - yuvImage.compressToJpeg(rect, quality, byteArrayOutputStream); - byte[] data = byteArrayOutputStream.toByteArray(); - byteArrayOutputStream.close(); - eventListener.onSnapshotTaken(Base64.encodeToString(data, Base64.NO_WRAP)); - } catch (IOException e) { - Log.d(TAG, "CameraPreview IOException"); - eventListener.onSnapshotTakenError("IO Error"); - } finally { - mCamera.setPreviewCallback(null); + public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) { + if (cameraDevice == null) { + return; } + captureSession = cameraCaptureSession; + eventListener.onCameraStarted(); + updatePreview(); } - } - ); + + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { + logException(new Exception("Camera preview configuration failed")); + } + }, null); + } catch (CameraAccessException e) { + logException(e); + } } - public void takePicture(final int width, final int height, final int quality) { - Log.d(TAG, "CameraPreview takePicture width: " + width + ", height: " + height + ", quality: " + quality); + private void updatePreview() { + if (cameraDevice == null) { + logException(new Exception("updatePreview error, return")); + } + captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); + try { + captureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + } catch (CameraAccessException e) { + logException(e); + } + } - if (mPreview != null) { - if (!canTakePicture) { - return; + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requestCode == REQUEST_CAMERA_PERMISSION) { + if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { + // Permission denied + activity.finish(); } + } + } - canTakePicture = false; + private Size getOptimalPreviewSize(Size[] sizes, int w, int h) { + final double ASPECT_TOLERANCE = 0.1; + double targetRatio = (double) w / h; + int sensorOrientation = mCameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + if (sensorOrientation == 90 || sensorOrientation == 270) { + targetRatio = (double) h / w; + } - new Thread() { - public void run() { - Camera.Parameters params = mCamera.getParameters(); + if (sizes == null) { + return null; + } - Camera.Size size = getOptimalPictureSize(width, height, params.getPreviewSize(), params.getSupportedPictureSizes()); - params.setPictureSize(size.width, size.height); - currentQuality = quality; + Size optimalSize = null; + double minDiff = Double.MAX_VALUE; - if (cameraCurrentlyLocked == Camera.CameraInfo.CAMERA_FACING_FRONT && !storeToFile) { - // The image will be recompressed in the callback - params.setJpegQuality(99); - } else { - params.setJpegQuality(quality); - } + int targetHeight = h; - if (cameraCurrentlyLocked == Camera.CameraInfo.CAMERA_FACING_FRONT && disableExifHeaderStripping) { - Activity activity = getActivity(); - int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); - int degrees = 0; - switch (rotation) { - case Surface.ROTATION_0: - degrees = 0; - break; - case Surface.ROTATION_90: - degrees = 180; - break; - case Surface.ROTATION_180: - degrees = 270; - break; - case Surface.ROTATION_270: - degrees = 0; - break; - } - int orientation; - Camera.CameraInfo info = new Camera.CameraInfo(); - if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { - orientation = (info.orientation + degrees) % 360; - if (degrees != 0) { - orientation = (360 - orientation) % 360; - } - } else { - orientation = (info.orientation - degrees + 360) % 360; - } - params.setRotation(orientation); - } else { - params.setRotation(mPreview.getDisplayOrientation()); - } + // Try to find an size match aspect ratio and size + for (Size size : sizes) { + double ratio = (double) size.getWidth() / size.getHeight(); + if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue; + if (Math.abs(size.getHeight() - targetHeight) < minDiff) { + optimalSize = size; + minDiff = Math.abs(size.getHeight() - targetHeight); + } + } - mCamera.setParameters(params); - mCamera.takePicture(shutterCallback, null, jpegPictureCallback); + // Cannot find the one match the aspect ratio, ignore the requirement + if (optimalSize == null) { + minDiff = Double.MAX_VALUE; + for (Size size : sizes) { + if (Math.abs(size.getHeight() - targetHeight) < minDiff) { + optimalSize = size; + minDiff = Math.abs(size.getHeight() - targetHeight); } } - .start(); - } else { - canTakePicture = true; } + +// Log.d(TAG, "optimal preview size: w: " + optimalSize.getWidth() + " h: " + optimalSize.getHeight()); + return optimalSize; } - public void startRecord( - final String filePath, - final String camera, - final int width, - final int height, - final int quality, - final boolean withFlash, - final int maxDuration - ) { - Log.d(TAG, "CameraPreview startRecord camera: " + camera + " width: " + width + ", height: " + height + ", quality: " + quality); - Activity activity = getActivity(); - muteStream(true, activity); - if (this.mRecordingState == RecordingState.STARTED) { - Log.d(TAG, "Already Recording"); + private int getOrientationHint() { + int deviceRotation = getDeviceOrientation(); + int sensorOrientation = getSensorOrientation(); + return (sensorOrientation - deviceRotation + 360) % 360; + } + + private int getDeviceOrientation() { + WindowManager windowManager = (WindowManager) activity.getSystemService(Context.WINDOW_SERVICE); + int rotation = windowManager.getDefaultDisplay().getRotation(); + + int deviceRotation = 0; + switch (rotation) { + case Surface.ROTATION_0: + deviceRotation = 0; + break; + case Surface.ROTATION_90: + deviceRotation = 90; + break; + case Surface.ROTATION_180: + deviceRotation = 180; + break; + case Surface.ROTATION_270: + deviceRotation = 270; + break; + } + return deviceRotation; + } + + private int getSensorOrientation() { + if (mCameraCharacteristics == null) return -1; + return mCameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + } + + private void configureTransform(int viewWidth, int viewHeight) { + if (cameraDevice == null || !textureView.isAvailable() || mPreviewSize == null) { return; } - this.recordFilePath = filePath; - int mOrientationHint = calculateOrientationHint(); - int videoWidth = 0; //set whatever - int videoHeight = 0; //set whatever + Matrix matrix = new Matrix(); - Camera.Parameters cameraParams = mCamera.getParameters(); - if (withFlash) { - cameraParams.setFlashMode(withFlash ? Camera.Parameters.FLASH_MODE_TORCH : Camera.Parameters.FLASH_MODE_OFF); - mCamera.setParameters(cameraParams); - mCamera.startPreview(); + int sensorRotation = getSensorOrientation(); + int deviceRotation = getDeviceOrientation(); + + int centerX = viewWidth / 2; + int centerY = viewHeight / 2; + + // Set the rotation transformation + if (getDeviceOrientation() == getSensorOrientation() || Math.abs(getDeviceOrientation() - getSensorOrientation()) == 180 || getDeviceOrientation() == 180) { + matrix.postRotate(-sensorRotation, centerX, centerY); + } else { + matrix.postRotate(sensorRotation, centerX, centerY); } - mCamera.unlock(); - mRecorder = new MediaRecorder(); - try { - mRecorder.setCamera(mCamera); + // Calculate aspect ratio scaling + float previewAspectRatio = (float) mPreviewSize.getWidth() / mPreviewSize.getHeight(); + float viewAspectRatio = (float) viewWidth / viewHeight; + float scaleX = 1.0f; + float scaleY = 1.0f; - CamcorderProfile profile; - if (CamcorderProfile.hasProfile(defaultCameraId, CamcorderProfile.QUALITY_HIGH)) { - profile = CamcorderProfile.get(defaultCameraId, CamcorderProfile.QUALITY_HIGH); + if (previewAspectRatio > viewAspectRatio) { + scaleX = previewAspectRatio / viewAspectRatio; + } else { + scaleY = viewAspectRatio / previewAspectRatio; + } + + // Apply the scale transformation + matrix.postScale(scaleX, scaleY, centerX, centerY); + + // Undo the rotation transformation + if (getDeviceOrientation() != getSensorOrientation()) { + if (Math.abs(getDeviceOrientation() - getSensorOrientation()) != 180) { + matrix.postRotate(-sensorRotation, centerX, centerY); } else { - if (CamcorderProfile.hasProfile(defaultCameraId, CamcorderProfile.QUALITY_480P)) { - profile = CamcorderProfile.get(defaultCameraId, CamcorderProfile.QUALITY_480P); - } else { - if (CamcorderProfile.hasProfile(defaultCameraId, CamcorderProfile.QUALITY_720P)) { - profile = CamcorderProfile.get(defaultCameraId, CamcorderProfile.QUALITY_720P); - } else { - if (CamcorderProfile.hasProfile(defaultCameraId, CamcorderProfile.QUALITY_1080P)) { - profile = CamcorderProfile.get(defaultCameraId, CamcorderProfile.QUALITY_1080P); - } else { - profile = CamcorderProfile.get(defaultCameraId, CamcorderProfile.QUALITY_LOW); - } - } - } + matrix.postRotate(sensorRotation - deviceRotation, centerX, centerY); } - mRecorder.setAudioSource(MediaRecorder.AudioSource.VOICE_RECOGNITION); - mRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); - mRecorder.setProfile(profile); - mRecorder.setOutputFile(filePath); - mRecorder.setOrientationHint(mOrientationHint); - mRecorder.setMaxDuration(maxDuration); + } - mRecorder.prepare(); - Log.d(TAG, "Starting recording"); - mRecorder.start(); - eventListener.onStartRecordVideo(); - } catch (IOException e) { - eventListener.onStartRecordVideoError(e.getMessage()); + // Apply the transformation + textureView.setTransform(matrix); + } + + public Size[] getSupportedPreviewSizes(String cameraId) { + CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); + try { + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId); + StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + + if (map != null) { + return map.getOutputSizes(SurfaceTexture.class); + } else { + // Handle the case where map is null + return new Size[0]; + } + } catch (CameraAccessException e) { + logException(e); + // Handle the exception + return new Size[0]; } } - public int calculateOrientationHint() { - DisplayMetrics dm = new DisplayMetrics(); - Camera.CameraInfo info = new Camera.CameraInfo(); - Camera.getCameraInfo(defaultCameraId, info); - int cameraRotationOffset = info.orientation; - Activity activity = getActivity(); - activity.getWindowManager().getDefaultDisplay().getMetrics(dm); - int currentScreenRotation = activity.getWindowManager().getDefaultDisplay().getRotation(); + @Override + public void onPause() { + closeCamera(); + stopBackgroundThread(); - int degrees = 0; - switch (currentScreenRotation) { - case Surface.ROTATION_0: - degrees = 0; - break; - case Surface.ROTATION_90: - degrees = 90; - break; - case Surface.ROTATION_180: - degrees = 180; - break; - case Surface.ROTATION_270: - degrees = 270; - break; - } + super.onPause(); + } - int orientation; - if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { - orientation = (cameraRotationOffset + degrees) % 360; - if (degrees != 0) { - orientation = (360 - orientation) % 360; - } + + @Override + public void onResume() { + super.onResume(); + if (textureView.isAvailable()) { + openCamera(); } else { - orientation = (cameraRotationOffset - degrees + 360) % 360; + textureView.setSurfaceTextureListener(textureListener); } - Log.w(TAG, "************orientationHint ***********= " + orientation); + startBackgroundThread(); - return orientation; } - public void stopRecord() { - Log.d(TAG, "stopRecord"); - - try { - mRecorder.stop(); - mRecorder.reset(); // clear recorder configuration - mRecorder.release(); // release the recorder object - mRecorder = null; - mCamera.lock(); - Camera.Parameters cameraParams = mCamera.getParameters(); - cameraParams.setFlashMode(Camera.Parameters.FLASH_MODE_OFF); - mCamera.setParameters(cameraParams); - mCamera.startPreview(); - eventListener.onStopRecordVideo(this.recordFilePath); - } catch (Exception e) { - eventListener.onStopRecordVideoError(e.getMessage()); + private void closeCamera() { + if (cameraDevice != null) { + cameraDevice.close(); + cameraDevice = null; + mCameraCharacteristics = null; + mSupportedPreviewSizes = null; + mPreviewSize = null; } } - public void muteStream(boolean mute, Activity activity) { - AudioManager audioManager = ((AudioManager) activity.getApplicationContext().getSystemService(Context.AUDIO_SERVICE)); - int direction = mute ? audioManager.ADJUST_MUTE : audioManager.ADJUST_UNMUTE; + private String getTempDirectoryPath() { + File cache = null; + + // Use internal storage + cache = activity.getCacheDir(); + + // Create the cache directory if it doesn't exist + cache.mkdirs(); + return cache.getAbsolutePath(); } - public void setFocusArea(final int pointX, final int pointY, final Camera.AutoFocusCallback callback) { - if (mCamera != null) { - mCamera.cancelAutoFocus(); + private String getTempFilePath() { + return getTempDirectoryPath() + "/cpcp_capture_" + UUID.randomUUID().toString().replace("-", "").substring(0, 8) + ".jpg"; + } - Camera.Parameters parameters = mCamera.getParameters(); + private void startBackgroundThread() { + mBackgroundThread = new HandlerThread("Camera Background"); + mBackgroundThread.start(); + mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); + } - Rect focusRect = calculateTapArea(pointX, pointY, 1f); - parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); - parameters.setFocusAreas(Arrays.asList(new Camera.Area(focusRect, 1000))); + private void stopBackgroundThread() { + mBackgroundThread.quitSafely(); + try { + mBackgroundThread.join(); + mBackgroundThread = null; + mBackgroundHandler = null; + } catch (InterruptedException e) { + logException(e); + } + } - if (parameters.getMaxNumMeteringAreas() > 0) { - Rect meteringRect = calculateTapArea(pointX, pointY, 1.5f); - parameters.setMeteringAreas(Arrays.asList(new Camera.Area(meteringRect, 1000))); + private void startPreview() throws Exception { + if (cameraDevice == null || !textureView.isAvailable() || mPreviewSize == null) { + return; + } + SurfaceTexture texture = textureView.getSurfaceTexture(); + assert texture != null; + texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); + Surface surface = new Surface(texture); + captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); + captureRequestBuilder.addTarget(surface); + cameraDevice.createCaptureSession(Collections.singletonList(surface), new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) { + if (cameraDevice == null) { + return; + } + captureSession = cameraCaptureSession; + updatePreview(); } - try { - setCameraParameters(parameters); - mCamera.autoFocus(callback); - } catch (Exception e) { - Log.d(TAG, e.getMessage()); - callback.onAutoFocus(false, this.mCamera); + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { + logException(new Exception("Configuration change")); } - } + }, null); } - private Rect calculateTapArea(float x, float y, float coefficient) { - if (x < 100) { - x = 100; - } - if (x > width - 100) { - x = width - 100; + private int getCameraToUse() { + if (cameraId != null) { + return Integer.parseInt(cameraId); } - if (y < 100) { - y = 100; - } - if (y > height - 100) { - y = height - 100; + return 0; + } + + private void logException(Exception e) { + logError(e.getMessage()); + } + + private void logException(String message, Exception e) { + logError(message + ": " + e.getMessage()); + } + + private void logError(String message) { + Log.e(TAG, message); + if (bridge != null) { + bridge.logToJs(TAG + ": " + message, "error"); } - return new Rect( - Math.round((x - 100) * 2000 / width - 1000), - Math.round((y - 100) * 2000 / height - 1000), - Math.round((x + 100) * 2000 / width - 1000), - Math.round((y + 100) * 2000 / height - 1000) - ); } - /** - * Determine the space between the first two fingers - */ - private static float getFingerSpacing(MotionEvent event) { - // ... - float x = event.getX(0) - event.getX(1); - float y = event.getY(0) - event.getY(1); - return (float) Math.sqrt(x * x + y * y); + private void logMessage(String message) { + Log.d(TAG, message); + if (bridge != null) { + bridge.logToJs(TAG + ": " + message, "debug"); + } } } diff --git a/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java b/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java index 425e93fc..8435068e 100644 --- a/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +++ b/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java @@ -2,19 +2,33 @@ import static android.Manifest.permission.CAMERA; +import android.annotation.SuppressLint; import android.app.FragmentManager; import android.app.FragmentTransaction; +import android.content.Context; import android.content.pm.ActivityInfo; +import android.content.res.Configuration; import android.graphics.Color; import android.graphics.Point; import android.hardware.Camera; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.os.Build; import android.util.DisplayMetrics; +import android.util.Size; +import android.util.SizeF; import android.util.TypedValue; import android.view.Display; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; + +import androidx.annotation.RequiresApi; + import com.getcapacitor.JSObject; import com.getcapacitor.Logger; import com.getcapacitor.PermissionState; @@ -25,8 +39,13 @@ import com.getcapacitor.annotation.Permission; import com.getcapacitor.annotation.PermissionCallback; import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.util.List; +import java.util.Set; + import org.json.JSONArray; +import org.json.JSONObject; @CapacitorPlugin(name = "CameraPreview", permissions = { @Permission(strings = { CAMERA }, alias = CameraPreview.CAMERA_PERMISSION_ALIAS) }) public class CameraPreview extends Plugin implements CameraActivity.CameraPreviewListener { @@ -47,6 +66,15 @@ public class CameraPreview extends Plugin implements CameraActivity.CameraPrevie private CameraActivity fragment; private int containerViewId = 20; + @PluginMethod + public void echo(PluginCall call) { + String value = call.getString("value"); + + JSObject ret = new JSObject(); + ret.put("value", value); + call.resolve(ret); + } + @PluginMethod public void start(PluginCall call) { if (PermissionState.GRANTED.equals(getPermissionState(CAMERA_PERMISSION_ALIAS))) { @@ -59,14 +87,313 @@ public void start(PluginCall call) { @PluginMethod public void flip(PluginCall call) { try { + bridge.saveCall(call); + cameraStartCallbackId = call.getCallbackId(); fragment.switchCamera(); - call.resolve(); + call.setKeepAlive(true); } catch (Exception e) { Logger.debug(getLogTag(), "Camera flip exception: " + e); call.reject("failed to flip camera"); } } + @PluginMethod + public void getCameraCharacteristics(PluginCall call) { + try { + // if device is running Android P or later, list available cameras and their focal lengths + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + CameraManager manager = (CameraManager) this.bridge.getContext().getSystemService(Context.CAMERA_SERVICE); + try { + JSONArray logicalCameras = new JSONArray(); + String[] cameraIdList = manager.getCameraIdList(); + for (String id : cameraIdList) { + /* + * Logical camera details + */ + JSObject logicalCamera = new JSObject(); + logicalCamera.put("LOGICAL_ID", id); + JSONArray physicalCameras = new JSONArray(); + + CameraCharacteristics characteristics = manager.getCameraCharacteristics(id); + + // INFO_SUPPORTED_HARDWARE_LEVEL + Integer supportLevel = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); + if(supportLevel != null) logicalCamera.put("INFO_SUPPORTED_HARDWARE_LEVEL", supportLevel); + + // LENS_FACING + Integer lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING); + if(lensFacing != null) logicalCamera.put("LENS_FACING", lensFacing); + + // SENSOR_INFO_PHYSICAL_SIZE + SizeF sensorInfoPhysicalSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE); + if(sensorInfoPhysicalSize != null) { + logicalCamera.put("SENSOR_INFO_PHYSICAL_SIZE_WIDTH", sensorInfoPhysicalSize.getWidth()); + logicalCamera.put("SENSOR_INFO_PHYSICAL_SIZE_HEIGHT", sensorInfoPhysicalSize.getHeight()); + } + + // SENSOR_INFO_PIXEL_ARRAY_SIZE + Size sensorInfoPixelSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); + if(sensorInfoPixelSize != null) { + logicalCamera.put("SENSOR_INFO_PIXEL_ARRAY_SIZE_WIDTH", sensorInfoPixelSize.getWidth()); + logicalCamera.put("SENSOR_INFO_PIXEL_ARRAY_SIZE_HEIGHT", sensorInfoPixelSize.getHeight()); + } + + // LENS_INFO_AVAILABLE_FOCAL_LENGTHS + float[] focalLengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS); + JSONArray focalLengthsArray = new JSONArray(); + for (int focusId=0; focusId physicalCameraIds = characteristics.getPhysicalCameraIds(); + for (String physicalId : physicalCameraIds) { + JSObject physicalCamera = new JSObject(); + physicalCamera.put("PHYSICAL_ID", physicalId); + + CameraCharacteristics physicalCharacteristics = manager.getCameraCharacteristics(physicalId); + + float[] lensFocalLengths = physicalCharacteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS); + if (lensFocalLengths != null && focalLengths.length > 0) { + float focalLength = lensFocalLengths[0]; + physicalCamera.put("LENS_INFO_AVAILABLE_FOCAL_LENGTHS", focalLength); + } + + StreamConfigurationMap map = physicalCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + if (map != null) { + int[] outputFormats = map.getOutputFormats(); + JSONArray formats = new JSONArray(); + for (int format : outputFormats) { + formats.put(format); + } + physicalCamera.put("SCALER_STREAM_CONFIGURATION_MAP", formats); + } + + Size[] outputSizes = map.getOutputSizes(256); + if(outputSizes != null && outputSizes.length > 0) { + JSONArray sizes = new JSONArray(); + for (Size size : outputSizes) { + JSONObject sizeObject = new JSONObject(); + sizeObject.put("WIDTH", size.getWidth()); + sizeObject.put("HEIGHT", size.getHeight()); + sizes.put(sizeObject); + } + physicalCamera.put("OUTPUT_SIZES", sizes); + } + + Size[] inputSizes = map.getInputSizes(256); + if(inputSizes != null && inputSizes.length > 0) { + JSONArray sizes = new JSONArray(); + for (Size size : inputSizes) { + JSONObject sizeObject = new JSONObject(); + sizeObject.put("WIDTH", size.getWidth()); + sizeObject.put("HEIGHT", size.getHeight()); + sizes.put(sizeObject); + } + physicalCamera.put("INPUT_SIZES", sizes); + } + + // get the list of available capabilities + int[] capabilities = physicalCharacteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES); + if(capabilities != null && capabilities.length > 0) { + JSONArray capabilitiesJson = new JSONArray(); + for (int capability : capabilities) { + String capabilityName; + switch(capability){ + case CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE: + capabilityName = "REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE"; + break; + case CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MANUAL_POST_PROCESSING: + capabilityName = "REQUEST_AVAILABLE_CAPABILITIES_MANUAL_POST_PROCESSING"; + break; + case CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR: + capabilityName = "REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR"; + break; + case CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW: + capabilityName = "REQUEST_AVAILABLE_CAPABILITIES_RAW"; + break; + case CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_READ_SENSOR_SETTINGS: + capabilityName = "REQUEST_AVAILABLE_CAPABILITIES_READ_SENSOR_SETTINGS"; + break; + case CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE: + capabilityName = "REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE"; + break; + case CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING: + capabilityName = "REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING"; + break; + case CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING: + capabilityName = "REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING"; + break; + case CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT: + capabilityName = "REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT"; + break; + case CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO: + capabilityName = "REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO"; + break; + case CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MOTION_TRACKING: + capabilityName = "REQUEST_AVAILABLE_CAPABILITIES_MOTION_TRACKING"; + break; + case CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA: + capabilityName = "REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA"; + break; + case CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MONOCHROME: + capabilityName = "REQUEST_AVAILABLE_CAPABILITIES_MONOCHROME"; + break; + case CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_SECURE_IMAGE_DATA: + capabilityName = "REQUEST_AVAILABLE_CAPABILITIES_SECURE_IMAGE_DATA"; + break; + default: + capabilityName = String.valueOf(capability); + } + + capabilitiesJson.put(capabilityName); + } + physicalCamera.put("REQUEST_AVAILABLE_CAPABILITIES", capabilitiesJson); + } + + int[] controlModes = physicalCharacteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_MODES); + if(controlModes != null && controlModes.length > 0) { + JSONArray controlModesArray = new JSONArray(); + for (int mode : controlModes) { + controlModesArray.put(mode); + } + physicalCamera.put("CONTROL_AVAILABLE_MODES", controlModesArray); + } + + int[] effects = physicalCharacteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_EFFECTS); + if(effects != null && effects.length > 0) { + JSONArray effectsArray = new JSONArray(); + for (int effect : effects) { + effectsArray.put(effect); + } + physicalCamera.put("CONTROL_AVAILABLE_EFFECTS", effectsArray); + } + + int[] sceneModes = physicalCharacteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_SCENE_MODES); + if(sceneModes != null && sceneModes.length > 0) { + JSONArray sceneModesArray = new JSONArray(); + for (int mode : sceneModes) { + sceneModesArray.put(mode); + } + physicalCamera.put("CONTROL_AVAILABLE_SCENE_MODES", sceneModesArray); + } + + int[] antiBandingModes = physicalCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_ANTIBANDING_MODES); + if(antiBandingModes != null && antiBandingModes.length > 0) { + JSONArray antiBandingModesArray = new JSONArray(); + for (int mode : antiBandingModes) { + antiBandingModesArray.put(mode); + } + physicalCamera.put("CONTROL_AE_AVAILABLE_ANTIBANDING_MODES", antiBandingModesArray); + } + + int[] autoExposureModes = physicalCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES); + if(autoExposureModes != null && autoExposureModes.length > 0) { + JSONArray autoExposureModesArray = new JSONArray(); + for (int mode : autoExposureModes) { + autoExposureModesArray.put(mode); + } + physicalCamera.put("CONTROL_AE_AVAILABLE_MODES", autoExposureModesArray); + } + + int[] autoFocusModes = physicalCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); + if(autoFocusModes != null && autoFocusModes.length > 0) { + JSONArray autoFocusModesArray = new JSONArray(); + for (int mode : autoFocusModes) { + autoFocusModesArray.put(mode); + } + physicalCamera.put("CONTROL_AF_AVAILABLE_MODES", autoFocusModesArray); + } + + int[] autoWhiteBalanceModes = physicalCharacteristics.get(CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES); + if(autoWhiteBalanceModes != null && autoWhiteBalanceModes.length > 0) { + JSONArray autoWhiteBalanceModesArray = new JSONArray(); + for (int mode : autoWhiteBalanceModes) { + autoWhiteBalanceModesArray.put(mode); + } + physicalCamera.put("CONTROL_AWB_AVAILABLE_MODES", autoWhiteBalanceModesArray); + } + + int[] colorCorrectionAberrationModes = physicalCharacteristics.get(CameraCharacteristics.COLOR_CORRECTION_AVAILABLE_ABERRATION_MODES); + if(colorCorrectionAberrationModes != null && colorCorrectionAberrationModes.length > 0) { + JSONArray colorCorrectionAberrationModesArray = new JSONArray(); + for (int mode : colorCorrectionAberrationModes) { + colorCorrectionAberrationModesArray.put(mode); + } + physicalCamera.put("COLOR_CORRECTION_AVAILABLE_ABERRATION_MODES", colorCorrectionAberrationModesArray); + } + + int[] edgeModes = physicalCharacteristics.get(CameraCharacteristics.EDGE_AVAILABLE_EDGE_MODES); + if(edgeModes != null && edgeModes.length > 0) { + JSONArray edgeModesArray = new JSONArray(); + for (int mode : edgeModes) { + edgeModesArray.put(mode); + } + physicalCamera.put("EDGE_AVAILABLE_EDGE_MODES", edgeModesArray); + } + + int[] hotPixelModes = physicalCharacteristics.get(CameraCharacteristics.HOT_PIXEL_AVAILABLE_HOT_PIXEL_MODES); + if(hotPixelModes != null && hotPixelModes.length > 0) { + JSONArray hotPixelModesArray = new JSONArray(); + for (int mode : hotPixelModes) { + hotPixelModesArray.put(mode); + } + physicalCamera.put("HOT_PIXEL_AVAILABLE_HOT_PIXEL_MODES", hotPixelModesArray); + } + + int[] lensShadingModes = physicalCharacteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION); + if(lensShadingModes != null && lensShadingModes.length > 0) { + JSONArray lensShadingModesArray = new JSONArray(); + for (int mode : lensShadingModes) { + lensShadingModesArray.put(mode); + } + physicalCamera.put("LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION", lensShadingModesArray); + } + + int[] noiseReductionModes = physicalCharacteristics.get(CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES); + if(noiseReductionModes != null && noiseReductionModes.length > 0) { + JSONArray noiseReductionModesArray = new JSONArray(); + for (int mode : noiseReductionModes) { + noiseReductionModesArray.put(mode); + } + physicalCamera.put("NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES", noiseReductionModesArray); + } + + int[] tonemapModes = physicalCharacteristics.get(CameraCharacteristics.TONEMAP_AVAILABLE_TONE_MAP_MODES); + if(tonemapModes != null && tonemapModes.length > 0) { + JSONArray tonemapModesArray = new JSONArray(); + for (int mode : tonemapModes) { + tonemapModesArray.put(mode); + } + physicalCamera.put("TONEMAP_AVAILABLE_TONE_MAP_MODES", tonemapModesArray); + } + physicalCameras.put(physicalCamera); + } + if(physicalCameras.length() > 0){ + logicalCamera.put("PHYSICAL_CAMERAS", physicalCameras); + } + logicalCameras.put(logicalCamera); + } + JSObject result = new JSObject(); + result.put("LOGICAL_CAMERAS", logicalCameras); + call.resolve(result); + } catch (CameraAccessException e) { + e.printStackTrace(); + } + }else{ + call.reject("This feature is only available on Android P or later."); + } + } catch (Exception e) { + Logger.debug(getLogTag(), "Exception retrieving camera characteristics: " + e); + call.reject("failed to retrieve camera characteristics"); + } + } + @PluginMethod public void setOpacity(PluginCall call) { if (this.hasCamera(call) == false) { @@ -87,12 +414,9 @@ public void setZoom(PluginCall call) { } try { - int zoom = call.getInt("zoom", 0); - Camera camera = fragment.getCamera(); - Camera.Parameters params = camera.getParameters(); - if(params.isZoomSupported()) { - params.setZoom(zoom); - fragment.setCameraParameters(params); + float zoom = call.getFloat("zoom", 1F); + if(fragment.isZoomSupported()) { + fragment.setCurrentZoomLevel(zoom); call.resolve(); } else { call.reject("Zoom not supported"); @@ -111,10 +435,8 @@ public void getZoom(PluginCall call) { } try { - Camera camera = fragment.getCamera(); - Camera.Parameters params = camera.getParameters(); - if(params.isZoomSupported()) { - int currentZoom = params.getZoom(); + if(fragment.isZoomSupported()) { + float currentZoom = fragment.getCurrentZoomLevel(); JSObject jsObject = new JSObject(); jsObject.put("value", currentZoom); call.resolve(jsObject); @@ -135,10 +457,8 @@ public void getMaxZoom(PluginCall call) { } try { - Camera camera = fragment.getCamera(); - Camera.Parameters params = camera.getParameters(); - if(params.isZoomSupported()) { - int maxZoom = params.getMaxZoom(); + if(fragment.isZoomSupported()) { + float maxZoom = fragment.getMaxZoomLevel(); JSObject jsObject = new JSObject(); jsObject.put("value", maxZoom); call.resolve(jsObject); @@ -164,7 +484,12 @@ public void capture(PluginCall call) { // Image Dimensions - Optional Integer width = call.getInt("width", 0); Integer height = call.getInt("height", 0); - fragment.takePicture(width, height, quality); + try{ + fragment.takePicture(width, height, quality); + } catch (Exception e) { + Logger.debug(getLogTag(), "Capture exception: " + e); + call.reject("failed to capture image"); + } } @PluginMethod @@ -177,7 +502,12 @@ public void captureSample(PluginCall call) { snapshotCallbackId = call.getCallbackId(); Integer quality = call.getInt("quality", 85); - fragment.takeSnapshot(quality); + try{ + fragment.takeSnapshot(quality); + } catch (Exception e) { + Logger.debug(getLogTag(), "Capture sample exception: " + e); + call.reject("failed to capture sample"); + } } @PluginMethod @@ -186,6 +516,7 @@ public void stop(final PluginCall call) { .getActivity() .runOnUiThread( new Runnable() { + @SuppressLint("WrongConstant") @Override public void run() { FrameLayout containerView = getBridge().getActivity().findViewById(containerViewId); @@ -218,15 +549,12 @@ public void getSupportedFlashModes(PluginCall call) { return; } - Camera camera = fragment.getCamera(); - Camera.Parameters params = camera.getParameters(); - List supportedFlashModes; - supportedFlashModes = params.getSupportedFlashModes(); + String[] supportedFlashModes = fragment.getSupportedFlashModes(); JSONArray jsonFlashModes = new JSONArray(); if (supportedFlashModes != null) { - for (int i = 0; i < supportedFlashModes.size(); i++) { - jsonFlashModes.put(new String(supportedFlashModes.get(i))); + for (String supportedFlashMode : supportedFlashModes) { + jsonFlashModes.put(supportedFlashMode); } } @@ -248,20 +576,22 @@ public void setFlashMode(PluginCall call) { return; } - Camera camera = fragment.getCamera(); - Camera.Parameters params = camera.getParameters(); + String[] supportedFlashModes = fragment.getSupportedFlashModes(); - List supportedFlashModes; - supportedFlashModes = camera.getParameters().getSupportedFlashModes(); - if (supportedFlashModes.indexOf(flashMode) > -1) { - params.setFlashMode(flashMode); - } else { - call.reject("Flash mode not recognised: " + flashMode); - return; + if (supportedFlashModes != null && supportedFlashModes.length > 0) { + boolean isSupported = false; + for (String supportedFlashMode : supportedFlashModes) { + if (supportedFlashMode.equals(flashMode)) { + isSupported = true; + break; + } + } + if (!isSupported) { + call.reject("Flash mode not supported: " + flashMode); + return; + } } - - fragment.setCameraParameters(params); - + fragment.setFlashMode(flashMode); call.resolve(); } @@ -281,6 +611,7 @@ public void startRecordVideo(final PluginCall call) { final Integer maxDuration = call.getInt("maxDuration", 0); // final Integer quality = call.getInt("quality", 0); bridge.saveCall(call); + call.setKeepAlive(true); recordCallbackId = call.getCallbackId(); bridge @@ -290,12 +621,16 @@ public void startRecordVideo(final PluginCall call) { @Override public void run() { // fragment.startRecord(getFilePath(filename), position, width, height, quality, withFlash); - fragment.startRecord(getFilePath(filename), position, width, height, 70, withFlash, maxDuration); + try{ + fragment.startRecord(getFilePath(filename), position, width, height, 70, withFlash, maxDuration); + call.resolve(); + } catch (Exception e) { + Logger.debug(getLogTag(), "Start record video exception: " + e); + call.reject("failed to start record video"); + } } } ); - - call.resolve(); } @PluginMethod @@ -349,13 +684,17 @@ private void startCamera(final PluginCall call) { final Boolean storeToFile = call.getBoolean("storeToFile", false); final Boolean enableOpacity = call.getBoolean("enableOpacity", false); final Boolean enableZoom = call.getBoolean("enableZoom", false); + final Boolean cropToPreview = call.getBoolean("cropToPreview", true); final Boolean disableExifHeaderStripping = call.getBoolean("disableExifHeaderStripping", true); final Boolean lockOrientation = call.getBoolean("lockAndroidOrientation", false); previousOrientationRequest = getBridge().getActivity().getRequestedOrientation(); fragment = new CameraActivity(); fragment.setEventListener(this); - fragment.defaultCamera = position; + fragment.bridge = getBridge(); + fragment.activity = getActivity(); + fragment.context = getContext(); + fragment.position = position; fragment.tapToTakePicture = false; fragment.dragEnabled = false; fragment.tapToFocus = true; @@ -364,6 +703,8 @@ private void startCamera(final PluginCall call) { fragment.toBack = toBack; fragment.enableOpacity = enableOpacity; fragment.enableZoom = enableZoom; + fragment.cropToPreview = cropToPreview; + bridge .getActivity() @@ -435,7 +776,7 @@ public void run() { // NOTE: we don't return invoke call.resolve here because it must be invoked in onCameraStarted // otherwise the plugin start method might resolve/return before the camera is actually set in CameraActivity - // onResume method (see this line mCamera = Camera.open(defaultCameraId);) and the next subsequent plugin + // onResume method and the next subsequent plugin // method invocations (for example, getSupportedFlashModes) might fails with "Camera is not running" error // because camera is not available yet and hasCamera method will return false // Please also see https://developer.android.com/reference/android/hardware/Camera.html#open%28int%29 @@ -454,6 +795,22 @@ protected void handleOnResume() { super.handleOnResume(); } + @Override + protected void handleOnConfigurationChanged(Configuration newConfig) { + super.handleOnConfigurationChanged(newConfig); + if(fragment == null) { + return; + } + if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { + fragment.onOrientationChange("landscape"); + } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { + fragment.onOrientationChange("portrait"); + }else{ + fragment.onOrientationChange("unknown"); + } + } + + @Override public void onPictureTaken(String originalPicture) { JSObject jsObject = new JSObject(); @@ -489,9 +846,13 @@ public void onBackButton() {} @Override public void onCameraStarted() { + if(cameraStartCallbackId.isEmpty()) { + return; + } PluginCall pluginCall = bridge.getSavedCall(cameraStartCallbackId); pluginCall.resolve(); bridge.releaseCall(pluginCall); + cameraStartCallbackId = ""; } @Override @@ -567,4 +928,20 @@ public boolean onTouch(View v, MotionEvent event) { } ); } + + private String getConstantName(Class c, int value) { + for (Field field : c.getDeclaredFields()) { + int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers) && Modifier.isFinal(modifiers)) { + try { + if (field.getInt(null) == value) { + return field.getName(); + } + } catch (IllegalAccessException e) { + // Handle exception + } + } + } + return null; + } } diff --git a/android/src/main/java/com/ahm/capacitor/camera/preview/CustomSurfaceView.java b/android/src/main/java/com/ahm/capacitor/camera/preview/CustomSurfaceView.java deleted file mode 100644 index 051a07fb..00000000 --- a/android/src/main/java/com/ahm/capacitor/camera/preview/CustomSurfaceView.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.ahm.capacitor.camera.preview; - -import android.content.Context; -import android.view.SurfaceHolder; -import android.view.SurfaceView; - -class CustomSurfaceView extends SurfaceView implements SurfaceHolder.Callback { - - private final String TAG = "CustomSurfaceView"; - - CustomSurfaceView(Context context) { - super(context); - } - - @Override - public void surfaceCreated(SurfaceHolder holder) {} - - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {} - - @Override - public void surfaceDestroyed(SurfaceHolder holder) {} -} diff --git a/android/src/main/java/com/ahm/capacitor/camera/preview/CustomTextureView.java b/android/src/main/java/com/ahm/capacitor/camera/preview/CustomTextureView.java deleted file mode 100644 index 6baac0e5..00000000 --- a/android/src/main/java/com/ahm/capacitor/camera/preview/CustomTextureView.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.ahm.capacitor.camera.preview; - -import android.content.Context; -import android.graphics.SurfaceTexture; -import android.view.SurfaceHolder; -import android.view.TextureView; - -class CustomTextureView extends TextureView implements TextureView.SurfaceTextureListener { - - private final String TAG = "CustomTextureView"; - - CustomTextureView(Context context) { - super(context); - } - - @Override - public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {} - - @Override - public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {} - - @Override - public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { - return true; - } - - @Override - public void onSurfaceTextureUpdated(SurfaceTexture surface) {} -} diff --git a/android/src/main/java/com/ahm/capacitor/camera/preview/Preview.java b/android/src/main/java/com/ahm/capacitor/camera/preview/Preview.java deleted file mode 100644 index 6daa426f..00000000 --- a/android/src/main/java/com/ahm/capacitor/camera/preview/Preview.java +++ /dev/null @@ -1,386 +0,0 @@ -package com.ahm.capacitor.camera.preview; - -import android.app.Activity; -import android.content.Context; -import android.graphics.SurfaceTexture; -import android.hardware.Camera; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.Surface; -import android.view.SurfaceHolder; -import android.view.TextureView; -import android.view.View; -import android.widget.RelativeLayout; -import java.io.IOException; -import java.util.List; - -class Preview extends RelativeLayout implements SurfaceHolder.Callback, TextureView.SurfaceTextureListener { - - private final String TAG = "Preview"; - - CustomSurfaceView mSurfaceView; - CustomTextureView mTextureView; - SurfaceHolder mHolder; - SurfaceTexture mSurface; - Camera.Size mPreviewSize; - List mSupportedPreviewSizes; - Camera mCamera; - int cameraId; - int displayOrientation; - int facing = Camera.CameraInfo.CAMERA_FACING_BACK; - int viewWidth; - int viewHeight; - private boolean enableOpacity = false; - private float opacity = 1F; - - Preview(Context context) { - this(context, false); - } - - Preview(Context context, boolean enableOpacity) { - super(context); - this.enableOpacity = enableOpacity; - if (!enableOpacity) { - mSurfaceView = new CustomSurfaceView(context); - addView(mSurfaceView); - requestLayout(); - - // Install a SurfaceHolder.Callback so we get notified when the - // underlying surface is created and destroyed. - mHolder = mSurfaceView.getHolder(); - mHolder.addCallback(this); - mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); - } else { - // Use a TextureView so we can manage opacity - mTextureView = new CustomTextureView(context); - // Install a SurfaceTextureListener so we get notified - mTextureView.setSurfaceTextureListener(this); - mTextureView.setLayerType(View.LAYER_TYPE_HARDWARE, null); - addView(mTextureView); - requestLayout(); - } - } - - public void setCamera(Camera camera, int cameraId) { - if (camera != null) { - mCamera = camera; - this.cameraId = cameraId; - mSupportedPreviewSizes = mCamera.getParameters().getSupportedPreviewSizes(); - setCameraDisplayOrientation(); - - List mFocusModes = mCamera.getParameters().getSupportedFocusModes(); - - Camera.Parameters params = mCamera.getParameters(); - if (mFocusModes.contains("continuous-picture")) { - params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); - } else if (mFocusModes.contains("continuous-video")) { - params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); - } else if (mFocusModes.contains("auto")) { - params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); - } - mCamera.setParameters(params); - } - } - - public int getDisplayOrientation() { - return displayOrientation; - } - - public int getCameraFacing() { - return facing; - } - - public void printPreviewSize(String from) { - Log.d(TAG, "printPreviewSize from " + from + ": > width: " + mPreviewSize.width + " height: " + mPreviewSize.height); - } - - public void setCameraPreviewSize() { - if (mCamera != null) { - Camera.Parameters parameters = mCamera.getParameters(); - parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height); - mCamera.setParameters(parameters); - } - } - - public void setCameraDisplayOrientation() { - Camera.CameraInfo info = new Camera.CameraInfo(); - int rotation = ((Activity) getContext()).getWindowManager().getDefaultDisplay().getRotation(); - int degrees = 0; - DisplayMetrics dm = new DisplayMetrics(); - - Camera.getCameraInfo(cameraId, info); - ((Activity) getContext()).getWindowManager().getDefaultDisplay().getMetrics(dm); - - switch (rotation) { - case Surface.ROTATION_0: - degrees = 0; - break; - case Surface.ROTATION_90: - degrees = 90; - break; - case Surface.ROTATION_180: - degrees = 180; - break; - case Surface.ROTATION_270: - degrees = 270; - break; - } - facing = info.facing; - if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { - displayOrientation = (info.orientation + degrees) % 360; - displayOrientation = (360 - displayOrientation) % 360; - } else { - displayOrientation = (info.orientation - degrees + 360) % 360; - } - - Log.d(TAG, "screen is rotated " + degrees + "deg from natural"); - Log.d( - TAG, - (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT ? "front" : "back") + - " camera is oriented -" + - info.orientation + - "deg from natural" - ); - Log.d(TAG, "need to rotate preview " + displayOrientation + "deg"); - mCamera.setDisplayOrientation(displayOrientation); - } - - public void switchCamera(Camera camera, int cameraId) { - try { - setCamera(camera, cameraId); - - Log.d("CameraPreview", "before set camera"); - - View v; - if (enableOpacity) { - camera.setPreviewTexture(mSurface); - v = mTextureView; - } else { - camera.setPreviewDisplay(mHolder); - v = mSurfaceView; - } - - Log.d("CameraPreview", "before getParameters"); - - Camera.Parameters parameters = camera.getParameters(); - - Log.d("CameraPreview", "before setPreviewSize"); - - mSupportedPreviewSizes = parameters.getSupportedPreviewSizes(); - mPreviewSize = getOptimalPreviewSize(mSupportedPreviewSizes, v.getWidth(), v.getHeight()); - parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height); - Log.d(TAG, mPreviewSize.width + " " + mPreviewSize.height); - - camera.setParameters(parameters); - } catch (IOException exception) { - Log.e(TAG, exception.getMessage()); - } - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - // We purposely disregard child measurements because act as a - // wrapper to a SurfaceView that centers the camera preview instead - // of stretching it. - final int width = resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec); - final int height = resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec); - setMeasuredDimension(width, height); - - if (mSupportedPreviewSizes != null) { - mPreviewSize = getOptimalPreviewSize(mSupportedPreviewSizes, width, height); - } - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - if (changed && getChildCount() > 0) { - final View child = getChildAt(0); - - int width = r - l; - int height = b - t; - - int previewWidth = width; - int previewHeight = height; - - if (mPreviewSize != null) { - previewWidth = mPreviewSize.width; - previewHeight = mPreviewSize.height; - - if (displayOrientation == 90 || displayOrientation == 270) { - previewWidth = mPreviewSize.height; - previewHeight = mPreviewSize.width; - } - // LOG.d(TAG, "previewWidth:" + previewWidth + " previewHeight:" + previewHeight); - } - - int nW; - int nH; - int top; - int left; - - float scale = 1.0f; - - // Center the child SurfaceView within the parent. - if (width * previewHeight < height * previewWidth) { - Log.d(TAG, "center horizontally"); - int scaledChildWidth = (int) ((previewWidth * height / previewHeight) * scale); - nW = (width + scaledChildWidth) / 2; - nH = (int) (height * scale); - top = 0; - left = (width - scaledChildWidth) / 2; - } else { - Log.d(TAG, "center vertically"); - int scaledChildHeight = (int) ((previewHeight * width / previewWidth) * scale); - nW = (int) (width * scale); - nH = (height + scaledChildHeight) / 2; - top = (height - scaledChildHeight) / 2; - left = 0; - } - child.layout(left, top, nW, nH); - - Log.d("layout", "left:" + left); - Log.d("layout", "top:" + top); - Log.d("layout", "right:" + nW); - Log.d("layout", "bottom:" + nH); - } - } - - public void surfaceCreated(SurfaceHolder holder) { - // The Surface has been created, acquire the camera and tell it where - // to draw. - try { - if (mCamera != null) { - mSurfaceView.setWillNotDraw(false); - mCamera.setPreviewDisplay(holder); - } - } catch (Exception exception) { - Log.e(TAG, "Exception caused by setPreviewDisplay()", exception); - } - } - - public void surfaceDestroyed(SurfaceHolder holder) { - // Surface will be destroyed when we return, so stop the preview. - try { - if (mCamera != null) { - mCamera.stopPreview(); - } - } catch (Exception exception) { - Log.e(TAG, "Exception caused by surfaceDestroyed()", exception); - } - } - - private Camera.Size getOptimalPreviewSize(List sizes, int w, int h) { - final double ASPECT_TOLERANCE = 0.1; - double targetRatio = (double) w / h; - if (displayOrientation == 90 || displayOrientation == 270) { - targetRatio = (double) h / w; - } - - if (sizes == null) { - return null; - } - - Camera.Size optimalSize = null; - double minDiff = Double.MAX_VALUE; - - int targetHeight = h; - - // Try to find an size match aspect ratio and size - for (Camera.Size size : sizes) { - double ratio = (double) size.width / size.height; - if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue; - if (Math.abs(size.height - targetHeight) < minDiff) { - optimalSize = size; - minDiff = Math.abs(size.height - targetHeight); - } - } - - // Cannot find the one match the aspect ratio, ignore the requirement - if (optimalSize == null) { - minDiff = Double.MAX_VALUE; - for (Camera.Size size : sizes) { - if (Math.abs(size.height - targetHeight) < minDiff) { - optimalSize = size; - minDiff = Math.abs(size.height - targetHeight); - } - } - } - - Log.d(TAG, "optimal preview size: w: " + optimalSize.width + " h: " + optimalSize.height); - return optimalSize; - } - - public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { - if (mCamera != null) { - try { - // Now that the size is known, set up the camera parameters and begin - // the preview. - mSupportedPreviewSizes = mCamera.getParameters().getSupportedPreviewSizes(); - if (mSupportedPreviewSizes != null) { - mPreviewSize = getOptimalPreviewSize(mSupportedPreviewSizes, w, h); - } - startCamera(); - } catch (Exception exception) { - Log.e(TAG, "Exception caused by surfaceChanged()", exception); - } - } - } - - private void startCamera() { - Camera.Parameters parameters = mCamera.getParameters(); - parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height); - requestLayout(); - //mCamera.setDisplayOrientation(90); - mCamera.setParameters(parameters); - mCamera.startPreview(); - } - - // Texture Callbacks - - public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { - // The Surface has been created, acquire the camera and tell it where - // to draw. - try { - mSurface = surface; - if (mSupportedPreviewSizes != null) { - mPreviewSize = getOptimalPreviewSize(mSupportedPreviewSizes, width, height); - } - if (mCamera != null) { - mTextureView.setAlpha(opacity); - mCamera.setPreviewTexture(surface); - startCamera(); - } - } catch (Exception exception) { - Log.e(TAG, "Exception caused by onSurfaceTextureAvailable()", exception); - } - } - - public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {} - - public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { - try { - if (mCamera != null) { - mCamera.stopPreview(); - } - } catch (Exception exception) { - Log.e(TAG, "Exception caused by onSurfaceTextureDestroyed()", exception); - return false; - } - return true; - } - - public void onSurfaceTextureUpdated(SurfaceTexture surface) {} - - public void setOneShotPreviewCallback(Camera.PreviewCallback callback) { - if (mCamera != null) { - mCamera.setOneShotPreviewCallback(callback); - } - } - - public void setOpacity(final float opacity) { - this.opacity = opacity; - if (mCamera != null && enableOpacity) { - mTextureView.setAlpha(opacity); - } - } -} diff --git a/src/definitions.ts b/src/definitions.ts index 9d69c77d..c306e93b 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -74,4 +74,5 @@ export interface CameraPreviewPlugin { getMaxZoom(): Promise<{ value: number }>; flip(): Promise; setOpacity(options: CameraOpacityOptions): Promise<{}>; + getCameraCharacteristics(): Promise<{}>; } diff --git a/src/web.ts b/src/web.ts index 6ea2f5d5..9b3a51a8 100644 --- a/src/web.ts +++ b/src/web.ts @@ -192,4 +192,8 @@ export class CameraPreviewWeb extends WebPlugin implements CameraPreviewPlugin { video.style.setProperty('opacity', _options['opacity'].toString()); } } + + async getCameraCharacteristics(): Promise { + throw new Error('getCameraCharacteristics not supported under the web platform'); + } } From ec762ec92a29bd1d2cfe6f7d2cbf43fa10aa898e Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Mon, 10 Jun 2024 09:28:15 +0100 Subject: [PATCH 04/32] (ios) bugfix: fix calculation of x/y offsets to use hard-coded factor of 2 rather than device pixel ratio. This fixes the offset for devices with pixel ratio of 3 while continuing to work on devices with pixel ratio of 2. --- ios/Plugin/Plugin.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/Plugin/Plugin.swift b/ios/Plugin/Plugin.swift index 044a9e68..958284a2 100644 --- a/ios/Plugin/Plugin.swift +++ b/ios/Plugin/Plugin.swift @@ -56,8 +56,8 @@ public class CameraPreview: CAPPlugin { } else { self.height = UIScreen.main.bounds.size.height } - self.x = call.getInt("x") != nil ? CGFloat(call.getInt("x")!)/UIScreen.main.scale: 0 - self.y = call.getInt("y") != nil ? CGFloat(call.getInt("y")!)/UIScreen.main.scale: 0 + self.x = call.getInt("x") != nil ? CGFloat(call.getInt("x")!)/2: 0 + self.y = call.getInt("y") != nil ? CGFloat(call.getInt("y")!)/2: 0 if call.getInt("paddingBottom") != nil { self.paddingBottom = CGFloat(call.getInt("paddingBottom")!) } From eb1d4007aa933416ec4325c2a13aa600690985a4 Mon Sep 17 00:00:00 2001 From: Dave Alden Date: Mon, 10 Jun 2024 11:10:26 +0100 Subject: [PATCH 05/32] (android) add support for getMaxZoomLimit(), setMaxZoomLimit(), maxZoomLimit startPreview() option --- README.md | 19 +++++++++++++ .../camera/preview/CameraActivity.java | 9 ++++++ .../camera/preview/CameraPreview.java | 28 +++++++++++++++++++ src/definitions.ts | 4 +++ src/web.ts | 8 ++++++ 5 files changed, 68 insertions(+) diff --git a/README.md b/README.md index 3b9510b9..a5847082 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ Starts the camera preview instance. | lockAndroidOrientation | boolean | (optional) Locks device orientation when camera is showing, default false. (applicable to Android only) | | enableOpacity | boolean | (optional) Make the camera preview see-through. Ideal for augmented reality uses. Default false (applicable to Android and web only) | enableZoom | boolean | (optional) Set if you can pinch to zoom. Default false (applicable to the android and ios platforms only) +| maxZoomLimit | number | (optional) Set the max zoom level to limit the camera device to. Set to -1 for unlimited. Default -1 (applicable to the android and ios platforms only)