From 3815d317bd9d3f87995dec45600c735b30478670 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Fri, 21 Jun 2019 16:52:54 -0500 Subject: [PATCH] Refactor preview package --- .../cameraview/internal/egl/EglViewport.java | 12 +- .../picture/SnapshotPictureRecorder.java | 5 +- .../cameraview/preview/CameraPreview.java | 180 +++++++++++++----- .../cameraview/preview/GlCameraPreview.java | 85 ++++----- .../preview/RendererFrameCallback.java | 32 ++++ .../cameraview/preview/RendererThread.java | 3 + .../preview/SurfaceCameraPreview.java | 16 +- .../preview/TextureCameraPreview.java | 22 ++- .../video/SnapshotVideoRecorder.java | 3 +- 9 files changed, 240 insertions(+), 118 deletions(-) create mode 100644 cameraview/src/main/java/com/otaliastudios/cameraview/preview/RendererFrameCallback.java diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java index 314cf468..9e8da8b8 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.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); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotPictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotPictureRecorder.java index f420f53d..cfe39b5f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotPictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotPictureRecorder.java @@ -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: diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java index c31d7ecf..27bb9375 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java @@ -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 { protected final static CameraLogger LOG = CameraLogger.create(CameraPreview.class.getSimpleName()); - // Used for testing. - Task 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 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 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 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 { 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 { 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 { * 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 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; } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java index 7ac07836..0fe71ac1 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java @@ -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 mRendererFrameCallbacks = Collections.synchronizedSet(new HashSet()); - /* 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 getOutputClass() { + public Class 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 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 { private final static CameraLogger LOG = CameraLogger.create(SurfaceCameraPreview.class.getSimpleName()); @@ -34,6 +37,7 @@ public class SurfaceCameraPreview extends CameraPreview getOutputClass() { + public Class getOutputClass() { return SurfaceHolder.class; } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/TextureCameraPreview.java b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/TextureCameraPreview.java index 9c42302f..bfd47adf 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/TextureCameraPreview.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/TextureCameraPreview.java @@ -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 { private View mRootView; @@ -62,19 +66,19 @@ public class TextureCameraPreview extends CameraPreview getOutputClass() { + public Class 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 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 1.02f || scaleY > 1.02f; LOG.i("crop:", "applied scaleX=", scaleX); LOG.i("crop:", "applied scaleY=", scaleY); - mCropTask.end(null); + task.end(null); } }); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java index 1c500c66..78808139 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java @@ -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();