Add cropping functionality to GLCameraPreview

pull/360/head
Mattia Iavarone 6 years ago
parent ab576286f3
commit e207041a31
  1. 5
      MIGRATION.md
  2. 20
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraPreviewTest.java
  3. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
  4. 116
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java
  5. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
  6. 47
      cameraview/src/main/views/com/otaliastudios/cameraview/CameraPreview.java
  7. 113
      cameraview/src/main/views/com/otaliastudios/cameraview/GLCameraPreview.java
  8. 13
      cameraview/src/main/views/com/otaliastudios/cameraview/TextureCameraPreview.java
  9. 1
      demo/src/main/java/com/otaliastudios/cameraview/demo/Control.java
  10. 4
      demo/src/main/res/layout/activity_camera.xml

@ -31,4 +31,7 @@
mode == Mode.PICTURE.
- VideoSizeSelector: added. It is needed to choose the capture size in VIDEO mode.
Defaults to SizeSelectors.biggest(), but you can choose by aspect ratio or whatever.
- isTakingPicture(): added on top of isTakingVideo().
- isTakingPicture(): added on top of isTakingVideo().
TODO: improve gles stuff
TODO: takeVideoSnapshot

@ -86,25 +86,25 @@ public abstract class CameraPreviewTest extends BaseTest {
@Test
public void testDesiredSize() {
preview.setDesiredSize(160, 90);
assertEquals(160, preview.getDesiredSize().getWidth());
assertEquals(90, preview.getDesiredSize().getHeight());
preview.setInputStreamSize(160, 90);
assertEquals(160, preview.getInputStreamSize().getWidth());
assertEquals(90, preview.getInputStreamSize().getHeight());
}
@Test
public void testSurfaceAvailable() {
ensureAvailable();
verify(callback, times(1)).onSurfaceAvailable();
assertEquals(surfaceSize.getWidth(), preview.getSurfaceSize().getWidth());
assertEquals(surfaceSize.getHeight(), preview.getSurfaceSize().getHeight());
assertEquals(surfaceSize.getWidth(), preview.getOutputSurfaceSize().getWidth());
assertEquals(surfaceSize.getHeight(), preview.getOutputSurfaceSize().getHeight());
}
@Test
public void testSurfaceDestroyed() {
ensureAvailable();
ensureDestroyed();
assertEquals(0, preview.getSurfaceSize().getWidth());
assertEquals(0, preview.getSurfaceSize().getHeight());
assertEquals(0, preview.getOutputSurfaceSize().getWidth());
assertEquals(0, preview.getOutputSurfaceSize().getHeight());
}
@Test
@ -136,19 +136,19 @@ public abstract class CameraPreviewTest extends BaseTest {
private void setDesiredAspectRatio(float desiredAspectRatio) {
preview.mCropTask.listen();
preview.setDesiredSize((int) (10f * desiredAspectRatio), 10); // Wait...
preview.setInputStreamSize((int) (10f * desiredAspectRatio), 10); // Wait...
preview.mCropTask.await();
assertEquals(desiredAspectRatio, getViewAspectRatioWithScale(), 0.01f);
}
private float getViewAspectRatio() {
Size size = preview.getSurfaceSize();
Size size = preview.getOutputSurfaceSize();
return AspectRatio.of(size.getWidth(), size.getHeight()).toFloat();
}
private float getViewAspectRatioWithScale() {
Size size = preview.getSurfaceSize();
Size size = preview.getOutputSurfaceSize();
int newWidth = (int) (((float) size.getWidth()) * preview.getView().getScaleX());
int newHeight = (int) (((float) size.getHeight()) * preview.getView().getScaleY());
return AspectRatio.of(newWidth, newHeight).toFloat();

@ -8,8 +8,6 @@ import android.graphics.SurfaceTexture;
import android.graphics.YuvImage;
import android.hardware.Camera;
import android.location.Location;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@ -73,7 +71,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
// Preview surface is now available. If camera is open, set up.
@Override
public void onSurfaceAvailable() {
LOG.i("onSurfaceAvailable:", "Size is", mPreview.getSurfaceSize());
LOG.i("onSurfaceAvailable:", "Size is", mPreview.getOutputSurfaceSize());
schedule(null, false, new Runnable() {
@Override
public void run() {
@ -87,7 +85,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
// This requires stopping and restarting the preview.
@Override
public void onSurfaceChanged() {
LOG.i("onSurfaceChanged, size is", mPreview.getSurfaceSize());
LOG.i("onSurfaceChanged, size is", mPreview.getOutputSurfaceSize());
schedule(null, true, new Runnable() {
@Override
public void run() {
@ -139,7 +137,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
mCameraCallbacks.onCameraPreviewSizeChanged();
Size previewSize = getPreviewSize(REF_VIEW);
mPreview.setDesiredSize(previewSize.getWidth(), previewSize.getHeight());
mPreview.setInputStreamSize(previewSize.getWidth(), previewSize.getHeight());
Camera.Parameters params = mCamera.getParameters();
mPreviewFormat = params.getPreviewFormat();

@ -1,116 +0,0 @@
package com.otaliastudios.cameraview;
import android.annotation.TargetApi;
import android.graphics.PointF;
import android.location.Location;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.File;
@TargetApi(21)
class Camera2 extends CameraController {
public Camera2(CameraView.CameraCallbacks callback) {
super(callback);
}
@Override
public void onSurfaceAvailable() {
}
@Override
public void onSurfaceChanged() {
}
@Override
void onStart() {
}
@Override
void onStop() {
}
@Override
void setMode(Mode mode) {
}
@Override
void setFacing(Facing facing) {
}
@Override
void setZoom(float zoom, PointF[] points, boolean notify) {
}
@Override
void setExposureCorrection(float EVvalue, float[] bounds, PointF[] points, boolean notify) {
}
@Override
void setFlash(Flash flash) {
}
@Override
void setWhiteBalance(WhiteBalance whiteBalance) {
}
@Override
void setHdr(Hdr hdr) {
}
@Override
void setAudio(Audio audio) {
}
@Override
void setLocation(Location location) {
}
@Override
void takePicture() {
}
@Override
void takePictureSnapshot(AspectRatio viewAspectRatio) {
}
@Override
void takeVideo(@NonNull File file) {
}
@Override
void stopVideo() {
}
@Override
void startAutoFocus(@Nullable Gesture gesture, PointF point) {
}
@Override
public void onBufferAvailable(byte[] buffer) {
}
@Override
void setPlaySounds(boolean playSounds) {
}
}

@ -4,8 +4,6 @@ import android.graphics.PointF;
import android.location.Location;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
@ -506,7 +504,7 @@ abstract class CameraController implements
// instead of flipping everything to REF_VIEW, we can just flip the
// surface size from REF_VIEW to REF_SENSOR, and reflip at the end.
AspectRatio targetRatio = AspectRatio.of(mCaptureSize.getWidth(), mCaptureSize.getHeight());
Size targetMinSize = mPreview.getSurfaceSize();
Size targetMinSize = mPreview.getOutputSurfaceSize();
boolean flip = flip(REF_VIEW, REF_SENSOR);
if (flip) targetMinSize = targetMinSize.flip();
LOG.i("size:", "computePreviewSize:", "targetRatio:", targetRatio, "targetMinSize:", targetMinSize);

@ -2,7 +2,6 @@ package com.otaliastudios.cameraview;
import android.content.Context;
import android.support.annotation.NonNull;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
@ -25,12 +24,12 @@ abstract class CameraPreview<T extends View, Output> {
protected boolean mCropping;
// These are the surface dimensions in REF_VIEW.
protected int mSurfaceWidth;
protected int mSurfaceHeight;
protected int mOutputSurfaceWidth;
protected int mOutputSurfaceHeight;
// These are the preview stream dimensions, in REF_VIEW.
protected int mDesiredWidth;
protected int mDesiredHeight;
protected int mInputStreamWidth;
protected int mInputStreamHeight;
CameraPreview(Context context, ViewGroup parent, SurfaceCallback callback) {
mView = onCreateView(context, parent);
@ -52,25 +51,25 @@ abstract class CameraPreview<T extends View, Output> {
// As far as I can see, these are the actual preview dimensions, as set in CameraParameters.
// This is called by the CameraImpl.
// These must be alredy rotated, if needed, to be consistent with surface/view sizes.
void setDesiredSize(int width, int height) {
LOG.i("setDesiredSize:", "desiredW=", width, "desiredH=", height);
mDesiredWidth = width;
mDesiredHeight = height;
void setInputStreamSize(int width, int height) {
LOG.i("setInputStreamSize:", "desiredW=", width, "desiredH=", height);
mInputStreamWidth = width;
mInputStreamHeight = height;
crop();
}
final Size getDesiredSize() {
return new Size(mDesiredWidth, mDesiredHeight);
final Size getInputStreamSize() {
return new Size(mInputStreamWidth, mInputStreamHeight);
}
final Size getSurfaceSize() {
return new Size(mSurfaceWidth, mSurfaceHeight);
final Size getOutputSurfaceSize() {
return new Size(mOutputSurfaceWidth, mOutputSurfaceHeight);
}
final void setSurfaceCallback(SurfaceCallback callback) {
mSurfaceCallback = callback;
// If surface already available, dispatch.
if (mSurfaceWidth != 0 || mSurfaceHeight != 0) {
if (mOutputSurfaceWidth != 0 || mOutputSurfaceHeight != 0) {
mSurfaceCallback.onSurfaceAvailable();
}
}
@ -78,8 +77,8 @@ abstract class CameraPreview<T extends View, Output> {
protected final void onSurfaceAvailable(int width, int height) {
LOG.i("onSurfaceAvailable:", "w=", width, "h=", height);
mSurfaceWidth = width;
mSurfaceHeight = height;
mOutputSurfaceWidth = width;
mOutputSurfaceHeight = height;
crop();
mSurfaceCallback.onSurfaceAvailable();
}
@ -89,17 +88,17 @@ abstract class CameraPreview<T extends View, Output> {
// This is called by subclasses.
protected final void onSurfaceSizeChanged(int width, int height) {
LOG.i("onSurfaceSizeChanged:", "w=", width, "h=", height);
if (width != mSurfaceWidth || height != mSurfaceHeight) {
mSurfaceWidth = width;
mSurfaceHeight = height;
if (width != mOutputSurfaceWidth || height != mOutputSurfaceHeight) {
mOutputSurfaceWidth = width;
mOutputSurfaceHeight = height;
crop();
mSurfaceCallback.onSurfaceChanged();
}
}
protected final void onSurfaceDestroyed() {
mSurfaceWidth = 0;
mSurfaceHeight = 0;
mOutputSurfaceWidth = 0;
mOutputSurfaceHeight = 0;
}
void onResume() {}
@ -109,13 +108,13 @@ abstract class CameraPreview<T extends View, Output> {
void onDestroy() {}
final boolean isReady() {
return mSurfaceWidth > 0 && mSurfaceHeight > 0;
return mOutputSurfaceWidth > 0 && mOutputSurfaceHeight > 0;
}
/**
* Here we must crop the visible part by applying a > 1 scale to one of our
* dimensions. This way our internal aspect ratio (mSurfaceWidth / mSurfaceHeight)
* will match the preview size aspect ratio (mDesiredWidth / mDesiredHeight).
* dimensions. This way our internal aspect ratio (mOutputSurfaceWidth / mOutputSurfaceHeight)
* will match the preview size aspect ratio (mInputStreamWidth / mInputStreamHeight).
*
* There might still be some absolute difference (e.g. same ratio but bigger / smaller).
* However that should be already managed by the framework.

@ -3,7 +3,9 @@ package com.otaliastudios.cameraview;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.opengl.GLSurfaceView;
import android.opengl.Matrix;
import android.support.annotation.NonNull;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.SurfaceHolder;
import android.view.View;
@ -12,13 +14,47 @@ import android.view.ViewGroup;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
/**
* - The android camera will stream image to the given {@link SurfaceTexture}.
*
* - in the SurfaceTexture constructor we pass the GL texture handle that we have created.
*
* - The SurfaceTexture is linked to the Camera1 object. It will pass down buffers of data with
* a specified size (that is, the Camera1 preview size).
*
* - When SurfaceTexture.updateTexImage() is called, it will take the latest image from the camera stream
* and update it into the GL texture that was passed.
*
* - Now we have a GL texture referencing data. It must be drawn.
* [Note: it must be drawn using a transformation matrix taken from SurfaceTexture]
*
* - The easy way to render an OpenGL texture is using the {@link GLSurfaceView} class.
* It manages the gl context, hosts a surface and runs a separated rendering thread that will perform
* the rendering.
*
* - As per docs, we ask the GLSurfaceView to delegate rendering to us, using
* {@link GLSurfaceView#setRenderer(GLSurfaceView.Renderer)}. We request a render on the SurfaceView
* anytime the SurfaceTexture notifies that it has new data available (see OnFrameAvailableListener below).
*
* - Everything is linked:
* - The SurfaceTexture has buffers of data of mInputStreamSize
* - The SurfaceView hosts a view (and surface) of size mOutputSurfaceSize
* - We have a GL rich texture to be drawn (in the given method & thread).
*
* TODO
* CROPPING: Managed to do this using Matrix transformation.
* UPDATING: Still bugged: if you change the surface size on the go, the stream is not updated.
* I guess we should create a new texture...
* TAKING PICTURES: Sometime the snapshot takes ages...
* TAKING VIDEOS: Still have not tried...
*/
class GLCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture> implements GLSurfaceView.Renderer {
private boolean mDispatched;
private final float[] mTransformMatrix = new float[16];
private int mTextureId = -1;
private SurfaceTexture mSurfaceTexture;
private GLViewport mViewport;
private int mOutputTextureId = -1;
private SurfaceTexture mInputSurfaceTexture;
private GLViewport mOutputViewport;
GLCameraPreview(Context context, ViewGroup parent, SurfaceCallback callback) {
super(context, parent, callback);
@ -33,9 +69,12 @@ class GLCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture> imple
glView.setEGLContextClientVersion(2);
glView.setRenderer(this);
glView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
// glView.getHolder().setFixedSize(600, 300);
glView.getHolder().addCallback(new SurfaceHolder.Callback() {
public void surfaceCreated(SurfaceHolder holder) {}
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Log.e("GlCameraPreview", "width: " + width + ", height: " + height);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
@ -60,22 +99,26 @@ class GLCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture> imple
@Override
void onDestroy() {
super.onDestroy();
if (mSurfaceTexture != null) {
mSurfaceTexture.release();
mSurfaceTexture = null;
if (mInputSurfaceTexture != null) {
mInputSurfaceTexture.release();
mInputSurfaceTexture = null;
}
if (mViewport != null) {
mViewport.release();
mViewport = null;
if (mOutputViewport != null) {
mOutputViewport.release();
mOutputViewport = null;
}
}
// Renderer thread
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
mViewport = new GLViewport();
mTextureId = mViewport.createTexture();
mSurfaceTexture = new SurfaceTexture(mTextureId);
mSurfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
mOutputViewport = new GLViewport();
mOutputTextureId = mOutputViewport.createTexture();
mInputSurfaceTexture = new SurfaceTexture(mOutputTextureId);
// Since we are using GLSurfaceView.RENDERMODE_WHEN_DIRTY, we must notify the SurfaceView
// of dirtyness, so that it draws again. This is how it's done.
mInputSurfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
// requestRender is thread-safe.
@ -84,6 +127,7 @@ class GLCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture> imple
});
}
// Renderer thread
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
if (!mDispatched) {
@ -99,15 +143,16 @@ class GLCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture> imple
public void onDrawFrame(GL10 gl) {
// Latch the latest frame. If there isn't anything new,
// we'll just re-use whatever was there before.
mSurfaceTexture.updateTexImage();
if (mDesiredWidth <= 0 || mDesiredHeight <= 0) {
mInputSurfaceTexture.updateTexImage();
if (mInputStreamWidth <= 0 || mInputStreamHeight <= 0) {
// Skip drawing. Camera was not opened.
return;
}
// Draw the video frame.
mSurfaceTexture.getTransformMatrix(mTransformMatrix);
mViewport.drawFrame(mTextureId, mTransformMatrix);
mInputSurfaceTexture.getTransformMatrix(mTransformMatrix);
Matrix.scaleM(mTransformMatrix, 0, mScaleX, mScaleY, 1);
mOutputViewport.drawFrame(mOutputTextureId, mTransformMatrix);
}
@Override
@ -117,7 +162,7 @@ class GLCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture> imple
@Override
SurfaceTexture getOutput() {
return mSurfaceTexture;
return mInputSurfaceTexture;
}
@ -126,9 +171,39 @@ class GLCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture> imple
return true;
}
private float mScaleX = 1F;
private float mScaleY = 1F;
/**
* To crop in GL, we could actually use view.setScaleX and setScaleY, but only from Android N onward.
* See documentation: https://developer.android.com/reference/android/view/SurfaceView
*
* Note: Starting in platform version Build.VERSION_CODES.N, SurfaceView's window position is updated
* synchronously with other View rendering. This means that translating and scaling a SurfaceView on
* screen will not cause rendering artifacts. Such artifacts may occur on previous versions of the
* platform when its window is positioned asynchronously.
*
* But to support older platforms, this seem to work - computing scale values and requesting a new frame,
* then drawing it with a scaled transformation matrix. See {@link #onDrawFrame(GL10)}.
*/
@Override
protected void crop() {
mCropTask.start();
if (mInputStreamWidth > 0 && mInputStreamHeight > 0 && mOutputSurfaceWidth > 0 && mOutputSurfaceHeight > 0) {
float scaleX = 1f, scaleY = 1f;
AspectRatio current = AspectRatio.of(mOutputSurfaceWidth, mOutputSurfaceHeight);
AspectRatio target = AspectRatio.of(mInputStreamWidth, mInputStreamHeight);
if (current.toFloat() >= target.toFloat()) {
// We are too short. Must increase height.
scaleY = current.toFloat() / target.toFloat();
} else {
// We must increase width.
scaleX = target.toFloat() / current.toFloat();
}
mScaleX = 1F / scaleX;
mScaleY = 1F / scaleY;
getView().requestRender();
}
mCropTask.end(null);
}
}

@ -5,7 +5,6 @@ import android.content.Context;
import android.graphics.SurfaceTexture;
import android.support.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
@ -59,8 +58,8 @@ class TextureCameraPreview extends CameraPreview<TextureView, SurfaceTexture> {
@TargetApi(15)
@Override
void setDesiredSize(int width, int height) {
super.setDesiredSize(width, height);
void setInputStreamSize(int width, int height) {
super.setInputStreamSize(width, height);
if (getView().getSurfaceTexture() != null) {
getView().getSurfaceTexture().setDefaultBufferSize(width, height);
}
@ -77,14 +76,14 @@ class TextureCameraPreview extends CameraPreview<TextureView, SurfaceTexture> {
getView().post(new Runnable() {
@Override
public void run() {
if (mDesiredHeight == 0 || mDesiredWidth == 0 ||
mSurfaceHeight == 0 || mSurfaceWidth == 0) {
if (mInputStreamHeight == 0 || mInputStreamWidth == 0 ||
mOutputSurfaceHeight == 0 || mOutputSurfaceWidth == 0) {
mCropTask.end(null);
return;
}
float scaleX = 1f, scaleY = 1f;
AspectRatio current = AspectRatio.of(mSurfaceWidth, mSurfaceHeight);
AspectRatio target = AspectRatio.of(mDesiredWidth, mDesiredHeight);
AspectRatio current = AspectRatio.of(mOutputSurfaceWidth, mOutputSurfaceHeight);
AspectRatio target = AspectRatio.of(mInputStreamWidth, mInputStreamHeight);
if (current.toFloat() >= target.toFloat()) {
// We are too short. Must increase height.
scaleY = current.toFloat() / target.toFloat();

@ -63,6 +63,7 @@ public enum Control {
int boundary = this == WIDTH ? root.getWidth() : root.getHeight();
if (boundary == 0) boundary = 1000;
int step = boundary / 10;
list.add(this == WIDTH ? 300 : 600);
list.add(ViewGroup.LayoutParams.WRAP_CONTENT);
list.add(ViewGroup.LayoutParams.MATCH_PARENT);
for (int i = step; i < boundary; i += step) {

@ -11,8 +11,8 @@
<com.otaliastudios.cameraview.CameraView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/camera"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="300px"
android:layout_height="600px"
android:layout_gravity="center"
android:keepScreenOn="true"
app:cameraExperimental="true"

Loading…
Cancel
Save