Skip to content

Commit

Permalink
[camerax] Implements setExposureMode (flutter#6110)
Browse files Browse the repository at this point in the history
Implements `setExposureMode`.

Fixes flutter#120468.

~To be landed after (1) flutter/packages#6059 then (2) flutter/packages#6109 Done :)
  • Loading branch information
camsim99 authored Feb 27, 2024
1 parent bc51dea commit e07eb50
Show file tree
Hide file tree
Showing 11 changed files with 176 additions and 11 deletions.
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+36

* Implements `setExposureMode`.

## 0.5.0+35

* Modifies `CameraInitializedEvent` that is sent when the camera is initialized to indicate that the initial focus
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 @@ -30,10 +30,6 @@ dependencies:
and thus, the plugin will fall back to 480p if configured with a
`ResolutionPreset`.

### Exposure mode configuration \[[Issue #120468][120468]\]

`setExposureMode`is unimplemented.

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

`setFocusMode` is unimplemented.
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());
}
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 @@ -545,6 +547,27 @@ class AndroidCameraCameraX extends CameraPlatform {
point: point, meteringMode: FocusMeteringAction.flagAf);
}

/// Sets the exposure mode for taking pictures.
///
/// Setting [ExposureMode.locked] will lock current exposure point until it
/// is unset by setting [ExposureMode.auto].
///
/// [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
24 changes: 24 additions & 0 deletions packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart
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+35
version: 0.5.0+36

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 @@ -2010,6 +2013,59 @@ 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();
camera.cameraControl = mockCameraControl;

// 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),
);

// 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

0 comments on commit e07eb50

Please sign in to comment.