Skip to content

Commit

Permalink
[camerax] Implements setFocusPoint, setExposurePoint, `setExposur…
Browse files Browse the repository at this point in the history
…eOffset` (flutter#6059)

This PR implements `setFocusPoint`, `setExposurePoint`, `setExposureOffset` and makes some small fixes here and there, each of which I have left a comment about for context.

Part of flutter/flutter#120468 & flutter/flutter#120467.

~NOTE: Should land after flutter#6068 done :)
  • Loading branch information
camsim99 authored and arc-yong committed Jun 14, 2024
1 parent 3c99d2e commit ee3ab19
Show file tree
Hide file tree
Showing 28 changed files with 1,512 additions and 185 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+34

* Implements `setFocusPoint`, `setExposurePoint`, and `setExposureOffset`.

## 0.5.0+33

* Fixes typo in `README.md`.
Expand Down
9 changes: 4 additions & 5 deletions packages/camera/camera_android_camerax/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,19 @@ dependencies:
## Missing features and limitations
### 240p resolution configuration for video recording
240p resolution configuration for video recording is unsupported by CameraX,
and thus, the plugin will fall back to 480p if configured with a
`ResolutionPreset`.

### Exposure mode, point, & offset configuration \[[Issue #120468][120468]\]
### Exposure mode configuration \[[Issue #120468][120468]\]

`setExposureMode`, `setExposurePoint`, & `setExposureOffset` are unimplemented.
`setExposureMode`is unimplemented.

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

`setFocusMode` & `setFocusPoint` are unimplemented.
`setFocusMode` is unimplemented.

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity
@VisibleForTesting @Nullable public ImageCaptureHostApiImpl imageCaptureHostApiImpl;
@VisibleForTesting @Nullable public CameraControlHostApiImpl cameraControlHostApiImpl;
@VisibleForTesting @Nullable public SystemServicesHostApiImpl systemServicesHostApiImpl;
@VisibleForTesting @Nullable public MeteringPointHostApiImpl meteringPointHostApiImpl;

@VisibleForTesting
public @Nullable DeviceOrientationManagerHostApiImpl deviceOrientationManagerHostApiImpl;
Expand Down Expand Up @@ -119,6 +120,12 @@ public void setUp(
cameraControlHostApiImpl =
new CameraControlHostApiImpl(binaryMessenger, instanceManager, context);
GeneratedCameraXLibrary.CameraControlHostApi.setup(binaryMessenger, cameraControlHostApiImpl);
GeneratedCameraXLibrary.FocusMeteringActionHostApi.setup(
binaryMessenger, new FocusMeteringActionHostApiImpl(instanceManager));
GeneratedCameraXLibrary.FocusMeteringResultHostApi.setup(
binaryMessenger, new FocusMeteringResultHostApiImpl(instanceManager));
meteringPointHostApiImpl = new MeteringPointHostApiImpl(instanceManager);
GeneratedCameraXLibrary.MeteringPointHostApi.setup(binaryMessenger, meteringPointHostApiImpl);
}

@Override
Expand Down Expand Up @@ -238,5 +245,8 @@ public void updateActivity(@Nullable Activity activity) {
if (deviceOrientationManagerHostApiImpl != null) {
deviceOrientationManagerHostApiImpl.setActivity(activity);
}
if (meteringPointHostApiImpl != null) {
meteringPointHostApiImpl.setActivity(activity);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ public void onSuccess(Void voidResult) {
}

public void onFailure(Throwable t) {
if (t instanceof CameraControl.OperationCanceledException) {
// Operation was canceled due to camera being closed or a new request was submitted, which
// is not actionable and should not block a new value from potentially being submitted.
result.success(null);
return;
}
result.error(t);
}
},
Expand All @@ -94,6 +100,9 @@ public void onFailure(Throwable t) {
*
* <p>Will trigger an auto focus action and enable auto focus/auto exposure/auto white balance
* metering regions.
*
* <p>Will send a {@link GeneratedCameraXLibrary.Result} with a null result if operation was
* canceled.
*/
public void startFocusAndMetering(
@NonNull CameraControl cameraControl,
Expand All @@ -117,6 +126,12 @@ public void onSuccess(FocusMeteringResult focusMeteringResult) {
}

public void onFailure(Throwable t) {
if (t instanceof CameraControl.OperationCanceledException) {
// Operation was canceled due to camera being closed or a new request was submitted, which
// is not actionable and should not block a new value from potentially being submitted.
result.success(null);
return;
}
result.error(t);
}
},
Expand Down Expand Up @@ -152,6 +167,9 @@ public void onFailure(Throwable t) {
* <p>The exposure compensation value set on the camera must be within the range of {@code
* ExposureState#getExposureCompensationRange()} for the current {@code ExposureState} for the
* call to succeed.
*
* <p>Will send a {@link GeneratedCameraXLibrary.Result} with a null result if operation was
* canceled.
*/
public void setExposureCompensationIndex(
@NonNull CameraControl cameraControl, @NonNull Long index, @NonNull Result<Long> result) {
Expand All @@ -166,6 +184,12 @@ public void onSuccess(Integer integerResult) {
}

public void onFailure(Throwable t) {
if (t instanceof CameraControl.OperationCanceledException) {
// Operation was canceled due to camera being closed or a new request was submitted, which
// is not actionable and should not block a new value from potentially being submitted.
result.success(null);
return;
}
result.error(t);
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,14 @@ private CameraStateType(final int index) {
* <p>If you need to add another type to support a type S to use a LiveData<S> in this plugin,
* ensure the following is done on the Dart side:
*
* <p>* In `../lib/src/live_data.dart`, add new cases for S in
* <p>* In `camera_android_camerax/lib/src/live_data.dart`, add new cases for S in
* `_LiveDataHostApiImpl#getValueFromInstances` to get the current value of type S from a
* LiveData<S> instance and in `LiveDataFlutterApiImpl#create` to create the expected type of
* LiveData<S> when requested.
*
* <p>On the native side, ensure the following is done:
*
* <p>* Update `LiveDataHostApiImpl#getValue` is updated to properly return identifiers for
* <p>* Make sure `LiveDataHostApiImpl#getValue` is updated to properly return identifiers for
* instances of type S. * Update `ObserverFlutterApiWrapper#onChanged` to properly handle
* receiving calls with instances of type S if a LiveData<S> instance is observed.
*/
Expand Down Expand Up @@ -146,6 +146,24 @@ private VideoResolutionFallbackRule(final int index) {
}
}

/**
* The types of capture request options this plugin currently supports.
*
* <p>If you need to add another option to support, ensure the following is done on the Dart side:
*
* <p>* In `camera_android_camerax/lib/src/capture_request_options.dart`, add new cases for this
* option in `_CaptureRequestOptionsHostApiImpl#createFromInstances` to create the expected Map
* entry of option key index and value to send to the native side.
*
* <p>On the native side, ensure the following is done:
*
* <p>* Update `CaptureRequestOptionsHostApiImpl#create` to set the correct `CaptureRequest` key
* with a valid value type for this option.
*
* <p>See https://developer.android.com/reference/android/hardware/camera2/CaptureRequest for the
* sorts of capture request options that can be supported via CameraX's interoperability with
* Camera2.
*/
public enum CaptureRequestKeySupportedType {
CONTROL_AE_LOCK(0);

Expand Down Expand Up @@ -3899,7 +3917,11 @@ public void create(@NonNull Long identifierArg, @NonNull Reply<Void> callback) {
public interface MeteringPointHostApi {

void create(
@NonNull Long identifier, @NonNull Double x, @NonNull Double y, @Nullable Double size);
@NonNull Long identifier,
@NonNull Double x,
@NonNull Double y,
@Nullable Double size,
@NonNull Long cameraInfoId);

@NonNull
Double getDefaultPointSize();
Expand Down Expand Up @@ -3927,12 +3949,14 @@ static void setup(
Double xArg = (Double) args.get(1);
Double yArg = (Double) args.get(2);
Double sizeArg = (Double) args.get(3);
Number cameraInfoIdArg = (Number) args.get(4);
try {
api.create(
(identifierArg == null) ? null : identifierArg.longValue(),
xArg,
yArg,
sizeArg);
sizeArg,
(cameraInfoIdArg == null) ? null : cameraInfoIdArg.longValue());
wrapped.add(0, null);
} catch (Throwable exception) {
ArrayList<Object> wrappedError = wrapError(exception);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@

package io.flutter.plugins.camerax;

import android.app.Activity;
import android.content.Context;
import android.os.Build;
import android.view.Display;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.DisplayOrientedMeteringPointFactory;
import androidx.camera.core.MeteringPoint;
import androidx.camera.core.MeteringPointFactory;
import androidx.camera.core.SurfaceOrientedMeteringPointFactory;
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.MeteringPointHostApi;
import java.util.Objects;

/**
* Host API implementation for {@link MeteringPoint}.
Expand All @@ -25,29 +32,51 @@ public class MeteringPointHostApiImpl implements MeteringPointHostApi {
/** Proxy for constructor and static methods of {@link MeteringPoint}. */
@VisibleForTesting
public static class MeteringPointProxy {
Activity activity;

/**
* Creates a surface oriented {@link MeteringPoint} with the specified x, y, and size.
*
* <p>A {@link SurfaceOrientedMeteringPointFactory} is used to construct the {@link
* MeteringPoint} because underlying the camera preview that this plugin uses is a Flutter
* texture that is backed by a {@link Surface} created by the Flutter Android embedding.
* <p>A {@link DisplayOrientedMeteringPointFactory} is used to construct the {@link
* MeteringPoint} because this factory handles the transformation of specified coordinates based
* on camera information and the device orientation automatically.
*/
@NonNull
public MeteringPoint create(@NonNull Double x, @NonNull Double y, @Nullable Double size) {
SurfaceOrientedMeteringPointFactory factory = getSurfaceOrientedMeteringPointFactory(1f, 1f);
public MeteringPoint create(
@NonNull Double x,
@NonNull Double y,
@Nullable Double size,
@NonNull CameraInfo cameraInfo) {
Display display = null;

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
display = activity.getDisplay();
} else {
display = getDefaultDisplayForAndroidVersionBelowR(activity);
}

DisplayOrientedMeteringPointFactory factory =
getDisplayOrientedMeteringPointFactory(display, cameraInfo, 1f, 1f);

if (size == null) {
return factory.createPoint(x.floatValue(), y.floatValue());
} else {
return factory.createPoint(x.floatValue(), y.floatValue(), size.floatValue());
}
}

@NonNull
@SuppressWarnings("deprecation")
private Display getDefaultDisplayForAndroidVersionBelowR(@NonNull Activity activity) {
return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay();
}

@VisibleForTesting
@NonNull
public SurfaceOrientedMeteringPointFactory getSurfaceOrientedMeteringPointFactory(
float width, float height) {
return new SurfaceOrientedMeteringPointFactory(width, height);
public DisplayOrientedMeteringPointFactory getDisplayOrientedMeteringPointFactory(
@NonNull Display display, @NonNull CameraInfo cameraInfo, float width, float height) {
return new DisplayOrientedMeteringPointFactory(display, cameraInfo, width, height);
}

/**
Expand Down Expand Up @@ -81,10 +110,23 @@ public MeteringPointHostApiImpl(@NonNull InstanceManager instanceManager) {
this.proxy = proxy;
}

public void setActivity(@NonNull Activity activity) {
this.proxy.activity = activity;
}

@Override
public void create(
@NonNull Long identifier, @NonNull Double x, @NonNull Double y, @Nullable Double size) {
MeteringPoint meteringPoint = proxy.create(x, y, size);
@NonNull Long identifier,
@NonNull Double x,
@NonNull Double y,
@Nullable Double size,
@NonNull Long cameraInfoId) {
MeteringPoint meteringPoint =
proxy.create(
x,
y,
size,
(CameraInfo) Objects.requireNonNull(instanceManager.getInstance(cameraInfoId)));
instanceManager.addDartCreatedInstance(meteringPoint, identifier);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
mock(SystemServicesHostApiImpl.class);
final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl =
mock(DeviceOrientationManagerHostApiImpl.class);
final MeteringPointHostApiImpl mockMeteringPointHostApiImpl =
mock(MeteringPointHostApiImpl.class);
final ArgumentCaptor<PermissionsRegistry> permissionsRegistryCaptor =
ArgumentCaptor.forClass(PermissionsRegistry.class);

Expand All @@ -103,13 +105,15 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class);
plugin.systemServicesHostApiImpl = mockSystemServicesHostApiImpl;
plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl;
plugin.meteringPointHostApiImpl = mockMeteringPointHostApiImpl;

plugin.onAttachedToEngine(flutterPluginBinding);
plugin.onAttachedToActivity(activityPluginBinding);

// Check Activity references are set.
verify(mockSystemServicesHostApiImpl).setActivity(mockActivity);
verify(mockDeviceOrientationManagerHostApiImpl).setActivity(mockActivity);
verify(mockMeteringPointHostApiImpl).setActivity(mockActivity);

// Check permissions registry reference is set.
verify(mockSystemServicesHostApiImpl)
Expand All @@ -129,11 +133,14 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
mock(SystemServicesHostApiImpl.class);
final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl =
mock(DeviceOrientationManagerHostApiImpl.class);
final MeteringPointHostApiImpl mockMeteringPointHostApiImpl =
mock(MeteringPointHostApiImpl.class);

plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl;
plugin.liveDataHostApiImpl = mockLiveDataHostApiImpl;
plugin.systemServicesHostApiImpl = mockSystemServicesHostApiImpl;
plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl;
plugin.meteringPointHostApiImpl = mockMeteringPointHostApiImpl;

plugin.onAttachedToEngine(flutterPluginBinding);
plugin.onDetachedFromActivityForConfigChanges();
Expand All @@ -142,6 +149,7 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
verify(mockLiveDataHostApiImpl).setLifecycleOwner(null);
verify(mockSystemServicesHostApiImpl).setActivity(null);
verify(mockDeviceOrientationManagerHostApiImpl).setActivity(null);
verify(mockMeteringPointHostApiImpl).setActivity(null);
}

@Test
Expand Down Expand Up @@ -251,6 +259,8 @@ public void onReattachedToActivityForConfigChanges_setsActivityAndPermissionsReg
mock(CameraControlHostApiImpl.class);
final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl =
mock(DeviceOrientationManagerHostApiImpl.class);
final MeteringPointHostApiImpl mockMeteringPointHostApiImpl =
mock(MeteringPointHostApiImpl.class);
final ArgumentCaptor<PermissionsRegistry> permissionsRegistryCaptor =
ArgumentCaptor.forClass(PermissionsRegistry.class);

Expand All @@ -265,6 +275,7 @@ public void onReattachedToActivityForConfigChanges_setsActivityAndPermissionsReg
plugin.imageAnalysisHostApiImpl = mockImageAnalysisHostApiImpl;
plugin.cameraControlHostApiImpl = mockCameraControlHostApiImpl;
plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl;
plugin.meteringPointHostApiImpl = mockMeteringPointHostApiImpl;
plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class);

plugin.onAttachedToEngine(flutterPluginBinding);
Expand All @@ -273,6 +284,7 @@ public void onReattachedToActivityForConfigChanges_setsActivityAndPermissionsReg
// Check Activity references are set.
verify(mockSystemServicesHostApiImpl).setActivity(mockActivity);
verify(mockDeviceOrientationManagerHostApiImpl).setActivity(mockActivity);
verify(mockMeteringPointHostApiImpl).setActivity(mockActivity);

// Check Activity as Context references are set.
verify(mockProcessCameraProviderHostApiImpl).setContext(mockActivity);
Expand Down Expand Up @@ -300,11 +312,14 @@ public void onDetachedFromActivity_removesReferencesToActivityPluginBindingAndAc
final LiveDataHostApiImpl mockLiveDataHostApiImpl = mock(LiveDataHostApiImpl.class);
final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl =
mock(DeviceOrientationManagerHostApiImpl.class);
final MeteringPointHostApiImpl mockMeteringPointHostApiImpl =
mock(MeteringPointHostApiImpl.class);

plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl;
plugin.liveDataHostApiImpl = mockLiveDataHostApiImpl;
plugin.systemServicesHostApiImpl = mockSystemServicesHostApiImpl;
plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl;
plugin.meteringPointHostApiImpl = mockMeteringPointHostApiImpl;

plugin.onAttachedToEngine(flutterPluginBinding);
plugin.onDetachedFromActivityForConfigChanges();
Expand All @@ -313,6 +328,7 @@ public void onDetachedFromActivity_removesReferencesToActivityPluginBindingAndAc
verify(mockLiveDataHostApiImpl).setLifecycleOwner(null);
verify(mockSystemServicesHostApiImpl).setActivity(null);
verify(mockDeviceOrientationManagerHostApiImpl).setActivity(null);
verify(mockMeteringPointHostApiImpl).setActivity(null);
}

@Test
Expand Down
Loading

0 comments on commit ee3ab19

Please sign in to comment.