Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[Camera] Support setting exposure compensation to back and front camera (Both Android and iOS) #2524

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.5.8

* Add feature to set exposure compensation value (brightness).

## 0.5.7+4

* Add `pedantic` to dev_dependency.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import android.media.ImageReader;
import android.media.MediaRecorder;
import android.os.Build;
import android.util.Range;
import android.util.Size;
import android.view.OrientationEventListener;
import android.view.Surface;
Expand All @@ -40,6 +41,8 @@
import java.util.Map;

public class Camera {
private static final int DEFAULT_MIN_COMPENSATION = -10;
private static final int DEFAULT_MAX_COMPENSATION = 10;
private final SurfaceTextureEntry flutterTexture;
private final CameraManager cameraManager;
private final OrientationEventListener orientationEventListener;
Expand Down Expand Up @@ -320,6 +323,7 @@ public void onConfigured(@NonNull CameraCaptureSession session) {
cameraCaptureSession = session;
captureRequestBuilder.set(
CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);

cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null);
if (onSuccessCallback != null) {
onSuccessCallback.run();
Expand Down Expand Up @@ -475,6 +479,55 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i
null);
}

public void applyExposureCompensation(@NonNull final Result result, int value) {
try {
applyExposureCompensationRequest(captureRequestBuilder, value);

cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null);

result.success(null);
} catch (Exception e) {
result.error("cameraExposureCompensationFailed", e.getMessage(), null);
}
}

public double getMaxExposureCompensation() {
Range<Integer> exposureCompensationRange = getExposureCompensationRange();
return exposureCompensationRange.getUpper().doubleValue();
}

public double getMinExposureCompensation() {
Range<Integer> exposureCompensationRange = getExposureCompensationRange();
return exposureCompensationRange.getLower().doubleValue();
}

private Range<Integer> getExposureCompensationRange() {
Range<Integer> range = Range.create(DEFAULT_MIN_COMPENSATION, DEFAULT_MAX_COMPENSATION);

try {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName);
range = characteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE);
} catch (CameraAccessException e) {
//In case of error we will send default range
e.printStackTrace();
}

return range;
}

private void applyExposureCompensationRequest(CaptureRequest.Builder builderRequest, int value) {
Range<Integer> exposureCompensationRange = getExposureCompensationRange();
if (value > exposureCompensationRange.getUpper()) {
builderRequest.set(
CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureCompensationRange.getUpper());
} else if (value < exposureCompensationRange.getLower()) {
builderRequest.set(
CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureCompensationRange.getLower());
} else {
builderRequest.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, value);
}
}

private void closeCaptureSession() {
if (cameraCaptureSession != null) {
cameraCaptureSession.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,21 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result)
}
break;
}
case "applyExposureCompensation":
{
camera.applyExposureCompensation(result, call.argument("exposureCompensation"));
break;
}
case "getMaxExposureCompensation":
{
result.success(camera.getMaxExposureCompensation());
break;
}
case "getMinExposureCompensation":
{
result.success(camera.getMinExposureCompensation());
break;
}
case "dispose":
{
if (camera != null) {
Expand Down
54 changes: 53 additions & 1 deletion packages/camera/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,19 @@ void logError(String code, String message) =>

class _CameraExampleHomeState extends State<CameraExampleHome>
with WidgetsBindingObserver {
static const double initialExposureCompensation = 0.0;
static const double initialMinExposureCompensation = -10.0;
static const double initialMaxExposureCompensation = 10.0;
CameraController controller;
String imagePath;
String videoPath;
VideoPlayerController videoController;
VoidCallback videoPlayerListener;
bool enableAudio = true;
double exposureCompensation = initialExposureCompensation;
double minExposureCompensation = initialMinExposureCompensation;
double maxExposureCompensation = initialMaxExposureCompensation;
bool isMinMaxExposureCompensationSet = false;

@override
void initState() {
Expand Down Expand Up @@ -103,6 +110,7 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
),
_captureControlRowWidget(),
_toggleAudioWidget(),
Center(child: _changeExposureCompensationWidget()),
Padding(
padding: const EdgeInsets.all(5.0),
child: Row(
Expand Down Expand Up @@ -269,6 +277,45 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
return Row(children: toggles);
}

// Display an exposure compensation slider to change the value of it.
Widget _changeExposureCompensationWidget() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Slider(
value: exposureCompensation,
min: minExposureCompensation,
max: maxExposureCompensation,
onChanged: (value) {
exposureCompensation = value;

controller
.applyExposureCompensation(
exposureValue: exposureCompensation.toInt())
.then((value) async {
//We should get and set the device camera's min and max exposure target bias once if we have not set it yet.
if (!isMinMaxExposureCompensationSet) {
minExposureCompensation =
await controller.getMinExposureCompensation();
maxExposureCompensation =
await controller.getMaxExposureCompensation();
isMinMaxExposureCompensationSet = true;
if (mounted) {
setState(() {
/* We should set state to refresh the ui and see the changed min and max values for the slider */
});
}
}
});
if (mounted) {
setState(() {
/* We should set state to refresh the ui and see the changed compensation effect in the camera view */
});
}
},
),
);
}

String timestamp() => DateTime.now().millisecondsSinceEpoch.toString();

void showInSnackBar(String message) {
Expand All @@ -287,7 +334,12 @@ class _CameraExampleHomeState extends State<CameraExampleHome>

// If the controller is updated then update the UI.
controller.addListener(() {
if (mounted) setState(() {});
if (mounted) {
setState(() {
//Reset the exposure compensation when the user switches between cameras.
exposureCompensation = 0;
});
}
if (controller.value.hasError) {
showInSnackBar('Camera error ${controller.value.errorDescription}');
}
Expand Down
64 changes: 64 additions & 0 deletions packages/camera/example/test_driver/camera_e2e.dart
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,68 @@ void main() {
},
skip: !Platform.isAndroid,
);

// This tests that the minimum exposure compensation is always a negative value.
// Returns whether the minimum value is negative or not.
Future<bool> testGettingMinimumExposureCompensation(
CameraController controller) async {
print(
'Getting minimum exposure compensation of camera ${controller.description.name}');

// Get minimum exposure compensation
double minimumExposureCompensation =
await controller.getMinExposureCompensation();

// Verify minimum exposure compensation is negative
expect(minimumExposureCompensation, isNegative);
return minimumExposureCompensation < 0;
}

testWidgets('Get minimum exposure compensation', (WidgetTester tester) async {
final List<CameraDescription> cameras = await availableCameras();
if (cameras.isEmpty) {
return;
}
for (CameraDescription cameraDescription in cameras) {
final CameraController controller =
CameraController(cameraDescription, ResolutionPreset.medium);
await controller.initialize();
final bool isSuccess =
await testGettingMinimumExposureCompensation(controller);
assert(isSuccess);
await controller.dispose();
}
}, skip: !Platform.isAndroid);

// This tests that the maximum exposure compensation is always a positive value.
// Returns whether the maximum value is positive or not.
Future<bool> testGettingMaximumExposureCompensation(
CameraController controller) async {
print(
'Getting maximum exposure compensation of camera ${controller.description.name}');

// Get maximum exposure compensation
double maximumExposureCompensation =
await controller.getMaxExposureCompensation();

// Verify maximum exposure compensation is positive
expect(maximumExposureCompensation, isPositive);
return maximumExposureCompensation > 0;
}

testWidgets('Get maximum exposure compensation', (WidgetTester tester) async {
final List<CameraDescription> cameras = await availableCameras();
if (cameras.isEmpty) {
return;
}
for (CameraDescription cameraDescription in cameras) {
final CameraController controller =
CameraController(cameraDescription, ResolutionPreset.medium);
await controller.initialize();
final bool isSuccess =
await testGettingMaximumExposureCompensation(controller);
assert(isSuccess);
await controller.dispose();
}
}, skip: !Platform.isAndroid);
}
38 changes: 38 additions & 0 deletions packages/camera/ios/Classes/CameraPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ @interface FLTCam : NSObject <FlutterTexture,
@property(readonly, nonatomic) int64_t textureId;
@property(nonatomic, copy) void (^onFrameAvailable)();
@property BOOL enableAudio;
@property NSInteger exposureCompensation;
@property(nonatomic) FlutterEventChannel *eventChannel;
@property(nonatomic) FLTImageStreamHandler *imageStreamHandler;
@property(nonatomic) FlutterEventSink eventSink;
Expand Down Expand Up @@ -261,6 +262,7 @@ - (instancetype)initWithCameraName:(NSString *)cameraName
[_motionManager startAccelerometerUpdates];

[self setCaptureSessionPreset:_resolutionPreset];

return self;
}

Expand Down Expand Up @@ -759,6 +761,36 @@ - (void)setUpCaptureSessionForAudio {
}
}
}

- (float)getMinExposureCompensation {
return _captureDevice.minExposureTargetBias;
}

- (float)getMaxExposureCompensation {
return _captureDevice.maxExposureTargetBias;
}

- (void)applyExposureCompensation:(NSNumber *)exposureValue result:(FlutterResult)result {
NSError *error = nil;
[_captureDevice lockForConfiguration:&error];
if (error) {
result(getFlutterError(error));
} else {
float minExposureCompensation = [self getMinExposureCompensation];
float maxExposureCompensation = [self getMaxExposureCompensation];

int exposureCompensation = MIN(exposureValue.intValue, (int)maxExposureCompensation);
exposureCompensation = MAX(exposureCompensation, (int)minExposureCompensation);
[_captureDevice setExposureTargetBias:(float)exposureCompensation
completionHandler:^(CMTime syncTime){
}];

[_captureDevice unlockForConfiguration];

result(nil);
}
}

@end

@interface CameraPlugin ()
Expand Down Expand Up @@ -871,6 +903,12 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re
} else if ([@"stopImageStream" isEqualToString:call.method]) {
[_camera stopImageStream];
result(nil);
} else if ([@"applyExposureCompensation" isEqualToString:call.method]) {
[_camera applyExposureCompensation:call.arguments[@"exposureCompensation"] result:result];
} else if ([@"getMinExposureCompensation" isEqualToString:call.method]) {
result(@([_camera getMinExposureCompensation]));
} else if ([@"getMaxExposureCompensation" isEqualToString:call.method]) {
result(@([_camera getMaxExposureCompensation]));
} else if ([@"pauseVideoRecording" isEqualToString:call.method]) {
[_camera pauseVideoRecording];
result(nil);
Expand Down
57 changes: 57 additions & 0 deletions packages/camera/lib/camera.dart
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,63 @@ class CameraController extends ValueNotifier<CameraValue> {
}
}

/// Set the exposure compensation value
///
/// Throws a [CameraException] if setting exposure compensation fails.
Future<void> applyExposureCompensation({int exposureValue = 0}) async {
if (!value.isInitialized || _isDisposed) {
throw CameraException(
'Uninitialized CameraController.',
'applyExposureCompensation was called on uninitialized CameraController',
);
}

try {
await _channel.invokeMethod<void>('applyExposureCompensation',
<String, dynamic>{'exposureCompensation': exposureValue});
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
}

/// Get the minimum exposure compensation value
///
/// Throws a [CameraException] if getting minimum exposure
/// target bias fails.
Future<double> getMinExposureCompensation() async {
if (!value.isInitialized || _isDisposed) {
throw CameraException(
'Uninitialized CameraController.',
'getMinExposureCompensation was called on uninitialized CameraController',
);
}

try {
return await _channel.invokeMethod<double>('getMinExposureCompensation');
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
}

/// Get the maximum exposure compensation value
///
/// Throws a [CameraException] if getting maximum exposure
/// target bias fails.
Future<double> getMaxExposureCompensation() async {
if (!value.isInitialized || _isDisposed) {
throw CameraException(
'Uninitialized CameraController.',
'getMaxExposureCompensation was called on uninitialized CameraController',
);
}

try {
return await _channel.invokeMethod<double>('getMaxExposureCompensation');
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
}

/// Releases the resources of this camera.
@override
Future<void> dispose() async {
Expand Down
Loading