diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraUtils.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraUtils.java index ba29fc13..724cdb80 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraUtils.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraUtils.java @@ -11,6 +11,7 @@ import android.os.Handler; import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.engine.Mapper; +import com.otaliastudios.cameraview.internal.utils.ExifHelper; import com.otaliastudios.cameraview.internal.utils.WorkerHandler; import androidx.annotation.NonNull; @@ -258,7 +259,7 @@ public class CameraUtils { stream = new ByteArrayInputStream(source); ExifInterface exif = new ExifInterface(stream); int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); - orientation = readExifOrientation(exifOrientation); + orientation = ExifHelper.readExifOrientation(exifOrientation); flip = exifOrientation == ExifInterface.ORIENTATION_FLIP_HORIZONTAL || exifOrientation == ExifInterface.ORIENTATION_FLIP_VERTICAL || exifOrientation == ExifInterface.ORIENTATION_TRANSPOSE || @@ -315,31 +316,6 @@ public class CameraUtils { return bitmap; } - private static int readExifOrientation(int exifOrientation) { - int orientation; - switch (exifOrientation) { - case ExifInterface.ORIENTATION_NORMAL: - case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: - orientation = 0; break; - - case ExifInterface.ORIENTATION_ROTATE_180: - case ExifInterface.ORIENTATION_FLIP_VERTICAL: - orientation = 180; break; - - case ExifInterface.ORIENTATION_ROTATE_90: - case ExifInterface.ORIENTATION_TRANSPOSE: - orientation = 90; break; - - case ExifInterface.ORIENTATION_ROTATE_270: - case ExifInterface.ORIENTATION_TRANSVERSE: - orientation = 270; break; - - default: orientation = 0; - } - return orientation; - } - - private static int computeSampleSize(int width, int height, int maxWidth, int maxHeight) { // https://developer.android.com/topic/performance/graphics/load-bitmap.html int inSampleSize = 1; 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 6cf28b81..4711bd14 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java @@ -211,7 +211,7 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac mPreview.setStreamSize(previewSize.getWidth(), previewSize.getHeight(), wasFlipped); Camera.Parameters params = mCamera.getParameters(); - mPreviewFormat = params.getPreviewFormat(); + mPreviewStreamFormat = params.getPreviewFormat(); params.setPreviewSize(mPreviewStreamSize.getWidth(), mPreviewStreamSize.getHeight()); // <- not allowed during preview if (mMode == Mode.PICTURE) { params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); // <- allowed @@ -227,7 +227,7 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac mCamera.setPreviewCallbackWithBuffer(null); // Release anything left mCamera.setPreviewCallbackWithBuffer(this); // Add ourselves - mFrameManager.allocateBuffers(ImageFormat.getBitsPerPixel(mPreviewFormat), mPreviewStreamSize); + mFrameManager.allocateBuffers(ImageFormat.getBitsPerPixel(mPreviewStreamFormat), mPreviewStreamSize); LOG.i(log, "Starting preview with startPreview()."); try { @@ -240,7 +240,7 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac } private void stopPreview() { - mPreviewFormat = 0; + mPreviewStreamFormat = 0; mFrameManager.release(); mCamera.setPreviewCallbackWithBuffer(null); // Release anything left try { @@ -634,7 +634,7 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac LOG.v("Rotations", "SO", offset(REF_SENSOR, REF_OUTPUT), "OS", offset(REF_OUTPUT, REF_SENSOR)); LOG.v("Rotations", "VO", offset(REF_VIEW, REF_OUTPUT), "OV", offset(REF_OUTPUT, REF_VIEW)); - mPictureRecorder = new SnapshotPictureRecorder(stub, Camera1Engine.this, mCamera, outputRatio); + mPictureRecorder = new SnapshotPictureRecorder(stub, Camera1Engine.this, mPreview, mCamera, outputRatio); mPictureRecorder.take(); } }); @@ -646,7 +646,7 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac System.currentTimeMillis(), offset(REF_SENSOR, REF_OUTPUT), mPreviewStreamSize, - mPreviewFormat); + mPreviewStreamFormat); mCallback.dispatchFrame(frame); } 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 743b5abb..b5760924 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java @@ -75,6 +75,7 @@ public abstract class CameraEngine implements public static final int REF_OUTPUT = 2; protected final Callback mCallback; + protected final FrameManager mFrameManager; protected CameraPreview mPreview; protected WorkerHandler mHandler; @VisibleForTesting Handler mCrashHandler; @@ -101,7 +102,6 @@ public abstract class CameraEngine implements protected int mCameraId; protected CameraOptions mCameraOptions; protected Mapper mMapper; - protected FrameManager mFrameManager; protected PictureRecorder mPictureRecorder; protected VideoRecorder mVideoRecorder; protected long mVideoMaxSize; @@ -110,7 +110,7 @@ public abstract class CameraEngine implements protected int mAudioBitRate; protected Size mCaptureSize; protected Size mPreviewStreamSize; - protected int mPreviewFormat; + protected int mPreviewStreamFormat; protected long mAutoFocusResetDelayMillis; protected int mSensorOffset; @@ -416,6 +416,15 @@ public abstract class CameraEngine implements //region final getters + @NonNull + public final FrameManager getFrameManager() { + return mFrameManager; + } + + public final int getPreviewStreamFormat() { + return mPreviewStreamFormat; + } + @Nullable public final CameraOptions getCameraOptions() { return mCameraOptions; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java index 9b9c0b86..0a869e5c 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java @@ -40,7 +40,7 @@ import java.nio.ByteOrder; * There can be multiple surfaces associated with a single context. */ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) -class EglBaseSurface extends EglElement { +public class EglBaseSurface extends EglElement { protected static final String TAG = EglBaseSurface.class.getSimpleName(); private final static CameraLogger LOG = CameraLogger.create(TAG); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglCore.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglCore.java index 761f363d..76d2a62b 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglCore.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglCore.java @@ -36,7 +36,7 @@ import android.view.Surface; * The EGLContext must only be attached to one thread at a time. This class is not thread-safe. */ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) -final class EglCore { +public final class EglCore { private static final String TAG = EglCore.class.getSimpleName(); /** diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglWindowSurface.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglWindowSurface.java index 9e47afbc..8605f3c6 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglWindowSurface.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglWindowSurface.java @@ -27,7 +27,7 @@ import android.view.Surface; * It's good practice to explicitly release() the surface, preferably from a "finally" block. */ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) -class EglWindowSurface extends EglBaseSurface { +public class EglWindowSurface extends EglBaseSurface { private Surface mSurface; private boolean mReleaseSurface; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/ExifHelper.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/ExifHelper.java new file mode 100644 index 00000000..83dc359e --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/ExifHelper.java @@ -0,0 +1,38 @@ +package com.otaliastudios.cameraview.internal.utils; + +import androidx.exifinterface.media.ExifInterface; + +/** + * Super basic exif utilities. + */ +public class ExifHelper { + + /** + * Maps an {@link ExifInterface} orientation value + * to the actual degrees. + */ + public static int readExifOrientation(int exifOrientation) { + int orientation; + switch (exifOrientation) { + case ExifInterface.ORIENTATION_NORMAL: + case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: + orientation = 0; break; + + case ExifInterface.ORIENTATION_ROTATE_180: + case ExifInterface.ORIENTATION_FLIP_VERTICAL: + orientation = 180; break; + + case ExifInterface.ORIENTATION_ROTATE_90: + case ExifInterface.ORIENTATION_TRANSPOSE: + orientation = 90; break; + + case ExifInterface.ORIENTATION_ROTATE_270: + case ExifInterface.ORIENTATION_TRANSVERSE: + orientation = 270; break; + + default: orientation = 0; + } + return orientation; + } +} + diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/RotationHelper.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/RotationHelper.java index 688cf3f9..bc62c379 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/RotationHelper.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/RotationHelper.java @@ -10,7 +10,7 @@ import androidx.annotation.NonNull; */ @SuppressWarnings("DeprecatedIsStillUsed") @Deprecated -class RotationHelper { +public class RotationHelper { /** * Rotates the given yuv image into another yuv array, by the given angle. diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/FullPictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/FullPictureRecorder.java index d2299f2c..7708ba45 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/FullPictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/FullPictureRecorder.java @@ -3,8 +3,8 @@ package com.otaliastudios.cameraview.picture; import android.hardware.Camera; import com.otaliastudios.cameraview.CameraLogger; -import com.otaliastudios.cameraview.CameraUtils; import com.otaliastudios.cameraview.PictureResult; +import com.otaliastudios.cameraview.internal.utils.ExifHelper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -19,6 +19,7 @@ import java.io.IOException; public class FullPictureRecorder extends PictureRecorder { private static final String TAG = FullPictureRecorder.class.getSimpleName(); + @SuppressWarnings("unused") private static final CameraLogger LOG = CameraLogger.create(TAG); private Camera mCamera; @@ -34,10 +35,8 @@ public class FullPictureRecorder extends PictureRecorder { mCamera.setParameters(params); } - // Camera2 constructor here... - @Override - void take() { + public void take() { mCamera.takePicture( new Camera.ShutterCallback() { @Override @@ -54,7 +53,7 @@ public class FullPictureRecorder extends PictureRecorder { try { ExifInterface exif = new ExifInterface(new ByteArrayInputStream(data)); int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); - exifRotation = CameraUtils.readExifOrientation(exifOrientation); + exifRotation = ExifHelper.readExifOrientation(exifOrientation); } catch (IOException e) { exifRotation = 0; } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/PictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/PictureRecorder.java index c2a432f2..e7fec686 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/PictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/PictureRecorder.java @@ -4,6 +4,7 @@ import com.otaliastudios.cameraview.PictureResult; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; /** * Interface for picture capturing. @@ -12,21 +13,58 @@ import androidx.annotation.Nullable; */ public abstract class PictureRecorder { - /* tests */ PictureResult.Stub mResult; - /* tests */ PictureResultListener mListener; + /** + * Listens for picture recorder events. + */ + public interface PictureResultListener { + + /** + * The shutter was activated. + * @param didPlaySound whether a sound was played + */ + void onPictureShutter(boolean didPlaySound); + + /** + * Picture was taken or there was some error, if + * the result is null. + * @param result the result or null if there was some error. + */ + void onPictureResult(@Nullable PictureResult.Stub result); + } + + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) PictureResult.Stub mResult; + @VisibleForTesting PictureResultListener mListener; - PictureRecorder(@NonNull PictureResult.Stub stub, @Nullable PictureResultListener listener) { + /** + * Creates a new picture recorder. + * @param stub a picture stub + * @param listener a listener + */ + @SuppressWarnings("WeakerAccess") + public PictureRecorder(@NonNull PictureResult.Stub stub, @Nullable PictureResultListener listener) { mResult = stub; mListener = listener; } + /** + * Takes a picture. + */ public abstract void take(); + /** + * Subclasses can call this to notify that the shutter was activated, + * and whether it did play some sound or not. + * @param didPlaySound whether it played sounds + */ @SuppressWarnings("WeakerAccess") protected void dispatchOnShutter(boolean didPlaySound) { if (mListener != null) mListener.onPictureShutter(didPlaySound); } + /** + * Subclasses can call this to notify that the result was obtained, + * either with some error (null result) or with the actual stub, filled. + */ protected void dispatchResult() { if (mListener != null) { mListener.onPictureResult(mResult); @@ -34,9 +72,4 @@ public abstract class PictureRecorder { mResult = null; } } - - public interface PictureResultListener { - void onPictureShutter(boolean didPlaySound); - void onPictureResult(@Nullable PictureResult.Stub result); - } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotPictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotPictureRecorder.java index cfe39b5f..96dad747 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotPictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotPictureRecorder.java @@ -42,22 +42,27 @@ public class SnapshotPictureRecorder extends PictureRecorder { private static final String TAG = SnapshotPictureRecorder.class.getSimpleName(); private static final CameraLogger LOG = CameraLogger.create(TAG); - private Camera1Engine mController; + private Camera1Engine mEngine1; private Camera mCamera; private CameraPreview mPreview; private AspectRatio mOutputRatio; - private Size mSensorPreviewSize; private int mFormat; - public SnapshotPictureRecorder(@NonNull PictureResult.Stub stub, @NonNull Camera1Engine controller, - @NonNull Camera camera, @NonNull AspectRatio outputRatio) { - super(stub, controller); - mController = controller; - mPreview = controller.mPreview; + /** + * Camera1 constructor. + */ + public SnapshotPictureRecorder( + @NonNull PictureResult.Stub stub, + @NonNull Camera1Engine engine, + @NonNull CameraPreview preview, + @NonNull Camera camera, + @NonNull AspectRatio outputRatio) { + super(stub, engine); + mEngine1 = engine; + mPreview = preview; mCamera = camera; mOutputRatio = outputRatio; - mFormat = mController.mPreviewFormat; - mSensorPreviewSize = mController.mPreviewStreamSize; + mFormat = engine.getPreviewStreamFormat(); } @Override @@ -125,7 +130,7 @@ public class SnapshotPictureRecorder extends PictureRecorder { // Apply scale and crop: // NOTE: scaleX and scaleY are in REF_VIEW, while our input appears to be in REF_SENSOR. - boolean flip = mController.flip(CameraEngine.REF_VIEW, CameraEngine.REF_SENSOR); + boolean flip = mEngine1.flip(CameraEngine.REF_VIEW, CameraEngine.REF_SENSOR); float realScaleX = flip ? scaleY : scaleX; float realScaleY = flip ? scaleX : scaleY; float scaleTranslX = (1F - realScaleX) / 2F; @@ -185,12 +190,17 @@ public class SnapshotPictureRecorder extends PictureRecorder { // Adding EXIF to a byte array, unfortunately, is hard. final int sensorToOutput = mResult.rotation; final Size outputSize = mResult.size; + final Size previewStreamSize = mEngine1.getPreviewStreamSize(CameraEngine.REF_SENSOR); + if (previewStreamSize == null) { + throw new IllegalStateException("Preview stream size should never be null here."); + } WorkerHandler.run(new Runnable() { @Override public void run() { // Rotate the picture, because no one will write EXIF data, // then crop if needed. In both cases, transform yuv to jpeg. - byte[] data = RotationHelper.rotate(yuv, mSensorPreviewSize, sensorToOutput); + //noinspection deprecation + byte[] data = RotationHelper.rotate(yuv, previewStreamSize, sensorToOutput); YuvImage yuv = new YuvImage(data, mFormat, outputSize.getWidth(), outputSize.getHeight(), null); ByteArrayOutputStream stream = new ByteArrayOutputStream(); @@ -208,19 +218,18 @@ public class SnapshotPictureRecorder extends PictureRecorder { // It seems that the buffers are already cleared here, so we need to allocate again. camera.setPreviewCallbackWithBuffer(null); // Release anything left - camera.setPreviewCallbackWithBuffer(mController); // Add ourselves - mController.mFrameManager.allocateBuffers(ImageFormat.getBitsPerPixel(mFormat), mController.mPreviewStreamSize); + camera.setPreviewCallbackWithBuffer(mEngine1); // Add ourselves + mEngine1.getFrameManager().allocateBuffers(ImageFormat.getBitsPerPixel(mFormat), previewStreamSize); } }); } @Override protected void dispatchResult() { - mController = null; + mEngine1 = null; mCamera = null; mOutputRatio = null; mFormat = 0; - mSensorPreviewSize = null; super.dispatchResult(); } }