Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

[camera] Implemented capture orientation locking. Fixed preview rotation issues. Fixed video and photo orientation upon save. #3390

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
2197235
Fixed video orientation on iOS
BeMacized Dec 30, 2020
5988830
Remove unnecessary check
BeMacized Jan 4, 2021
6bcf07d
Merge branch 'master' into fix/video-photo-preview-rotation
BeMacized Jan 4, 2021
9c4d4ca
Expand platform interface to support reporting device orientation
BeMacized Jan 4, 2021
484e02f
Merge branch 'fix/video-photo-preview-rotation-platform-interface' in…
BeMacized Jan 4, 2021
ed56fac
Switch to flutter DeviceOrientation enum
BeMacized Jan 4, 2021
1092177
Merge branch 'fix/video-photo-preview-rotation-platform-interface' in…
BeMacized Jan 4, 2021
07931a7
Fix preview rotation on iOS
BeMacized Jan 4, 2021
abe4360
Fix preview rotation for android
BeMacized Jan 4, 2021
4dcbbf7
Update unit tests
BeMacized Jan 4, 2021
69f752d
Fix rotation on initialise.
BeMacized Jan 4, 2021
fb42049
Keep EXIF data and picture orientation
mvanbeusekom Jan 5, 2021
13b9305
Fix photo capture orientation
mvanbeusekom Jan 5, 2021
eab58fe
Add interface methods for (un)locking the capture orientation.
BeMacized Jan 5, 2021
273d6e0
Merge branch 'fix/video-photo-preview-rotation-platform-interface' in…
BeMacized Jan 5, 2021
27bba29
Update capture orientation interfaces and add unit tests.
BeMacized Jan 5, 2021
751921a
Merge branch 'fix/video-photo-preview-rotation-platform-interface' in…
BeMacized Jan 5, 2021
859076f
Made device orientation mandatory for locking capture orientation in …
BeMacized Jan 5, 2021
d13b8ae
Merge branch 'fix/video-photo-preview-rotation-platform-interface' in…
BeMacized Jan 5, 2021
a8a9d43
Add capture orientation locking (iOS done, Android WIP)
BeMacized Jan 5, 2021
165c1ed
Code format
BeMacized Jan 5, 2021
3e9aad7
Add orientation lock to android implementation
BeMacized Jan 5, 2021
118bd00
Update comment
BeMacized Jan 5, 2021
ef56d9d
Merge branch 'fix/video-photo-preview-rotation-platform-interface' in…
BeMacized Jan 5, 2021
aa2c392
Maintain preview rotation while recording
BeMacized Jan 5, 2021
0aba73a
Merge branch 'master' into fix/video-photo-preview-rotation-platform-…
BeMacized Jan 5, 2021
0c74a4b
Merge branch 'fix/video-photo-preview-rotation-platform-interface' in…
BeMacized Jan 5, 2021
0bb07be
Update comment.
BeMacized Jan 5, 2021
5f845fe
Merge branch 'fix/video-photo-preview-rotation-platform-interface' in…
BeMacized Jan 5, 2021
a2c3fbf
Update changelog and pubspec version
BeMacized Jan 5, 2021
99ddc49
Merge branch 'fix/video-photo-preview-rotation-platform-interface' in…
BeMacized Jan 5, 2021
9242af0
Updated changelog and pubspec version
BeMacized Jan 5, 2021
1d409b9
Update packages/camera/camera_platform_interface/lib/src/events/devic…
BeMacized Jan 5, 2021
96a739a
Merge branch 'master' into fix/video-photo-preview-rotation-platform-…
mvanbeusekom Jan 6, 2021
3df1a7b
Merge branch 'master' into fix/video-photo-preview-rotation
mvanbeusekom Jan 6, 2021
3a69c94
Merge branch 'fix/video-photo-preview-rotation-platform-interface' in…
mvanbeusekom Jan 6, 2021
5ece84b
Merge with master
mvanbeusekom Jan 6, 2021
e69f746
Fix formatting
mvanbeusekom Jan 6, 2021
1d84bae
Fix deprecation warning
mvanbeusekom Jan 6, 2021
f779fdc
Merge branch 'master' into fix/video-photo-preview-rotation
BeMacized Jan 11, 2021
3ca8eaf
Update platform interface dependency
BeMacized Jan 11, 2021
2aa1b53
Merge branch 'master' into fix/video-photo-preview-rotation
BeMacized Jan 11, 2021
3135953
Rollback update to Android compileSdkVersion 30
mvanbeusekom Jan 13, 2021
fd412a3
Revert "Rollback update to Android compileSdkVersion 30"
mvanbeusekom Jan 13, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## 0.7.0

* Added support for capture orientation locking on Android and iOS.
* Fixed camera preview not rotating correctly on Android and iOS.
* Fixed camera preview sometimes appearing stretched on Android and iOS.
* Fixed videos & photos saving with the incorrect rotation on iOS.
* BREAKING CHANGE: `CameraValue.aspectRatio` now returns `width / height` rather than `height / width`.

## 0.6.6

* Adds auto focus support for Android and iOS implementations.
Expand All @@ -20,7 +28,7 @@

## 0.6.4+2

* Set ImageStreamReader listener to null to prevent stale images when streaming images.
* Set ImageStreamReader listener to null to prevent stale images when streaming images.

## 0.6.4+1

Expand Down
2 changes: 1 addition & 1 deletion packages/camera/camera/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ project.getTasks().withType(JavaCompile){
apply plugin: 'com.android.library'

android {
compileSdkVersion 29
compileSdkVersion 30
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: We recently updated compileSdkVersions to 29 for all the plugins in our repo: #3042

Do we need to update this to 30 for this plugin?

cc @cyanglaz

Copy link
Contributor

Choose a reason for hiding this comment

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

@BeMacized What's the reason for updating it to 30? Is it a requirement for a particular API used in this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@BeMacized What's the reason for updating it to 30? Is it a requirement for a particular API used in this PR?

I'm honestly not quite sure. I think it came from this commit.

cc @mvanbeusekom.

Copy link
Contributor

Choose a reason for hiding this comment

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

@cyanglaz, @bparrishMines, I indeed added this change because the "build_all_plugins_apk" build step returned the following deprecation error (see here for details):

/tmp/cirrus-ci-build/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java:177: warning: [deprecation] getDefaultDisplay() in WindowManager has been deprecated
    int rotation = windowManager.getDefaultDisplay().getRotation();

According to the documentation on Android API 30 we should use the activity.getDisplay() method instead, which doesn't exists on lower API levels. This is why I changed the compileSdkVersion to 30.

Copy link
Contributor

Choose a reason for hiding this comment

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

offline discussion: this is good! LGTM


defaultConfig {
minSdkVersion 21
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

package io.flutter.plugins.camera;

import static android.view.OrientationEventListener.ORIENTATION_UNKNOWN;
import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize;

import android.annotation.SuppressLint;
Expand Down Expand Up @@ -41,10 +40,10 @@
import android.util.Range;
import android.util.Rational;
import android.util.Size;
import android.view.OrientationEventListener;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugins.camera.PictureCaptureRequest.State;
Expand Down Expand Up @@ -76,7 +75,7 @@ public class Camera {

private final SurfaceTextureEntry flutterTexture;
private final CameraManager cameraManager;
private final OrientationEventListener orientationEventListener;
private final DeviceOrientationManager deviceOrientationListener;
private final boolean isFrontFacing;
private final int sensorOrientation;
private final String cameraName;
Expand All @@ -97,7 +96,6 @@ public class Camera {
private MediaRecorder mediaRecorder;
private boolean recordingVideo;
private File videoRecordingFile;
private int currentOrientation = ORIENTATION_UNKNOWN;
private FlashMode flashMode;
private ExposureMode exposureMode;
private FocusMode focusMode;
Expand All @@ -106,6 +104,7 @@ public class Camera {
private int exposureOffset;
private boolean useAutoFocus = true;
private Range<Integer> fpsRange;
private PlatformChannel.DeviceOrientation lockedCaptureOrientation;

private static final HashMap<String, Integer> supportedImageFormats;
// Current supported outputs
Expand Down Expand Up @@ -136,18 +135,6 @@ public Camera(
this.exposureMode = ExposureMode.auto;
this.focusMode = FocusMode.auto;
this.exposureOffset = 0;
orientationEventListener =
new OrientationEventListener(activity.getApplicationContext()) {
@Override
public void onOrientationChanged(int i) {
if (i == ORIENTATION_UNKNOWN) {
return;
}
// Convert the raw deg angle to the nearest multiple of 90.
currentOrientation = (int) Math.round(i / 90.0) * 90;
}
};
orientationEventListener.enable();

cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName);
initFps(cameraCharacteristics);
Expand All @@ -164,6 +151,10 @@ public void onOrientationChanged(int i) {
new CameraZoom(
cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE),
cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM));

deviceOrientationListener =
new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation);
deviceOrientationListener.start();
}

private void initFps(CameraCharacteristics cameraCharacteristics) {
Expand Down Expand Up @@ -195,7 +186,10 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException {
mediaRecorder =
new MediaRecorderBuilder(recordingProfile, outputFilePath)
.setEnableAudio(enableAudio)
.setMediaOrientation(getMediaOrientation())
.setMediaOrientation(
lockedCaptureOrientation == null
? deviceOrientationListener.getMediaOrientation()
: deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation))
.build();
}

Expand Down Expand Up @@ -545,7 +539,11 @@ private void runPictureCapture() {
final CaptureRequest.Builder captureBuilder =
cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
captureBuilder.addTarget(pictureImageReader.getSurface());
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getMediaOrientation());
captureBuilder.set(
CaptureRequest.JPEG_ORIENTATION,
lockedCaptureOrientation == null
? deviceOrientationListener.getMediaOrientation()
: deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation));
switch (flashMode) {
case off:
captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
Expand Down Expand Up @@ -968,6 +966,14 @@ public void setZoomLevel(@NonNull final Result result, float zoom) throws Camera
result.success(null);
}

public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) {
this.lockedCaptureOrientation = orientation;
}

public void unlockCaptureOrientation() {
this.lockedCaptureOrientation = null;
}

private void updateFpsRange() {
if (fpsRange == null) {
return;
Expand Down Expand Up @@ -1160,14 +1166,6 @@ public void close() {
public void dispose() {
close();
flutterTexture.release();
orientationEventListener.disable();
}

private int getMediaOrientation() {
final int sensorOrientationOffset =
(currentOrientation == ORIENTATION_UNKNOWN)
? 0
: (isFrontFacing) ? -currentOrientation : currentOrientation;
return (sensorOrientationOffset + sensorOrientation + 360) % 360;
deviceOrientationListener.stop();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.CamcorderProfile;
import android.util.Size;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.plugins.camera.types.ResolutionPreset;
import java.util.ArrayList;
import java.util.Arrays;
Expand All @@ -28,6 +29,59 @@ public final class CameraUtils {

private CameraUtils() {}

static PlatformChannel.DeviceOrientation getDeviceOrientationFromDegrees(int degrees) {
// Round to the nearest 90 degrees.
degrees = (int) (Math.round(degrees / 90.0) * 90) % 360;
// Determine the corresponding device orientation.
switch (degrees) {
case 90:
return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT;
case 180:
return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN;
case 270:
return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT;
case 0:
default:
return PlatformChannel.DeviceOrientation.PORTRAIT_UP;
}
}

static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orientation) {
if (orientation == null)
throw new UnsupportedOperationException("Could not serialize null device orientation.");
switch (orientation) {
case PORTRAIT_UP:
return "portraitUp";
case PORTRAIT_DOWN:
return "portraitDown";
case LANDSCAPE_LEFT:
return "landscapeLeft";
case LANDSCAPE_RIGHT:
return "landscapeRight";
default:
throw new UnsupportedOperationException(
"Could not serialize device orientation: " + orientation.toString());
}
}

static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String orientation) {
if (orientation == null)
throw new UnsupportedOperationException("Could not deserialize null device orientation.");
switch (orientation) {
case "portraitUp":
return PlatformChannel.DeviceOrientation.PORTRAIT_UP;
case "portraitDown":
return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN;
case "landscapeLeft":
return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT;
case "landscapeRight":
return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT;
default:
throw new UnsupportedOperationException(
"Could not deserialize device orientation: " + orientation);
}
}

static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) {
if (preset.ordinal() > ResolutionPreset.high.ordinal()) {
preset = ResolutionPreset.high;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import android.text.TextUtils;
import androidx.annotation.Nullable;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugins.camera.types.ExposureMode;
Expand All @@ -14,16 +15,44 @@
import java.util.Map;

class DartMessenger {
@Nullable private MethodChannel channel;
@Nullable private MethodChannel cameraChannel;
@Nullable private MethodChannel deviceChannel;

enum EventType {
ERROR,
CAMERA_CLOSING,
INITIALIZED,
enum DeviceEventType {
ORIENTATION_CHANGED("orientation_changed");
private final String method;

DeviceEventType(String method) {
this.method = method;
}
}

enum CameraEventType {
ERROR("error"),
CLOSING("camera_closing"),
INITIALIZED("initialized");

private final String method;

CameraEventType(String method) {
this.method = method;
}
}

DartMessenger(BinaryMessenger messenger, long cameraId) {
channel = new MethodChannel(messenger, "flutter.io/cameraPlugin/camera" + cameraId);
cameraChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/camera" + cameraId);
deviceChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/device");
}

void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) {
assert (orientation != null);
this.send(
DeviceEventType.ORIENTATION_CHANGED,
new HashMap<String, Object>() {
{
put("orientation", CameraUtils.serializeDeviceOrientation(orientation));
}
});
}

void sendCameraInitializedEvent(
Expand All @@ -40,7 +69,7 @@ void sendCameraInitializedEvent(
assert (exposurePointSupported != null);
assert (focusPointSupported != null);
this.send(
EventType.INITIALIZED,
CameraEventType.INITIALIZED,
new HashMap<String, Object>() {
{
put("previewWidth", previewWidth.doubleValue());
Expand All @@ -54,27 +83,38 @@ void sendCameraInitializedEvent(
}

void sendCameraClosingEvent() {
send(EventType.CAMERA_CLOSING);
send(CameraEventType.CLOSING);
}

void sendCameraErrorEvent(@Nullable String description) {
this.send(
EventType.ERROR,
CameraEventType.ERROR,
new HashMap<String, Object>() {
{
if (!TextUtils.isEmpty(description)) put("description", description);
}
});
}

void send(EventType eventType) {
void send(CameraEventType eventType) {
send(eventType, new HashMap<>());
}

void send(CameraEventType eventType, Map<String, Object> args) {
if (cameraChannel == null) {
return;
}
cameraChannel.invokeMethod(eventType.method, args);
}

void send(DeviceEventType eventType) {
send(eventType, new HashMap<>());
}

void send(EventType eventType, Map<String, Object> args) {
if (channel == null) {
void send(DeviceEventType eventType, Map<String, Object> args) {
if (deviceChannel == null) {
return;
}
channel.invokeMethod(eventType.toString().toLowerCase(), args);
deviceChannel.invokeMethod(eventType.method, args);
}
}
Loading