Skip to content

Commit

Permalink
[camerax] Add fix for camera preview rotation on landscape-oriented d…
Browse files Browse the repository at this point in the history
…evices and set up fix for Impeller support (#7044)

Partially lands #6856. This PR specifically includes:

#### 1: A fix for correctly rotating the camera preview
This fix is required for `Surface`s not backed by a `SurfaceTexture` because they don't contain the transformation information needed to correctly rotate the camera preview. In that case, we use the logic described in https://developer.android.com/media/camera/camera2/camera-preview#orientation_calculation.

The fix  is **not currently used** (the logic is not reachable) as Impeller support has not been added back to the plugin, but has been tested in #6856 and will be turned on when flutter/flutter#149294 is fixed.

Part of flutter/flutter#149294.

#### 2: A fix for correctly rotating the camera preview on naturally landscape-oriented devices
I believe this issue was caused because we assume that the natural orientation of the device is portrait up. We fix this here by adding an extra rotation for the camera preview based on the natural orientation of the device.

Fixes flutter/flutter#149177.
  • Loading branch information
camsim99 authored Jul 5, 2024
1 parent 754de19 commit 97bad7e
Show file tree
Hide file tree
Showing 25 changed files with 817 additions and 34 deletions.
7 changes: 7 additions & 0 deletions packages/camera/camera_android_camerax/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 0.6.6

* Adds logic to support building a camera preview with Android `Surface`s not backed by a `SurfaceTexture`
to which CameraX cannot not automatically apply the transformation required to achieve the correct rotation.
* Adds fix for incorrect camera preview rotation on naturally landscape-oriented devices.
* Updates example app's minimum supported SDK version to Flutter 3.22/Dart 3.4.

## 0.6.5+6

* Updates Guava version to 33.2.1.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,26 +29,30 @@ public class Camera2CameraInfoHostApiImpl implements Camera2CameraInfoHostApi {

/** Proxy for methods of {@link Camera2CameraInfo}. */
@VisibleForTesting
@OptIn(markerClass = ExperimentalCamera2Interop.class)
public static class Camera2CameraInfoProxy {

@NonNull
@OptIn(markerClass = ExperimentalCamera2Interop.class)
public Camera2CameraInfo createFrom(@NonNull CameraInfo cameraInfo) {
return Camera2CameraInfo.from(cameraInfo);
}

@NonNull
@OptIn(markerClass = ExperimentalCamera2Interop.class)
public Integer getSupportedHardwareLevel(@NonNull Camera2CameraInfo camera2CameraInfo) {
return camera2CameraInfo.getCameraCharacteristic(
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
}

@NonNull
@OptIn(markerClass = ExperimentalCamera2Interop.class)
public String getCameraId(@NonNull Camera2CameraInfo camera2CameraInfo) {
return camera2CameraInfo.getCameraId();
}

@NonNull
public Long getSensorOrientation(@NonNull Camera2CameraInfo camera2CameraInfo) {
return Long.valueOf(
camera2CameraInfo.getCameraCharacteristic(CameraCharacteristics.SENSOR_ORIENTATION));
}
}

/**
Expand Down Expand Up @@ -105,6 +109,12 @@ public String getCameraId(@NonNull Long identifier) {
return proxy.getCameraId(getCamera2CameraInfoInstance(identifier));
}

@Override
@NonNull
public Long getSensorOrientation(@NonNull Long identifier) {
return proxy.getSensorOrientation(getCamera2CameraInfoInstance(identifier));
}

private Camera2CameraInfo getCamera2CameraInfoInstance(@NonNull Long identifier) {
return Objects.requireNonNull(instanceManager.getInstance(identifier));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ interface DeviceOrientationChangeCallback {
* Starts listening to the device's sensors or UI for orientation updates.
*
* <p>When orientation information is updated, the callback method of the {@link
* DeviceOrientationChangeCallback} is called with the new orientation. This latest value can also
* be retrieved through the {@link #getVideoOrientation()} accessor.
* DeviceOrientationChangeCallback} is called with the new orientation.
*
* <p>If the device's ACCELEROMETER_ROTATION setting is enabled the {@link
* DeviceOrientationManager} will report orientation updates based on the sensor information. If
Expand Down Expand Up @@ -124,7 +123,7 @@ static void handleOrientationChange(
*/
// Configuration.ORIENTATION_SQUARE is deprecated.
@SuppressWarnings("deprecation")
@VisibleForTesting
@NonNull
PlatformChannel.DeviceOrientation getUIOrientation() {
final int rotation = getDefaultRotation();
final int orientation = activity.getResources().getConfiguration().orientation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ public void stopListeningForDeviceOrientationChange() {
* for instance for more information on how this default value is used.
*/
@Override
public @NonNull Long getDefaultDisplayRotation() {
@NonNull
public Long getDefaultDisplayRotation() {
int defaultRotation;
try {
defaultRotation = deviceOrientationManager.getDefaultRotation();
Expand All @@ -106,4 +107,11 @@ public void stopListeningForDeviceOrientationChange() {

return Long.valueOf(defaultRotation);
}

/** Gets current UI orientation based on the current device orientation and rotation. */
@Override
@NonNull
public String getUiOrientation() {
return serializeDeviceOrientation(deviceOrientationManager.getUIOrientation());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1441,6 +1441,9 @@ void requestCameraPermissions(
@NonNull
String getTempFilePath(@NonNull String prefix, @NonNull String suffix);

@NonNull
Boolean isPreviewPreTransformed();

/** The codec used by SystemServicesHostApi. */
static @NonNull MessageCodec<Object> getCodec() {
return SystemServicesHostApiCodec.INSTANCE;
Expand Down Expand Up @@ -1508,6 +1511,29 @@ public void error(Throwable error) {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.SystemServicesHostApi.isPreviewPreTransformed",
getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
try {
Boolean output = api.isPreviewPreTransformed();
wrapped.add(0, output);
} catch (Throwable exception) {
ArrayList<Object> wrappedError = wrapError(exception);
wrapped = wrappedError;
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
}
}
/** Generated class from Pigeon that represents Flutter messages that can be called from Java. */
Expand Down Expand Up @@ -1550,6 +1576,9 @@ void startListeningForDeviceOrientationChange(
@NonNull
Long getDefaultDisplayRotation();

@NonNull
String getUiOrientation();

/** The codec used by DeviceOrientationManagerHostApi. */
static @NonNull MessageCodec<Object> getCodec() {
return new StandardMessageCodec();
Expand Down Expand Up @@ -1634,6 +1663,29 @@ static void setup(
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.DeviceOrientationManagerHostApi.getUiOrientation",
getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
try {
String output = api.getUiOrientation();
wrapped.add(0, output);
} catch (Throwable exception) {
ArrayList<Object> wrappedError = wrapError(exception);
wrapped = wrappedError;
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
}
}
/** Generated class from Pigeon that represents Flutter messages that can be called from Java. */
Expand Down Expand Up @@ -4389,6 +4441,9 @@ public interface Camera2CameraInfoHostApi {
@NonNull
String getCameraId(@NonNull Long identifier);

@NonNull
Long getSensorOrientation(@NonNull Long identifier);

/** The codec used by Camera2CameraInfoHostApi. */
static @NonNull MessageCodec<Object> getCodec() {
return new StandardMessageCodec();
Expand Down Expand Up @@ -4481,6 +4536,33 @@ static void setup(
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.Camera2CameraInfoHostApi.getSensorOrientation",
getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
Number identifierArg = (Number) args.get(0);
try {
Long output =
api.getSensorOrientation(
(identifierArg == null) ? null : identifierArg.longValue());
wrapped.add(0, output);
} catch (Throwable exception) {
ArrayList<Object> wrappedError = wrapError(exception);
wrapped = wrappedError;
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
}
}
/** Generated class from Pigeon that represents Flutter messages that can be called from Java. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import android.app.Activity;
import android.content.Context;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
Expand Down Expand Up @@ -103,4 +104,18 @@ public String getTempFilePath(@NonNull String prefix, @NonNull String suffix) {
null);
}
}

/**
* Returns whether or not a {@code SurfaceTexture} backs the {@code Surface} provided to CameraX
* to build the camera preview. If it is backed by a {@code Surface}, then the transformation
* needed to correctly rotate the preview has already been applied.
*
* <p>This is determined by the engine, who uses {@code SurfaceTexture}s on Android SDKs 29 and
* below.
*/
@Override
@NonNull
public Boolean isPreviewPreTransformed() {
return Build.VERSION.SDK_INT <= 29;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,16 @@ public void getDefaultDisplayRotation_returnsExpectedRotation() {

assertEquals(hostApi.getDefaultDisplayRotation(), Long.valueOf(defaultRotation));
}

@Test
public void getUiOrientation_returnsExpectedOrientation() {
final DeviceOrientationManagerHostApiImpl hostApi =
new DeviceOrientationManagerHostApiImpl(mockBinaryMessenger, mockInstanceManager);
final DeviceOrientation uiOrientation = DeviceOrientation.LANDSCAPE_LEFT;

hostApi.deviceOrientationManager = mockDeviceOrientationManager;
when(mockDeviceOrientationManager.getUIOrientation()).thenReturn(uiOrientation);

assertEquals(hostApi.getUiOrientation(), uiOrientation.toString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
package io.flutter.plugins.camerax;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
Expand All @@ -24,12 +26,16 @@
import java.io.IOException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

@RunWith(RobolectricTestRunner.class)
public class SystemServicesTest {
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();

Expand Down Expand Up @@ -130,4 +136,28 @@ public void getTempFilePath_throwsRuntimeExceptionOnIOException() {

mockedStaticFile.close();
}

@Test
@Config(sdk = 28)
public void isPreviewPreTransformed_returnsTrueWhenRunningBelowSdk29() {
final SystemServicesHostApiImpl systemServicesHostApi =
new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext);
assertTrue(systemServicesHostApi.isPreviewPreTransformed());
}

@Test
@Config(sdk = 29)
public void isPreviewPreTransformed_returnsTrueWhenRunningSdk29() {
final SystemServicesHostApiImpl systemServicesHostApi =
new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext);
assertTrue(systemServicesHostApi.isPreviewPreTransformed());
}

@Test
@Config(sdk = 30)
public void isPreviewPreTransformed_returnsFalseWhenRunningAboveSdk29() {
final SystemServicesHostApiImpl systemServicesHostApi =
new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext);
assertFalse(systemServicesHostApi.isPreviewPreTransformed());
}
}
4 changes: 2 additions & 2 deletions packages/camera/camera_android_camerax/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ description: Demonstrates how to use the camera_android_camerax plugin.
publish_to: 'none'

environment:
sdk: ^3.2.0
flutter: ">=3.16.0"
sdk: ^3.4.0
flutter: ">=3.22.0"

dependencies:
camera_android_camerax:
Expand Down
Loading

1 comment on commit 97bad7e

@ruicraveiro
Copy link

Choose a reason for hiding this comment

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

Hi, just a heads up. This actually breaks rotation when not running with Impeller. I don't know exactly what it is doing, just that my prototypes started displaying the preview upside down, and through git bisect I arrived at this commit. To be sure, I made a local git revert of this commit on top of main and everything went back to normal. I tested with several Samsung devices, an A23, an S23 and a tablet S6 Lite.

Please sign in to comment.