Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[camerax] Implements setExposureMode #6110

Merged
merged 11 commits into from
Feb 27, 2024
4 changes: 4 additions & 0 deletions packages/camera/camera_android_camerax/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.5.0+35

* Implements `setExposureMode`.

## 0.5.0+34

* Implements `setFocusPoint`, `setExposurePoint`, and `setExposureOffset`.
Expand Down
4 changes: 0 additions & 4 deletions packages/camera/camera_android_camerax/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ and thus, the plugin will fall back to 480p if configured with a

`setExposureMode`is unimplemented.

### Focus mode configuration \[[Issue #120467][120467]\]

`setFocusMode` is unimplemented.

### Setting maximum duration and stream options for video capture

Calling `startVideoCapturing` with `VideoCaptureOptions` configured with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity
@VisibleForTesting @Nullable public SystemServicesHostApiImpl systemServicesHostApiImpl;
@VisibleForTesting @Nullable public MeteringPointHostApiImpl meteringPointHostApiImpl;

@VisibleForTesting @Nullable
public Camera2CameraControlHostApiImpl camera2CameraControlHostApiImpl;

@VisibleForTesting
public @Nullable DeviceOrientationManagerHostApiImpl deviceOrientationManagerHostApiImpl;

Expand Down Expand Up @@ -120,6 +123,11 @@ public void setUp(
cameraControlHostApiImpl =
new CameraControlHostApiImpl(binaryMessenger, instanceManager, context);
GeneratedCameraXLibrary.CameraControlHostApi.setup(binaryMessenger, cameraControlHostApiImpl);
camera2CameraControlHostApiImpl = new Camera2CameraControlHostApiImpl(instanceManager, context);
GeneratedCameraXLibrary.Camera2CameraControlHostApi.setup(
binaryMessenger, camera2CameraControlHostApiImpl);
GeneratedCameraXLibrary.CaptureRequestOptionsHostApi.setup(
binaryMessenger, new CaptureRequestOptionsHostApiImpl(instanceManager));
GeneratedCameraXLibrary.FocusMeteringActionHostApi.setup(
binaryMessenger, new FocusMeteringActionHostApiImpl(instanceManager));
GeneratedCameraXLibrary.FocusMeteringResultHostApi.setup(
Expand Down Expand Up @@ -217,6 +225,9 @@ public void updateContext(@NonNull Context context) {
if (cameraControlHostApiImpl != null) {
cameraControlHostApiImpl.setContext(context);
}
if (camera2CameraControlHostApiImpl != null) {
camera2CameraControlHostApiImpl.setContext(context);
}
}

/** Sets {@code LifecycleOwner} that is used to control the lifecycle of the camera by CameraX. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ public void create(@NonNull Long identifier, @NonNull Map<Long, Object> options)
Map<CaptureRequestKeySupportedType, Object> decodedOptions =
new HashMap<CaptureRequestKeySupportedType, Object>();
for (Map.Entry<Long, Object> option : options.entrySet()) {
decodedOptions.put(
CaptureRequestKeySupportedType.values()[option.getKey().intValue()], option.getValue());
Integer index = ((Number) option.getKey()).intValue();
decodedOptions.put(CaptureRequestKeySupportedType.values()[index], option.getValue());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a fix for a casting error I got.

}
instanceManager.addDartCreatedInstance(proxy.create(decodedOptions), identifier);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
mock(ImageAnalysisHostApiImpl.class);
final CameraControlHostApiImpl mockCameraControlHostApiImpl =
mock(CameraControlHostApiImpl.class);
final Camera2CameraControlHostApiImpl mockCamera2CameraControlHostApiImpl =
mock(Camera2CameraControlHostApiImpl.class);

when(flutterPluginBinding.getApplicationContext()).thenReturn(mockContext);

Expand All @@ -180,6 +182,7 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
plugin.imageAnalysisHostApiImpl = mockImageAnalysisHostApiImpl;
plugin.cameraControlHostApiImpl = mockCameraControlHostApiImpl;
plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class);
plugin.camera2CameraControlHostApiImpl = mockCamera2CameraControlHostApiImpl;

plugin.onAttachedToEngine(flutterPluginBinding);
plugin.onDetachedFromActivityForConfigChanges();
Expand All @@ -191,6 +194,7 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
verify(mockImageCaptureHostApiImpl).setContext(mockContext);
verify(mockImageAnalysisHostApiImpl).setContext(mockContext);
verify(mockCameraControlHostApiImpl).setContext(mockContext);
verify(mockCamera2CameraControlHostApiImpl).setContext(mockContext);
}

@Test
Expand Down Expand Up @@ -259,6 +263,8 @@ public void onReattachedToActivityForConfigChanges_setsActivityAndPermissionsReg
mock(CameraControlHostApiImpl.class);
final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl =
mock(DeviceOrientationManagerHostApiImpl.class);
final Camera2CameraControlHostApiImpl mockCamera2CameraControlHostApiImpl =
mock(Camera2CameraControlHostApiImpl.class);
final MeteringPointHostApiImpl mockMeteringPointHostApiImpl =
mock(MeteringPointHostApiImpl.class);
final ArgumentCaptor<PermissionsRegistry> permissionsRegistryCaptor =
Expand All @@ -277,6 +283,7 @@ public void onReattachedToActivityForConfigChanges_setsActivityAndPermissionsReg
plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl;
plugin.meteringPointHostApiImpl = mockMeteringPointHostApiImpl;
plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class);
plugin.camera2CameraControlHostApiImpl = mockCamera2CameraControlHostApiImpl;

plugin.onAttachedToEngine(flutterPluginBinding);
plugin.onReattachedToActivityForConfigChanges(activityPluginBinding);
Expand All @@ -294,6 +301,7 @@ public void onReattachedToActivityForConfigChanges_setsActivityAndPermissionsReg
verify(mockImageCaptureHostApiImpl).setContext(mockActivity);
verify(mockImageAnalysisHostApiImpl).setContext(mockActivity);
verify(mockCameraControlHostApiImpl).setContext(mockActivity);
verify(mockCamera2CameraControlHostApiImpl).setContext(mockActivity);

// Check permissions registry reference is set.
verify(mockSystemServicesHostApiImpl)
Expand Down Expand Up @@ -347,6 +355,8 @@ public void onDetachedFromActivity_setsContextReferencesBasedOnFlutterPluginBind
final ImageCaptureHostApiImpl mockImageCaptureHostApiImpl = mock(ImageCaptureHostApiImpl.class);
final CameraControlHostApiImpl mockCameraControlHostApiImpl =
mock(CameraControlHostApiImpl.class);
final Camera2CameraControlHostApiImpl mockCamera2CameraControlHostApiImpl =
mock(Camera2CameraControlHostApiImpl.class);
final ArgumentCaptor<PermissionsRegistry> permissionsRegistryCaptor =
ArgumentCaptor.forClass(PermissionsRegistry.class);

Expand All @@ -360,6 +370,7 @@ public void onDetachedFromActivity_setsContextReferencesBasedOnFlutterPluginBind
plugin.imageAnalysisHostApiImpl = mockImageAnalysisHostApiImpl;
plugin.cameraControlHostApiImpl = mockCameraControlHostApiImpl;
plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class);
plugin.camera2CameraControlHostApiImpl = mockCamera2CameraControlHostApiImpl;

plugin.onAttachedToEngine(flutterPluginBinding);
plugin.onDetachedFromActivity();
Expand All @@ -371,5 +382,6 @@ public void onDetachedFromActivity_setsContextReferencesBasedOnFlutterPluginBind
verify(mockImageCaptureHostApiImpl).setContext(mockContext);
verify(mockImageAnalysisHostApiImpl).setContext(mockContext);
verify(mockCameraControlHostApiImpl).setContext(mockContext);
verify(mockCamera2CameraControlHostApiImpl).setContext(mockContext);
}
}
12 changes: 8 additions & 4 deletions packages/camera/camera_android_camerax/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -393,8 +393,10 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
children: <Widget>[
TextButton(
style: styleAuto,
onPressed:
() {}, // TODO(camsim99): Add functionality back here.
onPressed: controller != null
? () =>
onSetExposureModeButtonPressed(ExposureMode.auto)
: null,
onLongPress: () {
if (controller != null) {
CameraPlatform.instance
Expand All @@ -406,8 +408,10 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
),
TextButton(
style: styleLocked,
onPressed:
() {}, // TODO(camsim99): Add functionality back here.
onPressed: controller != null
? () =>
onSetExposureModeButtonPressed(ExposureMode.locked)
: null,
child: const Text('LOCKED'),
),
TextButton(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import 'package:stream_transform/stream_transform.dart';

import 'analyzer.dart';
import 'camera.dart';
import 'camera2_camera_control.dart';
import 'camera_control.dart';
import 'camera_info.dart';
import 'camera_selector.dart';
import 'camera_state.dart';
import 'camerax_library.g.dart';
import 'camerax_proxy.dart';
import 'capture_request_options.dart';
import 'device_orientation_manager.dart';
import 'exposure_state.dart';
import 'fallback_strategy.dart';
Expand Down Expand Up @@ -548,6 +550,24 @@ class AndroidCameraCameraX extends CameraPlatform {
point: point, meteringMode: FocusMeteringAction.flagAf);
}

/// Sets the exposure mode for taking pictures.
///
/// [cameraId] is not used.
@override
Future<void> setExposureMode(int cameraId, ExposureMode mode) async {
final Camera2CameraControl camera2Control =
proxy.getCamera2CameraControl(cameraControl);
final bool lockExposureMode = mode == ExposureMode.locked;

final CaptureRequestOptions captureRequestOptions = proxy
.createCaptureRequestOptions(<(
CaptureRequestKeySupportedType,
Object?
)>[(CaptureRequestKeySupportedType.controlAeLock, lockExposureMode)]);

await camera2Control.addCaptureRequestOptions(captureRequestOptions);
}

/// Gets the maximum supported zoom level for the selected camera.
///
/// [cameraId] not used.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
import 'dart:ui' show Size;

import 'analyzer.dart';
import 'camera2_camera_control.dart';
import 'camera_control.dart';
import 'camera_info.dart';
import 'camera_selector.dart';
import 'camera_state.dart';
import 'camerax_library.g.dart';
import 'capture_request_options.dart';
import 'device_orientation_manager.dart';
import 'fallback_strategy.dart';
import 'focus_metering_action.dart';
Expand Down Expand Up @@ -52,6 +55,8 @@ class CameraXProxy {
_startListeningForDeviceOrientationChange,
this.setPreviewSurfaceProvider = _setPreviewSurfaceProvider,
this.getDefaultDisplayRotation = _getDefaultDisplayRotation,
this.getCamera2CameraControl = _getCamera2CameraControl,
this.createCaptureRequestOptions = _createCaptureRequestOptions,
this.createMeteringPoint = _createMeteringPoint,
this.createFocusMeteringAction = _createFocusMeteringAction,
});
Expand Down Expand Up @@ -142,6 +147,15 @@ class CameraXProxy {
/// rotation constants.
Future<int> Function() getDefaultDisplayRotation;

/// Get [Camera2CameraControl] instance from [cameraControl].
Camera2CameraControl Function(CameraControl cameraControl)
getCamera2CameraControl;

/// Create [CapureRequestOptions] with specified options.
CaptureRequestOptions Function(
List<(CaptureRequestKeySupportedType, Object?)> options)
createCaptureRequestOptions;

/// Returns a [MeteringPoint] with the specified coordinates based on
/// [cameraInfo].
MeteringPoint Function(double x, double y, CameraInfo cameraInfo)
Expand Down Expand Up @@ -255,6 +269,16 @@ class CameraXProxy {
return DeviceOrientationManager.getDefaultDisplayRotation();
}

static Camera2CameraControl _getCamera2CameraControl(
CameraControl cameraControl) {
return Camera2CameraControl(cameraControl: cameraControl);
}

static CaptureRequestOptions _createCaptureRequestOptions(
List<(CaptureRequestKeySupportedType, Object?)> options) {
return CaptureRequestOptions(requestedOptions: options);
}

static MeteringPoint _createMeteringPoint(
double x, double y, CameraInfo cameraInfo) {
return MeteringPoint(x: x, y: y, cameraInfo: cameraInfo);
Expand Down
2 changes: 1 addition & 1 deletion packages/camera/camera_android_camerax/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: camera_android_camerax
description: Android implementation of the camera plugin using the CameraX library.
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
version: 0.5.0+34
version: 0.5.0+35

environment:
sdk: ^3.1.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import 'package:async/async.dart';
import 'package:camera_android_camerax/camera_android_camerax.dart';
import 'package:camera_android_camerax/src/analyzer.dart';
import 'package:camera_android_camerax/src/camera.dart';
import 'package:camera_android_camerax/src/camera2_camera_control.dart';
import 'package:camera_android_camerax/src/camera_control.dart';
import 'package:camera_android_camerax/src/camera_info.dart';
import 'package:camera_android_camerax/src/camera_selector.dart';
import 'package:camera_android_camerax/src/camera_state.dart';
import 'package:camera_android_camerax/src/camera_state_error.dart';
import 'package:camera_android_camerax/src/camerax_library.g.dart';
import 'package:camera_android_camerax/src/camerax_proxy.dart';
import 'package:camera_android_camerax/src/capture_request_options.dart';
import 'package:camera_android_camerax/src/device_orientation_manager.dart';
import 'package:camera_android_camerax/src/exposure_state.dart';
import 'package:camera_android_camerax/src/fallback_strategy.dart';
Expand Down Expand Up @@ -78,6 +80,7 @@ import 'test_camerax_library.g.dart';
MockSpec<TestInstanceManagerHostApi>(),
MockSpec<TestSystemServicesHostApi>(),
MockSpec<ZoomState>(),
MockSpec<Camera2CameraControl>(),
])
@GenerateMocks(<Type>[], customMocks: <MockSpec<Object>>[
MockSpec<LiveData<CameraState>>(as: #MockLiveCameraState),
Expand Down Expand Up @@ -2014,6 +2017,61 @@ void main() {
expect(camera.captureOrientationLocked, isFalse);
});

test('setExposureMode sets expected controlAeLock value via Camera2 interop',
() async {
final AndroidCameraCameraX camera = AndroidCameraCameraX();
const int cameraId = 78;
final MockCameraControl mockCameraControl = MockCameraControl();
final MockCamera2CameraControl mockCamera2CameraControl =
MockCamera2CameraControl();

// Set directly for test versus calling createCamera.
camera.camera = MockCamera();

// Tell plugin to create detached Camera2CameraControl and
// CaptureRequestOptions instances for testing.
camera.proxy = CameraXProxy(
getCamera2CameraControl: (CameraControl cameraControl) =>
cameraControl == mockCameraControl
? mockCamera2CameraControl
: Camera2CameraControl.detached(cameraControl: cameraControl),
createCaptureRequestOptions:
(List<(CaptureRequestKeySupportedType, Object?)> options) =>
CaptureRequestOptions.detached(requestedOptions: options),
);

when(camera.camera!.getCameraControl())
.thenAnswer((_) async => mockCameraControl);

// Test auto mode.
await camera.setExposureMode(cameraId, ExposureMode.auto);

VerificationResult verificationResult =
verify(mockCamera2CameraControl.addCaptureRequestOptions(captureAny));
CaptureRequestOptions capturedCaptureRequestOptions =
verificationResult.captured.single as CaptureRequestOptions;
List<(CaptureRequestKeySupportedType, Object?)> requestedOptions =
capturedCaptureRequestOptions.requestedOptions;
expect(requestedOptions.length, equals(1));
expect(requestedOptions.first.$1,
equals(CaptureRequestKeySupportedType.controlAeLock));
expect(requestedOptions.first.$2, equals(false));

// Test locked mode.
clearInteractions(mockCamera2CameraControl);
await camera.setExposureMode(cameraId, ExposureMode.locked);

verificationResult =
verify(mockCamera2CameraControl.addCaptureRequestOptions(captureAny));
capturedCaptureRequestOptions =
verificationResult.captured.single as CaptureRequestOptions;
requestedOptions = capturedCaptureRequestOptions.requestedOptions;
expect(requestedOptions.length, equals(1));
expect(requestedOptions.first.$1,
equals(CaptureRequestKeySupportedType.controlAeLock));
expect(requestedOptions.first.$2, equals(true));
});

test(
'setExposurePoint clears current auto-exposure metering point as expected',
() async {
Expand Down
Loading