Refactor preview package

pull/482/head
Mattia Iavarone 6 years ago
parent 75b417b88f
commit 3815d317bd
  1. 12
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java
  2. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotPictureRecorder.java
  3. 180
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java
  4. 85
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java
  5. 32
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/RendererFrameCallback.java
  6. 3
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/RendererThread.java
  7. 16
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreview.java
  8. 22
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/TextureCameraPreview.java
  9. 3
      cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java

@ -11,7 +11,7 @@ import java.nio.FloatBuffer;
/**
* This is a mix of 3 grafika classes, FullFrameRect, Texture2dProgram, Drawable2d.
*/
class EglViewport extends EglElement {
public class EglViewport extends EglElement {
private final static CameraLogger LOG = CameraLogger.create(EglViewport.class.getSimpleName());
@ -74,7 +74,7 @@ class EglViewport extends EglElement {
// private int muTexOffsetLoc; // Used for filtering
// private int muColorAdjustLoc; // Used for filtering
EglViewport() {
public EglViewport() {
mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES;
mProgramHandle = createProgram(SIMPLE_VERTEX_SHADER, SIMPLE_FRAGMENT_SHADER);
maPositionLocation = GLES20.glGetAttribLocation(mProgramHandle, "aPosition");
@ -90,16 +90,16 @@ class EglViewport extends EglElement {
}
void release(boolean doEglCleanup) {
public void release(boolean doEglCleanup) {
if (doEglCleanup) GLES20.glDeleteProgram(mProgramHandle);
mProgramHandle = -1;
}
void release() {
public void release() {
release(true);
}
int createTexture() {
public int createTexture() {
int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0);
check("glGenTextures");
@ -117,7 +117,7 @@ class EglViewport extends EglElement {
return texId;
}
void drawFrame(int textureId, float[] textureMatrix) {
public void drawFrame(int textureId, float[] textureMatrix) {
drawFrame(textureId, textureMatrix,
mVertexCoordinatesArray,
mTextureCoordinatesArray);

@ -25,6 +25,7 @@ import com.otaliastudios.cameraview.internal.utils.RotationHelper;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
import com.otaliastudios.cameraview.preview.CameraPreview;
import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.preview.RendererFrameCallback;
import com.otaliastudios.cameraview.preview.RendererThread;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;
@ -70,7 +71,7 @@ public class SnapshotPictureRecorder extends PictureRecorder {
@TargetApi(Build.VERSION_CODES.KITKAT)
private void takeGl(@NonNull final GlCameraPreview preview) {
preview.addRendererFrameCallback(new GlCameraPreview.RendererFrameCallback() {
preview.addRendererFrameCallback(new RendererFrameCallback() {
int mTextureId;
SurfaceTexture mSurfaceTexture;
@ -89,7 +90,7 @@ public class SnapshotPictureRecorder extends PictureRecorder {
@RendererThread
@Override
public void onRendererFrame(SurfaceTexture surfaceTexture, final float scaleX, final float scaleY) {
public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, final float scaleX, final float scaleY) {
preview.removeRendererFrameCallback(this);
// This kinda work but has drawbacks:

@ -3,12 +3,14 @@ package com.otaliastudios.cameraview.preview;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import android.view.View;
import android.view.ViewGroup;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.Task;
import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.internal.utils.Task;
import com.otaliastudios.cameraview.size.Size;
/**
@ -22,99 +24,176 @@ public abstract class CameraPreview<T extends View, Output> {
protected final static CameraLogger LOG = CameraLogger.create(CameraPreview.class.getSimpleName());
// Used for testing.
Task<Void> mCropTask = new Task<>();
// This is used to notify CameraEngine to recompute its camera Preview size.
// After that, CameraView will need a new layout pass to adapt to the Preview size.
/**
* This is used to notify CameraEngine to recompute its camera Preview size.
* After that, CameraView will need a new layout pass to adapt to the Preview size.
*/
public interface SurfaceCallback {
/**
* Called when the surface is available.
*/
void onSurfaceAvailable();
/**
* Called when the surface has changed.
*/
void onSurfaceChanged();
/**
* Called when the surface was destroyed.
*/
void onSurfaceDestroyed();
}
@VisibleForTesting Task<Void> mCropTask = new Task<>();
private SurfaceCallback mSurfaceCallback;
private T mView;
protected boolean mCropping;
boolean mCropping;
// These are the surface dimensions in REF_VIEW.
protected int mOutputSurfaceWidth;
protected int mOutputSurfaceHeight;
int mOutputSurfaceWidth;
int mOutputSurfaceHeight;
// These are the preview stream dimensions, in REF_VIEW.
protected int mInputStreamWidth;
protected int mInputStreamHeight;
protected boolean mInputFlipped;
int mInputStreamWidth;
int mInputStreamHeight;
private boolean mInputFlipped;
CameraPreview(@NonNull Context context, @NonNull ViewGroup parent, @Nullable SurfaceCallback callback) {
/**
* Creates a new preview.
* @param context a context
* @param parent where to inflate our view
* @param callback the callback
*/
public CameraPreview(@NonNull Context context, @NonNull ViewGroup parent, @Nullable SurfaceCallback callback) {
mView = onCreateView(context, parent);
mSurfaceCallback = callback;
}
/**
* Sets a callback to be notified of surface events (creation, change, destruction)
* @param callback a callback
*/
public final void setSurfaceCallback(@NonNull SurfaceCallback callback) {
mSurfaceCallback = callback;
// If surface already available, dispatch.
if (mOutputSurfaceWidth != 0 || mOutputSurfaceHeight != 0) {
mSurfaceCallback.onSurfaceAvailable();
}
}
/**
* Called at creation time. Implementors should inflate the hierarchy into the
* parent ViewGroup, and return the View that actually hosts the surface.
*
* @param context a context
* @param parent where to inflate
* @return the view hosting the Surface
*/
@NonNull
protected abstract T onCreateView(@NonNull Context context, @NonNull ViewGroup parent);
/**
* Returns the view hosting the Surface.
* @return the view
*/
@NonNull
public final T getView() {
return mView;
}
// Only used in tests
/**
* For testing purposes, should return the root view that was inflated into the
* parent during {@link #onCreateView(Context, ViewGroup)}.
* @return the root view
*/
@SuppressWarnings("unused")
@VisibleForTesting
@NonNull
abstract View getRootView();
/**
* Returns the output surface object (for example a SurfaceHolder
* or a SurfaceTexture).
* @return the surface object
*/
@NonNull
public abstract Class<Output> getOutputClass();
public abstract Output getOutput();
/**
* Returns the type of the output returned by {@link #getOutput()}.
* @return the output type
*/
@NonNull
public abstract Output getOutput();
public abstract Class<Output> getOutputClass();
// 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.
/**
* Called to notify the preview of the input stream size. The width and height must be
* rotated before calling this, if needed, to be consistent with the VIEW reference.
*
* @param width width of the preview stream, in view coordinates
* @param height height of the preview stream, in view coordinates
* @param wasFlipped whether width and height were flipped before calling this
*/
public void setStreamSize(int width, int height, boolean wasFlipped) {
LOG.i("setStreamSize:", "desiredW=", width, "desiredH=", height);
mInputStreamWidth = width;
mInputStreamHeight = height;
mInputFlipped = wasFlipped;
if (mInputStreamWidth > 0 && mInputStreamHeight > 0) {
crop();
crop(mCropTask);
}
}
/**
* Returns the current input stream size, in view coordinates.
* @return the current input stream size
*/
@SuppressWarnings("unused")
@NonNull
final Size getStreamSize() {
return new Size(mInputStreamWidth, mInputStreamHeight);
}
/**
* Returns the current output surface size, in view coordinates.
* @return the current output surface size.
*/
@NonNull
public final Size getSurfaceSize() {
return new Size(mOutputSurfaceWidth, mOutputSurfaceHeight);
}
public final void setSurfaceCallback(@NonNull SurfaceCallback callback) {
mSurfaceCallback = callback;
// If surface already available, dispatch.
if (mOutputSurfaceWidth != 0 || mOutputSurfaceHeight != 0) {
mSurfaceCallback.onSurfaceAvailable();
}
/**
* Whether we have a valid surface already.
* @return whether we have a surface
*/
public final boolean hasSurface() {
return mOutputSurfaceWidth > 0 && mOutputSurfaceHeight > 0;
}
/**
* Subclasses can call this to notify that the surface is available.
* @param width surface width
* @param height surface height
*/
@SuppressWarnings("WeakerAccess")
protected final void dispatchOnSurfaceAvailable(int width, int height) {
LOG.i("dispatchOnSurfaceAvailable:", "w=", width, "h=", height);
mOutputSurfaceWidth = width;
mOutputSurfaceHeight = height;
if (mOutputSurfaceWidth > 0 && mOutputSurfaceHeight > 0) {
crop();
crop(mCropTask);
}
mSurfaceCallback.onSurfaceAvailable();
}
// As far as I can see, these are the view/surface dimensions.
// This is called by subclasses.
/**
* Subclasses can call this to notify that the surface has changed.
* @param width surface width
* @param height surface height
*/
@SuppressWarnings("WeakerAccess")
protected final void dispatchOnSurfaceSizeChanged(int width, int height) {
LOG.i("dispatchOnSurfaceSizeChanged:", "w=", width, "h=", height);
@ -122,12 +201,15 @@ public abstract class CameraPreview<T extends View, Output> {
mOutputSurfaceWidth = width;
mOutputSurfaceHeight = height;
if (width > 0 && height > 0) {
crop();
crop(mCropTask);
}
mSurfaceCallback.onSurfaceChanged();
}
}
/**
* Subclasses can call this to notify that the surface has been destroyed.
*/
@SuppressWarnings("WeakerAccess")
protected final void dispatchOnSurfaceDestroyed() {
mOutputSurfaceWidth = 0;
@ -135,19 +217,24 @@ public abstract class CameraPreview<T extends View, Output> {
mSurfaceCallback.onSurfaceDestroyed();
}
// Public for mockito (CameraViewTest)
/**
* Called by the hosting {@link com.otaliastudios.cameraview.CameraView},
* this is a lifecycle event.
*/
public void onResume() {}
// Public for mockito (CameraViewTest)
/**
* Called by the hosting {@link com.otaliastudios.cameraview.CameraView},
* this is a lifecycle event.
*/
public void onPause() {}
// Public for mockito (CameraViewTest)
/**
* Called by the hosting {@link com.otaliastudios.cameraview.CameraView},
* this is a lifecycle event.
*/
public void onDestroy() {}
public final boolean hasSurface() {
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 (mOutputSurfaceWidth / mOutputSurfaceHeight)
@ -156,22 +243,27 @@ public abstract class CameraPreview<T extends View, Output> {
* There might still be some absolute difference (e.g. same ratio but bigger / smaller).
* However that should be already managed by the framework.
*/
protected void crop() {
protected void crop(@NonNull Task<Void> task) {
// The base implementation does not support cropping.
mCropTask.start();
mCropTask.end(null);
task.start();
task.end(null);
}
/**
* Whether this preview implementation supports cropping.
* The base implementation does not, but it is strongly recommended to do so.
* @return true if cropping is supported
*/
public boolean supportsCropping() {
return false;
}
/**
* Whether we are cropping the output.
* Whether we are currently cropping the output.
* If false, this means that the output image will match the visible bounds.
* @return true if cropping
*/
/* not final for tests */ boolean isCropping() {
public boolean isCropping() {
return mCropping;
}
}

@ -6,16 +6,16 @@ import android.opengl.GLSurfaceView;
import android.opengl.Matrix;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import android.view.LayoutInflater;
import android.view.SurfaceHolder;
import android.view.View;
import android.view.ViewGroup;
import com.otaliastudios.cameraview.EglViewport;
import com.otaliastudios.cameraview.R;
import com.otaliastudios.cameraview.RendererThread;
import com.otaliastudios.cameraview.preview.CameraPreview;
import com.otaliastudios.cameraview.internal.egl.EglViewport;
import com.otaliastudios.cameraview.internal.utils.Task;
import com.otaliastudios.cameraview.size.AspectRatio;
import java.util.Collections;
@ -65,11 +65,34 @@ public class GlCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture
private SurfaceTexture mInputSurfaceTexture;
private EglViewport mOutputViewport;
private Set<RendererFrameCallback> mRendererFrameCallbacks = Collections.synchronizedSet(new HashSet<RendererFrameCallback>());
/* for tests */ float mScaleX = 1F;
/* for tests */ float mScaleY = 1F;
@VisibleForTesting float mScaleX = 1F;
@VisibleForTesting float mScaleY = 1F;
private View mRootView;
/**
* Method specific to the GL preview. Adds a {@link RendererFrameCallback}
* to receive renderer frame events.
* @param callback a callback
*/
public void addRendererFrameCallback(@NonNull final RendererFrameCallback callback) {
getView().queueEvent(new Runnable() {
@Override
public void run() {
mRendererFrameCallbacks.add(callback);
if (mOutputTextureId != 0) callback.onRendererTextureCreated(mOutputTextureId);
}
});
}
/**
* Method specific to the GL preview. Removes a {@link RendererFrameCallback}
* that was previously added to receive renderer frame events.
* @param callback a callback
*/
public void removeRendererFrameCallback(@NonNull final RendererFrameCallback callback) {
mRendererFrameCallbacks.remove(callback);
}
public GlCameraPreview(@NonNull Context context, @NonNull ViewGroup parent, @Nullable SurfaceCallback callback) {
super(context, parent, callback);
}
@ -205,18 +228,18 @@ public class GlCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture
@NonNull
@Override
Class<SurfaceTexture> getOutputClass() {
public Class<SurfaceTexture> getOutputClass() {
return SurfaceTexture.class;
}
@NonNull
@Override
SurfaceTexture getOutput() {
public SurfaceTexture getOutput() {
return mInputSurfaceTexture;
}
@Override
boolean supportsCropping() {
public boolean supportsCropping() {
return true;
}
@ -233,8 +256,8 @@ public class GlCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture
* then drawing it with a scaled transformation matrix. See {@link #onDrawFrame(GL10)}.
*/
@Override
protected void crop() {
mCropTask.start();
protected void crop(@NonNull Task<Void> task) {
task.start();
if (mInputStreamWidth > 0 && mInputStreamHeight > 0 && mOutputSurfaceWidth > 0 && mOutputSurfaceHeight > 0) {
float scaleX = 1f, scaleY = 1f;
AspectRatio current = AspectRatio.of(mOutputSurfaceWidth, mOutputSurfaceHeight);
@ -251,44 +274,6 @@ public class GlCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture
mScaleY = 1F / scaleY;
getView().requestRender();
}
mCropTask.end(null);
}
interface RendererFrameCallback {
/**
* Called on the renderer thread, hopefully only once, to notify that
* the texture was created (or to inform a new callback of the old texture).
*
* @param textureId the GL texture linked to the image stream
*/
@RendererThread
void onRendererTextureCreated(int textureId);
/**
* Called on the renderer thread after each frame was drawn.
* You are not supposed to hold for too long onto this thread, because
* well, it is the rendering thread.
*
* @param surfaceTexture the texture to get transformation
* @param scaleX the scaleX (in REF_VIEW) value
* @param scaleY the scaleY (in REF_VIEW) value
*/
@RendererThread
void onRendererFrame(SurfaceTexture surfaceTexture, float scaleX, float scaleY);
}
void addRendererFrameCallback(@NonNull final RendererFrameCallback callback) {
getView().queueEvent(new Runnable() {
@Override
public void run() {
mRendererFrameCallbacks.add(callback);
if (mOutputTextureId != 0) callback.onRendererTextureCreated(mOutputTextureId);
}
});
}
void removeRendererFrameCallback(@NonNull final RendererFrameCallback callback) {
mRendererFrameCallbacks.remove(callback);
task.end(null);
}
}

@ -0,0 +1,32 @@
package com.otaliastudios.cameraview.preview;
import android.graphics.SurfaceTexture;
import androidx.annotation.NonNull;
/**
* Callback for renderer frames.
*/
public interface RendererFrameCallback {
/**
* Called on the renderer thread, hopefully only once, to notify that
* the texture was created (or to inform a new callback of the old texture).
*
* @param textureId the GL texture linked to the image stream
*/
@RendererThread
void onRendererTextureCreated(int textureId);
/**
* Called on the renderer thread after each frame was drawn.
* You are not supposed to hold for too long onto this thread, because
* well, it is the rendering thread.
*
* @param surfaceTexture the texture to get transformation
* @param scaleX the scaleX (in REF_VIEW) value
* @param scaleY the scaleY (in REF_VIEW) value
*/
@RendererThread
void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, float scaleX, float scaleY);
}

@ -1,3 +1,6 @@
package com.otaliastudios.cameraview.preview;
/**
* Indicates that some action is being executed on the renderer thread.
*/
public @interface RendererThread {}

@ -12,10 +12,13 @@ import android.view.ViewGroup;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.R;
// Fallback preview when hardware acceleration is off.
// Currently this does NOT support cropping (e. g. the crop inside behavior),
// so we return false in supportsCropping() in order to have proper measuring.
// This means that CameraView is forced to be wrap_content.
/**
* This is the fallback preview when hardware acceleration is off, and is the last resort.
* Currently does not support cropping, which means that {@link com.otaliastudios.cameraview.CameraView}
* is forced to be wrap_content.
*
* Do not use.
*/
public class SurfaceCameraPreview extends CameraPreview<SurfaceView, SurfaceHolder> {
private final static CameraLogger LOG = CameraLogger.create(SurfaceCameraPreview.class.getSimpleName());
@ -34,6 +37,7 @@ public class SurfaceCameraPreview extends CameraPreview<SurfaceView, SurfaceHold
parent.addView(root, 0);
SurfaceView surfaceView = root.findViewById(R.id.surface_view);
final SurfaceHolder holder = surfaceView.getHolder();
//noinspection deprecation
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
holder.addCallback(new SurfaceHolder.Callback() {
@ -73,13 +77,13 @@ public class SurfaceCameraPreview extends CameraPreview<SurfaceView, SurfaceHold
@NonNull
@Override
SurfaceHolder getOutput() {
public SurfaceHolder getOutput() {
return getView().getHolder();
}
@NonNull
@Override
Class<SurfaceHolder> getOutputClass() {
public Class<SurfaceHolder> getOutputClass() {
return SurfaceHolder.class;
}

@ -11,9 +11,13 @@ import android.view.View;
import android.view.ViewGroup;
import com.otaliastudios.cameraview.R;
import com.otaliastudios.cameraview.preview.CameraPreview;
import com.otaliastudios.cameraview.internal.utils.Task;
import com.otaliastudios.cameraview.size.AspectRatio;
/**
* A preview implementation based on {@link TextureView}.
* Better than {@link SurfaceCameraPreview} but much less powerful than {@link GlCameraPreview}.
*/
public class TextureCameraPreview extends CameraPreview<TextureView, SurfaceTexture> {
private View mRootView;
@ -62,19 +66,19 @@ public class TextureCameraPreview extends CameraPreview<TextureView, SurfaceText
@NonNull
@Override
Class<SurfaceTexture> getOutputClass() {
public Class<SurfaceTexture> getOutputClass() {
return SurfaceTexture.class;
}
@NonNull
@Override
SurfaceTexture getOutput() {
public SurfaceTexture getOutput() {
return getView().getSurfaceTexture();
}
@TargetApi(15)
@Override
void setStreamSize(int width, int height, boolean wasFlipped) {
public void setStreamSize(int width, int height, boolean wasFlipped) {
super.setStreamSize(width, height, wasFlipped);
if (getView().getSurfaceTexture() != null) {
getView().getSurfaceTexture().setDefaultBufferSize(width, height);
@ -82,19 +86,19 @@ public class TextureCameraPreview extends CameraPreview<TextureView, SurfaceText
}
@Override
boolean supportsCropping() {
public boolean supportsCropping() {
return true;
}
@Override
protected void crop() {
mCropTask.start();
protected void crop(final @NonNull Task<Void> task) {
task.start();
getView().post(new Runnable() {
@Override
public void run() {
if (mInputStreamHeight == 0 || mInputStreamWidth == 0 ||
mOutputSurfaceHeight == 0 || mOutputSurfaceWidth == 0) {
mCropTask.end(null);
task.end(null);
return;
}
float scaleX = 1f, scaleY = 1f;
@ -114,7 +118,7 @@ public class TextureCameraPreview extends CameraPreview<TextureView, SurfaceText
mCropping = scaleX > 1.02f || scaleY > 1.02f;
LOG.i("crop:", "applied scaleX=", scaleX);
LOG.i("crop:", "applied scaleY=", scaleY);
mCropTask.end(null);
task.end(null);
}
});
}

@ -12,6 +12,7 @@ import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.VideoCodec;
import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.preview.RendererFrameCallback;
import com.otaliastudios.cameraview.preview.RendererThread;
import com.otaliastudios.cameraview.size.Size;
@ -23,7 +24,7 @@ import androidx.annotation.RequiresApi;
* A {@link VideoRecorder} that uses {@link android.media.MediaCodec} APIs.
*/
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
public class SnapshotVideoRecorder extends VideoRecorder implements GlCameraPreview.RendererFrameCallback,
public class SnapshotVideoRecorder extends VideoRecorder implements RendererFrameCallback,
MediaEncoderEngine.Listener {
private static final String TAG = SnapshotVideoRecorder.class.getSimpleName();

Loading…
Cancel
Save