From c7edfe940815ff0310e9185cf307b29a5f4673aa Mon Sep 17 00:00:00 2001 From: Giacomo Randazzo Date: Fri, 12 Jul 2019 17:19:55 +0200 Subject: [PATCH] Overlays (#421) * get overlay working * fix overlay drawing * allow disabling overlay in pictures or videos * Fix picture snapshot colors when there is an overlay * Bug fixes * Update example with watermark * Fix bug * Fix overlay orientation in pictures * Fix overlay orientation in videos * Fix overlay when changing preview size * Fix bug * Experiment * Refactor EglViewport * Refactor SnapshotPictureRecorder * Use single EglViewport * Refactor SnapshotVideoRecorder * Bug fix * fix some of the requested changes * clean adding View to OverlayLayout * Specify where to draw the overlay * Refactor * Remove unnecessary variable from CameraPreview * Use mWithOverlay in SnapshotVideoRecorder * Use multiple OverlayLayout * Add explanation for OverlayLayoutManager * override removeView --- .../cameraview/CameraOptions.java | 23 +++ .../otaliastudios/cameraview/CameraView.java | 117 ++++++++++++++ .../cameraview/DisableOverlayFor.java | 57 +++++++ .../cameraview/OverlayLayout.java | 27 ++++ .../cameraview/OverlayLayoutManager.java | 145 ++++++++++++++++++ .../cameraview/SurfaceDrawer.java | 8 + .../cameraview/engine/Camera1Engine.java | 4 +- .../cameraview/engine/Camera2Engine.java | 4 +- .../cameraview/engine/CameraEngine.java | 33 ++++ .../cameraview/internal/egl/EglViewport.java | 6 + .../picture/SnapshotGlPictureRecorder.java | 80 +++++++++- .../cameraview/preview/GlCameraPreview.java | 2 +- .../preview/SurfaceCameraPreview.java | 2 +- .../preview/TextureCameraPreview.java | 2 +- .../video/SnapshotVideoRecorder.java | 56 ++++++- .../video/encoding/TextureMediaEncoder.java | 15 ++ cameraview/src/main/res/values/attrs.xml | 8 + .../cameraview/demo/CameraActivity.java | 4 + demo/src/main/res/layout/activity_camera.xml | 26 +++- 19 files changed, 608 insertions(+), 11 deletions(-) create mode 100644 cameraview/src/main/java/com/otaliastudios/cameraview/DisableOverlayFor.java create mode 100644 cameraview/src/main/java/com/otaliastudios/cameraview/OverlayLayout.java create mode 100644 cameraview/src/main/java/com/otaliastudios/cameraview/OverlayLayoutManager.java create mode 100644 cameraview/src/main/java/com/otaliastudios/cameraview/SurfaceDrawer.java diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java index c10abfc8..ac47f247 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java @@ -55,6 +55,7 @@ public class CameraOptions { private Set supportedVideoSizes = new HashSet<>(5); private Set supportedPictureAspectRatio = new HashSet<>(4); private Set supportedVideoAspectRatio = new HashSet<>(3); + private Set supportedDisableOverlayFor = new HashSet<>(3); private boolean zoomSupported; private boolean exposureCorrectionSupported; @@ -147,6 +148,12 @@ public class CameraOptions { supportedVideoAspectRatio.add(AspectRatio.of(width, height)); } } + + + // Disable overlay for + supportedDisableOverlayFor.add(DisableOverlayFor.NONE); + supportedDisableOverlayFor.add(DisableOverlayFor.PICTURE); + supportedDisableOverlayFor.add(DisableOverlayFor.VIDEO); } // Camera2Engine constructor. @@ -312,6 +319,8 @@ public class CameraOptions { return (Collection) Arrays.asList(Engine.values()); } else if (controlClass.equals(Preview.class)) { return (Collection) Arrays.asList(Preview.values()); + } else if (controlClass.equals(DisableOverlayFor.class)) { + return (Collection) getSupportedDisableOverlayFor(); } // Unrecognized control. return Collections.emptyList(); @@ -486,4 +495,18 @@ public class CameraOptions { public float getExposureCorrectionMaxValue() { return exposureCorrectionMaxValue; } + + /** + * Set of supported mode values for which the overlay can be disabled. + * + * @see DisableOverlayFor#NONE + * @see DisableOverlayFor#PICTURE + * @see DisableOverlayFor#VIDEO + * @return a collection of supported values. + */ + @SuppressWarnings("WeakerAccess") + @NonNull + public Collection getSupportedDisableOverlayFor() { + return Collections.unmodifiableSet(supportedDisableOverlayFor); + } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index 2c6292fa..d9d9b363 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -28,6 +28,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import android.util.AttributeSet; import android.view.MotionEvent; +import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -131,6 +132,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver { @SuppressWarnings({"FieldCanBeLocal", "unused"}) private boolean mExperimental; + // Overlays + private OverlayLayoutManager mOverlayLayoutManager; // see OverlayLayoutManager for why having two of them + private OverlayLayoutManager mOverlayLayoutManagerBelow; + // Threading private Handler mUiHandler; private WorkerHandler mFrameProcessorsHandler; @@ -187,9 +192,13 @@ public class CameraView extends FrameLayout implements LifecycleObserver { // Views mGridLinesLayout = new GridLinesLayout(context); + mOverlayLayoutManager = new OverlayLayoutManager(context); + mOverlayLayoutManagerBelow = new OverlayLayoutManager(context); mMarkerLayout = new MarkerLayout(context); addView(mGridLinesLayout); addView(mMarkerLayout); + addView(mOverlayLayoutManager); + addView(mOverlayLayoutManagerBelow, 0); // put it at the bottom of the FrameLayout // Create the engine doInstantiateEngine(); @@ -300,9 +309,14 @@ public class CameraView extends FrameLayout implements LifecycleObserver { protected void onAttachedToWindow() { super.onAttachedToWindow(); if (mCameraPreview == null) { + // isHardwareAccelerated will return the real value only after we are // attached. That's why we instantiate the preview here. doInstantiatePreview(); + mCameraEngine.addPictureSurfaceDrawer(mOverlayLayoutManager); + mCameraEngine.addPictureSurfaceDrawer(mOverlayLayoutManagerBelow); + mCameraEngine.addVideoSurfaceDrawer(mOverlayLayoutManager); + mCameraEngine.addVideoSurfaceDrawer(mOverlayLayoutManagerBelow); } if (!isInEditMode()) { mOrientationHelper.enable(getContext()); @@ -317,6 +331,49 @@ public class CameraView extends FrameLayout implements LifecycleObserver { super.onDetachedFromWindow(); } + @Override + public void addView(View child, ViewGroup.LayoutParams params) { + if (params instanceof OverlayLayoutParams) { + if (((OverlayLayoutParams) params).drawInPreview) { + mOverlayLayoutManager.addView(child, params); + } else { + mOverlayLayoutManagerBelow.addView(child, params); + } + } else { + super.addView(child, params); + } + } + + @Override + public void removeView(View child) { + if (child.getLayoutParams() instanceof OverlayLayoutParams) { + if (((OverlayLayoutParams) child.getLayoutParams()).drawInPreview) { + mOverlayLayoutManager.removeView(child); + } else { + mOverlayLayoutManagerBelow.removeView(child); + } + } else { + super.removeView(child); + } + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attributeSet) { + OverlayLayoutParams toBeChecked = new OverlayLayoutParams(this.getContext(), attributeSet); + if (toBeChecked.isOverlay()) { + return toBeChecked; + } + return super.generateLayoutParams(attributeSet); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + if (p instanceof OverlayLayoutParams) { + return p; + } + return super.generateLayoutParams(p); + } + //endregion //region Measuring behavior @@ -1218,6 +1275,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { mCameraEngine.setAutoFocusResetDelay(delayMillis); } + /** * Returns the current delay in milliseconds to reset the focus after an autofocus process. * @return the current autofocus reset delay in milliseconds. @@ -2077,5 +2135,64 @@ public class CameraView extends FrameLayout implements LifecycleObserver { } } + public static class OverlayLayoutParams extends FrameLayout.LayoutParams { + + private boolean drawInPreview = false; + private boolean drawInPictureSnapshot = false; + private boolean drawInVideoSnapshot = false; + + public OverlayLayoutParams(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + this.readStyleParameters(context, attributeSet); + } + + public OverlayLayoutParams(int width, int height) { + super(width, height); + } + + public OverlayLayoutParams(ViewGroup.LayoutParams layoutParams) { + super(layoutParams); + } + + private void readStyleParameters(Context context, AttributeSet attributeSet) { + TypedArray a = context.obtainStyledAttributes(attributeSet, R.styleable.CameraView_Layout); + try { + this.drawInPreview = a.getBoolean(R.styleable.CameraView_Layout_layout_drawInPreview, false); + this.drawInPictureSnapshot = a.getBoolean(R.styleable.CameraView_Layout_layout_drawInPictureSnapshot, false); + this.drawInVideoSnapshot = a.getBoolean(R.styleable.CameraView_Layout_layout_drawInVideoSnapshot, false); + } finally { + a.recycle(); + } + } + + public boolean isDrawInPreview() { + return drawInPreview; + } + + public void setDrawInPreview(boolean drawInPreview) { + this.drawInPreview = drawInPreview; + } + + public boolean isDrawInPictureSnapshot() { + return drawInPictureSnapshot; + } + + public void setDrawInPictureSnapshot(boolean drawInPictureSnapshot) { + this.drawInPictureSnapshot = drawInPictureSnapshot; + } + + public boolean isDrawInVideoSnapshot() { + return drawInVideoSnapshot; + } + + public void setDrawInVideoSnapshot(boolean drawInVideoSnapshot) { + this.drawInVideoSnapshot = drawInVideoSnapshot; + } + + public boolean isOverlay() { + return drawInPreview || drawInPictureSnapshot || drawInVideoSnapshot; + } + } + //endregion } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/DisableOverlayFor.java b/cameraview/src/main/java/com/otaliastudios/cameraview/DisableOverlayFor.java new file mode 100644 index 00000000..1a07d80d --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/DisableOverlayFor.java @@ -0,0 +1,57 @@ +package com.otaliastudios.cameraview; + + +import androidx.annotation.Nullable; + +import com.otaliastudios.cameraview.controls.Control; + +/** + * DisableOverlayFor value allow the user to prevent the overlay from being recorded. + * + * @see CameraView#setDisableOverlayFor(DisableOverlayFor) + */ +public enum DisableOverlayFor implements Control { + + /** + * Record the overlay both in picture and video snapshots. + */ + NONE(0), + + /** + * The picture snapshots will not contain the overlay. + * + * @see CameraOptions#getSupportedDisableOverlayFor() + */ + PICTURE(1), + + + /** + * The picture snapshots will not contain the overlay. + * + * @see CameraOptions#getSupportedDisableOverlayFor() + */ + VIDEO(2); + + static final DisableOverlayFor DEFAULT = NONE; + + private int value; + + DisableOverlayFor(int value) { + this.value = value; + } + + int value() { + return value; + } + + @Nullable + static DisableOverlayFor fromValue(int value) { + DisableOverlayFor[] list = DisableOverlayFor.values(); + for (DisableOverlayFor action : list) { + if (action.value() == value) { + return action; + } + } + return null; + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/OverlayLayout.java b/cameraview/src/main/java/com/otaliastudios/cameraview/OverlayLayout.java new file mode 100644 index 00000000..56e38d99 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/OverlayLayout.java @@ -0,0 +1,27 @@ +package com.otaliastudios.cameraview; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +class OverlayLayout extends FrameLayout { + + public OverlayLayout(@NonNull Context context) { + super(context); + setWillNotDraw(false); + } + + public OverlayLayout(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + setWillNotDraw(false); + } + + public void drawOverlay(Canvas canvas) { + super.draw(canvas); + } + +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/OverlayLayoutManager.java b/cameraview/src/main/java/com/otaliastudios/cameraview/OverlayLayoutManager.java new file mode 100644 index 00000000..713b87de --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/OverlayLayoutManager.java @@ -0,0 +1,145 @@ +package com.otaliastudios.cameraview; + + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * This class manages {@link OverlayLayout}s. + * The necessity for this class comes from two features of {@link View}s: + * - a {@link View} can only have one parent + * - the View framework does not provide a straightforward way for a {@link ViewGroup} to draw + * only a subset of it's children. + * We have three possible target for a overlay {@link View} to be drawn on: + * - camera preview + * - picture snapshot + * - video snapshot + * Given the two constraints above in order to draw exclusively on a subset of targets we need a + * different {@link OverlayLayout} for each subset of targets. This class manages those different + * {@link OverlayLayout}s. + * + * A problem remains: the views are drawn on preview when {@link #draw(Canvas)} is called on this + * class, for not drawing on the preview but drawing on picture snapshot, for instance, we cannot + * change the child's visibility. + * One way to solve this problem is to have two instances of {@link OverlayLayoutManager} and layer + * them so that the one below is covered and hidden by the camera preview. This way only the top + * {@link OverlayLayoutManager} is shown on top of the camera preview and we can still access the + * bottom one's {@link OverlayLayout#draw(Canvas)} for drawing on picture snapshots. + */ +class OverlayLayoutManager extends FrameLayout implements SurfaceDrawer { + + private Map mLayouts = new HashMap<>(); + + public OverlayLayoutManager(@NonNull Context context) { + super(context); + } + + public OverlayLayoutManager(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void addView(View child, ViewGroup.LayoutParams params) { + // params must be instance of OverlayLayoutParams + if (!(params instanceof CameraView.OverlayLayoutParams)) { + return; + } + + OverlayType viewOverlayType = new OverlayType((CameraView.OverlayLayoutParams) params); + if (mLayouts.containsKey(viewOverlayType)) { + mLayouts.get(viewOverlayType).addView(child, params); + } else { + OverlayLayout newLayout = new OverlayLayout(getContext()); + newLayout.addView(child, params); + super.addView(newLayout); + mLayouts.put(viewOverlayType, newLayout); + } + } + + @Override + public void removeView(View child) { + // params must be instance of OverlayLayoutParams + if (!(child.getLayoutParams() instanceof CameraView.OverlayLayoutParams)) { + return; + } + + OverlayType viewOverlayType = new OverlayType((CameraView.OverlayLayoutParams) child.getLayoutParams()); + if (mLayouts.containsKey(viewOverlayType)) { + mLayouts.get(viewOverlayType).removeView(child); + } + } + + @Override + public void drawOnSurfaceForPictureSnapshot(Canvas surfaceCanvas) { + surfaceCanvas.save(); + // scale factor between canvas width and this View's width + float widthScale = surfaceCanvas.getWidth() / (float) getWidth(); + // scale factor between canvas height and this View's height + float heightScale = surfaceCanvas.getHeight() / (float) getHeight(); + surfaceCanvas.scale(widthScale, heightScale); + for (Map.Entry entry : mLayouts.entrySet()) { + if (entry.getKey().pictureSnapshot) { + entry.getValue().drawOverlay(surfaceCanvas); + } + } + surfaceCanvas.restore(); + } + + @Override + public void drawOnSurfaceForVideoSnapshot(Canvas surfaceCanvas) { + surfaceCanvas.save(); + // scale factor between canvas width and this View's width + float widthScale = surfaceCanvas.getWidth() / (float) getWidth(); + // scale factor between canvas height and this View's height + float heightScale = surfaceCanvas.getHeight() / (float) getHeight(); + surfaceCanvas.scale(widthScale, heightScale); + for (Map.Entry entry : mLayouts.entrySet()) { + if (entry.getKey().videoSnapshot) { + entry.getValue().drawOverlay(surfaceCanvas); + } + } + surfaceCanvas.restore(); + } + + private class OverlayType { + boolean preview = false; + boolean pictureSnapshot = false; + boolean videoSnapshot = false; + + OverlayType(CameraView.OverlayLayoutParams params) { + this.preview = params.isDrawInPreview(); + this.pictureSnapshot = params.isDrawInPictureSnapshot(); + this.videoSnapshot = params.isDrawInVideoSnapshot(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OverlayType that = (OverlayType) o; + return preview == that.preview && + pictureSnapshot == that.pictureSnapshot && + videoSnapshot == that.videoSnapshot; + } + + @Override + public int hashCode() { + int result = 0; + result = 31*result + (preview ? 1 : 0); + result = 31*result + (pictureSnapshot ? 1 : 0); + result = 31*result + (videoSnapshot ? 1 : 0); + return result; + } + } + +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/SurfaceDrawer.java b/cameraview/src/main/java/com/otaliastudios/cameraview/SurfaceDrawer.java new file mode 100644 index 00000000..652d7884 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/SurfaceDrawer.java @@ -0,0 +1,8 @@ +package com.otaliastudios.cameraview; + +import android.graphics.Canvas; + +public interface SurfaceDrawer { + void drawOnSurfaceForPictureSnapshot(Canvas surfaceCanvas); + void drawOnSurfaceForVideoSnapshot(Canvas surfaceCanvas); +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java index 0efad6b2..c7e6cec2 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java @@ -312,7 +312,7 @@ public class Camera1Engine extends CameraEngine implements AspectRatio outputRatio = getAngles().flip(Reference.OUTPUT, Reference.VIEW) ? viewAspectRatio.flip() : viewAspectRatio; 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, getPictureSurfaceDrawers()); } else { mPictureRecorder = new Snapshot1PictureRecorder(stub, this, mCamera, outputRatio); } @@ -362,7 +362,7 @@ public class Camera1Engine extends CameraEngine implements stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE); // Start. - mVideoRecorder = new SnapshotVideoRecorder(Camera1Engine.this, glPreview); + mVideoRecorder = new SnapshotVideoRecorder(Camera1Engine.this, glPreview, getVideoSurfaceDrawers()); mVideoRecorder.start(stub); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java index 964eb976..4e397f2c 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java @@ -615,7 +615,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); // Actually it will be rotated and set to 0. AspectRatio outputRatio = getAngles().flip(Reference.OUTPUT, Reference.VIEW) ? viewAspectRatio.flip() : viewAspectRatio; if (mPreview instanceof GlCameraPreview) { - mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio); + mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio, getPictureSurfaceDrawers()); } else { throw new RuntimeException("takePictureSnapshot with Camera2 is only supported with Preview.GL_SURFACE"); } @@ -709,7 +709,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE); // Start. - mVideoRecorder = new SnapshotVideoRecorder(this, glPreview); + mVideoRecorder = new SnapshotVideoRecorder(this, glPreview, getVideoSurfaceDrawers()); mVideoRecorder.start(stub); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java index 9de9388c..bc51c606 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java @@ -18,6 +18,7 @@ import com.otaliastudios.cameraview.CameraException; import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraOptions; import com.otaliastudios.cameraview.PictureResult; +import com.otaliastudios.cameraview.SurfaceDrawer; import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.engine.offset.Angles; import com.otaliastudios.cameraview.engine.offset.Reference; @@ -184,6 +185,38 @@ public abstract class CameraEngine implements private long mAutoFocusResetDelayMillis; 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 final List pictureSurfaceDrawers = new ArrayList<>(); + private final List videoSurfaceDrawers = new ArrayList<>(); + + public void addPictureSurfaceDrawer(@NonNull SurfaceDrawer surfaceDrawer) { + if (!pictureSurfaceDrawers.contains(surfaceDrawer)) { + pictureSurfaceDrawers.add(surfaceDrawer); + } + } + + public void removePictureSurfaceDrawer(@NonNull SurfaceDrawer surfaceDrawer) { + pictureSurfaceDrawers.remove(surfaceDrawer); + } + + @NonNull + protected List getPictureSurfaceDrawers() { + return pictureSurfaceDrawers; + } + + public void addVideoSurfaceDrawer(@NonNull SurfaceDrawer surfaceDrawer) { + if (!videoSurfaceDrawers.contains(surfaceDrawer)) { + videoSurfaceDrawers.add(surfaceDrawer); + } + } + + public void removeVideoSurfaceDrawer(@NonNull SurfaceDrawer surfaceDrawer) { + videoSurfaceDrawers.remove(surfaceDrawer); + } + + @NonNull + protected List getVideoSurfaceDrawers() { + return videoSurfaceDrawers; + } // Steps private final Step.Callback mStepCallback = new Step.Callback() { 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 3496bf95..54114c9d 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 @@ -145,6 +145,12 @@ public class EglViewport extends EglElement { GLES20.glUseProgram(mProgramHandle); check("glUseProgram"); + // enable blending, from: http://www.learnopengles.com/android-lesson-five-an-introduction-to-blending/ + GLES20.glDisable(GLES20.GL_CULL_FACE); + GLES20.glDisable(GLES20.GL_DEPTH_TEST); + GLES20.glEnable(GLES20.GL_BLEND); + GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); + // Set the texture. GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(mTextureTarget, textureId); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java index e969a67a..026939e7 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java @@ -2,6 +2,9 @@ package com.otaliastudios.cameraview.picture; import android.annotation.TargetApi; import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.SurfaceTexture; import android.opengl.EGL14; @@ -11,8 +14,10 @@ import android.os.Build; import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.PictureResult; +import com.otaliastudios.cameraview.SurfaceDrawer; import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.engine.CameraEngine; +import com.otaliastudios.cameraview.engine.offset.Axis; import com.otaliastudios.cameraview.engine.offset.Reference; import com.otaliastudios.cameraview.internal.egl.EglCore; import com.otaliastudios.cameraview.internal.egl.EglViewport; @@ -27,6 +32,9 @@ import com.otaliastudios.cameraview.size.Size; import androidx.annotation.NonNull; +import android.view.Surface; + +import java.util.List; public class SnapshotGlPictureRecorder extends PictureRecorder { @@ -37,15 +45,21 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { private GlCameraPreview mPreview; private AspectRatio mOutputRatio; + private List mSurfaceDrawerList; + private boolean mWithOverlay; + public SnapshotGlPictureRecorder( @NonNull PictureResult.Stub stub, @NonNull CameraEngine engine, @NonNull GlCameraPreview preview, - @NonNull AspectRatio outputRatio) { + @NonNull AspectRatio outputRatio, + @NonNull List surfaceDrawerList) { super(stub, engine); mEngine = engine; mPreview = preview; mOutputRatio = outputRatio; + mWithOverlay = true; + mSurfaceDrawerList = surfaceDrawerList; } @TargetApi(Build.VERSION_CODES.KITKAT) @@ -54,18 +68,35 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { mPreview.addRendererFrameCallback(new RendererFrameCallback() { int mTextureId; + int mOverlayTextureId = 0; SurfaceTexture mSurfaceTexture; + SurfaceTexture mOverlaySurfaceTexture; float[] mTransform; + float[] mOverlayTransform; + EglViewport viewport; @RendererThread public void onRendererTextureCreated(int textureId) { mTextureId = textureId; + viewport = new EglViewport(); + if (mWithOverlay) { + mOverlayTextureId = viewport.createTexture(); + } mSurfaceTexture = new SurfaceTexture(mTextureId, true); + if (mWithOverlay) { + mOverlaySurfaceTexture = new SurfaceTexture(mOverlayTextureId, true); + } // Need to crop the size. Rect crop = CropHelper.computeCrop(mResult.size, mOutputRatio); mResult.size = new Size(crop.width(), crop.height()); mSurfaceTexture.setDefaultBufferSize(mResult.size.getWidth(), mResult.size.getHeight()); + if (mWithOverlay) { + mOverlaySurfaceTexture.setDefaultBufferSize(mResult.size.getWidth(), mResult.size.getHeight()); + } mTransform = new float[16]; + if (mWithOverlay) { + mOverlayTransform = new float[16]; + } } @RendererThread @@ -99,9 +130,25 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { public void run() { EglWindowSurface surface = new EglWindowSurface(core, mSurfaceTexture); surface.makeCurrent(); - EglViewport viewport = new EglViewport(); +// EglViewport viewport = new EglViewport(); mSurfaceTexture.updateTexImage(); mSurfaceTexture.getTransformMatrix(mTransform); + Surface drawOnto = new Surface(mOverlaySurfaceTexture); + if (mWithOverlay) { + try { + final Canvas surfaceCanvas = drawOnto.lockCanvas(null); + surfaceCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + for (SurfaceDrawer surfaceDrawer : mSurfaceDrawerList) { + surfaceDrawer.drawOnSurfaceForPictureSnapshot(surfaceCanvas); + } + + drawOnto.unlockCanvasAndPost(surfaceCanvas); + } catch (Surface.OutOfResourcesException e) { + e.printStackTrace(); + } + mOverlaySurfaceTexture.updateTexImage(); + mOverlaySurfaceTexture.getTransformMatrix(mOverlayTransform); + } // Apply scale and crop: // NOTE: scaleX and scaleY are in REF_VIEW, while our input appears to be in REF_SENSOR. @@ -117,25 +164,50 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { // Not sure why we need the minus here... It makes no sense to me. LOG.w("Recording frame. Rotation:", mResult.rotation, "Actual:", -mResult.rotation); int rotation = -mResult.rotation; + int overlayRotation = mEngine.getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE); // TODO check axis + // apparently with front facing camera with don't need the minus sign + if (mResult.facing == Facing.FRONT) { + overlayRotation = -overlayRotation; + } mResult.rotation = 0; // Go back to 0,0 so that rotate and flip work well. Matrix.translateM(mTransform, 0, 0.5F, 0.5F, 0); + if (mOverlayTransform != null) { + Matrix.translateM(mOverlayTransform, 0, 0.5F, 0.5F, 0); + } // Apply rotation: Matrix.rotateM(mTransform, 0, rotation, 0, 0, 1); + if (mOverlayTransform != null) { + Matrix.rotateM(mOverlayTransform, 0, overlayRotation, 0, 0, 1); + } // Flip horizontally for front camera: if (mResult.facing == Facing.FRONT) { Matrix.scaleM(mTransform, 0, -1, 1, 1); + if (mOverlayTransform != null) { + // not sure why we have to flip the mirror the y axis + Matrix.scaleM(mOverlayTransform, 0, -1, -1, 1); + } + } + if (mOverlayTransform != null) { + // not sure why we have to flip the mirror the y axis + Matrix.scaleM(mOverlayTransform, 0, 1, -1, 1); } // Go back to old position. Matrix.translateM(mTransform, 0, -0.5F, -0.5F, 0); + if (mOverlayTransform != null) { + Matrix.translateM(mOverlayTransform, 0, -0.5F, -0.5F, 0); + } // Future note: passing scale values to the viewport? // They are simply realScaleX and realScaleY. viewport.drawFrame(mTextureId, mTransform); + if (mWithOverlay) { + viewport.drawFrame(mOverlayTextureId, mOverlayTransform); + } // don't - surface.swapBuffers(); mResult.data = surface.saveFrameTo(Bitmap.CompressFormat.JPEG); mResult.format = PictureResult.FORMAT_JPEG; @@ -144,7 +216,11 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { // EGL14.eglMakeCurrent(oldDisplay, oldSurface, oldSurface, eglContext); surface.release(); viewport.release(); + drawOnto.release(); mSurfaceTexture.release(); + if (mOverlaySurfaceTexture != null) { + mOverlaySurfaceTexture.release(); + } core.release(); dispatchResult(); } 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 947114e0..b3b5a63d 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java @@ -91,7 +91,7 @@ public class GlCameraPreview extends CameraPreview mSurfaceDrawerList; + private boolean mWithOverlay; + public SnapshotVideoRecorder(@NonNull CameraEngine engine, - @NonNull GlCameraPreview preview) { + @NonNull GlCameraPreview preview, + @NonNull List surfaceDrawerList) { super(engine); mPreview = preview; mEngine = engine; + + mWithOverlay = true; + mSurfaceDrawerList = surfaceDrawerList; } @Override @@ -72,6 +91,14 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram @Override public void onRendererTextureCreated(int textureId) { mTextureId = textureId; + if (mWithOverlay) { + EglViewport temp = new EglViewport(); + mOverlayTextureId = temp.createTexture(); + + mOverlaySurfaceTexture = new SurfaceTexture(mOverlayTextureId); + mOverlaySurfaceTexture.setDefaultBufferSize(mResult.size.getWidth(), mResult.size.getHeight()); + mOverlaySurface = new Surface(mOverlaySurfaceTexture); + } } @RendererThread @@ -103,6 +130,7 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram mResult.videoFrameRate, mResult.rotation, type, mTextureId, + mWithOverlay ? mOverlayTextureId : 0, scaleX, scaleY, mFlipped, EGL14.eglGetCurrentContext() @@ -129,6 +157,24 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram TextureMediaEncoder.TextureFrame textureFrame = textureEncoder.acquireFrame(); textureFrame.timestamp = surfaceTexture.getTimestamp(); surfaceTexture.getTransformMatrix(textureFrame.transform); + + // get overlay + if (mWithOverlay) { + try { + final Canvas surfaceCanvas = mOverlaySurface.lockCanvas(null); + surfaceCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + for (SurfaceDrawer surfaceDrawer : mSurfaceDrawerList) { + surfaceDrawer.drawOnSurfaceForVideoSnapshot(surfaceCanvas); + } + + mOverlaySurface.unlockCanvasAndPost(surfaceCanvas); + } catch (Surface.OutOfResourcesException e) { + e.printStackTrace(); + } + mOverlaySurfaceTexture.updateTexImage(); + mOverlaySurfaceTexture.getTransformMatrix(textureFrame.overlayTransform); + } + if (mEncoderEngine != null) { // can happen on teardown mEncoderEngine.notify(TextureMediaEncoder.FRAME_EVENT, textureFrame); @@ -142,6 +188,14 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram mEncoderEngine = null; mPreview.removeRendererFrameCallback(SnapshotVideoRecorder.this); mPreview = null; + if (mOverlaySurfaceTexture != null) { + mOverlaySurfaceTexture.release(); + mOverlaySurfaceTexture = null; + } + if (mOverlaySurface != null) { + mOverlaySurface.release(); + mOverlaySurface = null; + } } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java index 8d6652a4..0b7b59fe 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java @@ -27,6 +27,7 @@ public class TextureMediaEncoder extends VideoMediaEncoder + + + + + + + + \ No newline at end of file diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java index 9b888ade..81136d88 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java @@ -11,6 +11,7 @@ import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; +import android.widget.TextView; import android.widget.Toast; import com.otaliastudios.cameraview.CameraException; @@ -206,12 +207,15 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis private void toggleCamera() { if (camera.isTakingPicture() || camera.isTakingVideo()) return; + TextView watermarkTitle = findViewById(R.id.watermark_title); switch (camera.toggleFacing()) { case BACK: + watermarkTitle.setText("Back facing"); message("Switched to back camera!", false); break; case FRONT: + watermarkTitle.setText("Front facing"); message("Switched to front camera!", false); break; } diff --git a/demo/src/main/res/layout/activity_camera.xml b/demo/src/main/res/layout/activity_camera.xml index 03b195b7..87721e2b 100644 --- a/demo/src/main/res/layout/activity_camera.xml +++ b/demo/src/main/res/layout/activity_camera.xml @@ -29,8 +29,32 @@ app:cameraGestureScrollHorizontal="exposureCorrection" app:cameraGestureScrollVertical="none" app:cameraMode="picture" - app:cameraAutoFocusMarker="@string/cameraview_default_autofocus_marker"/> + app:cameraAutoFocusMarker="@string/cameraview_default_autofocus_marker"> + + + + + + +