New angle classes, fix snapshot rotation

pull/494/head
Mattia Iavarone 6 years ago
parent f844040a29
commit f92c5b1e37
  1. 19
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java
  2. 11
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  3. 85
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
  4. 58
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  5. 123
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
  6. 125
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Angles.java
  7. 28
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Axis.java
  8. 28
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/offset/Reference.java
  9. 3
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot1PictureRecorder.java
  10. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java
  11. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java
  12. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/TextureCameraPreview.java
  13. 9
      cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java
  14. 2
      demo/src/main/res/layout/activity_camera.xml

@ -177,23 +177,4 @@ public class MockCameraEngine extends CameraEngine {
protected boolean collectCameraInfo(@NonNull Facing facing) { protected boolean collectCameraInfo(@NonNull Facing facing) {
return true; return true;
} }
/*
@Override
public void setFacing(@NonNull Facing facing) {
mFacing = facing;
}
@Override
public void setMode(@NonNull Mode mode) {
mMode = mode;
}
@Override
public void setAudio(@NonNull Audio audio) {
mAudio = audio;
}
*/
} }

@ -37,6 +37,7 @@ import com.otaliastudios.cameraview.controls.Engine;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.Flash; import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.engine.Camera2Engine; import com.otaliastudios.cameraview.engine.Camera2Engine;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.markers.MarkerLayout; import com.otaliastudios.cameraview.markers.MarkerLayout;
import com.otaliastudios.cameraview.engine.Camera1Engine; import com.otaliastudios.cameraview.engine.Camera1Engine;
import com.otaliastudios.cameraview.engine.CameraEngine; import com.otaliastudios.cameraview.engine.CameraEngine;
@ -342,7 +343,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
*/ */
@Override @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Size previewSize = mCameraEngine.getPreviewStreamSize(CameraEngine.REF_VIEW); Size previewSize = mCameraEngine.getPreviewStreamSize(Reference.VIEW);
if (previewSize == null) { if (previewSize == null) {
LOG.w("onMeasure:", "surface is not ready. Calling default behavior."); LOG.w("onMeasure:", "surface is not ready. Calling default behavior.");
super.onMeasure(widthMeasureSpec, heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec);
@ -1549,12 +1550,12 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
// Get the preview size and crop according to the current view size. // Get the preview size and crop according to the current view size.
// It's better to do calculations in the REF_VIEW reference, and then flip if needed. // It's better to do calculations in the REF_VIEW reference, and then flip if needed.
Size preview = mCameraEngine.getUncroppedSnapshotSize(CameraEngine.REF_VIEW); Size preview = mCameraEngine.getUncroppedSnapshotSize(Reference.VIEW);
if (preview == null) return null; // Should never happen. if (preview == null) return null; // Should never happen.
AspectRatio viewRatio = AspectRatio.of(getWidth(), getHeight()); AspectRatio viewRatio = AspectRatio.of(getWidth(), getHeight());
Rect crop = CropHelper.computeCrop(preview, viewRatio); Rect crop = CropHelper.computeCrop(preview, viewRatio);
Size cropSize = new Size(crop.width(), crop.height()); Size cropSize = new Size(crop.width(), crop.height());
if (mCameraEngine.flip(CameraEngine.REF_VIEW, CameraEngine.REF_OUTPUT)) { if (mCameraEngine.getAngles().flip(Reference.VIEW, Reference.OUTPUT)) {
return cropSize.flip(); return cropSize.flip();
} else { } else {
return cropSize; return cropSize;
@ -1573,7 +1574,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
*/ */
@Nullable @Nullable
public Size getPictureSize() { public Size getPictureSize() {
return mCameraEngine.getPictureSize(CameraEngine.REF_OUTPUT); return mCameraEngine.getPictureSize(Reference.OUTPUT);
} }
@ -1588,7 +1589,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
*/ */
@Nullable @Nullable
public Size getVideoSize() { public Size getVideoSize() {
return mCameraEngine.getVideoSize(CameraEngine.REF_OUTPUT); return mCameraEngine.getVideoSize(Reference.OUTPUT);
} }

@ -3,7 +3,6 @@ package com.otaliastudios.cameraview.engine;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.graphics.ImageFormat; import android.graphics.ImageFormat;
import android.graphics.PixelFormat;
import android.graphics.PointF; import android.graphics.PointF;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
@ -15,7 +14,6 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import android.util.Log;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Task;
@ -24,6 +22,8 @@ import com.otaliastudios.cameraview.CameraException;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.CameraOptions; import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.controls.Engine; import com.otaliastudios.cameraview.controls.Engine;
import com.otaliastudios.cameraview.engine.offset.Axis;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
@ -35,7 +35,6 @@ import com.otaliastudios.cameraview.controls.Hdr;
import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.controls.Mode;
import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.controls.WhiteBalance;
import com.otaliastudios.cameraview.internal.utils.CropHelper; import com.otaliastudios.cameraview.internal.utils.CropHelper;
import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.picture.Full1PictureRecorder; import com.otaliastudios.cameraview.picture.Full1PictureRecorder;
import com.otaliastudios.cameraview.picture.Snapshot1PictureRecorder; import com.otaliastudios.cameraview.picture.Snapshot1PictureRecorder;
import com.otaliastudios.cameraview.picture.SnapshotGlPictureRecorder; import com.otaliastudios.cameraview.picture.SnapshotGlPictureRecorder;
@ -124,7 +123,7 @@ public class Camera1Engine extends CameraEngine implements
for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) { for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) {
Camera.getCameraInfo(i, cameraInfo); Camera.getCameraInfo(i, cameraInfo);
if (cameraInfo.facing == internalFacing) { if (cameraInfo.facing == internalFacing) {
mSensorOffset = cameraInfo.orientation; setSensorOffset(facing, cameraInfo.orientation);
mCameraId = i; mCameraId = i;
return true; return true;
} }
@ -151,10 +150,10 @@ public class Camera1Engine extends CameraEngine implements
// Set parameters that might have been set before the camera was opened. // Set parameters that might have been set before the camera was opened.
LOG.i("onStartEngine:", "Applying default parameters."); LOG.i("onStartEngine:", "Applying default parameters.");
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
mCameraOptions = new CameraOptions(params, flip(REF_SENSOR, REF_VIEW)); mCameraOptions = new CameraOptions(params, getAngles().flip(Reference.SENSOR, Reference.VIEW));
applyAllParameters(params); applyAllParameters(params);
mCamera.setParameters(params); mCamera.setParameters(params);
mCamera.setDisplayOrientation(offset(REF_SENSOR, REF_VIEW)); // <- not allowed during preview mCamera.setDisplayOrientation(getAngles().offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE)); // <- not allowed during preview
LOG.i("onStartEngine:", "Ended"); LOG.i("onStartEngine:", "Ended");
return Tasks.forResult(null); return Tasks.forResult(null);
} }
@ -188,7 +187,7 @@ public class Camera1Engine extends CameraEngine implements
LOG.i("onStartPreview", "Dispatching onCameraPreviewStreamSizeChanged."); LOG.i("onStartPreview", "Dispatching onCameraPreviewStreamSizeChanged.");
mCallback.onCameraPreviewStreamSizeChanged(); mCallback.onCameraPreviewStreamSizeChanged();
Size previewSize = getPreviewStreamSize(REF_VIEW); Size previewSize = getPreviewStreamSize(Reference.VIEW);
if (previewSize == null) { if (previewSize == null) {
throw new IllegalStateException("previewStreamSize should not be null at this point."); throw new IllegalStateException("previewStreamSize should not be null at this point.");
} }
@ -299,8 +298,8 @@ public class Camera1Engine extends CameraEngine implements
@WorkerThread @WorkerThread
@Override @Override
protected void onTakePicture(@NonNull PictureResult.Stub stub) { protected void onTakePicture(@NonNull PictureResult.Stub stub) {
stub.rotation = offset(REF_SENSOR, REF_OUTPUT); stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR);
stub.size = getPictureSize(REF_OUTPUT); stub.size = getPictureSize(Reference.OUTPUT);
mPictureRecorder = new Full1PictureRecorder(stub, Camera1Engine.this, mCamera); mPictureRecorder = new Full1PictureRecorder(stub, Camera1Engine.this, mCamera);
mPictureRecorder.take(); mPictureRecorder.take();
} }
@ -308,9 +307,9 @@ public class Camera1Engine extends CameraEngine implements
@WorkerThread @WorkerThread
@Override @Override
protected void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio viewAspectRatio) { protected void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio viewAspectRatio) {
stub.size = getUncroppedSnapshotSize(REF_OUTPUT); // Not the real size: it will be cropped to match the view ratio stub.size = getUncroppedSnapshotSize(Reference.OUTPUT); // Not the real size: it will be cropped to match the view ratio
stub.rotation = offset(REF_SENSOR, REF_OUTPUT); // Actually it will be rotated and set to 0. stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); // Actually it will be rotated and set to 0.
AspectRatio outputRatio = flip(REF_OUTPUT, REF_VIEW) ? viewAspectRatio.flip() : viewAspectRatio; AspectRatio outputRatio = getAngles().flip(Reference.OUTPUT, Reference.VIEW) ? viewAspectRatio.flip() : viewAspectRatio;
if (mPreview instanceof GlCameraPreview && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (mPreview instanceof GlCameraPreview && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio); mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio);
@ -326,8 +325,8 @@ public class Camera1Engine extends CameraEngine implements
@Override @Override
protected void onTakeVideo(@NonNull VideoResult.Stub stub) { protected void onTakeVideo(@NonNull VideoResult.Stub stub) {
stub.rotation = offset(REF_SENSOR, REF_OUTPUT); stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR);
stub.size = flip(REF_SENSOR, REF_OUTPUT) ? mCaptureSize.flip() : mCaptureSize; stub.size = getAngles().flip(Reference.SENSOR, Reference.OUTPUT) ? mCaptureSize.flip() : mCaptureSize;
// Unlock the camera and start recording. // Unlock the camera and start recording.
try { try {
mCamera.unlock(); mCamera.unlock();
@ -337,9 +336,7 @@ public class Camera1Engine extends CameraEngine implements
onVideoResult(null, e); onVideoResult(null, e);
return; return;
} }
if (!(mVideoRecorder instanceof Full1VideoRecorder)) { mVideoRecorder = new Full1VideoRecorder(Camera1Engine.this, mCamera, mCameraId);
mVideoRecorder = new Full1VideoRecorder(Camera1Engine.this, mCamera, mCameraId);
}
mVideoRecorder.start(stub); mVideoRecorder.start(stub);
} }
@ -355,56 +352,19 @@ public class Camera1Engine extends CameraEngine implements
} }
GlCameraPreview glPreview = (GlCameraPreview) mPreview; GlCameraPreview glPreview = (GlCameraPreview) mPreview;
// Size and rotation turned out to be extremely tricky. In case of Snapshot1PictureRecorder // Output size is easy:
// we use the preview size in REF_OUTPUT (cropped) and offset(REF_SENSOR, REF_OUTPUT) as rotation. Size outputSize = getUncroppedSnapshotSize(Reference.OUTPUT);
// These values mean that we expect input to be in the REF_SENSOR system.
// Here everything seems different. We would expect a difference because the two snapshot
// recorders have different mechanics (the picture one uses a SurfaceTexture with setBufferSize,
// the video one here uses the MediaCodec input surface which we can't control).
// The strangest thing is the fact that the correct angle seems to be the same for FRONT and
// BACK sensor, which means that our sensor correction actually screws things up. For this reason
// facing value is temporarily set to BACK.
Facing realFacing = mFacing;
mFacing = Facing.BACK;
// These are the angles that make it work on a Nexus5X, compared to the offset() results.
// For instance, SV means offset(REF_SENSOR, REF_VIEW). The rest should be clear.
// CONFIG | WANTED | SV | VS | VO | OV | SO | OS |
// ------------|--------|--------|--------|--------|--------|--------|--------|
// Vertical | 0 | 270 | 90 | 0 | 0 | 270 | 90 |
// Left | 270 | 270 | 90 | 270 | 90 | 180 | 180 |
// Right | 90 | 270 | 90 | 90 | 270 | 0 | 0 |
// Upside down | 180 | 270 | 90 | 180 | 180 | 90 | 270 |
// The VO is the only correct value. Things change when using FRONT camera, in which case,
// no value is actually correct, and the needed values are the same of BACK!
// CONFIG | WANTED | SV | VS | VO | OV | SO | OS |
// ------------|--------|--------|--------|--------|--------|--------|--------|
// Vertical | 0 | 90 | 270 | 180 | 180 | 270 | 90 |
// Left | 270 | 90 | 270 | 270 | 90 | 0 | 0 |
// Right | 90 | 90 | 270 | 90 | 270 | 180 | 180 |
// Upside down | 180 | 90 | 270 | 0 | 0 | 90 | 270 |
// Based on this we will use VO for everything. See if we get issues about distortion
// and maybe we can improve. The reason why this happen is beyond my understanding.
Size outputSize = getUncroppedSnapshotSize(REF_OUTPUT);
if (outputSize == null) { if (outputSize == null) {
throw new IllegalStateException("outputSize should not be null."); throw new IllegalStateException("outputSize should not be null.");
} }
AspectRatio outputRatio = flip(REF_OUTPUT, REF_VIEW) ? viewAspectRatio.flip() : viewAspectRatio; AspectRatio outputRatio = getAngles().flip(Reference.OUTPUT, Reference.VIEW) ? viewAspectRatio.flip() : viewAspectRatio;
Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio); Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio);
outputSize = new Size(outputCrop.width(), outputCrop.height()); outputSize = new Size(outputCrop.width(), outputCrop.height());
stub.size = outputSize; stub.size = outputSize;
stub.rotation = offset(REF_VIEW, REF_OUTPUT); stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
// Reset facing and start. // Start.
mFacing = realFacing; mVideoRecorder = new SnapshotVideoRecorder(Camera1Engine.this, glPreview);
if (!(mVideoRecorder instanceof SnapshotVideoRecorder)) {
mVideoRecorder = new SnapshotVideoRecorder(Camera1Engine.this, glPreview);
}
mVideoRecorder.start(stub); mVideoRecorder.start(stub);
} }
@ -691,7 +651,7 @@ public class Camera1Engine extends CameraEngine implements
public void onPreviewFrame(@NonNull byte[] data, Camera camera) { public void onPreviewFrame(@NonNull byte[] data, Camera camera) {
Frame frame = getFrameManager().getFrame(data, Frame frame = getFrameManager().getFrame(data,
System.currentTimeMillis(), System.currentTimeMillis(),
offset(REF_SENSOR, REF_OUTPUT), getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR),
mPreviewStreamSize, mPreviewStreamSize,
PREVIEW_FORMAT); PREVIEW_FORMAT);
mCallback.dispatchFrame(frame); mCallback.dispatchFrame(frame);
@ -718,8 +678,9 @@ public class Camera1Engine extends CameraEngine implements
if (getEngineState() < STATE_STARTED) return; if (getEngineState() < STATE_STARTED) return;
if (!mCameraOptions.isAutoFocusSupported()) return; if (!mCameraOptions.isAutoFocusSupported()) return;
final PointF p = new PointF(point.x, point.y); // copy. final PointF p = new PointF(point.x, point.y); // copy.
int offset = getAngles().offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE);
List<Camera.Area> meteringAreas2 = computeMeteringAreas(p.x, p.y, List<Camera.Area> meteringAreas2 = computeMeteringAreas(p.x, p.y,
viewWidthF, viewHeightF, offset(REF_SENSOR, REF_VIEW)); viewWidthF, viewHeightF, offset);
List<Camera.Area> meteringAreas1 = meteringAreas2.subList(0, 1); List<Camera.Area> meteringAreas1 = meteringAreas2.subList(0, 1);
// At this point we are sure that camera supports auto focus... right? Look at CameraView.onTouchEvent(). // At this point we are sure that camera supports auto focus... right? Look at CameraView.onTouchEvent().

@ -44,6 +44,8 @@ import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.controls.Hdr; import com.otaliastudios.cameraview.controls.Hdr;
import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.controls.Mode;
import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.controls.WhiteBalance;
import com.otaliastudios.cameraview.engine.offset.Axis;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameManager; import com.otaliastudios.cameraview.frame.FrameManager;
import com.otaliastudios.cameraview.gesture.Gesture; import com.otaliastudios.cameraview.gesture.Gesture;
@ -299,7 +301,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
CameraCharacteristics characteristics = mManager.getCameraCharacteristics(cameraId); CameraCharacteristics characteristics = mManager.getCameraCharacteristics(cameraId);
if (internalFacing == readCharacteristic(characteristics, CameraCharacteristics.LENS_FACING, -99)) { if (internalFacing == readCharacteristic(characteristics, CameraCharacteristics.LENS_FACING, -99)) {
mCameraId = cameraId; mCameraId = cameraId;
mSensorOffset = readCharacteristic(characteristics, CameraCharacteristics.SENSOR_ORIENTATION, 0); int sensorOffset = readCharacteristic(characteristics, CameraCharacteristics.SENSOR_ORIENTATION, 0);
setSensorOffset(facing, sensorOffset);
return true; return true;
} }
} catch (CameraAccessException ignore) { } catch (CameraAccessException ignore) {
@ -330,7 +333,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
try { try {
LOG.i("createCamera:", "Applying default parameters."); LOG.i("createCamera:", "Applying default parameters.");
mCameraCharacteristics = mManager.getCameraCharacteristics(mCameraId); mCameraCharacteristics = mManager.getCameraCharacteristics(mCameraId);
mCameraOptions = new CameraOptions(mManager, mCameraId, flip(REF_SENSOR, REF_VIEW)); boolean flip = getAngles().flip(Reference.SENSOR, Reference.VIEW);
mCameraOptions = new CameraOptions(mManager, mCameraId, flip);
createRepeatingRequestBuilder(CameraDevice.TEMPLATE_PREVIEW); createRepeatingRequestBuilder(CameraDevice.TEMPLATE_PREVIEW);
} catch (CameraAccessException e) { } catch (CameraAccessException e) {
task.trySetException(createCameraException(e)); task.trySetException(createCameraException(e));
@ -489,12 +493,12 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
LOG.i("onStartPreview", "Dispatching onCameraPreviewStreamSizeChanged."); LOG.i("onStartPreview", "Dispatching onCameraPreviewStreamSizeChanged.");
mCallback.onCameraPreviewStreamSizeChanged(); mCallback.onCameraPreviewStreamSizeChanged();
Size previewSizeForView = getPreviewStreamSize(REF_VIEW); Size previewSizeForView = getPreviewStreamSize(Reference.VIEW);
if (previewSizeForView == null) { if (previewSizeForView == null) {
throw new IllegalStateException("previewStreamSize should not be null at this point."); throw new IllegalStateException("previewStreamSize should not be null at this point.");
} }
mPreview.setStreamSize(previewSizeForView.getWidth(), previewSizeForView.getHeight()); mPreview.setStreamSize(previewSizeForView.getWidth(), previewSizeForView.getHeight());
mPreview.setDrawRotation(getDisplayOffset()); mPreview.setDrawRotation(getAngles().offset(Reference.BASE, Reference.VIEW, Axis.ABSOLUTE));
if (hasFrameProcessors()) { if (hasFrameProcessors()) {
getFrameManager().setUp(ImageFormat.getBitsPerPixel(FRAME_PROCESSING_FORMAT), mFrameProcessingSize); getFrameManager().setUp(ImageFormat.getBitsPerPixel(FRAME_PROCESSING_FORMAT), mFrameProcessingSize);
} }
@ -613,9 +617,9 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@WorkerThread @WorkerThread
@Override @Override
protected void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio viewAspectRatio) { protected void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio viewAspectRatio) {
stub.size = getUncroppedSnapshotSize(REF_OUTPUT); // Not the real size: it will be cropped to match the view ratio stub.size = getUncroppedSnapshotSize(Reference.OUTPUT); // Not the real size: it will be cropped to match the view ratio
stub.rotation = offset(REF_SENSOR, REF_OUTPUT); // Actually it will be rotated and set to 0. stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); // Actually it will be rotated and set to 0.
AspectRatio outputRatio = flip(REF_OUTPUT, REF_VIEW) ? viewAspectRatio.flip() : viewAspectRatio; AspectRatio outputRatio = getAngles().flip(Reference.OUTPUT, Reference.VIEW) ? viewAspectRatio.flip() : viewAspectRatio;
if (mPreview instanceof GlCameraPreview) { if (mPreview instanceof GlCameraPreview) {
mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio); mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio);
} else { } else {
@ -626,8 +630,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override @Override
protected void onTakePicture(@NonNull PictureResult.Stub stub) { protected void onTakePicture(@NonNull PictureResult.Stub stub) {
stub.rotation = offset(REF_SENSOR, REF_OUTPUT); stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR);
stub.size = getPictureSize(REF_OUTPUT); stub.size = getPictureSize(Reference.OUTPUT);
try { try {
CaptureRequest.Builder builder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); CaptureRequest.Builder builder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
applyAllParameters(builder); applyAllParameters(builder);
@ -661,8 +665,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override @Override
protected void onTakeVideo(@NonNull VideoResult.Stub stub) { protected void onTakeVideo(@NonNull VideoResult.Stub stub) {
LOG.i("onTakeVideo", "called."); LOG.i("onTakeVideo", "called.");
stub.rotation = offset(REF_SENSOR, REF_OUTPUT); stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR);
stub.size = flip(REF_SENSOR, REF_OUTPUT) ? mCaptureSize.flip() : mCaptureSize; stub.size = getAngles().flip(Reference.SENSOR, Reference.OUTPUT) ? mCaptureSize.flip() : mCaptureSize;
if (!Full2VideoRecorder.SUPPORTS_PERSISTENT_SURFACE) { if (!Full2VideoRecorder.SUPPORTS_PERSISTENT_SURFACE) {
// On API 21 and 22, we must restart the session at each time. // On API 21 and 22, we must restart the session at each time.
// Save the pending data and restart the session. // Save the pending data and restart the session.
@ -675,9 +679,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
} }
private void doTakeVideo(@NonNull final VideoResult.Stub stub) { private void doTakeVideo(@NonNull final VideoResult.Stub stub) {
if (!(mVideoRecorder instanceof Full2VideoRecorder)) { mVideoRecorder = new Full2VideoRecorder(this, mCameraId, mFullVideoPersistentSurface);
mVideoRecorder = new Full2VideoRecorder(this, mCameraId, mFullVideoPersistentSurface);
}
Full2VideoRecorder recorder = (Full2VideoRecorder) mVideoRecorder; Full2VideoRecorder recorder = (Full2VideoRecorder) mVideoRecorder;
try { try {
createRepeatingRequestBuilder(CameraDevice.TEMPLATE_RECORD); createRepeatingRequestBuilder(CameraDevice.TEMPLATE_RECORD);
@ -697,10 +699,6 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
} }
} }
/**
* See {@link CameraEngine#onTakeVideoSnapshot(VideoResult.Stub, AspectRatio)}
* to read about the size and rotation computation.
*/
@WorkerThread @WorkerThread
@Override @Override
protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio viewAspectRatio) { protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio viewAspectRatio) {
@ -708,23 +706,20 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
throw new IllegalStateException("Video snapshots are only supported with GlCameraPreview."); throw new IllegalStateException("Video snapshots are only supported with GlCameraPreview.");
} }
GlCameraPreview glPreview = (GlCameraPreview) mPreview; GlCameraPreview glPreview = (GlCameraPreview) mPreview;
Facing realFacing = mFacing;
mFacing = Facing.BACK; // Output size is easy:
Size outputSize = getUncroppedSnapshotSize(REF_OUTPUT); Size outputSize = getUncroppedSnapshotSize(Reference.OUTPUT);
if (outputSize == null) { if (outputSize == null) {
throw new IllegalStateException("outputSize should not be null."); throw new IllegalStateException("outputSize should not be null.");
} }
AspectRatio outputRatio = flip(REF_OUTPUT, REF_VIEW) ? viewAspectRatio.flip() : viewAspectRatio; AspectRatio outputRatio = getAngles().flip(Reference.OUTPUT, Reference.VIEW) ? viewAspectRatio.flip() : viewAspectRatio;
Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio); Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio);
outputSize = new Size(outputCrop.width(), outputCrop.height()); outputSize = new Size(outputCrop.width(), outputCrop.height());
stub.size = outputSize; stub.size = outputSize;
stub.rotation = offset(REF_VIEW, REF_OUTPUT); stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
// Reset facing and start. // Start.
mFacing = realFacing; mVideoRecorder = new SnapshotVideoRecorder(this, glPreview);
if (!(mVideoRecorder instanceof SnapshotVideoRecorder)) {
mVideoRecorder = new SnapshotVideoRecorder(this, glPreview);
}
mVideoRecorder.start(stub); mVideoRecorder.start(stub);
} }
@ -1052,7 +1047,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
if (getEngineState() == STATE_STARTED) { if (getEngineState() == STATE_STARTED) {
Frame frame = getFrameManager().getFrame(data, Frame frame = getFrameManager().getFrame(data,
System.currentTimeMillis(), System.currentTimeMillis(),
offset(REF_SENSOR, REF_OUTPUT), getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR),
mFrameProcessingSize, mFrameProcessingSize,
FRAME_PROCESSING_FORMAT); FRAME_PROCESSING_FORMAT);
mCallback.dispatchFrame(frame); mCallback.dispatchFrame(frame);
@ -1101,12 +1096,11 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
// This is a good Q/A. https://stackoverflow.com/a/33181620/4288782 // This is a good Q/A. https://stackoverflow.com/a/33181620/4288782
// At first, the point is relative to the View system and does not account our own cropping. // At first, the point is relative to the View system and does not account our own cropping.
// Will keep updating these two below. // Will keep updating these two below.
//noinspection UnnecessaryLocalVariable
PointF referencePoint = new PointF(point.x, point.y); PointF referencePoint = new PointF(point.x, point.y);
Size referenceSize /* = previewSurfaceSize */; Size referenceSize /* = previewSurfaceSize */;
// 1. Account for cropping. // 1. Account for cropping.
Size previewStreamSize = getPreviewStreamSize(REF_VIEW); Size previewStreamSize = getPreviewStreamSize(Reference.VIEW);
Size previewSurfaceSize = mPreview.getSurfaceSize(); Size previewSurfaceSize = mPreview.getSurfaceSize();
if (previewStreamSize == null) throw new IllegalStateException("getPreviewStreamSize should not be null at this point."); if (previewStreamSize == null) throw new IllegalStateException("getPreviewStreamSize should not be null at this point.");
AspectRatio previewStreamAspectRatio = AspectRatio.of(previewStreamSize); AspectRatio previewStreamAspectRatio = AspectRatio.of(previewStreamSize);
@ -1133,7 +1127,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
// 3. Rotate to the stream coordinate system. // 3. Rotate to the stream coordinate system.
// Not elegant, but the sin/cos way was failing. // Not elegant, but the sin/cos way was failing.
int angle = offset(REF_SENSOR, REF_VIEW); int angle = getAngles().offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE);
boolean flip = angle % 180 != 0; boolean flip = angle % 180 != 0;
float tempX = referencePoint.x; float tempY = referencePoint.y; float tempX = referencePoint.x; float tempY = referencePoint.y;
if (angle == 0) { if (angle == 0) {

@ -1,15 +1,12 @@
package com.otaliastudios.cameraview.engine; package com.otaliastudios.cameraview.engine;
import android.content.Context; import android.content.Context;
import android.graphics.Camera;
import android.graphics.PointF; import android.graphics.PointF;
import android.hardware.camera2.CameraCharacteristics;
import android.location.Location; import android.location.Location;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.Log;
import com.google.android.gms.tasks.Continuation; import com.google.android.gms.tasks.Continuation;
import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.OnCompleteListener;
@ -22,6 +19,8 @@ import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.CameraOptions; import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.engine.offset.Angles;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameManager; import com.otaliastudios.cameraview.frame.FrameManager;
import com.otaliastudios.cameraview.internal.utils.Op; import com.otaliastudios.cameraview.internal.utils.Op;
@ -147,10 +146,6 @@ public abstract class CameraEngine implements
public static final int STATE_STARTING = CameraEngineStep.STATE_STARTING; public static final int STATE_STARTING = CameraEngineStep.STATE_STARTING;
public static final int STATE_STARTED = CameraEngineStep.STATE_STARTED; public static final int STATE_STARTED = CameraEngineStep.STATE_STARTED;
public static final int REF_SENSOR = 0;
public static final int REF_VIEW = 1;
public static final int REF_OUTPUT = 2;
// Need to be protected // Need to be protected
@SuppressWarnings("WeakerAccess") protected WorkerHandler mHandler; @SuppressWarnings("WeakerAccess") protected WorkerHandler mHandler;
@SuppressWarnings("WeakerAccess") protected final Callback mCallback; @SuppressWarnings("WeakerAccess") protected final Callback mCallback;
@ -161,7 +156,6 @@ public abstract class CameraEngine implements
@SuppressWarnings("WeakerAccess") protected VideoRecorder mVideoRecorder; @SuppressWarnings("WeakerAccess") protected VideoRecorder mVideoRecorder;
@SuppressWarnings("WeakerAccess") protected Size mCaptureSize; @SuppressWarnings("WeakerAccess") protected Size mCaptureSize;
@SuppressWarnings("WeakerAccess") protected Size mPreviewStreamSize; @SuppressWarnings("WeakerAccess") protected Size mPreviewStreamSize;
@SuppressWarnings("WeakerAccess") protected Facing mFacing;
@SuppressWarnings("WeakerAccess") protected Flash mFlash; @SuppressWarnings("WeakerAccess") protected Flash mFlash;
@SuppressWarnings("WeakerAccess") protected WhiteBalance mWhiteBalance; @SuppressWarnings("WeakerAccess") protected WhiteBalance mWhiteBalance;
@SuppressWarnings("WeakerAccess") protected VideoCodec mVideoCodec; @SuppressWarnings("WeakerAccess") protected VideoCodec mVideoCodec;
@ -170,14 +164,15 @@ public abstract class CameraEngine implements
@SuppressWarnings("WeakerAccess") protected float mZoomValue; @SuppressWarnings("WeakerAccess") protected float mZoomValue;
@SuppressWarnings("WeakerAccess") protected float mExposureCorrectionValue; @SuppressWarnings("WeakerAccess") protected float mExposureCorrectionValue;
@SuppressWarnings("WeakerAccess") protected boolean mPlaySounds; @SuppressWarnings("WeakerAccess") protected boolean mPlaySounds;
@SuppressWarnings("WeakerAccess") protected int mSensorOffset;
// Can be private // Can be private
@VisibleForTesting Handler mCrashHandler; @VisibleForTesting Handler mCrashHandler;
private final FrameManager mFrameManager; private final FrameManager mFrameManager;
private final Angles mAngles;
@Nullable private SizeSelector mPreviewStreamSizeSelector; @Nullable private SizeSelector mPreviewStreamSizeSelector;
private SizeSelector mPictureSizeSelector; private SizeSelector mPictureSizeSelector;
private SizeSelector mVideoSizeSelector; private SizeSelector mVideoSizeSelector;
private Facing mFacing;
private Mode mMode; private Mode mMode;
private Audio mAudio; private Audio mAudio;
private long mVideoMaxSize; private long mVideoMaxSize;
@ -188,8 +183,6 @@ public abstract class CameraEngine implements
private long mAutoFocusResetDelayMillis; private long mAutoFocusResetDelayMillis;
private int mSnapshotMaxWidth = Integer.MAX_VALUE; // in REF_VIEW for consistency with SizeSelectors private int mSnapshotMaxWidth = Integer.MAX_VALUE; // in REF_VIEW for consistency with SizeSelectors
private int mSnapshotMaxHeight = Integer.MAX_VALUE; // in REF_VIEW for consistency with SizeSelectors private int mSnapshotMaxHeight = Integer.MAX_VALUE; // in REF_VIEW for consistency with SizeSelectors
private int mDisplayOffset;
private int mDeviceOrientation;
// Steps // Steps
private final CameraEngineStep.Callback mStepCallback = new CameraEngineStep.Callback() { private final CameraEngineStep.Callback mStepCallback = new CameraEngineStep.Callback() {
@ -219,6 +212,7 @@ public abstract class CameraEngine implements
mHandler = WorkerHandler.get("CameraViewEngine"); mHandler = WorkerHandler.get("CameraViewEngine");
mHandler.getThread().setUncaughtExceptionHandler(new CrashExceptionHandler()); mHandler.getThread().setUncaughtExceptionHandler(new CrashExceptionHandler());
mFrameManager = instantiateFrameManager(); mFrameManager = instantiateFrameManager();
mAngles = new Angles();
} }
public void setPreview(@NonNull CameraPreview cameraPreview) { public void setPreview(@NonNull CameraPreview cameraPreview) {
@ -573,7 +567,7 @@ public abstract class CameraEngine implements
*/ */
@Override @Override
public final void onSurfaceAvailable() { public final void onSurfaceAvailable() {
LOG.i("onSurfaceAvailable:", "Size is", getPreviewSurfaceSize(REF_VIEW)); LOG.i("onSurfaceAvailable:", "Size is", getPreviewSurfaceSize(Reference.VIEW));
mHandler.run(new Runnable() { mHandler.run(new Runnable() {
@Override @Override
public void run() { public void run() {
@ -590,7 +584,7 @@ public abstract class CameraEngine implements
@Override @Override
public final void onSurfaceChanged() { public final void onSurfaceChanged() {
LOG.i("onSurfaceChanged:", "Size is", getPreviewSurfaceSize(REF_VIEW), "Posting."); LOG.i("onSurfaceChanged:", "Size is", getPreviewSurfaceSize(Reference.VIEW), "Posting.");
mHandler.run(new Runnable() { mHandler.run(new Runnable() {
@Override @Override
public void run() { public void run() {
@ -786,19 +780,24 @@ public abstract class CameraEngine implements
//region Final setters and getters //region Final setters and getters
// This is called before start() and never again. @SuppressWarnings("WeakerAccess")
public final void setDisplayOffset(int displayOffset) { public final Angles getAngles() {
mDisplayOffset = displayOffset; return mAngles;
} }
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
public final int getDisplayOffset() { protected final void setSensorOffset(@NonNull Facing facing, int sensorOffset) {
return mDisplayOffset; 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. // This can be called multiple times.
public final void setDeviceOrientation(int deviceOrientation) { public final void setDeviceOrientation(int deviceOrientation) {
mDeviceOrientation = deviceOrientation; mAngles.setDeviceOrientation(deviceOrientation);
} }
public final void setPreviewStreamSizeSelector(@Nullable SizeSelector selector) { public final void setPreviewStreamSizeSelector(@Nullable SizeSelector selector) {
@ -1019,8 +1018,8 @@ public abstract class CameraEngine implements
* Camera is about to be opened. Implementors should look into available cameras * Camera is about to be opened. Implementors should look into available cameras
* and see if anyone matches the given {@link Facing value}. * and see if anyone matches the given {@link Facing value}.
* *
* If so, implementors should set {@link #mSensorOffset} and any other information * If so, implementors should set {@link #setSensorOffset(Facing, int)} and any other information
* (like camera ID) needed to start teh engine. * (like camera ID) needed to start the engine.
* *
* @param facing the facing value * @param facing the facing value
* @return true if we have one * @return true if we have one
@ -1087,7 +1086,7 @@ public abstract class CameraEngine implements
} }
/** /**
* The snapshot size is the {@link #getPreviewStreamSize(int)}, but cropped based on the * The snapshot size is the {@link #getPreviewStreamSize(Reference)}, but cropped based on the
* view/surface aspect ratio. * view/surface aspect ratio.
* @param stub a picture stub * @param stub a picture stub
* @param viewAspectRatio the view aspect ratio * @param viewAspectRatio the view aspect ratio
@ -1104,9 +1103,6 @@ public abstract class CameraEngine implements
stub.isSnapshot = true; stub.isSnapshot = true;
stub.facing = mFacing; stub.facing = mFacing;
// Leave the other parameters to subclasses. // Leave the other parameters to subclasses.
LOG.v("takePictureSnapshot", "Rotations", "SV", offset(REF_SENSOR, REF_VIEW), "VS", offset(REF_VIEW, REF_SENSOR));
LOG.v("takePictureSnapshot", "Rotations", "SO", offset(REF_SENSOR, REF_OUTPUT), "OS", offset(REF_OUTPUT, REF_SENSOR));
LOG.v("takePictureSnapshot", "Rotations", "VO", offset(REF_VIEW, REF_OUTPUT), "OV", offset(REF_OUTPUT, REF_VIEW));
onTakePictureSnapshot(stub, viewAspectRatio); onTakePictureSnapshot(stub, viewAspectRatio);
} }
}); });
@ -1228,73 +1224,31 @@ public abstract class CameraEngine implements
//endregion //endregion
//region Orientation utils //region Size utilities
private int computeSensorToViewOffset() {
if (mFacing == Facing.FRONT) {
return (360 - ((mSensorOffset + mDisplayOffset) % 360)) % 360;
} else {
return (mSensorOffset - mDisplayOffset + 360) % 360;
}
}
private int computeSensorToOutputOffset() {
if (mFacing == Facing.FRONT) {
return (mSensorOffset - mDeviceOrientation + 360) % 360;
} else {
return (mSensorOffset + mDeviceOrientation) % 360;
}
}
// o(S, V) - o(S, O)
// displayOffset - deviceOrientation
// Returns the offset between two reference systems.
@SuppressWarnings("WeakerAccess")
public final int offset(int fromReference, int toReference) {
if (fromReference == toReference) return 0;
// We only know how to compute offsets with respect to REF_SENSOR.
// That's why we separate the two cases.
if (fromReference == REF_SENSOR) {
return toReference == REF_VIEW ?
computeSensorToViewOffset() :
computeSensorToOutputOffset();
}
// Maybe the sensor is the other.
if (toReference == REF_SENSOR) {
return (-offset(toReference, fromReference) + 360) % 360;
}
// None of them is the sensor. Use a difference.
return (offset(REF_SENSOR, toReference) - offset(REF_SENSOR, fromReference) + 360) % 360;
}
public final boolean flip(int reference1, int reference2) {
return offset(reference1, reference2) % 180 != 0;
}
@Nullable @Nullable
public final Size getPictureSize(@SuppressWarnings("SameParameterValue") int reference) { public final Size getPictureSize(@SuppressWarnings("SameParameterValue") @NonNull Reference reference) {
if (mCaptureSize == null || mMode == Mode.VIDEO) return null; if (mCaptureSize == null || mMode == Mode.VIDEO) return null;
return flip(REF_SENSOR, reference) ? mCaptureSize.flip() : mCaptureSize; return getAngles().flip(Reference.SENSOR, reference) ? mCaptureSize.flip() : mCaptureSize;
} }
@Nullable @Nullable
public final Size getVideoSize(@SuppressWarnings("SameParameterValue") int reference) { public final Size getVideoSize(@SuppressWarnings("SameParameterValue") @NonNull Reference reference) {
if (mCaptureSize == null || mMode == Mode.PICTURE) return null; if (mCaptureSize == null || mMode == Mode.PICTURE) return null;
return flip(REF_SENSOR, reference) ? mCaptureSize.flip() : mCaptureSize; return getAngles().flip(Reference.SENSOR, reference) ? mCaptureSize.flip() : mCaptureSize;
} }
@Nullable @Nullable
public final Size getPreviewStreamSize(int reference) { public final Size getPreviewStreamSize(@NonNull Reference reference) {
if (mPreviewStreamSize == null) return null; if (mPreviewStreamSize == null) return null;
return flip(REF_SENSOR, reference) ? mPreviewStreamSize.flip() : mPreviewStreamSize; return getAngles().flip(Reference.SENSOR, reference) ? mPreviewStreamSize.flip() : mPreviewStreamSize;
} }
@SuppressWarnings("SameParameterValue") @SuppressWarnings("SameParameterValue")
@Nullable @Nullable
private Size getPreviewSurfaceSize(int reference) { private Size getPreviewSurfaceSize(@NonNull Reference reference) {
if (mPreview == null) return null; if (mPreview == null) return null;
return flip(REF_VIEW, reference) ? mPreview.getSurfaceSize().flip() : mPreview.getSurfaceSize(); return getAngles().flip(Reference.VIEW, reference) ? mPreview.getSurfaceSize().flip() : mPreview.getSurfaceSize();
} }
/** /**
@ -1318,10 +1272,10 @@ public abstract class CameraEngine implements
* apply, despite the capturing mechanism being different. * apply, despite the capturing mechanism being different.
*/ */
@Nullable @Nullable
public final Size getUncroppedSnapshotSize(int reference) { public final Size getUncroppedSnapshotSize(@NonNull Reference reference) {
Size baseSize = getPreviewStreamSize(reference); Size baseSize = getPreviewStreamSize(reference);
if (baseSize == null) return null; if (baseSize == null) return null;
boolean flip = flip(reference, REF_VIEW); boolean flip = getAngles().flip(reference, Reference.VIEW);
int maxWidth = flip ? mSnapshotMaxHeight : mSnapshotMaxWidth; int maxWidth = flip ? mSnapshotMaxHeight : mSnapshotMaxWidth;
int maxHeight = flip ? mSnapshotMaxWidth : mSnapshotMaxHeight; int maxHeight = flip ? mSnapshotMaxWidth : mSnapshotMaxHeight;
float baseRatio = AspectRatio.of(baseSize).toFloat(); float baseRatio = AspectRatio.of(baseSize).toFloat();
@ -1339,11 +1293,6 @@ public abstract class CameraEngine implements
} }
} }
//endregion
//region Size utils
/** /**
* This is called either on cameraView.start(), or when the underlying surface changes. * This is called either on cameraView.start(), or when the underlying surface changes.
* It is possible that in the first call the preview surface has not already computed its * It is possible that in the first call the preview surface has not already computed its
@ -1358,10 +1307,10 @@ public abstract class CameraEngine implements
} }
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
protected final Size computeCaptureSize(Mode mode) { protected final Size computeCaptureSize(@NonNull Mode mode) {
// We want to pass stuff into the REF_VIEW reference, not the sensor one. // We want to pass stuff into the REF_VIEW reference, not the sensor one.
// This is already managed by CameraOptions, so we just flip again at the end. // This is already managed by CameraOptions, so we just flip again at the end.
boolean flip = flip(REF_SENSOR, REF_VIEW); boolean flip = getAngles().flip(Reference.SENSOR, Reference.VIEW);
SizeSelector selector; SizeSelector selector;
Collection<Size> sizes; Collection<Size> sizes;
if (mode == Mode.PICTURE) { if (mode == Mode.PICTURE) {
@ -1397,7 +1346,7 @@ public abstract class CameraEngine implements
@NonNull List<Size> previewSizes = getPreviewStreamAvailableSizes(); @NonNull List<Size> previewSizes = getPreviewStreamAvailableSizes();
// These sizes come in REF_SENSOR. Since there is an external selector involved, // These sizes come in REF_SENSOR. Since there is an external selector involved,
// we must convert all of them to REF_VIEW, then flip back when returning. // we must convert all of them to REF_VIEW, then flip back when returning.
boolean flip = flip(REF_SENSOR, REF_VIEW); boolean flip = getAngles().flip(Reference.SENSOR, Reference.VIEW);
List<Size> sizes = new ArrayList<>(previewSizes.size()); List<Size> sizes = new ArrayList<>(previewSizes.size());
for (Size size : previewSizes) { for (Size size : previewSizes) {
sizes.add(flip ? size.flip() : size); sizes.add(flip ? size.flip() : size);
@ -1405,7 +1354,7 @@ public abstract class CameraEngine implements
// Create our own default selector, which will be used if the external mPreviewStreamSizeSelector // Create our own default selector, which will be used if the external mPreviewStreamSizeSelector
// is null, or if it fails in finding a size. // is null, or if it fails in finding a size.
Size targetMinSize = getPreviewSurfaceSize(REF_VIEW); Size targetMinSize = getPreviewSurfaceSize(Reference.VIEW);
if (targetMinSize == null) throw new IllegalStateException("targetMinSize should not be null here."); if (targetMinSize == null) throw new IllegalStateException("targetMinSize should not be null here.");
AspectRatio targetRatio = AspectRatio.of(mCaptureSize.getWidth(), mCaptureSize.getHeight()); AspectRatio targetRatio = AspectRatio.of(mCaptureSize.getWidth(), mCaptureSize.getHeight());
if (flip) targetRatio = targetRatio.flip(); if (flip) targetRatio = targetRatio.flip();

@ -0,0 +1,125 @@
package com.otaliastudios.cameraview.engine.offset;
import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.controls.Facing;
public class Angles {
private Facing mSensorFacing;
private int mSensorOffset = 0;
private int mDisplayOffset = 0;
private int mDeviceOrientation = 0;
/**
* We want to keep everything in the {@link Axis#ABSOLUTE} reference,
* so a front facing sensor offset must be inverted.
*
* @param sensorFacing sensor facing value
* @param sensorOffset sensor offset
*/
public void setSensorOffset(@NonNull Facing sensorFacing, int sensorOffset) {
sanitizeInput(sensorOffset);
mSensorFacing = sensorFacing;
mSensorOffset = sensorOffset;
if (mSensorFacing == Facing.FRONT) {
mSensorOffset = sanitizeOutput(360 - mSensorOffset);
}
}
/**
* Sets the display offset.
* @param displayOffset the display offset
*/
public void setDisplayOffset(int displayOffset) {
sanitizeInput(displayOffset);
mDisplayOffset = displayOffset;
}
/**
* Sets the device orientation.
* @param deviceOrientation the device orientation
*/
public void setDeviceOrientation(int deviceOrientation) {
sanitizeInput(deviceOrientation);
mDeviceOrientation = deviceOrientation;
}
/**
* Returns the offset between two reference systems, computed along the given axis.
* @param from the source reference system
* @param to the destination reference system
* @param axis the axis
* @return the offset
*/
public int offset(@NonNull Reference from, @NonNull Reference to, @NonNull Axis axis) {
int offset = absoluteOffset(from, to);
if (axis == Axis.RELATIVE_TO_SENSOR) {
if (mSensorFacing == Facing.FRONT) {
offset = sanitizeOutput(360 - offset);
}
}
return offset;
}
/**
* Old results:
* offset(REF_SENSOR, REF_OUTPUT, Facing.BACK) = mSensorOffset + mDeviceOrientation
* offset(REF_SENSOR, REF_OUTPUT, Facing.FRONT) = -mSensorOffset - mDeviceOrientation
* offset(REF_SENSOR, REF_VIEW, Facing.BACK) = mSensorOffset - mDisplayOffset
* offset(REF_SENSOR, REF_VIEW, Facing.FRONT) = mSensorOffset - mDisplayOffset
* offset(REF_OUTPUT, REF_VIEW, Facing.BACK) = (mSensorOffset - mDisplayOffset) - (mSensorOffset + mDeviceOrientation) = -mDisplayOffset - mDeviceOrientation
* offset(REF_OUTPUT, REF_VIEW, Facing.FRONT) = (mSensorOffset - mDisplayOffset) - (-mSensorOffset - mDeviceOrientation) = 2*mSensorOffset + mDisplayOffset + mDeviceOrientation
*/
/**
* New results:
* offset(REF_SENSOR, REF_OUTPUT) = mDeviceOrientation + mSensorOffset
* -> REF_SENSOR, REF_OUTPUT is typically wanted in the RELATIVE_TO_SENSOR axis.
*
* offset(REF_SENSOR, REF_VIEW) = -mDisplayOffset + mSensorOffset
* -> REF_SENSOR, REF_VIEW is typically wanted in the ABSOLUTE axis.
*/
private int absoluteOffset(@NonNull Reference from, @NonNull Reference to) {
if (from == to) {
return 0;
} else if (to == Reference.BASE) {
return 360 - absoluteOffset(to, from);
} else if (from == Reference.BASE) {
switch (to) {
case VIEW: return sanitizeOutput(360 - mDisplayOffset);
case OUTPUT: return sanitizeOutput(mDeviceOrientation);
case SENSOR: return sanitizeOutput(360 - mSensorOffset);
default: throw new RuntimeException("Unknown reference: " + to);
}
} else {
return sanitizeOutput(
absoluteOffset(Reference.BASE, to)
- absoluteOffset(Reference.BASE, from));
}
}
/**
* Whether the two references systems are flipped.
* @param from source
* @param to destination
* @return true if flipped
*/
public boolean flip(@NonNull Reference from, @NonNull Reference to) {
return offset(from, to, Axis.ABSOLUTE) % 180 != 0;
}
private void sanitizeInput(int value) {
if (value != 0
&& value != 90
&& value != 180
&& value != 270) {
throw new IllegalStateException("This value is not sanitized: " + value);
}
}
private int sanitizeOutput(int value) {
return (value + 360) % 360;
}
}

@ -0,0 +1,28 @@
package com.otaliastudios.cameraview.engine.offset;
/**
* The axis around which offsets are computed. We have two possibilities:
* - an axis going out of the device screen towards the user's face
* - an axis going out of the device screen towards the back
*
* We are mostly interested in the first one, but some APIs will require
* angles in the sensor reference, in which case, for front cameras, we are
* required to use {@link #RELATIVE_TO_SENSOR}.
*/
public enum Axis {
/**
* This rotation axis is the one going out of the device screen
* towards the user's face.
*/
ABSOLUTE,
/**
* This rotation axis takes into account the current
* {@link com.otaliastudios.cameraview.controls.Facing} value.
*
* - for {@link com.otaliastudios.cameraview.controls.Facing#BACK}, this equals {@link #ABSOLUTE}
* - for {@link com.otaliastudios.cameraview.controls.Facing#FRONT}, this is inverted
*/
RELATIVE_TO_SENSOR
}

@ -0,0 +1,28 @@
package com.otaliastudios.cameraview.engine.offset;
public enum Reference {
/**
* The base reference system has its 'north' aligned with the device natural
* orientation.
*/
BASE,
/**
* This reference system has its 'north' aligned with the camera sensor.
*/
SENSOR,
/**
* This reference system has its 'north' aligned with the View hierarchy.
* This can be different than {@link #BASE} if the activity is allowed to rotate
* (or forced into a non natural position).
*/
VIEW,
/**
* This reference system has its 'north' aligned with the output picture/video.
* This means that it takes into account the device orientation.
*/
OUTPUT
}

@ -9,6 +9,7 @@ import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.engine.Camera1Engine; import com.otaliastudios.cameraview.engine.Camera1Engine;
import com.otaliastudios.cameraview.engine.CameraEngine; import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.internal.utils.CropHelper; import com.otaliastudios.cameraview.internal.utils.CropHelper;
import com.otaliastudios.cameraview.internal.utils.RotationHelper; import com.otaliastudios.cameraview.internal.utils.RotationHelper;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler; import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
@ -57,7 +58,7 @@ public class Snapshot1PictureRecorder extends PictureRecorder {
// Adding EXIF to a byte array, unfortunately, is hard. // Adding EXIF to a byte array, unfortunately, is hard.
final int sensorToOutput = mResult.rotation; final int sensorToOutput = mResult.rotation;
final Size outputSize = mResult.size; final Size outputSize = mResult.size;
final Size previewStreamSize = mEngine1.getPreviewStreamSize(CameraEngine.REF_SENSOR); final Size previewStreamSize = mEngine1.getPreviewStreamSize(Reference.SENSOR);
if (previewStreamSize == null) { if (previewStreamSize == null) {
throw new IllegalStateException("Preview stream size should never be null here."); throw new IllegalStateException("Preview stream size should never be null here.");
} }

@ -4,7 +4,6 @@ import android.annotation.TargetApi;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.opengl.EGL14; import android.opengl.EGL14;
import android.opengl.EGLContext; import android.opengl.EGLContext;
import android.opengl.Matrix; import android.opengl.Matrix;
@ -13,14 +12,13 @@ import android.os.Build;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.engine.Camera1Engine;
import com.otaliastudios.cameraview.engine.CameraEngine; import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.internal.egl.EglCore; import com.otaliastudios.cameraview.internal.egl.EglCore;
import com.otaliastudios.cameraview.internal.egl.EglViewport; import com.otaliastudios.cameraview.internal.egl.EglViewport;
import com.otaliastudios.cameraview.internal.egl.EglWindowSurface; import com.otaliastudios.cameraview.internal.egl.EglWindowSurface;
import com.otaliastudios.cameraview.internal.utils.CropHelper; import com.otaliastudios.cameraview.internal.utils.CropHelper;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler; import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
import com.otaliastudios.cameraview.preview.CameraPreview;
import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.preview.RendererFrameCallback; import com.otaliastudios.cameraview.preview.RendererFrameCallback;
import com.otaliastudios.cameraview.preview.RendererThread; import com.otaliastudios.cameraview.preview.RendererThread;
@ -107,7 +105,7 @@ public class SnapshotGlPictureRecorder extends PictureRecorder {
// Apply scale and crop: // Apply scale and crop:
// NOTE: scaleX and scaleY are in REF_VIEW, while our input appears to be in REF_SENSOR. // NOTE: scaleX and scaleY are in REF_VIEW, while our input appears to be in REF_SENSOR.
boolean flip = mEngine.flip(CameraEngine.REF_VIEW, CameraEngine.REF_SENSOR); boolean flip = mEngine.getAngles().flip(Reference.VIEW, Reference.SENSOR);
float realScaleX = flip ? scaleY : scaleX; float realScaleX = flip ? scaleY : scaleX;
float realScaleY = flip ? scaleX : scaleY; float realScaleY = flip ? scaleX : scaleY;
float scaleTranslX = (1F - realScaleX) / 2F; float scaleTranslX = (1F - realScaleX) / 2F;

@ -190,7 +190,7 @@ public class GlCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture
// See TextureCameraPreview.setDrawRotation() for info. // See TextureCameraPreview.setDrawRotation() for info.
if (mDrawRotation != 0) { if (mDrawRotation != 0) {
Matrix.translateM(mTransformMatrix, 0, 0.5F, 0.5F, 0); Matrix.translateM(mTransformMatrix, 0, 0.5F, 0.5F, 0);
Matrix.rotateM(mTransformMatrix, 0, -mDrawRotation, 0, 0, 1); Matrix.rotateM(mTransformMatrix, 0, mDrawRotation, 0, 0, 1);
Matrix.translateM(mTransformMatrix, 0, -0.5F, -0.5F, 0); Matrix.translateM(mTransformMatrix, 0, -0.5F, -0.5F, 0);
} }

@ -140,7 +140,7 @@ public class TextureCameraPreview extends CameraPreview<TextureView, SurfaceText
float scaleX = (float) mOutputSurfaceHeight / mOutputSurfaceWidth; float scaleX = (float) mOutputSurfaceHeight / mOutputSurfaceWidth;
matrix.postScale(scaleX, 1F / scaleX, outputCenterX, outputCenterY); matrix.postScale(scaleX, 1F / scaleX, outputCenterX, outputCenterY);
} }
matrix.postRotate((float) -drawRotation, outputCenterX, outputCenterY); matrix.postRotate((float) drawRotation, outputCenterX, outputCenterY);
getView().setTransform(matrix); getView().setTransform(matrix);
task.setResult(null); task.setResult(null);
} }

@ -4,11 +4,11 @@ import android.graphics.SurfaceTexture;
import android.opengl.EGL14; import android.opengl.EGL14;
import android.os.Build; import android.os.Build;
import com.otaliastudios.cameraview.CameraException;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.engine.CameraEngine; import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.preview.RendererFrameCallback; import com.otaliastudios.cameraview.preview.RendererFrameCallback;
import com.otaliastudios.cameraview.preview.RendererThread; import com.otaliastudios.cameraview.preview.RendererThread;
@ -58,7 +58,7 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
@Override @Override
protected void onStart() { protected void onStart() {
mPreview.addRendererFrameCallback(this); mPreview.addRendererFrameCallback(this);
mFlipped = mEngine.flip(CameraEngine.REF_SENSOR, CameraEngine.REF_VIEW); mFlipped = mEngine.getAngles().flip(Reference.SENSOR, Reference.VIEW);
mDesiredState = STATE_RECORDING; mDesiredState = STATE_RECORDING;
} }
@ -128,7 +128,10 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
TextureMediaEncoder.TextureFrame textureFrame = textureEncoder.acquireFrame(); TextureMediaEncoder.TextureFrame textureFrame = textureEncoder.acquireFrame();
textureFrame.timestamp = surfaceTexture.getTimestamp(); textureFrame.timestamp = surfaceTexture.getTimestamp();
surfaceTexture.getTransformMatrix(textureFrame.transform); surfaceTexture.getTransformMatrix(textureFrame.transform);
mEncoderEngine.notify(TextureMediaEncoder.FRAME_EVENT, textureFrame); if (mEncoderEngine != null) {
// can happen on teardown
mEncoderEngine.notify(TextureMediaEncoder.FRAME_EVENT, textureFrame);
}
} }
if (mCurrentState == STATE_RECORDING && mDesiredState == STATE_NOT_RECORDING) { if (mCurrentState == STATE_RECORDING && mDesiredState == STATE_NOT_RECORDING) {

@ -17,7 +17,7 @@
android:layout_marginBottom="88dp" android:layout_marginBottom="88dp"
android:keepScreenOn="true" android:keepScreenOn="true"
app:cameraExperimental="true" app:cameraExperimental="true"
app:cameraEngine="camera2" app:cameraEngine="camera1"
app:cameraPreview="glSurface" app:cameraPreview="glSurface"
app:cameraPlaySounds="true" app:cameraPlaySounds="true"
app:cameraGrid="off" app:cameraGrid="off"

Loading…
Cancel
Save