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

Commit

Permalink
[camera] Implemented capture orientation locking. Fixed preview rotat…
Browse files Browse the repository at this point in the history
…ion issues. Fixed video and photo orientation upon save. (#3390)
  • Loading branch information
BeMacized committed Jan 13, 2021
1 parent 1eabad7 commit 100c747
Show file tree
Hide file tree
Showing 18 changed files with 930 additions and 121 deletions.
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

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

0 comments on commit 100c747

Please sign in to comment.