Skip to content

Commit

Permalink
[camerax] Implement lockCaptureOrientation & `unlockCaptureOrientat…
Browse files Browse the repository at this point in the history
…ion` (#5285)

Implements `lockCaptureOrientation` & `unlockCaptureOrientation` for all camera `UseCase`s. 

Also fixes small bug concerning not initially setting the target rotation of the `UseCase`s to the requested sensor orientation when `createCamera` is called.

Fixes flutter/flutter#125915.
  • Loading branch information
camsim99 committed Jan 2, 2024
1 parent 34622db commit bbb4134
Show file tree
Hide file tree
Showing 50 changed files with 2,071 additions and 810 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+25

* Implements `lockCaptureOrientation` and `unlockCaptureOrientation`.

## 0.5.0+24

* Updates example app to use non-deprecated video_player method.
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 @@ -31,10 +31,6 @@ dependencies:
and thus, the plugin will fall back to 480p if configured with a
`ResolutionPreset`.

### Locking/Unlocking capture orientation \[[Issue #125915][125915]\]

`lockCaptureOrientation` & `unLockCaptureOrientation` are unimplemented.

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

`setExposureMode`, `setExposurePoint`, & `setExposureOffset` are unimplemented.
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
private ImageCaptureHostApiImpl imageCaptureHostApiImpl;
private CameraControlHostApiImpl cameraControlHostApiImpl;
public @Nullable SystemServicesHostApiImpl systemServicesHostApiImpl;
public @Nullable DeviceOrientationManagerHostApiImpl deviceOrientationManagerHostApiImpl;

@VisibleForTesting
public @Nullable ProcessCameraProviderHostApiImpl processCameraProviderHostApiImpl;
Expand Down Expand Up @@ -71,6 +72,10 @@ public void setUp(
systemServicesHostApiImpl =
new SystemServicesHostApiImpl(binaryMessenger, instanceManager, context);
GeneratedCameraXLibrary.SystemServicesHostApi.setup(binaryMessenger, systemServicesHostApiImpl);
deviceOrientationManagerHostApiImpl =
new DeviceOrientationManagerHostApiImpl(binaryMessenger, instanceManager);
GeneratedCameraXLibrary.DeviceOrientationManagerHostApi.setup(
binaryMessenger, deviceOrientationManagerHostApiImpl);
GeneratedCameraXLibrary.PreviewHostApi.setup(
binaryMessenger, new PreviewHostApiImpl(binaryMessenger, instanceManager, textureRegistry));
imageCaptureHostApiImpl =
Expand Down Expand Up @@ -145,6 +150,7 @@ public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBi
systemServicesHostApiImpl.setActivity(activity);
systemServicesHostApiImpl.setPermissionsRegistry(
activityPluginBinding::addRequestPermissionsResultListener);
deviceOrientationManagerHostApiImpl.setActivity(activity);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import android.view.Surface;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation;
Expand Down Expand Up @@ -85,120 +84,6 @@ public void stop() {
broadcastReceiver = null;
}

/**
* Returns the device's photo orientation in degrees based on the sensor orientation and the last
* known UI orientation.
*
* <p>Returns one of 0, 90, 180 or 270.
*
* @return The device's photo orientation in degrees.
*/
public int getPhotoOrientation() {
return this.getPhotoOrientation(this.lastOrientation);
}

/**
* Returns the device's photo orientation in degrees based on the sensor orientation and the
* supplied {@link PlatformChannel.DeviceOrientation} value.
*
* <p>Returns one of 0, 90, 180 or 270.
*
* @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted
* into degrees.
* @return The device's photo orientation in degrees.
*/
public int getPhotoOrientation(@Nullable PlatformChannel.DeviceOrientation orientation) {
int angle = 0;
// Fallback to device orientation when the orientation value is null.
if (orientation == null) {
orientation = getUIOrientation();
}

switch (orientation) {
case PORTRAIT_UP:
angle = 90;
break;
case PORTRAIT_DOWN:
angle = 270;
break;
case LANDSCAPE_LEFT:
angle = isFrontFacing ? 180 : 0;
break;
case LANDSCAPE_RIGHT:
angle = isFrontFacing ? 0 : 180;
break;
}

// Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X).
// This has to be taken into account so the JPEG is rotated properly.
// For devices with orientation of 90, this simply returns the mapping from ORIENTATIONS.
// For devices with orientation of 270, the JPEG is rotated 180 degrees instead.
return (angle + sensorOrientation + 270) % 360;
}

/**
* Returns the device's video orientation in clockwise degrees based on the sensor orientation and
* the last known UI orientation.
*
* <p>Returns one of 0, 90, 180 or 270.
*
* @return The device's video orientation in clockwise degrees.
*/
public int getVideoOrientation() {
return this.getVideoOrientation(this.lastOrientation);
}

/**
* Returns the device's video orientation in clockwise degrees based on the sensor orientation and
* the supplied {@link PlatformChannel.DeviceOrientation} value.
*
* <p>Returns one of 0, 90, 180 or 270.
*
* <p>More details can be found in the official Android documentation:
* https://developer.android.com/reference/android/media/MediaRecorder#setOrientationHint(int)
*
* <p>See also:
* https://developer.android.com/training/camera2/camera-preview-large-screens#orientation_calculation
*
* @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted
* into degrees.
* @return The device's video orientation in clockwise degrees.
*/
public int getVideoOrientation(@Nullable PlatformChannel.DeviceOrientation orientation) {
int angle = 0;

// Fallback to device orientation when the orientation value is null.
if (orientation == null) {
orientation = getUIOrientation();
}

switch (orientation) {
case PORTRAIT_UP:
angle = 0;
break;
case PORTRAIT_DOWN:
angle = 180;
break;
case LANDSCAPE_LEFT:
angle = 270;
break;
case LANDSCAPE_RIGHT:
angle = 90;
break;
}

if (isFrontFacing) {
angle *= -1;
}

return (angle + sensorOrientation + 360) % 360;
}

/** @return the last received UI orientation. */
public @Nullable PlatformChannel.DeviceOrientation getLastUIOrientation() {
return this.lastOrientation;
}

/**
* Handles orientation changes based on change events triggered by the OrientationIntentFilter.
*
Expand Down Expand Up @@ -241,7 +126,7 @@ static void handleOrientationChange(
@SuppressWarnings("deprecation")
@VisibleForTesting
PlatformChannel.DeviceOrientation getUIOrientation() {
final int rotation = getDisplay().getRotation();
final int rotation = getDefaultRotation();
final int orientation = activity.getResources().getConfiguration().orientation;

switch (orientation) {
Expand All @@ -265,57 +150,18 @@ PlatformChannel.DeviceOrientation getUIOrientation() {
}

/**
* Calculates the sensor orientation based on the supplied angle.
*
* <p>This method is visible for testing purposes only and should never be used outside this
* class.
*
* @param angle Orientation angle.
* @return The sensor orientation based on the supplied angle.
*/
@VisibleForTesting
PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) {
final int tolerance = 45;
angle += tolerance;

// Orientation is 0 in the default orientation mode. This is portrait-mode for phones
// and landscape for tablets. We have to compensate for this by calculating the default
// orientation, and apply an offset accordingly.
int defaultDeviceOrientation = getDeviceDefaultOrientation();
if (defaultDeviceOrientation == Configuration.ORIENTATION_LANDSCAPE) {
angle += 90;
}
// Determine the orientation
angle = angle % 360;
return new PlatformChannel.DeviceOrientation[] {
PlatformChannel.DeviceOrientation.PORTRAIT_UP,
PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT,
PlatformChannel.DeviceOrientation.PORTRAIT_DOWN,
PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT,
}
[angle / 90];
}

/**
* Gets the default orientation of the device.
* Gets default capture rotation for CameraX {@code UseCase}s.
*
* <p>This method is visible for testing purposes only and should never be used outside this
* class.
* <p>See
* https://developer.android.com/reference/androidx/camera/core/ImageCapture#setTargetRotation(int),
* for instance.
*
* @return The default orientation of the device.
* @return The rotation of the screen from its "natural" orientation; one of {@code
* Surface.ROTATION_0}, {@code Surface.ROTATION_90}, {@code Surface.ROTATION_180}, {@code
* Surface.ROTATION_270}
*/
@VisibleForTesting
int getDeviceDefaultOrientation() {
Configuration config = activity.getResources().getConfiguration();
int rotation = getDisplay().getRotation();
if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180)
&& config.orientation == Configuration.ORIENTATION_LANDSCAPE)
|| ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270)
&& config.orientation == Configuration.ORIENTATION_PORTRAIT)) {
return Configuration.ORIENTATION_LANDSCAPE;
} else {
return Configuration.ORIENTATION_PORTRAIT;
}
int getDefaultRotation() {
return getDisplay().getRotation();
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package io.flutter.plugins.camerax;

import androidx.annotation.NonNull;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.DeviceOrientationManagerFlutterApi;

public class DeviceOrientationManagerFlutterApiImpl extends DeviceOrientationManagerFlutterApi {
public DeviceOrientationManagerFlutterApiImpl(@NonNull BinaryMessenger binaryMessenger) {
super(binaryMessenger);
}

public void sendDeviceOrientationChangedEvent(
@NonNull String orientation, @NonNull Reply<Void> reply) {
super.onDeviceOrientationChanged(orientation, reply);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package io.flutter.plugins.camerax;

import android.app.Activity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugins.camerax.CameraPermissionsManager.PermissionsRegistry;
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.DeviceOrientationManagerHostApi;

public class DeviceOrientationManagerHostApiImpl implements DeviceOrientationManagerHostApi {
private final BinaryMessenger binaryMessenger;
private final InstanceManager instanceManager;

@VisibleForTesting public @NonNull CameraXProxy cameraXProxy = new CameraXProxy();
@VisibleForTesting public @Nullable DeviceOrientationManager deviceOrientationManager;

@VisibleForTesting
public @NonNull DeviceOrientationManagerFlutterApiImpl deviceOrientationManagerFlutterApiImpl;

private Activity activity;
private PermissionsRegistry permissionsRegistry;

public DeviceOrientationManagerHostApiImpl(
@NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) {
this.binaryMessenger = binaryMessenger;
this.instanceManager = instanceManager;
this.deviceOrientationManagerFlutterApiImpl =
new DeviceOrientationManagerFlutterApiImpl(binaryMessenger);
}

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

/**
* Starts listening for device orientation changes using an instance of a {@link
* DeviceOrientationManager}.
*
* <p>Whenever a change in device orientation is detected by the {@code DeviceOrientationManager},
* the {@link SystemServicesFlutterApi} will be used to notify the Dart side.
*/
@Override
public void startListeningForDeviceOrientationChange(
@NonNull Boolean isFrontFacing, @NonNull Long sensorOrientation) {
deviceOrientationManager =
cameraXProxy.createDeviceOrientationManager(
activity,
isFrontFacing,
sensorOrientation.intValue(),
(DeviceOrientation newOrientation) -> {
deviceOrientationManagerFlutterApiImpl.sendDeviceOrientationChangedEvent(
serializeDeviceOrientation(newOrientation), reply -> {});
});
deviceOrientationManager.start();
}

/** Serializes {@code DeviceOrientation} into a String that the Dart side is able to recognize. */
String serializeDeviceOrientation(DeviceOrientation orientation) {
return orientation.toString();
}

/**
* Tells the {@code deviceOrientationManager} to stop listening for orientation updates.
*
* <p>Has no effect if the {@code deviceOrientationManager} was never created to listen for device
* orientation updates.
*/
@Override
public void stopListeningForDeviceOrientationChange() {
if (deviceOrientationManager != null) {
deviceOrientationManager.stop();
}
}

/**
* Gets default capture rotation for CameraX {@code UseCase}s.
*
* <p>The default capture rotation for CameraX is the rotation of default {@code Display} at the
* time that a {@code UseCase} is bound, but the default {@code Display} does not change in this
* plugin, so this value is {@code Display}-agnostic.
*
* <p>See
* https://developer.android.com/reference/androidx/camera/core/ImageCapture#setTargetRotation(int)
* for instance for more information on how this default value is used.
*/
@Override
public @NonNull Long getDefaultDisplayRotation() {
int defaultRotation;
try {
defaultRotation = deviceOrientationManager.getDefaultRotation();
} catch (NullPointerException e) {
throw new IllegalStateException(
"startListeningForDeviceOrientationChange must first be called to subscribe to device orientation changes in order to retrieve default rotation.");
}

return Long.valueOf(defaultRotation);
}
}
Loading

0 comments on commit bbb4134

Please sign in to comment.