-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[camera] Add iOS and Android implementations for managing auto exposure. #3346
Changes from 9 commits
c9f7e21
532b22f
1844b24
fd8dd40
2b98bdd
06f436f
70804a0
7b4445d
14c6ab6
a52f690
1e6ed87
9a88bca
589f442
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ | |
| import android.hardware.camera2.CaptureRequest; | ||
| import android.hardware.camera2.CaptureResult; | ||
| import android.hardware.camera2.TotalCaptureResult; | ||
| import android.hardware.camera2.params.MeteringRectangle; | ||
| import android.hardware.camera2.params.OutputConfiguration; | ||
| import android.hardware.camera2.params.SessionConfiguration; | ||
| import android.media.CamcorderProfile; | ||
|
|
@@ -32,6 +33,8 @@ | |
| import android.os.Build.VERSION_CODES; | ||
| import android.os.Handler; | ||
| import android.os.Looper; | ||
| import android.util.Range; | ||
| import android.util.Rational; | ||
| import android.util.Size; | ||
| import android.view.OrientationEventListener; | ||
| import android.view.Surface; | ||
|
|
@@ -40,6 +43,7 @@ | |
| import io.flutter.plugin.common.MethodChannel.Result; | ||
| import io.flutter.plugins.camera.PictureCaptureRequest.State; | ||
| import io.flutter.plugins.camera.media.MediaRecorderBuilder; | ||
| import io.flutter.plugins.camera.types.ExposureMode; | ||
| import io.flutter.plugins.camera.types.FlashMode; | ||
| import io.flutter.plugins.camera.types.ResolutionPreset; | ||
| import io.flutter.view.TextureRegistry.SurfaceTextureEntry; | ||
|
|
@@ -80,7 +84,10 @@ public class Camera { | |
| private File videoRecordingFile; | ||
| private int currentOrientation = ORIENTATION_UNKNOWN; | ||
| private FlashMode flashMode; | ||
| private ExposureMode exposureMode; | ||
| private PictureCaptureRequest pictureCaptureRequest; | ||
| private MeteringRectangle aeMeteringRectangle; | ||
| private int exposureOffset; | ||
|
|
||
| public Camera( | ||
| final Activity activity, | ||
|
|
@@ -100,6 +107,8 @@ public Camera( | |
| this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); | ||
| this.applicationContext = activity.getApplicationContext(); | ||
| this.flashMode = FlashMode.auto; | ||
| this.exposureMode = ExposureMode.auto; | ||
| this.exposureOffset = 0; | ||
| orientationEventListener = | ||
| new OrientationEventListener(activity.getApplicationContext()) { | ||
| @Override | ||
|
|
@@ -159,14 +168,15 @@ public void onOpened(@NonNull CameraDevice device) { | |
| cameraDevice = device; | ||
| try { | ||
| startPreview(); | ||
| dartMessenger.sendCameraInitializedEvent( | ||
| previewSize.getWidth(), | ||
| previewSize.getHeight(), | ||
| exposureMode, | ||
| isExposurePointSupported()); | ||
| } catch (CameraAccessException e) { | ||
| dartMessenger.sendCameraErrorEvent(e.getMessage()); | ||
| close(); | ||
| return; | ||
| } | ||
|
|
||
| dartMessenger.sendCameraInitializedEvent( | ||
| previewSize.getWidth(), previewSize.getHeight()); | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -605,16 +615,11 @@ public void resumeVideoRecording(@NonNull final Result result) { | |
| public void setFlashMode(@NonNull final Result result, FlashMode mode) | ||
| throws CameraAccessException { | ||
| // Get the flash availability | ||
| Boolean flashAvailable; | ||
| try { | ||
| flashAvailable = | ||
| cameraManager | ||
| .getCameraCharacteristics(cameraDevice.getId()) | ||
| .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); | ||
| } catch (CameraAccessException e) { | ||
| result.error("setFlashModeFailed", e.getMessage(), null); | ||
| return; | ||
| } | ||
| Boolean flashAvailable = | ||
| cameraManager | ||
| .getCameraCharacteristics(cameraDevice.getId()) | ||
| .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); | ||
|
|
||
| // Check if flash is available. | ||
| if (flashAvailable == null || !flashAvailable) { | ||
| result.error("setFlashModeFailed", "Device does not have flash capabilities", null); | ||
|
|
@@ -676,8 +681,151 @@ private void updateFlash(FlashMode mode) { | |
| } | ||
| } | ||
|
|
||
| public void setExposureMode(@NonNull final Result result, ExposureMode mode) | ||
| throws CameraAccessException { | ||
| this.exposureMode = mode; | ||
| initPreviewCaptureBuilder(); | ||
| cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); | ||
| result.success(null); | ||
| } | ||
|
|
||
| public void setExposurePoint(@NonNull final Result result, Double x, Double y) | ||
| throws CameraAccessException { | ||
| // Check if exposure point functionality is available. | ||
| if (!isExposurePointSupported()) { | ||
| result.error( | ||
| "setExposurePointFailed", "Device does not have exposure point capabilities", null); | ||
| return; | ||
| } | ||
| // Check if we are doing a reset or not | ||
| if (x == null || y == null) { | ||
| x = 0.5; | ||
| y = 0.5; | ||
| } | ||
| // Get the current exposure point boundaries. | ||
| Size maxBoundaries = getExposureBoundaries(); | ||
| if (maxBoundaries == null) { | ||
| result.error("setExposurePointFailed", "Could not determine max region boundaries", null); | ||
| return; | ||
| } | ||
| // Interpolate the target coordinate | ||
| int targetX = (int) Math.round(x * ((double) (maxBoundaries.getWidth() - 1))); | ||
| int targetY = (int) Math.round(y * ((double) (maxBoundaries.getHeight() - 1))); | ||
| // Determine the dimensions of the metering triangle (1th of the viewport) | ||
| int targetWidth = (int) Math.round(((double) maxBoundaries.getWidth()) / 10d); | ||
| int targetHeight = (int) Math.round(((double) maxBoundaries.getHeight()) / 10d); | ||
| // Adjust target coordinate to represent top-left corner of metering rectangle | ||
| targetX -= targetWidth / 2; | ||
| targetY -= targetHeight / 2; | ||
| // Adjust target coordinate as to not fall out of bounds | ||
| if (targetX < 0) targetX = 0; | ||
| if (targetY < 0) targetY = 0; | ||
| int maxTargetX = maxBoundaries.getWidth() - 1 - targetWidth; | ||
| int maxTargetY = maxBoundaries.getHeight() - 1 - targetHeight; | ||
| if (targetX > maxTargetX) targetX = maxTargetX; | ||
| if (targetY > maxTargetY) targetY = maxTargetY; | ||
| // Set the metering rectangle | ||
| aeMeteringRectangle = new MeteringRectangle(targetX, targetY, targetWidth, targetHeight, 1); | ||
| // Apply it | ||
| initPreviewCaptureBuilder(); | ||
| this.cameraCaptureSession.setRepeatingRequest( | ||
| captureRequestBuilder.build(), pictureCaptureCallback, null); | ||
| result.success(null); | ||
| } | ||
|
|
||
| @SuppressLint("NewApi") | ||
|
||
| private Size getExposureBoundaries() throws CameraAccessException { | ||
| // Check if the device supports distortion correction | ||
| boolean supportsDistortionCorrection = false; | ||
| if (android.os.Build.VERSION.SDK_INT >= VERSION_CODES.P) { | ||
| int[] availableDistortionCorrectionModes = | ||
| cameraManager | ||
| .getCameraCharacteristics(cameraDevice.getId()) | ||
| .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); | ||
| if (availableDistortionCorrectionModes == null) | ||
| availableDistortionCorrectionModes = new int[0]; | ||
| long nonOffModesSupported = | ||
| Arrays.stream(availableDistortionCorrectionModes) | ||
| .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) | ||
| .count(); | ||
| supportsDistortionCorrection = nonOffModesSupported > 0; | ||
| } | ||
| // No distortion correction support | ||
| if (!supportsDistortionCorrection) { | ||
| return cameraManager | ||
| .getCameraCharacteristics(cameraDevice.getId()) | ||
| .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); | ||
| } | ||
| // Get the current distortion correction mode | ||
| Integer distortionCorrectionMode = | ||
| captureRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); | ||
| // Return the correct boundaries depending on the mode | ||
| android.graphics.Rect rect; | ||
| if (distortionCorrectionMode == null | ||
| || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { | ||
| rect = | ||
| cameraManager | ||
| .getCameraCharacteristics(cameraDevice.getId()) | ||
| .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); | ||
| } else { | ||
| rect = | ||
| cameraManager | ||
| .getCameraCharacteristics(cameraDevice.getId()) | ||
| .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); | ||
| } | ||
| return rect == null ? null : new Size(rect.width(), rect.height()); | ||
| } | ||
|
|
||
| private boolean isExposurePointSupported() throws CameraAccessException { | ||
| Integer supportedRegions = | ||
| cameraManager | ||
| .getCameraCharacteristics(cameraDevice.getId()) | ||
| .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); | ||
| return supportedRegions != null && supportedRegions > 0; | ||
| } | ||
|
|
||
| public double getMinExposureOffset() throws CameraAccessException { | ||
| Range<Integer> range = | ||
| cameraManager | ||
| .getCameraCharacteristics(cameraDevice.getId()) | ||
| .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); | ||
| double minStepped = range == null ? 0 : range.getLower(); | ||
| double stepSize = getExposureOffsetStepSize(); | ||
| return minStepped * stepSize; | ||
| } | ||
|
|
||
| public double getMaxExposureOffset() throws CameraAccessException { | ||
| Range<Integer> range = | ||
| cameraManager | ||
| .getCameraCharacteristics(cameraDevice.getId()) | ||
| .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); | ||
| double maxStepped = range == null ? 0 : range.getUpper(); | ||
| double stepSize = getExposureOffsetStepSize(); | ||
| return maxStepped * stepSize; | ||
| } | ||
|
|
||
| public double getExposureOffsetStepSize() throws CameraAccessException { | ||
| Rational stepSize = | ||
| cameraManager | ||
| .getCameraCharacteristics(cameraDevice.getId()) | ||
| .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); | ||
| return stepSize == null ? 0.0 : stepSize.doubleValue(); | ||
| } | ||
|
|
||
| public void setExposureOffset(@NonNull final Result result, double offset) | ||
| throws CameraAccessException { | ||
| // Set the exposure offset | ||
| double stepSize = getExposureOffsetStepSize(); | ||
| exposureOffset = (int) (offset / stepSize); | ||
| // Apply it | ||
| initPreviewCaptureBuilder(); | ||
| this.cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); | ||
| result.success(offset); | ||
| } | ||
|
|
||
| private void initPreviewCaptureBuilder() { | ||
| captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO); | ||
| // Applying flash modes | ||
| switch (flashMode) { | ||
| case off: | ||
| captureRequestBuilder.set( | ||
|
|
@@ -701,6 +849,22 @@ private void initPreviewCaptureBuilder() { | |
| captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); | ||
| break; | ||
| } | ||
| // Applying auto exposure | ||
| if (aeMeteringRectangle != null) { | ||
| captureRequestBuilder.set( | ||
| CaptureRequest.CONTROL_AE_REGIONS, new MeteringRectangle[] {aeMeteringRectangle}); | ||
| } | ||
| switch (exposureMode) { | ||
| case locked: | ||
| captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); | ||
| break; | ||
| case auto: | ||
| default: | ||
| captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); | ||
| break; | ||
| } | ||
| captureRequestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); | ||
| // Applying auto focus | ||
| captureRequestBuilder.set( | ||
| CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.