Skip to content

Commit 96e2328

Browse files
authored
[camera] Add iOS and Android implementations for managing auto exposure. (flutter#3346)
* Added platform interface methods for setting auto exposure. * Added platform interface methods for setting auto exposure. * Remove workspace files * Added auto exposure implementations for Android and iOS * iOS fix for setting the exposure point * Removed unnecessary check * Update platform interface dependency * Implement PR feedback * Restore test * Small improvements for exposure point resetting
1 parent 72cc8e2 commit 96e2328

File tree

18 files changed

+1453
-121
lines changed

18 files changed

+1453
-121
lines changed

packages/camera/camera/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.6.4
2+
3+
* Adds auto exposure support for Android and iOS implementations.
4+
15
## 0.6.3+4
26

37
* Revert previous dependency update: Changed dependency on camera_platform_interface to >=1.04 <1.1.0.

packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java

Lines changed: 161 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import android.hardware.camera2.CaptureRequest;
2222
import android.hardware.camera2.CaptureResult;
2323
import android.hardware.camera2.TotalCaptureResult;
24+
import android.hardware.camera2.params.MeteringRectangle;
2425
import android.hardware.camera2.params.OutputConfiguration;
2526
import android.hardware.camera2.params.SessionConfiguration;
2627
import android.media.CamcorderProfile;
@@ -32,6 +33,8 @@
3233
import android.os.Build.VERSION_CODES;
3334
import android.os.Handler;
3435
import android.os.Looper;
36+
import android.util.Range;
37+
import android.util.Rational;
3538
import android.util.Size;
3639
import android.view.OrientationEventListener;
3740
import android.view.Surface;
@@ -40,6 +43,7 @@
4043
import io.flutter.plugin.common.MethodChannel.Result;
4144
import io.flutter.plugins.camera.PictureCaptureRequest.State;
4245
import io.flutter.plugins.camera.media.MediaRecorderBuilder;
46+
import io.flutter.plugins.camera.types.ExposureMode;
4347
import io.flutter.plugins.camera.types.FlashMode;
4448
import io.flutter.plugins.camera.types.ResolutionPreset;
4549
import io.flutter.view.TextureRegistry.SurfaceTextureEntry;
@@ -80,7 +84,10 @@ public class Camera {
8084
private File videoRecordingFile;
8185
private int currentOrientation = ORIENTATION_UNKNOWN;
8286
private FlashMode flashMode;
87+
private ExposureMode exposureMode;
8388
private PictureCaptureRequest pictureCaptureRequest;
89+
private CameraRegions cameraRegions;
90+
private int exposureOffset;
8491

8592
public Camera(
8693
final Activity activity,
@@ -100,6 +107,8 @@ public Camera(
100107
this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
101108
this.applicationContext = activity.getApplicationContext();
102109
this.flashMode = FlashMode.auto;
110+
this.exposureMode = ExposureMode.auto;
111+
this.exposureOffset = 0;
103112
orientationEventListener =
104113
new OrientationEventListener(activity.getApplicationContext()) {
105114
@Override
@@ -158,15 +167,17 @@ public void open() throws CameraAccessException {
158167
public void onOpened(@NonNull CameraDevice device) {
159168
cameraDevice = device;
160169
try {
170+
cameraRegions = new CameraRegions(getRegionBoundaries());
161171
startPreview();
172+
dartMessenger.sendCameraInitializedEvent(
173+
previewSize.getWidth(),
174+
previewSize.getHeight(),
175+
exposureMode,
176+
isExposurePointSupported());
162177
} catch (CameraAccessException e) {
163178
dartMessenger.sendCameraErrorEvent(e.getMessage());
164179
close();
165-
return;
166180
}
167-
168-
dartMessenger.sendCameraInitializedEvent(
169-
previewSize.getWidth(), previewSize.getHeight());
170181
}
171182

172183
@Override
@@ -605,16 +616,11 @@ public void resumeVideoRecording(@NonNull final Result result) {
605616
public void setFlashMode(@NonNull final Result result, FlashMode mode)
606617
throws CameraAccessException {
607618
// Get the flash availability
608-
Boolean flashAvailable;
609-
try {
610-
flashAvailable =
611-
cameraManager
612-
.getCameraCharacteristics(cameraDevice.getId())
613-
.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
614-
} catch (CameraAccessException e) {
615-
result.error("setFlashModeFailed", e.getMessage(), null);
616-
return;
617-
}
619+
Boolean flashAvailable =
620+
cameraManager
621+
.getCameraCharacteristics(cameraDevice.getId())
622+
.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
623+
618624
// Check if flash is available.
619625
if (flashAvailable == null || !flashAvailable) {
620626
result.error("setFlashModeFailed", "Device does not have flash capabilities", null);
@@ -676,8 +682,133 @@ private void updateFlash(FlashMode mode) {
676682
}
677683
}
678684

685+
public void setExposureMode(@NonNull final Result result, ExposureMode mode)
686+
throws CameraAccessException {
687+
this.exposureMode = mode;
688+
initPreviewCaptureBuilder();
689+
cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null);
690+
result.success(null);
691+
}
692+
693+
public void setExposurePoint(@NonNull final Result result, Double x, Double y)
694+
throws CameraAccessException {
695+
// Check if exposure point functionality is available.
696+
if (!isExposurePointSupported()) {
697+
result.error(
698+
"setExposurePointFailed", "Device does not have exposure point capabilities", null);
699+
return;
700+
}
701+
// Check if we are doing a reset or not
702+
if (x == null || y == null) {
703+
x = 0.5;
704+
y = 0.5;
705+
}
706+
// Get the current region boundaries.
707+
Size maxBoundaries = getRegionBoundaries();
708+
if (maxBoundaries == null) {
709+
result.error("setExposurePointFailed", "Could not determine max region boundaries", null);
710+
return;
711+
}
712+
// Set the metering rectangle
713+
cameraRegions.setAutoExposureMeteringRectangleFromPoint(x, y);
714+
// Apply it
715+
initPreviewCaptureBuilder();
716+
this.cameraCaptureSession.setRepeatingRequest(
717+
captureRequestBuilder.build(), pictureCaptureCallback, null);
718+
result.success(null);
719+
}
720+
721+
@TargetApi(VERSION_CODES.P)
722+
private boolean supportsDistortionCorrection() throws CameraAccessException {
723+
int[] availableDistortionCorrectionModes =
724+
cameraManager
725+
.getCameraCharacteristics(cameraDevice.getId())
726+
.get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES);
727+
if (availableDistortionCorrectionModes == null) availableDistortionCorrectionModes = new int[0];
728+
long nonOffModesSupported =
729+
Arrays.stream(availableDistortionCorrectionModes)
730+
.filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF)
731+
.count();
732+
return nonOffModesSupported > 0;
733+
}
734+
735+
private Size getRegionBoundaries() throws CameraAccessException {
736+
// No distortion correction support
737+
if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) {
738+
return cameraManager
739+
.getCameraCharacteristics(cameraDevice.getId())
740+
.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE);
741+
}
742+
// Get the current distortion correction mode
743+
Integer distortionCorrectionMode =
744+
captureRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE);
745+
// Return the correct boundaries depending on the mode
746+
android.graphics.Rect rect;
747+
if (distortionCorrectionMode == null
748+
|| distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) {
749+
rect =
750+
cameraManager
751+
.getCameraCharacteristics(cameraDevice.getId())
752+
.get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE);
753+
} else {
754+
rect =
755+
cameraManager
756+
.getCameraCharacteristics(cameraDevice.getId())
757+
.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
758+
}
759+
return rect == null ? null : new Size(rect.width(), rect.height());
760+
}
761+
762+
private boolean isExposurePointSupported() throws CameraAccessException {
763+
Integer supportedRegions =
764+
cameraManager
765+
.getCameraCharacteristics(cameraDevice.getId())
766+
.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE);
767+
return supportedRegions != null && supportedRegions > 0;
768+
}
769+
770+
public double getMinExposureOffset() throws CameraAccessException {
771+
Range<Integer> range =
772+
cameraManager
773+
.getCameraCharacteristics(cameraDevice.getId())
774+
.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE);
775+
double minStepped = range == null ? 0 : range.getLower();
776+
double stepSize = getExposureOffsetStepSize();
777+
return minStepped * stepSize;
778+
}
779+
780+
public double getMaxExposureOffset() throws CameraAccessException {
781+
Range<Integer> range =
782+
cameraManager
783+
.getCameraCharacteristics(cameraDevice.getId())
784+
.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE);
785+
double maxStepped = range == null ? 0 : range.getUpper();
786+
double stepSize = getExposureOffsetStepSize();
787+
return maxStepped * stepSize;
788+
}
789+
790+
public double getExposureOffsetStepSize() throws CameraAccessException {
791+
Rational stepSize =
792+
cameraManager
793+
.getCameraCharacteristics(cameraDevice.getId())
794+
.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP);
795+
return stepSize == null ? 0.0 : stepSize.doubleValue();
796+
}
797+
798+
public void setExposureOffset(@NonNull final Result result, double offset)
799+
throws CameraAccessException {
800+
// Set the exposure offset
801+
double stepSize = getExposureOffsetStepSize();
802+
exposureOffset = (int) (offset / stepSize);
803+
// Apply it
804+
initPreviewCaptureBuilder();
805+
this.cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null);
806+
result.success(offset);
807+
}
808+
679809
private void initPreviewCaptureBuilder() {
680810
captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO);
811+
// Applying flash modes
681812
switch (flashMode) {
682813
case off:
683814
captureRequestBuilder.set(
@@ -701,6 +832,22 @@ private void initPreviewCaptureBuilder() {
701832
captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);
702833
break;
703834
}
835+
// Applying auto exposure
836+
MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle();
837+
captureRequestBuilder.set(
838+
CaptureRequest.CONTROL_AE_REGIONS,
839+
aeRect == null ? null : new MeteringRectangle[] {cameraRegions.getAEMeteringRectangle()});
840+
switch (exposureMode) {
841+
case locked:
842+
captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true);
843+
break;
844+
case auto:
845+
default:
846+
captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false);
847+
break;
848+
}
849+
captureRequestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset);
850+
// Applying auto focus
704851
captureRequestBuilder.set(
705852
CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
706853
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package io.flutter.plugins.camera;
2+
3+
import android.hardware.camera2.params.MeteringRectangle;
4+
import android.util.Size;
5+
6+
public final class CameraRegions {
7+
private MeteringRectangle aeMeteringRectangle;
8+
private Size maxBoundaries;
9+
10+
public CameraRegions(Size maxBoundaries) {
11+
assert (maxBoundaries == null || maxBoundaries.getWidth() > 0);
12+
assert (maxBoundaries == null || maxBoundaries.getHeight() > 0);
13+
this.maxBoundaries = maxBoundaries;
14+
}
15+
16+
public MeteringRectangle getAEMeteringRectangle() {
17+
return aeMeteringRectangle;
18+
}
19+
20+
public Size getMaxBoundaries() {
21+
return this.maxBoundaries;
22+
}
23+
24+
public void resetAutoExposureMeteringRectangle() {
25+
this.aeMeteringRectangle = null;
26+
}
27+
28+
public void setAutoExposureMeteringRectangleFromPoint(double x, double y) {
29+
this.aeMeteringRectangle = getMeteringRectangleForPoint(maxBoundaries, x, y);
30+
}
31+
32+
public MeteringRectangle getMeteringRectangleForPoint(Size maxBoundaries, double x, double y) {
33+
assert (x >= 0 && x <= 1);
34+
assert (y >= 0 && y <= 1);
35+
if (maxBoundaries == null)
36+
throw new IllegalStateException(
37+
"Functionality for managing metering rectangles is unavailable as this CameraRegions instance was initialized with null boundaries.");
38+
39+
// Interpolate the target coordinate
40+
int targetX = (int) Math.round(x * ((double) (maxBoundaries.getWidth() - 1)));
41+
int targetY = (int) Math.round(y * ((double) (maxBoundaries.getHeight() - 1)));
42+
// Determine the dimensions of the metering triangle (10th of the viewport)
43+
int targetWidth = (int) Math.round(((double) maxBoundaries.getWidth()) / 10d);
44+
int targetHeight = (int) Math.round(((double) maxBoundaries.getHeight()) / 10d);
45+
// Adjust target coordinate to represent top-left corner of metering rectangle
46+
targetX -= targetWidth / 2;
47+
targetY -= targetHeight / 2;
48+
// Adjust target coordinate as to not fall out of bounds
49+
if (targetX < 0) targetX = 0;
50+
if (targetY < 0) targetY = 0;
51+
int maxTargetX = maxBoundaries.getWidth() - 1 - targetWidth;
52+
int maxTargetY = maxBoundaries.getHeight() - 1 - targetHeight;
53+
if (targetX > maxTargetX) targetX = maxTargetX;
54+
if (targetY > maxTargetY) targetY = maxTargetY;
55+
56+
// Build the metering rectangle
57+
return new MeteringRectangle(targetX, targetY, targetWidth, targetHeight, 1);
58+
}
59+
}

packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import androidx.annotation.Nullable;
55
import io.flutter.plugin.common.BinaryMessenger;
66
import io.flutter.plugin.common.MethodChannel;
7+
import io.flutter.plugins.camera.types.ExposureMode;
78
import java.util.HashMap;
89
import java.util.Map;
910

@@ -20,13 +21,23 @@ enum EventType {
2021
channel = new MethodChannel(messenger, "flutter.io/cameraPlugin/camera" + cameraId);
2122
}
2223

23-
void sendCameraInitializedEvent(Integer previewWidth, Integer previewHeight) {
24+
void sendCameraInitializedEvent(
25+
Integer previewWidth,
26+
Integer previewHeight,
27+
ExposureMode exposureMode,
28+
Boolean exposurePointSupported) {
29+
assert (previewWidth != null);
30+
assert (previewHeight != null);
31+
assert (exposureMode != null);
32+
assert (exposurePointSupported != null);
2433
this.send(
2534
EventType.INITIALIZED,
2635
new HashMap<String, Object>() {
2736
{
28-
if (previewWidth != null) put("previewWidth", previewWidth.doubleValue());
29-
if (previewHeight != null) put("previewHeight", previewHeight.doubleValue());
37+
put("previewWidth", previewWidth.doubleValue());
38+
put("previewHeight", previewHeight.doubleValue());
39+
put("exposureMode", exposureMode.toString());
40+
put("exposurePointSupported", exposurePointSupported);
3041
}
3142
});
3243
}

0 commit comments

Comments
 (0)