[camerax] Implements setExposureMode (#6110)

Implements `setExposureMode`.

Fixes https://github.com/flutter/flutter/issues/120468.

~To be landed after (1) https://github.com/flutter/packages/pull/6059 then (2) https://github.com/flutter/packages/pull/6109.~ Done :)
This commit is contained in:
Camille Simon
2024-02-27 16:45:09 -05:00
committed by GitHub
parent bc51deab5d
commit e07eb50a0f
11 changed files with 176 additions and 11 deletions

View File

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

View File

@ -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.

View File

@ -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;
@ -120,6 +123,11 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity
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(
@ -217,6 +225,9 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity
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. */

View File

@ -110,8 +110,8 @@ public class CaptureRequestOptionsHostApiImpl implements CaptureRequestOptionsHo
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);
}

View File

@ -169,6 +169,8 @@ public class CameraAndroidCameraxPluginTest {
mock(ImageAnalysisHostApiImpl.class);
final CameraControlHostApiImpl mockCameraControlHostApiImpl =
mock(CameraControlHostApiImpl.class);
final Camera2CameraControlHostApiImpl mockCamera2CameraControlHostApiImpl =
mock(Camera2CameraControlHostApiImpl.class);
when(flutterPluginBinding.getApplicationContext()).thenReturn(mockContext);
@ -180,6 +182,7 @@ public class CameraAndroidCameraxPluginTest {
plugin.imageAnalysisHostApiImpl = mockImageAnalysisHostApiImpl;
plugin.cameraControlHostApiImpl = mockCameraControlHostApiImpl;
plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class);
plugin.camera2CameraControlHostApiImpl = mockCamera2CameraControlHostApiImpl;
plugin.onAttachedToEngine(flutterPluginBinding);
plugin.onDetachedFromActivityForConfigChanges();
@ -191,6 +194,7 @@ public class CameraAndroidCameraxPluginTest {
verify(mockImageCaptureHostApiImpl).setContext(mockContext);
verify(mockImageAnalysisHostApiImpl).setContext(mockContext);
verify(mockCameraControlHostApiImpl).setContext(mockContext);
verify(mockCamera2CameraControlHostApiImpl).setContext(mockContext);
}
@Test
@ -259,6 +263,8 @@ public class CameraAndroidCameraxPluginTest {
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 =
@ -277,6 +283,7 @@ public class CameraAndroidCameraxPluginTest {
plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl;
plugin.meteringPointHostApiImpl = mockMeteringPointHostApiImpl;
plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class);
plugin.camera2CameraControlHostApiImpl = mockCamera2CameraControlHostApiImpl;
plugin.onAttachedToEngine(flutterPluginBinding);
plugin.onReattachedToActivityForConfigChanges(activityPluginBinding);
@ -294,6 +301,7 @@ public class CameraAndroidCameraxPluginTest {
verify(mockImageCaptureHostApiImpl).setContext(mockActivity);
verify(mockImageAnalysisHostApiImpl).setContext(mockActivity);
verify(mockCameraControlHostApiImpl).setContext(mockActivity);
verify(mockCamera2CameraControlHostApiImpl).setContext(mockActivity);
// Check permissions registry reference is set.
verify(mockSystemServicesHostApiImpl)
@ -347,6 +355,8 @@ public class CameraAndroidCameraxPluginTest {
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);
@ -360,6 +370,7 @@ public class CameraAndroidCameraxPluginTest {
plugin.imageAnalysisHostApiImpl = mockImageAnalysisHostApiImpl;
plugin.cameraControlHostApiImpl = mockCameraControlHostApiImpl;
plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class);
plugin.camera2CameraControlHostApiImpl = mockCamera2CameraControlHostApiImpl;
plugin.onAttachedToEngine(flutterPluginBinding);
plugin.onDetachedFromActivity();
@ -371,5 +382,6 @@ public class CameraAndroidCameraxPluginTest {
verify(mockImageCaptureHostApiImpl).setContext(mockContext);
verify(mockImageAnalysisHostApiImpl).setContext(mockContext);
verify(mockCameraControlHostApiImpl).setContext(mockContext);
verify(mockCamera2CameraControlHostApiImpl).setContext(mockContext);
}
}

View File

@ -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
@ -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(

View File

@ -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';
@ -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.

View File

@ -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';
@ -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,
});
@ -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)
@ -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);

View File

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

View File

@ -9,6 +9,7 @@ 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';
@ -16,6 +17,7 @@ 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';
@ -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),
@ -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 {

View File

@ -8,11 +8,14 @@ import 'dart:typed_data' as _i29;
import 'package:camera_android_camerax/src/analyzer.dart' as _i15;
import 'package:camera_android_camerax/src/camera.dart' as _i9;
import 'package:camera_android_camerax/src/camera2_camera_control.dart' as _i38;
import 'package:camera_android_camerax/src/camera_control.dart' as _i3;
import 'package:camera_android_camerax/src/camera_info.dart' as _i2;
import 'package:camera_android_camerax/src/camera_selector.dart' as _i22;
import 'package:camera_android_camerax/src/camera_state.dart' as _i18;
import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i7;
import 'package:camera_android_camerax/src/capture_request_options.dart'
as _i39;
import 'package:camera_android_camerax/src/exposure_state.dart' as _i5;
import 'package:camera_android_camerax/src/fallback_strategy.dart' as _i23;
import 'package:camera_android_camerax/src/focus_metering_action.dart' as _i21;
@ -1317,6 +1320,38 @@ class MockZoomState extends _i1.Mock implements _i19.ZoomState {
) as double);
}
/// A class which mocks [Camera2CameraControl].
///
/// See the documentation for Mockito's code generation for more information.
// ignore: must_be_immutable
class MockCamera2CameraControl extends _i1.Mock
implements _i38.Camera2CameraControl {
@override
_i3.CameraControl get cameraControl => (super.noSuchMethod(
Invocation.getter(#cameraControl),
returnValue: _FakeCameraControl_1(
this,
Invocation.getter(#cameraControl),
),
returnValueForMissingStub: _FakeCameraControl_1(
this,
Invocation.getter(#cameraControl),
),
) as _i3.CameraControl);
@override
_i16.Future<void> addCaptureRequestOptions(
_i39.CaptureRequestOptions? captureRequestOptions) =>
(super.noSuchMethod(
Invocation.method(
#addCaptureRequestOptions,
[captureRequestOptions],
),
returnValue: _i16.Future<void>.value(),
returnValueForMissingStub: _i16.Future<void>.value(),
) as _i16.Future<void>);
}
/// A class which mocks [LiveData].
///
/// See the documentation for Mockito's code generation for more information.