From b047cdd4db0ae06613ecb169d4c60d69291070e0 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Thu, 11 Jul 2019 23:08:40 -0300 Subject: [PATCH] Add cameraUseDeviceOrientation flag --- .../otaliastudios/cameraview/CameraView.java | 35 ++++++++++++++++- .../cameraview/engine/Camera1Engine.java | 6 +-- .../cameraview/engine/Camera2Engine.java | 39 +++++++------------ .../cameraview/engine/CameraEngine.java | 38 ++++++------------ .../{CameraEngineStep.java => Step.java} | 6 +-- .../picture/SnapshotGlPictureRecorder.java | 2 +- .../cameraview/video/Full2VideoRecorder.java | 31 +++------------ cameraview/src/main/res/values/attrs.xml | 2 + demo/src/main/res/layout/activity_camera.xml | 1 + 9 files changed, 72 insertions(+), 88 deletions(-) rename cameraview/src/main/java/com/otaliastudios/cameraview/engine/{CameraEngineStep.java => Step.java} (97%) diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index 284a1ecc..a344f03b 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -101,6 +101,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { // Self managed parameters private boolean mPlaySounds; + private boolean mUseDeviceOrientation; private HashMap mGestureMap = new HashMap<>(4); private Preview mPreview; private Engine mEngine; @@ -152,6 +153,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { // Self managed boolean playSounds = a.getBoolean(R.styleable.CameraView_cameraPlaySounds, DEFAULT_PLAY_SOUNDS); + boolean useDeviceOrientation = a.getBoolean(R.styleable.CameraView_cameraUseDeviceOrientation, true); mExperimental = a.getBoolean(R.styleable.CameraView_cameraExperimental, false); mPreview = controls.getPreview(); mEngine = controls.getEngine(); @@ -192,6 +194,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { // Apply self managed setPlaySounds(playSounds); + setUseDeviceOrientation(useDeviceOrientation); setGrid(controls.getGrid()); setGridColor(gridColor); @@ -640,7 +643,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { if (checkPermissions(getAudio())) { // Update display orientation for current CameraEngine mOrientationHelper.enable(getContext()); - mCameraEngine.setDisplayOffset(mOrientationHelper.getDisplayOffset()); + mCameraEngine.getAngles().setDisplayOffset(mOrientationHelper.getDisplayOffset()); mCameraEngine.start(); } } @@ -1648,6 +1651,27 @@ public class CameraView extends FrameLayout implements LifecycleObserver { return mPlaySounds; } + /** + * Controls whether picture and video output should consider the current device orientation. + * For example, when true, if the user rotates the device before taking a picture, the picture + * will be rotated as well. + * + * @param useDeviceOrientation true to consider device orientation for outputs + */ + public void setUseDeviceOrientation(boolean useDeviceOrientation) { + mUseDeviceOrientation = useDeviceOrientation; + } + + /** + * Gets the current behavior for considering the device orientation when returning picture + * or video outputs. + * + * @see #setUseDeviceOrientation(boolean) + * @return whether we are using the device orientation for outputs + */ + public boolean getUseDeviceOrientation() { + return mUseDeviceOrientation; + } /** * Sets the encoder for video recordings. @@ -1890,8 +1914,15 @@ public class CameraView extends FrameLayout implements LifecycleObserver { @Override public void onDeviceOrientationChanged(int deviceOrientation) { mLogger.i("onDeviceOrientationChanged", deviceOrientation); - mCameraEngine.setDeviceOrientation(deviceOrientation); int displayOffset = mOrientationHelper.getDisplayOffset(); + if (!mUseDeviceOrientation) { + // To fool the engine to return outputs in the VIEW reference system, + // The device orientation should be set to -displayOffset. + int fakeDeviceOrientation = (360 - displayOffset) % 360; + mCameraEngine.getAngles().setDeviceOrientation(fakeDeviceOrientation); + } else { + mCameraEngine.getAngles().setDeviceOrientation(deviceOrientation); + } final int value = (deviceOrientation + displayOffset) % 360; mUiHandler.post(new Runnable() { @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java index 983acc82..0efad6b2 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java @@ -123,7 +123,7 @@ public class Camera1Engine extends CameraEngine implements for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) { Camera.getCameraInfo(i, cameraInfo); if (cameraInfo.facing == internalFacing) { - setSensorOffset(facing, cameraInfo.orientation); + getAngles().setSensorOffset(facing, cameraInfo.orientation); mCameraId = i; return true; } @@ -351,13 +351,11 @@ public class Camera1Engine extends CameraEngine implements throw new IllegalStateException("Video snapshots are only supported starting from API 18."); } GlCameraPreview glPreview = (GlCameraPreview) mPreview; - - // Output size is easy: Size outputSize = getUncroppedSnapshotSize(Reference.OUTPUT); if (outputSize == null) { throw new IllegalStateException("outputSize should not be null."); } - AspectRatio outputRatio = getAngles().flip(Reference.OUTPUT, Reference.VIEW) ? viewAspectRatio.flip() : viewAspectRatio; + AspectRatio outputRatio = getAngles().flip(Reference.VIEW, Reference.OUTPUT) ? viewAspectRatio.flip() : viewAspectRatio; Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio); outputSize = new Size(outputCrop.width(), outputCrop.height()); stub.size = outputSize; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java index 7c700d68..354fae7e 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java @@ -19,7 +19,6 @@ import android.hardware.camera2.params.StreamConfigurationMap; import android.location.Location; import android.media.Image; import android.media.ImageReader; -import android.media.MediaCodec; import android.os.Build; import android.util.Rational; import android.view.Surface; @@ -96,8 +95,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv private Surface mPreviewStreamSurface; // Video recording - private Surface mFullVideoPersistentSurface; // API 23+. The surface is created before. - private VideoResult.Stub mFullVideoPendingStub; // API 21-22. When takeVideo is called, we have to reset the session. + private VideoResult.Stub mFullVideoPendingStub; // When takeVideo is called, we have to reset the session. // Picture capturing private ImageReader mPictureReader; @@ -302,7 +300,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv if (internalFacing == readCharacteristic(characteristics, CameraCharacteristics.LENS_FACING, -99)) { mCameraId = cameraId; int sensorOffset = readCharacteristic(characteristics, CameraCharacteristics.SENSOR_ORIENTATION, 0); - setSensorOffset(facing, sensorOffset); + getAngles().setSensorOffset(facing, sensorOffset); return true; } } catch (CameraAccessException ignore) { @@ -411,11 +409,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv // 2. VIDEO RECORDING if (getMode() == Mode.VIDEO) { - if (Full2VideoRecorder.SUPPORTS_PERSISTENT_SURFACE) { - mFullVideoPersistentSurface = MediaCodec.createPersistentInputSurface(); - outputSurfaces.add(mFullVideoPersistentSurface); - } else if (mFullVideoPendingStub != null) { - Full2VideoRecorder recorder = new Full2VideoRecorder(this, mCameraId, null); + if (mFullVideoPendingStub != null) { + Full2VideoRecorder recorder = new Full2VideoRecorder(this, mCameraId); try { outputSurfaces.add(recorder.createInputSurface(mFullVideoPendingStub)); } catch (Full2VideoRecorder.PrepareException e) { @@ -567,10 +562,6 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv @Override protected Task onStopBind() { LOG.i("onStopBind:", "About to clean up."); - if (mFullVideoPersistentSurface != null) { - mFullVideoPersistentSurface.release(); - mFullVideoPersistentSurface = null; - } mFrameProcessingSurface = null; mPreviewStreamSurface = null; mPreviewStreamSize = null; @@ -667,19 +658,17 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv LOG.i("onTakeVideo", "called."); stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); stub.size = getAngles().flip(Reference.SENSOR, Reference.OUTPUT) ? mCaptureSize.flip() : mCaptureSize; - if (!Full2VideoRecorder.SUPPORTS_PERSISTENT_SURFACE) { - // On API 21 and 22, we must restart the session at each time. - // Save the pending data and restart the session. - LOG.w("onTakeVideo", "calling restartBind."); - mFullVideoPendingStub = stub; - restartBind(); - } else { - doTakeVideo(stub); - } + // We must restart the session at each time. + // Save the pending data and restart the session. + LOG.w("onTakeVideo", "calling restartBind."); + mFullVideoPendingStub = stub; + restartBind(); } private void doTakeVideo(@NonNull final VideoResult.Stub stub) { - mVideoRecorder = new Full2VideoRecorder(this, mCameraId, mFullVideoPersistentSurface); + if (!(mVideoRecorder instanceof Full2VideoRecorder)) { + mVideoRecorder = new Full2VideoRecorder(this, mCameraId); + } Full2VideoRecorder recorder = (Full2VideoRecorder) mVideoRecorder; try { createRepeatingRequestBuilder(CameraDevice.TEMPLATE_RECORD); @@ -706,13 +695,11 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv throw new IllegalStateException("Video snapshots are only supported with GlCameraPreview."); } GlCameraPreview glPreview = (GlCameraPreview) mPreview; - - // Output size is easy: Size outputSize = getUncroppedSnapshotSize(Reference.OUTPUT); if (outputSize == null) { throw new IllegalStateException("outputSize should not be null."); } - AspectRatio outputRatio = getAngles().flip(Reference.OUTPUT, Reference.VIEW) ? viewAspectRatio.flip() : viewAspectRatio; + AspectRatio outputRatio = getAngles().flip(Reference.VIEW, Reference.OUTPUT) ? viewAspectRatio.flip() : viewAspectRatio; Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio); outputSize = new Size(outputCrop.width(), outputCrop.height()); stub.size = outputSize; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java index f07e1048..4eea126a 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java @@ -70,7 +70,7 @@ import java.util.concurrent.TimeUnit; * So at the end of both step 1 and 2, the engine should check if both have * been performed and trigger the steps 3 and 4. * - * We use an abstraction for each step called {@link CameraEngineStep} that manages the state of + * We use an abstraction for each step called {@link Step} that manages the state of * each step and ensures that start and stop operations, for each step, are never called if the * previous one has not ended. * @@ -140,11 +140,11 @@ public abstract class CameraEngine implements private static final CameraLogger LOG = CameraLogger.create(TAG); @SuppressWarnings({"WeakerAccess", "unused"}) - public static final int STATE_STOPPING = CameraEngineStep.STATE_STOPPING; - public static final int STATE_STOPPED = CameraEngineStep.STATE_STOPPED; + public static final int STATE_STOPPING = Step.STATE_STOPPING; + public static final int STATE_STOPPED = Step.STATE_STOPPED; @SuppressWarnings({"WeakerAccess", "unused"}) - public static final int STATE_STARTING = CameraEngineStep.STATE_STARTING; - public static final int STATE_STARTED = CameraEngineStep.STATE_STARTED; + public static final int STATE_STARTING = Step.STATE_STARTING; + public static final int STATE_STARTED = Step.STATE_STARTED; // Need to be protected @SuppressWarnings("WeakerAccess") protected WorkerHandler mHandler; @@ -185,16 +185,17 @@ public abstract class CameraEngine implements private int mSnapshotMaxHeight = Integer.MAX_VALUE; // in REF_VIEW for consistency with SizeSelectors // Steps - private final CameraEngineStep.Callback mStepCallback = new CameraEngineStep.Callback() { + private final Step.Callback mStepCallback = new Step.Callback() { @Override @NonNull public Executor getExecutor() { return mHandler.getExecutor(); } @Override public void handleException(@NonNull Exception exception) { CameraEngine.this.handleException(Thread.currentThread(), exception, false); } }; - @VisibleForTesting CameraEngineStep mEngineStep = new CameraEngineStep("engine", mStepCallback); - private CameraEngineStep mBindStep = new CameraEngineStep("bind", mStepCallback); - private CameraEngineStep mPreviewStep = new CameraEngineStep("preview", mStepCallback); - private CameraEngineStep mAllStep = new CameraEngineStep("all", mStepCallback); + @VisibleForTesting + Step mEngineStep = new Step("engine", mStepCallback); + private Step mBindStep = new Step("bind", mStepCallback); + private Step mPreviewStep = new Step("preview", mStepCallback); + private Step mAllStep = new Step("all", mStepCallback); // Ops used for testing. @VisibleForTesting Op mStartVideoOp = new Op<>(); @@ -785,21 +786,6 @@ public abstract class CameraEngine implements return mAngles; } - @SuppressWarnings("WeakerAccess") - protected final void setSensorOffset(@NonNull Facing facing, int sensorOffset) { - mAngles.setSensorOffset(facing, sensorOffset); - } - - // This is called before start() and never again. - public final void setDisplayOffset(int displayOffset) { - mAngles.setDisplayOffset(displayOffset); - } - - // This can be called multiple times. - public final void setDeviceOrientation(int deviceOrientation) { - mAngles.setDeviceOrientation(deviceOrientation); - } - public final void setPreviewStreamSizeSelector(@Nullable SizeSelector selector) { mPreviewStreamSizeSelector = selector; } @@ -1018,7 +1004,7 @@ public abstract class CameraEngine implements * Camera is about to be opened. Implementors should look into available cameras * and see if anyone matches the given {@link Facing value}. * - * If so, implementors should set {@link #setSensorOffset(Facing, int)} and any other information + * If so, implementors should set {@link Angles#setSensorOffset(Facing, int)} and any other information * (like camera ID) needed to start the engine. * * @param facing the facing value diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngineStep.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Step.java similarity index 97% rename from cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngineStep.java rename to cameraview/src/main/java/com/otaliastudios/cameraview/engine/Step.java index 0744aef6..349c17f5 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngineStep.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Step.java @@ -34,9 +34,9 @@ import androidx.annotation.VisibleForTesting; * * This class is NOT thread safe! */ -class CameraEngineStep { +class Step { - private static final String TAG = CameraEngineStep.class.getSimpleName(); + private static final String TAG = Step.class.getSimpleName(); private static final CameraLogger LOG = CameraLogger.create(TAG); interface Callback { @@ -59,7 +59,7 @@ class CameraEngineStep { private final String name; private final Callback callback; - CameraEngineStep(@NonNull String name, @NonNull Callback callback) { + Step(@NonNull String name, @NonNull Callback callback) { this.name = name.toUpperCase(); this.callback = callback; } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java index 88ea8a65..e969a67a 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java @@ -114,7 +114,7 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { Matrix.scaleM(mTransform, 0, realScaleX, realScaleY, 1); // Fix rotation: - // TODO Not sure why we need the minus here... It makes no sense to me. + // Not sure why we need the minus here... It makes no sense to me. LOG.w("Recording frame. Rotation:", mResult.rotation, "Actual:", -mResult.rotation); int rotation = -mResult.rotation; mResult.rotation = 0; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full2VideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full2VideoRecorder.java index 060c56b0..48715688 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full2VideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/Full2VideoRecorder.java @@ -1,14 +1,6 @@ package com.otaliastudios.cameraview.video; import android.annotation.SuppressLint; -import android.graphics.SurfaceTexture; -import android.hardware.camera2.CameraAccessException; -import android.hardware.camera2.CameraCaptureSession; -import android.hardware.camera2.CameraDevice; -import android.hardware.camera2.CaptureFailure; -import android.hardware.camera2.CaptureRequest; -import android.media.Image; -import android.media.MediaActionSound; import android.media.MediaRecorder; import android.os.Build; import android.view.Surface; @@ -33,21 +25,13 @@ public class Full2VideoRecorder extends FullVideoRecorder { private static final String TAG = Full2VideoRecorder.class.getSimpleName(); private static final CameraLogger LOG = CameraLogger.create(TAG); - // This actually didn't work as I expected, we're never using this. Should remove. - @SuppressWarnings("PointlessBooleanExpression") // TODO - public static final boolean SUPPORTS_PERSISTENT_SURFACE = false && Build.VERSION.SDK_INT >= 23; - private final String mCameraId; private Surface mInputSurface; - private final boolean mUseInputSurface; public Full2VideoRecorder(@NonNull Camera2Engine engine, - @NonNull String cameraId, - @Nullable Surface surface) { + @NonNull String cameraId) { super(engine); mCameraId = cameraId; - mUseInputSurface = surface != null && SUPPORTS_PERSISTENT_SURFACE; - mInputSurface = mUseInputSurface ? surface : null; } @SuppressLint("NewApi") @@ -56,10 +40,10 @@ public class Full2VideoRecorder extends FullVideoRecorder { mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); Size size = stub.rotation % 180 != 0 ? stub.size.flip() : stub.size; mProfile = CamcorderProfiles.get(mCameraId, size); - if (mUseInputSurface) { - //noinspection ConstantConditions - mediaRecorder.setInputSurface(mInputSurface); - } + // This was an option: get the surface from outside this class, using MediaCodec.createPersistentInputSurface() + // But it doesn't really help since the Camera2 engine refuses a surface that has not been configured, + // so even with that trick we would have to attach the surface to this recorder before creating the CameraSession. + // mediaRecorder.setInputSurface(mInputSurface); return super.onPrepareMediaRecorder(stub, mediaRecorder); } @@ -72,9 +56,6 @@ public class Full2VideoRecorder extends FullVideoRecorder { */ @NonNull public Surface createInputSurface(@NonNull VideoResult.Stub stub) throws PrepareException { - if (mUseInputSurface) { - throw new IllegalStateException("We are using the input surface (API23+), can't createInputSurface here."); - } if (!prepareMediaRecorder(stub)) { throw new PrepareException(mError); } @@ -87,8 +68,6 @@ public class Full2VideoRecorder extends FullVideoRecorder { return mInputSurface; } - - @SuppressWarnings("WeakerAccess") public class PrepareException extends Exception { private PrepareException(Throwable cause) { super(cause); diff --git a/cameraview/src/main/res/values/attrs.xml b/cameraview/src/main/res/values/attrs.xml index 0fe089c4..e680f319 100644 --- a/cameraview/src/main/res/values/attrs.xml +++ b/cameraview/src/main/res/values/attrs.xml @@ -131,5 +131,7 @@ + + \ No newline at end of file diff --git a/demo/src/main/res/layout/activity_camera.xml b/demo/src/main/res/layout/activity_camera.xml index 03b195b7..164432c0 100644 --- a/demo/src/main/res/layout/activity_camera.xml +++ b/demo/src/main/res/layout/activity_camera.xml @@ -17,6 +17,7 @@ android:layout_marginBottom="88dp" android:keepScreenOn="true" app:cameraExperimental="true" + app:cameraUseDeviceOrientation="false" app:cameraEngine="camera2" app:cameraPreview="glSurface" app:cameraPlaySounds="true"