Address issues

pull/502/head
Mattia Iavarone 6 years ago
parent 3a6f3426b0
commit 573415323f
  1. 122
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  2. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
  3. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  4. 73
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
  5. 10
      cameraview/src/main/java/com/otaliastudios/cameraview/overlay/Overlay.java
  6. 245
      cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayLayout.java
  7. 27
      cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayLayoutInner.java
  8. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/overlay/SurfaceDrawer.java
  9. 150
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java
  10. 28
      cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java
  11. 16
      cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java
  12. 8
      cameraview/src/main/res/values/attrs.xml
  13. 6
      demo/src/main/res/layout/activity_camera.xml

@ -134,8 +134,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
private boolean mExperimental;
// Overlays
private OverlayLayout mOverlayLayoutManager; // see OverlayLayoutManager for why having two of them
private OverlayLayout mOverlayLayoutManagerBelow;
private OverlayLayout mOverlayLayout;
// Threading
private Handler mUiHandler;
@ -193,13 +192,11 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
// Views
mGridLinesLayout = new GridLinesLayout(context);
mOverlayLayoutManager = new OverlayLayout(context);
mOverlayLayoutManagerBelow = new OverlayLayout(context);
mOverlayLayout = new OverlayLayout(context);
mMarkerLayout = new MarkerLayout(context);
addView(mGridLinesLayout);
addView(mMarkerLayout);
addView(mOverlayLayoutManager);
addView(mOverlayLayoutManagerBelow, 0); // put it at the bottom of the FrameLayout
addView(mOverlayLayout);
// Create the engine
doInstantiateEngine();
@ -248,16 +245,12 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
*/
private void doInstantiateEngine() {
if (mCameraEngine != null) {
mCameraEngine.removePictureSurfaceDrawer(mOverlayLayoutManager);
mCameraEngine.removePictureSurfaceDrawer(mOverlayLayoutManagerBelow);
mCameraEngine.removeVideoSurfaceDrawer(mOverlayLayoutManager);
mCameraEngine.removeVideoSurfaceDrawer(mOverlayLayoutManagerBelow);
mCameraEngine.removePictureOverlay(mOverlayLayout);
mCameraEngine.removeVideoOverlay(mOverlayLayout);
}
mCameraEngine = instantiateCameraEngine(mEngine, mCameraCallbacks);
mCameraEngine.addPictureSurfaceDrawer(mOverlayLayoutManager);
mCameraEngine.addPictureSurfaceDrawer(mOverlayLayoutManagerBelow);
mCameraEngine.addVideoSurfaceDrawer(mOverlayLayoutManager);
mCameraEngine.addVideoSurfaceDrawer(mOverlayLayoutManagerBelow);
mCameraEngine.addPictureOverlay(mOverlayLayout);
mCameraEngine.addVideoOverlay(mOverlayLayout);
}
/**
@ -2103,106 +2096,21 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
//region Overlays
@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;
if (mOverlayLayout.isOverlay(attributeSet)) {
return mOverlayLayout.generateLayoutParams(attributeSet);
}
return super.generateLayoutParams(attributeSet);
}
// We don't support removeView on overlays for now.
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
if (p instanceof OverlayLayoutParams) {
return p;
}
return super.generateLayoutParams(p);
}
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;
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if (mOverlayLayout.isOverlay(params)) {
mOverlayLayout.addView(child, params);
} else {
super.addView(child, index, params);
}
}

@ -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, getPictureSurfaceDrawers());
mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio, getPictureOverlays());
} 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, getVideoSurfaceDrawers());
mVideoRecorder = new SnapshotVideoRecorder(Camera1Engine.this, glPreview, getVideoOverlays());
mVideoRecorder.start(stub);
}

@ -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, getPictureSurfaceDrawers());
mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio, getPictureOverlays());
} 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, getVideoSurfaceDrawers());
mVideoRecorder = new SnapshotVideoRecorder(this, glPreview, getVideoOverlays());
mVideoRecorder.start(stub);
}

@ -18,7 +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.overlay.SurfaceDrawer;
import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.engine.offset.Angles;
import com.otaliastudios.cameraview.engine.offset.Reference;
@ -185,38 +185,8 @@ 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<SurfaceDrawer> pictureSurfaceDrawers = new ArrayList<>();
private final List<SurfaceDrawer> 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<SurfaceDrawer> 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<SurfaceDrawer> getVideoSurfaceDrawers() {
return videoSurfaceDrawers;
}
private final List<Overlay> pictureOverlays = new ArrayList<>();
private final List<Overlay> videoOverlays = new ArrayList<>();
// Steps
private final Step.Callback mStepCallback = new Step.Callback() {
@ -1419,4 +1389,41 @@ public abstract class CameraEngine implements
}
//endregion
//region Overlays
public void addPictureOverlay(@NonNull Overlay overlay) {
if (!pictureOverlays.contains(overlay)) {
pictureOverlays.add(overlay);
}
}
public void removePictureOverlay(@NonNull Overlay overlay) {
pictureOverlays.remove(overlay);
}
public void addVideoOverlay(@NonNull Overlay overlay) {
if (!videoOverlays.contains(overlay)) {
videoOverlays.add(overlay);
}
}
public void removeVideoOverlay(@NonNull Overlay overlay) {
videoOverlays.remove(overlay);
}
@SuppressWarnings("WeakerAccess")
@NonNull
protected List<Overlay> getPictureOverlays() {
return pictureOverlays;
}
@SuppressWarnings("WeakerAccess")
@NonNull
protected List<Overlay> getVideoOverlays() {
return videoOverlays;
}
//endregion
}

@ -0,0 +1,10 @@
package com.otaliastudios.cameraview.overlay;
import android.graphics.Canvas;
import androidx.annotation.NonNull;
public interface Overlay {
void drawOnPicture(@NonNull Canvas canvas);
void drawOnVideo(@NonNull Canvas canvas);
}

@ -1,7 +1,9 @@
package com.otaliastudios.cameraview.overlay;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;
@ -11,137 +13,172 @@ import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.otaliastudios.cameraview.CameraView;
import java.util.HashMap;
import java.util.Map;
/**
* This class manages {@link OverlayLayoutInner}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 OverlayLayoutInner} for each subset of targets. This class manages those different
* {@link OverlayLayoutInner}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 OverlayLayout} and layer
* them so that the one below is covered and hidden by the camera preview. This way only the top
* {@link OverlayLayout} is shown on top of the camera preview and we can still access the
* bottom one's {@link OverlayLayoutInner#draw(Canvas)} for drawing on picture snapshots.
*/
public class OverlayLayout extends FrameLayout implements SurfaceDrawer {
private Map<OverlayType, OverlayLayoutInner> mLayouts = new HashMap<>();
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.R;
@SuppressLint("CustomViewStyleable")
public class OverlayLayout extends FrameLayout implements Overlay {
private static final String TAG = OverlayLayout.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG);
private static final int DRAWING_PREVIEW = 0;
private static final int DRAWING_PICTURE = 1;
private static final int DRAWING_VIDEO = 2;
private int target = DRAWING_PREVIEW;
public OverlayLayout(@NonNull Context context) {
super(context);
}
public OverlayLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
/**
* Returns true if this {@link AttributeSet} belongs to an overlay.
* @param set an attribute set
* @return true if overlay
*/
public boolean isOverlay(@Nullable AttributeSet set) {
if (set == null) return false;
TypedArray a = getContext().obtainStyledAttributes(set, R.styleable.CameraView_Layout);
boolean isOverlay = a.getBoolean(R.styleable.CameraView_Layout_layout_isOverlay, false);
a.recycle();
return isOverlay;
}
@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 {
OverlayLayoutInner newLayout = new OverlayLayoutInner(getContext());
newLayout.addView(child, params);
super.addView(newLayout);
mLayouts.put(viewOverlayType, newLayout);
}
/**
* Returns true if this {@link ViewGroup.LayoutParams} belongs to an overlay.
* @param params a layout params
* @return true if overlay
*/
public boolean isOverlay(@NonNull ViewGroup.LayoutParams params) {
return params instanceof LayoutParams;
}
/**
* Generates our own overlay layout params.
* @param attrs input attrs
* @return our params
*/
@Override
public void removeView(View child) {
// params must be instance of OverlayLayoutParams
if (!(child.getLayoutParams() instanceof CameraView.OverlayLayoutParams)) {
return;
}
public OverlayLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
OverlayType viewOverlayType = new OverlayType((CameraView.OverlayLayoutParams) child.getLayoutParams());
if (mLayouts.containsKey(viewOverlayType)) {
mLayouts.get(viewOverlayType).removeView(child);
/**
* This is called by the View hierarchy, so at this point we are
* likely drawing on the preview.
* @param canvas View canvas
*/
@Override
public void draw(Canvas canvas) {
synchronized (this) {
target = DRAWING_PREVIEW;
super.draw(canvas);
}
}
/**
* This is called by the overlay drawer.
* We call {@link #dispatchDraw(Canvas)} to draw our children.
*
* The input canvas has the Surface dimensions which means it is guaranteed
* to have our own aspect ratio. But we might still have to apply some scale.
*
* @param canvas the overlay canvas
*/
@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<OverlayType, OverlayLayoutInner> entry : mLayouts.entrySet()) {
if (entry.getKey().pictureSnapshot) {
entry.getValue().drawOverlay(surfaceCanvas);
}
public void drawOnPicture(@NonNull Canvas canvas) {
canvas.save();
float widthScale = canvas.getWidth() / (float) getWidth();
float heightScale = canvas.getHeight() / (float) getHeight();
LOG.i("drawOnPicture",
"widthScale:", widthScale,
"heightScale:", heightScale,
"canvasWidth:", canvas.getWidth(),
"canvasHeight:", canvas.getHeight());
canvas.scale(widthScale, heightScale);
synchronized (this) {
target = DRAWING_PICTURE;
dispatchDraw(canvas);
}
surfaceCanvas.restore();
canvas.restore();
}
/**
* This is called by the overlay drawer.
* We call {@link #dispatchDraw(Canvas)} to draw our children.
* @param canvas the overlay canvas
*/
@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<OverlayType, OverlayLayoutInner> entry : mLayouts.entrySet()) {
if (entry.getKey().videoSnapshot) {
entry.getValue().drawOverlay(surfaceCanvas);
}
public void drawOnVideo(@NonNull Canvas canvas) {
canvas.save();
float widthScale = canvas.getWidth() / (float) getWidth();
float heightScale = canvas.getHeight() / (float) getHeight();
LOG.i("drawOnVideo",
"widthScale:", widthScale,
"heightScale:", heightScale,
"canvasWidth:", canvas.getWidth(),
"canvasHeight:", canvas.getHeight());
canvas.scale(widthScale, heightScale);
synchronized (this) {
target = DRAWING_VIDEO;
dispatchDraw(canvas);
}
surfaceCanvas.restore();
canvas.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();
/**
* We end up here in all three cases, and should filter out
* views that are not meant to be drawn on that specific surface.
*/
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
LayoutParams params = (LayoutParams) child.getLayoutParams();
boolean draw = ((target == DRAWING_PREVIEW && params.drawOnPreview)
|| (target == DRAWING_VIDEO && params.drawOnVideoSnapshot)
|| (target == DRAWING_PICTURE && params.drawOnPictureSnapshot)
);
if (draw) {
return super.drawChild(canvas, child, drawingTime);
} else {
LOG.v("Skipping drawing for view:", child.getClass().getSimpleName(),
"target:", target,
"params:", params);
return false;
}
}
@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;
@SuppressWarnings("WeakerAccess")
public static class LayoutParams extends FrameLayout.LayoutParams {
@SuppressWarnings("unused")
private boolean isOverlay;
public boolean drawOnPreview;
public boolean drawOnPictureSnapshot;
public boolean drawOnVideoSnapshot;
public LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView_Layout);
try {
this.isOverlay = a.getBoolean(R.styleable.CameraView_Layout_layout_isOverlay, false);
this.drawOnPreview = a.getBoolean(R.styleable.CameraView_Layout_layout_drawOnPreview, false);
this.drawOnPictureSnapshot = a.getBoolean(R.styleable.CameraView_Layout_layout_drawOnPictureSnapshot, false);
this.drawOnVideoSnapshot = a.getBoolean(R.styleable.CameraView_Layout_layout_drawOnVideoSnapshot, false);
} finally {
a.recycle();
}
}
@NonNull
@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;
public String toString() {
return getClass().getName() + "[isOverlay:" + isOverlay
+ ",drawOnPreview:" + drawOnPreview
+ ",drawOnPictureSnapshot:" + drawOnPictureSnapshot
+ ",drawOnVideoSnapshot:" + drawOnVideoSnapshot
+ "]";
}
}
}

@ -1,27 +0,0 @@
package com.otaliastudios.cameraview.overlay;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class OverlayLayoutInner extends FrameLayout {
public OverlayLayoutInner(@NonNull Context context) {
super(context);
setWillNotDraw(false);
}
public OverlayLayoutInner(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
setWillNotDraw(false);
}
public void drawOverlay(Canvas canvas) {
super.draw(canvas);
}
}

@ -1,8 +0,0 @@
package com.otaliastudios.cameraview.overlay;
import android.graphics.Canvas;
public interface SurfaceDrawer {
void drawOnSurfaceForPictureSnapshot(Canvas surfaceCanvas);
void drawOnSurfaceForVideoSnapshot(Canvas surfaceCanvas);
}

@ -14,7 +14,7 @@ import android.os.Build;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.overlay.SurfaceDrawer;
import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.engine.offset.Axis;
@ -31,9 +31,11 @@ import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.Surface;
import java.util.ArrayList;
import java.util.List;
public class SnapshotGlPictureRecorder extends PictureRecorder {
@ -45,21 +47,23 @@ public class SnapshotGlPictureRecorder extends PictureRecorder {
private GlCameraPreview mPreview;
private AspectRatio mOutputRatio;
private List<SurfaceDrawer> mSurfaceDrawerList;
private boolean mWithOverlay;
private List<Overlay> mOverlays;
private boolean mHasOverlays;
public SnapshotGlPictureRecorder(
@NonNull PictureResult.Stub stub,
@NonNull CameraEngine engine,
@NonNull GlCameraPreview preview,
@NonNull AspectRatio outputRatio,
@NonNull List<SurfaceDrawer> surfaceDrawerList) {
@Nullable List<Overlay> overlays) {
super(stub, engine);
mEngine = engine;
mPreview = preview;
mOutputRatio = outputRatio;
mWithOverlay = true;
mSurfaceDrawerList = surfaceDrawerList;
mHasOverlays = overlays != null && !overlays.isEmpty();
if (mHasOverlays) {
mOverlays = new ArrayList<>(overlays);
}
}
@TargetApi(Build.VERSION_CODES.KITKAT)
@ -68,33 +72,32 @@ public class SnapshotGlPictureRecorder extends PictureRecorder {
mPreview.addRendererFrameCallback(new RendererFrameCallback() {
int mTextureId;
int mOverlayTextureId = 0;
SurfaceTexture mSurfaceTexture;
SurfaceTexture mOverlaySurfaceTexture;
float[] mTransform;
int mOverlayTextureId = 0;
SurfaceTexture mOverlaySurfaceTexture;
Surface mOverlaySurface;
float[] mOverlayTransform;
EglViewport viewport;
EglViewport mViewport;
@RendererThread
public void onRendererTextureCreated(int textureId) {
mTextureId = textureId;
viewport = new EglViewport();
if (mWithOverlay) {
mOverlayTextureId = viewport.createTexture();
}
mViewport = new EglViewport();
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) {
if (mHasOverlays) {
mOverlayTextureId = mViewport.createTexture();
mOverlaySurfaceTexture = new SurfaceTexture(mOverlayTextureId, true);
mOverlaySurfaceTexture.setDefaultBufferSize(mResult.size.getWidth(), mResult.size.getHeight());
mOverlaySurface = new Surface(mOverlaySurfaceTexture);
mOverlayTransform = new float[16];
}
}
@ -128,30 +131,14 @@ public class SnapshotGlPictureRecorder extends PictureRecorder {
WorkerHandler.execute(new Runnable() {
@Override
public void run() {
// 1. Get latest texture
EglWindowSurface surface = new EglWindowSurface(core, mSurfaceTexture);
surface.makeCurrent();
// 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.
// 2. Apply scale and crop:
// scaleX and scaleY are in REF_VIEW, while our input appears to be in REF_SENSOR.
boolean flip = mEngine.getAngles().flip(Reference.VIEW, Reference.SENSOR);
float realScaleX = flip ? scaleY : scaleX;
float realScaleY = flip ? scaleX : scaleY;
@ -160,65 +147,66 @@ public class SnapshotGlPictureRecorder extends PictureRecorder {
Matrix.translateM(mTransform, 0, scaleTranslX, scaleTranslY, 0);
Matrix.scaleM(mTransform, 0, realScaleX, realScaleY, 1);
// Fix rotation:
// 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.
// 3. 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);
}
// 4. Apply rotation:
// Not sure why we need the minus here.
Matrix.rotateM(mTransform, 0, -mResult.rotation, 0, 0, 1);
mResult.rotation = 0;
// Flip horizontally for front camera:
// 5. 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.
// 6. Go back to old position.
Matrix.translateM(mTransform, 0, -0.5F, -0.5F, 0);
if (mOverlayTransform != null) {
// 7. Do pretty much the same for overlays, though with
// some differences.
if (mHasOverlays) {
// 1. First we must draw on the texture and get latest image.
try {
final Canvas surfaceCanvas = mOverlaySurface.lockCanvas(null);
surfaceCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
for (Overlay overlay : mOverlays) {
overlay.drawOnPicture(surfaceCanvas);
}
mOverlaySurface.unlockCanvasAndPost(surfaceCanvas);
} catch (Surface.OutOfResourcesException e) {
LOG.w("Got Surface.OutOfResourcesException while drawing picture overlays", e);
}
mOverlaySurfaceTexture.updateTexImage();
mOverlaySurfaceTexture.getTransformMatrix(mOverlayTransform);
// 2. Then we can apply the transformations.
// TODO check the sign of this angle.
int rotation = mEngine.getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR);
Matrix.translateM(mOverlayTransform, 0, 0.5F, 0.5F, 0);
Matrix.rotateM(mOverlayTransform, 0, -rotation, 0, 0, 1);
// Not sure why we have to flip the y axis (twice?).
if (mResult.facing == Facing.FRONT) {
Matrix.scaleM(mOverlayTransform, 0, -1, -1, 1);
}
Matrix.scaleM(mOverlayTransform, 0, 1, -1, 1);
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);
}
// 8. Draw and save
mViewport.drawFrame(mTextureId, mTransform);
if (mHasOverlays) mViewport.drawFrame(mOverlayTextureId, mOverlayTransform);
// don't - surface.swapBuffers();
mResult.data = surface.saveFrameTo(Bitmap.CompressFormat.JPEG);
mResult.format = PictureResult.FORMAT_JPEG;
mSurfaceTexture.releaseTexImage();
// EGL14.eglMakeCurrent(oldDisplay, oldSurface, oldSurface, eglContext);
// 9. Cleanup
mSurfaceTexture.releaseTexImage();
surface.release();
viewport.release();
drawOnto.release();
mViewport.release();
mSurfaceTexture.release();
if (mOverlaySurfaceTexture != null) {
if (mHasOverlays) {
mOverlaySurface.release();
mOverlaySurfaceTexture.release();
}
core.release();

@ -8,10 +8,11 @@ import android.opengl.EGL14;
import android.os.Build;
import android.view.Surface;
import java.util.ArrayList;
import java.util.List;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.overlay.SurfaceDrawer;
import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.engine.CameraEngine;
@ -59,18 +60,20 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
private int mOverlayTextureId = 0;
private SurfaceTexture mOverlaySurfaceTexture;
private Surface mOverlaySurface;
private List<SurfaceDrawer> mSurfaceDrawerList;
private boolean mWithOverlay;
private List<Overlay> mOverlays;
private boolean mHasOverlays;
public SnapshotVideoRecorder(@NonNull CameraEngine engine,
@NonNull GlCameraPreview preview,
@NonNull List<SurfaceDrawer> surfaceDrawerList) {
@Nullable List<Overlay> overlays) {
super(engine);
mPreview = preview;
mEngine = engine;
mWithOverlay = true;
mSurfaceDrawerList = surfaceDrawerList;
mHasOverlays = overlays != null && !overlays.isEmpty();
if (mHasOverlays) {
mOverlays = new ArrayList<>(overlays);
}
}
@Override
@ -90,10 +93,9 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
@Override
public void onRendererTextureCreated(int textureId) {
mTextureId = textureId;
if (mWithOverlay) {
if (mHasOverlays) {
EglViewport temp = new EglViewport();
mOverlayTextureId = temp.createTexture();
mOverlaySurfaceTexture = new SurfaceTexture(mOverlayTextureId);
mOverlaySurfaceTexture.setDefaultBufferSize(mResult.size.getWidth(), mResult.size.getHeight());
mOverlaySurface = new Surface(mOverlaySurfaceTexture);
@ -129,7 +131,7 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
mResult.videoFrameRate,
mResult.rotation,
type, mTextureId,
mWithOverlay ? mOverlayTextureId : 0,
mHasOverlays ? mOverlayTextureId : TextureMediaEncoder.NO_TEXTURE,
scaleX, scaleY,
mFlipped,
EGL14.eglGetCurrentContext()
@ -158,17 +160,17 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
surfaceTexture.getTransformMatrix(textureFrame.transform);
// get overlay
if (mWithOverlay) {
if (mHasOverlays) {
try {
final Canvas surfaceCanvas = mOverlaySurface.lockCanvas(null);
surfaceCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
for (SurfaceDrawer surfaceDrawer : mSurfaceDrawerList) {
surfaceDrawer.drawOnSurfaceForVideoSnapshot(surfaceCanvas);
for (Overlay overlay : mOverlays) {
overlay.drawOnVideo(surfaceCanvas);
}
mOverlaySurface.unlockCanvasAndPost(surfaceCanvas);
} catch (Surface.OutOfResourcesException e) {
e.printStackTrace();
LOG.w("Got Surface.OutOfResourcesException while drawing video overlays", e);
}
mOverlaySurfaceTexture.updateTexImage();
mOverlaySurfaceTexture.getTransformMatrix(textureFrame.overlayTransform);

@ -24,6 +24,7 @@ public class TextureMediaEncoder extends VideoMediaEncoder<TextureMediaEncoder.C
private static final CameraLogger LOG = CameraLogger.create(TAG);
public final static String FRAME_EVENT = "frame";
public final static int NO_TEXTURE = Integer.MIN_VALUE;
public static class Config extends VideoMediaEncoder.Config {
int textureId;
@ -34,11 +35,6 @@ public class TextureMediaEncoder extends VideoMediaEncoder<TextureMediaEncoder.C
EGLContext eglContext;
int transformRotation;
public Config(int width, int height, int bitRate, int frameRate, int rotation, String mimeType,
int textureId, float scaleX, float scaleY, boolean scaleFlipped, EGLContext eglContext) {
this(width, height, bitRate, frameRate, rotation, mimeType, textureId, 0, scaleX, scaleY, scaleFlipped, eglContext);
}
public Config(int width, int height, int bitRate, int frameRate, int rotation, String mimeType,
int textureId, int overlayTextureId, float scaleX, float scaleY, boolean scaleFlipped, EGLContext eglContext) {
// We rotate the texture using transformRotation. Pass rotation=0 to super so that
@ -137,11 +133,12 @@ public class TextureMediaEncoder extends VideoMediaEncoder<TextureMediaEncoder.C
// stream, but the output video, must be correctly rotated based on the device rotation at the moment.
// Rotation also takes place with respect to the origin (the Z axis), so we must
// translate to origin, rotate, then back to where we were.
Matrix.translateM(transform, 0, 0.5F, 0.5F, 0);
Matrix.rotateM(transform, 0, mConfig.transformRotation, 0, 0, 1);
Matrix.translateM(transform, 0, -0.5F, -0.5F, 0);
if (overlayTransform != null) {
boolean hasOverlay = mConfig.overlayTextureId != NO_TEXTURE;
if (hasOverlay) {
Matrix.translateM(overlayTransform, 0, 0.5F, 0.5F, 0);
Matrix.rotateM(overlayTransform, 0, mConfig.transformRotation, 0, 0, 1);
Matrix.translateM(overlayTransform, 0, -0.5F, -0.5F, 0);
@ -153,7 +150,10 @@ public class TextureMediaEncoder extends VideoMediaEncoder<TextureMediaEncoder.C
// but flipped based on the mConfig.scaleFlipped boolean.
LOG.v("onEvent", "frameNum:", thisFrameNum, "realFrameNum:", mFrameNum, "calling drawFrame.");
mViewport.drawFrame(mConfig.textureId, transform);
mViewport.drawFrame(mConfig.overlayTextureId, overlayTransform);
if (hasOverlay) {
mViewport.drawFrame(mConfig.overlayTextureId, overlayTransform);
}
mWindow.setPresentationTime(frame.timestamp);
mWindow.swapBuffers();
mFramePool.recycle(frame);

@ -136,10 +136,10 @@
</declare-styleable>
<declare-styleable name="CameraView_Layout">
<attr name="layout_drawInPreview" format="boolean"/>
<attr name="layout_drawInPictureSnapshot" format="boolean"/>
<attr name="layout_drawInVideoSnapshot" format="boolean"/>
<attr name="layout_isOverlay" format="boolean"/>
<attr name="layout_drawOnPreview" format="boolean"/>
<attr name="layout_drawOnPictureSnapshot" format="boolean"/>
<attr name="layout_drawOnVideoSnapshot" format="boolean"/>
</declare-styleable>
</resources>

@ -37,9 +37,9 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="bottom|end"
app:layout_drawInPreview="false"
app:layout_drawInVideoSnapshot="true"
app:layout_drawInPictureSnapshot="true"
app:layout_drawOnPreview="false"
app:layout_drawOnVideoSnapshot="true"
app:layout_drawOnPictureSnapshot="true"
android:gravity="center"
android:padding="8dp">
<ImageView

Loading…
Cancel
Save