Small fixes (#524)

* Add picture snapshot comments

* Release temp viewport

* Add ImageReader version of picture recorder

* Fix default camera

* Increase I frame interval, add comments

* Improve demo app

* Use surface size instead of view size for snapshot ratio

* Avoid weird sizes in snapshot output

* Review snapshot picture recorder again

* Fix auto focus callback
debug-base
Mattia Iavarone 5 years ago committed by GitHub
parent bc3c72cdc8
commit be037d6393
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java
  2. 18
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  3. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
  4. 10
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  5. 18
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
  6. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java
  7. 10
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/utils/CropHelper.java
  8. 287
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java
  9. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/size/AspectRatio.java
  10. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java
  11. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java
  12. 10
      cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/VideoMediaEncoder.java
  13. 5
      demo/src/main/AndroidManifest.xml
  14. 20
      demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java
  15. 27
      demo/src/main/java/com/otaliastudios/cameraview/demo/VideoPreviewActivity.java
  16. 2
      demo/src/main/res/layout/activity_camera.xml

@ -128,7 +128,7 @@ public class MockCameraEngine extends CameraEngine {
} }
@Override @Override
protected void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio viewAspectRatio) { protected void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio outputRatio) {
} }
@ -138,7 +138,7 @@ public class MockCameraEngine extends CameraEngine {
} }
@Override @Override
protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio viewAspectRatio) { protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio outputRatio) {
} }

@ -425,10 +425,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
int height, width; int height, width;
if (freeWidth) { if (freeWidth) {
height = heightValue; height = heightValue;
width = (int) (height / ratio); width = Math.round(height / ratio);
} else { } else {
width = widthValue; width = widthValue;
height = (int) (width * ratio); height = Math.round(width * ratio);
} }
LOG.i("onMeasure:", "one dimension was free, we adapted it to fit the aspect ratio.", LOG.i("onMeasure:", "one dimension was free, we adapted it to fit the aspect ratio.",
"(" + width + "x" + height + ")"); "(" + width + "x" + height + ")");
@ -445,10 +445,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
int height, width; int height, width;
if (freeWidth) { if (freeWidth) {
height = heightValue; height = heightValue;
width = Math.min((int) (height / ratio), widthValue); width = Math.min(Math.round(height / ratio), widthValue);
} else { } else {
width = widthValue; width = widthValue;
height = Math.min((int) (width * ratio), heightValue); height = Math.min(Math.round(width * ratio), heightValue);
} }
LOG.i("onMeasure:", "one dimension was EXACTLY, another AT_MOST.", LOG.i("onMeasure:", "one dimension was EXACTLY, another AT_MOST.",
"We have TRIED to fit the aspect ratio, but it's not guaranteed.", "We have TRIED to fit the aspect ratio, but it's not guaranteed.",
@ -465,10 +465,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
if (atMostRatio >= ratio) { if (atMostRatio >= ratio) {
// We must reduce height. // We must reduce height.
width = widthValue; width = widthValue;
height = (int) (width * ratio); height = Math.round(width * ratio);
} else { } else {
height = heightValue; height = heightValue;
width = (int) (height / ratio); width = Math.round(height / ratio);
} }
LOG.i("onMeasure:", "both dimension were AT_MOST.", LOG.i("onMeasure:", "both dimension were AT_MOST.",
"We fit the preview aspect ratio.", "We fit the preview aspect ratio.",
@ -1465,9 +1465,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
* @see #takePicture() * @see #takePicture()
*/ */
public void takePictureSnapshot() { public void takePictureSnapshot() {
if (getWidth() == 0 || getHeight() == 0) return;
PictureResult.Stub stub = new PictureResult.Stub(); PictureResult.Stub stub = new PictureResult.Stub();
mCameraEngine.takePictureSnapshot(stub, AspectRatio.of(getWidth(), getHeight())); mCameraEngine.takePictureSnapshot(stub);
} }
@ -1499,9 +1498,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
* @param file a file where the video will be saved * @param file a file where the video will be saved
*/ */
public void takeVideoSnapshot(@NonNull File file) { public void takeVideoSnapshot(@NonNull File file) {
if (getWidth() == 0 || getHeight() == 0) return;
VideoResult.Stub stub = new VideoResult.Stub(); VideoResult.Stub stub = new VideoResult.Stub();
mCameraEngine.takeVideoSnapshot(stub, file, AspectRatio.of(getWidth(), getHeight())); mCameraEngine.takeVideoSnapshot(stub, file);
mUiHandler.post(new Runnable() { mUiHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {

@ -306,10 +306,9 @@ public class Camera1Engine extends CameraEngine implements
@WorkerThread @WorkerThread
@Override @Override
protected void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio viewAspectRatio) { protected void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio outputRatio) {
stub.size = getUncroppedSnapshotSize(Reference.OUTPUT); // Not the real size: it will be cropped to match the view ratio stub.size = getUncroppedSnapshotSize(Reference.OUTPUT); // Not the real size: it will be cropped to match the view ratio
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); // Actually it will be rotated and set to 0. 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 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (mPreview instanceof GlCameraPreview && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio, getOverlay()); mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio, getOverlay());
@ -343,7 +342,7 @@ public class Camera1Engine extends CameraEngine implements
@SuppressLint("NewApi") @SuppressLint("NewApi")
@WorkerThread @WorkerThread
@Override @Override
protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio viewAspectRatio) { protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio outputRatio) {
if (!(mPreview instanceof GlCameraPreview)) { if (!(mPreview instanceof GlCameraPreview)) {
throw new IllegalStateException("Video snapshots are only supported with GlCameraPreview."); throw new IllegalStateException("Video snapshots are only supported with GlCameraPreview.");
} }
@ -355,7 +354,6 @@ public class Camera1Engine extends CameraEngine implements
if (outputSize == null) { if (outputSize == null) {
throw new IllegalStateException("outputSize should not be null."); throw new IllegalStateException("outputSize should not be null.");
} }
AspectRatio outputRatio = getAngles().flip(Reference.VIEW, Reference.OUTPUT) ? viewAspectRatio.flip() : viewAspectRatio;
Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio); Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio);
outputSize = new Size(outputCrop.width(), outputCrop.height()); outputSize = new Size(outputCrop.width(), outputCrop.height());
stub.size = outputSize; stub.size = outputSize;

@ -610,10 +610,9 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@WorkerThread @WorkerThread
@Override @Override
protected void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio viewAspectRatio) { protected void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio outputRatio) {
stub.size = getUncroppedSnapshotSize(Reference.OUTPUT); // Not the real size: it will be cropped to match the view ratio stub.size = getUncroppedSnapshotSize(Reference.OUTPUT); // Not the real size: it will be cropped to match the view ratio
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); // Actually it will be rotated and set to 0. 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) { if (mPreview instanceof GlCameraPreview) {
mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio, getOverlay()); mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio, getOverlay());
} else { } else {
@ -695,7 +694,7 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@WorkerThread @WorkerThread
@Override @Override
protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio viewAspectRatio) { protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio outputRatio) {
if (!(mPreview instanceof GlCameraPreview)) { if (!(mPreview instanceof GlCameraPreview)) {
throw new IllegalStateException("Video snapshots are only supported with GlCameraPreview."); throw new IllegalStateException("Video snapshots are only supported with GlCameraPreview.");
} }
@ -704,7 +703,6 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
if (outputSize == null) { if (outputSize == null) {
throw new IllegalStateException("outputSize should not be null."); throw new IllegalStateException("outputSize should not be null.");
} }
AspectRatio outputRatio = getAngles().flip(Reference.VIEW, Reference.OUTPUT) ? viewAspectRatio.flip() : viewAspectRatio;
Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio); Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio);
outputSize = new Size(outputCrop.width(), outputCrop.height()); outputSize = new Size(outputCrop.width(), outputCrop.height());
stub.size = outputSize; stub.size = outputSize;
@ -1257,8 +1255,8 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
private void onAutoFocusCapture(@NonNull CaptureResult result) { private void onAutoFocusCapture(@NonNull CaptureResult result) {
Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
if (afState == null) { if (afState == null) {
LOG.e("onAutoFocusCapture", "afState is null! Assuming AF failed."); LOG.i("onAutoFocusCapture", "afState is null! This can happen for partial results. Waiting.");
afState = CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED; return;
} }
switch (afState) { switch (afState) {
case CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED: { case CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED: {

@ -1087,9 +1087,8 @@ public abstract class CameraEngine implements
* The snapshot size is the {@link #getPreviewStreamSize(Reference)}, but cropped based on the * The snapshot size is the {@link #getPreviewStreamSize(Reference)}, but cropped based on the
* view/surface aspect ratio. * view/surface aspect ratio.
* @param stub a picture stub * @param stub a picture stub
* @param viewAspectRatio the view aspect ratio
*/ */
public final void takePictureSnapshot(final @NonNull PictureResult.Stub stub, @NonNull final AspectRatio viewAspectRatio) { public final void takePictureSnapshot(final @NonNull PictureResult.Stub stub) {
LOG.v("takePictureSnapshot", "scheduling"); LOG.v("takePictureSnapshot", "scheduling");
mHandler.run(new Runnable() { mHandler.run(new Runnable() {
@Override @Override
@ -1101,7 +1100,9 @@ public abstract class CameraEngine implements
stub.isSnapshot = true; stub.isSnapshot = true;
stub.facing = mFacing; stub.facing = mFacing;
// Leave the other parameters to subclasses. // Leave the other parameters to subclasses.
onTakePictureSnapshot(stub, viewAspectRatio); //noinspection ConstantConditions
AspectRatio ratio = AspectRatio.of(getPreviewSurfaceSize(Reference.OUTPUT));
onTakePictureSnapshot(stub, ratio);
} }
}); });
} }
@ -1155,9 +1156,8 @@ public abstract class CameraEngine implements
/** /**
* @param stub a video stub * @param stub a video stub
* @param file the output file * @param file the output file
* @param viewAspectRatio the view aspect ratio
*/ */
public final void takeVideoSnapshot(final @NonNull VideoResult.Stub stub, @NonNull final File file, @NonNull final AspectRatio viewAspectRatio) { public final void takeVideoSnapshot(final @NonNull VideoResult.Stub stub, @NonNull final File file) {
LOG.v("takeVideoSnapshot", "scheduling"); LOG.v("takeVideoSnapshot", "scheduling");
mHandler.run(new Runnable() { mHandler.run(new Runnable() {
@Override @Override
@ -1175,7 +1175,9 @@ public abstract class CameraEngine implements
stub.audio = mAudio; stub.audio = mAudio;
stub.maxSize = mVideoMaxSize; stub.maxSize = mVideoMaxSize;
stub.maxDuration = mVideoMaxDuration; stub.maxDuration = mVideoMaxDuration;
onTakeVideoSnapshot(stub, viewAspectRatio); //noinspection ConstantConditions
AspectRatio ratio = AspectRatio.of(getPreviewSurfaceSize(Reference.OUTPUT));
onTakeVideoSnapshot(stub, ratio);
} }
}); });
} }
@ -1220,10 +1222,10 @@ public abstract class CameraEngine implements
protected abstract void onTakePicture(@NonNull PictureResult.Stub stub); protected abstract void onTakePicture(@NonNull PictureResult.Stub stub);
@WorkerThread @WorkerThread
protected abstract void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio viewAspectRatio); protected abstract void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio outputRatio);
@WorkerThread @WorkerThread
protected abstract void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio viewAspectRatio); protected abstract void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio outputRatio);
@WorkerThread @WorkerThread
protected abstract void onTakeVideo(@NonNull VideoResult.Stub stub); protected abstract void onTakeVideo(@NonNull VideoResult.Stub stub);

@ -51,7 +51,7 @@ public class EglBaseSurface extends EglElement {
private int mWidth = -1; private int mWidth = -1;
private int mHeight = -1; private int mHeight = -1;
protected EglBaseSurface(EglCore eglCore) { public EglBaseSurface(EglCore eglCore) {
mEglCore = eglCore; mEglCore = eglCore;
} }

@ -17,7 +17,7 @@ public class CropHelper {
public static Rect computeCrop(@NonNull Size currentSize, @NonNull AspectRatio targetRatio) { public static Rect computeCrop(@NonNull Size currentSize, @NonNull AspectRatio targetRatio) {
int currentWidth = currentSize.getWidth(); int currentWidth = currentSize.getWidth();
int currentHeight = currentSize.getHeight(); int currentHeight = currentSize.getHeight();
if (targetRatio.matches(currentSize)) { if (targetRatio.matches(currentSize, 0.0005F)) {
return new Rect(0, 0, currentWidth, currentHeight); return new Rect(0, 0, currentWidth, currentHeight);
} }
@ -26,13 +26,13 @@ public class CropHelper {
int x, y, width, height; int x, y, width, height;
if (currentRatio.toFloat() > targetRatio.toFloat()) { if (currentRatio.toFloat() > targetRatio.toFloat()) {
height = currentHeight; height = currentHeight;
width = (int) (height * targetRatio.toFloat()); width = Math.round(height * targetRatio.toFloat());
y = 0; y = 0;
x = (currentWidth - width) / 2; x = Math.round((currentWidth - width) / 2F);
} else { } else {
width = currentWidth; width = currentWidth;
height = (int) (width / targetRatio.toFloat()); height = Math.round(width / targetRatio.toFloat());
y = (currentHeight - height) / 2; y = Math.round((currentHeight - height) / 2F);
x = 0; x = 0;
} }
return new Rect(x, y, x + width, y + height); return new Rect(x, y, x + width, y + height);

@ -14,6 +14,7 @@ import android.os.Build;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.internal.egl.EglBaseSurface;
import com.otaliastudios.cameraview.overlay.Overlay; import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.engine.CameraEngine; import com.otaliastudios.cameraview.engine.CameraEngine;
@ -35,6 +36,23 @@ import androidx.annotation.Nullable;
import android.view.Surface; import android.view.Surface;
/**
* API 19.
* Records a picture snapshots from the {@link GlCameraPreview}. It works as follows:
*
* - We register a one time {@link RendererFrameCallback} on the preview
* - We get the textureId and the frame callback on the {@link RendererThread}
* - [Optional: we construct another textureId for overlays]
* - We take a handle of the EGL context from the {@link RendererThread}
* - We move to another thread, and create a new EGL surface for that EGL context.
* - We make this new surface current, and re-draw the textureId on it
* - [Optional: fill the overlayTextureId and draw it on the same surface]
* - We use glReadPixels (through {@link EglBaseSurface#saveFrameTo(Bitmap.CompressFormat)}) and save to file.
*
* We create a new EGL surface and redraw the frame because:
* 1. We want to go off the renderer thread as soon as possible
* 2. We have overlays to be drawn - we don't want to draw them on the preview surface, not even for a frame.
*/
public class SnapshotGlPictureRecorder extends PictureRecorder { public class SnapshotGlPictureRecorder extends PictureRecorder {
private static final String TAG = SnapshotGlPictureRecorder.class.getSimpleName(); private static final String TAG = SnapshotGlPictureRecorder.class.getSimpleName();
@ -47,6 +65,17 @@ public class SnapshotGlPictureRecorder extends PictureRecorder {
private Overlay mOverlay; private Overlay mOverlay;
private boolean mHasOverlay; private boolean mHasOverlay;
private int mTextureId;
private SurfaceTexture mSurfaceTexture;
private float[] mTransform;
private int mOverlayTextureId = 0;
private SurfaceTexture mOverlaySurfaceTexture;
private Surface mOverlaySurface;
private float[] mOverlayTransform;
private EglViewport mViewport;
public SnapshotGlPictureRecorder( public SnapshotGlPictureRecorder(
@NonNull PictureResult.Stub stub, @NonNull PictureResult.Stub stub,
@NonNull CameraEngine engine, @NonNull CameraEngine engine,
@ -66,142 +95,148 @@ public class SnapshotGlPictureRecorder extends PictureRecorder {
public void take() { public void take() {
mPreview.addRendererFrameCallback(new RendererFrameCallback() { mPreview.addRendererFrameCallback(new RendererFrameCallback() {
int mTextureId;
SurfaceTexture mSurfaceTexture;
float[] mTransform;
int mOverlayTextureId = 0;
SurfaceTexture mOverlaySurfaceTexture;
Surface mOverlaySurface;
float[] mOverlayTransform;
EglViewport mViewport;
@RendererThread @RendererThread
public void onRendererTextureCreated(int textureId) { public void onRendererTextureCreated(int textureId) {
mTextureId = textureId; SnapshotGlPictureRecorder.this.onRendererTextureCreated(textureId);
mViewport = new EglViewport();
mSurfaceTexture = new SurfaceTexture(mTextureId, 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());
mTransform = new float[16];
if (mHasOverlay) {
mOverlayTextureId = mViewport.createTexture();
mOverlaySurfaceTexture = new SurfaceTexture(mOverlayTextureId, true);
mOverlaySurfaceTexture.setDefaultBufferSize(mResult.size.getWidth(), mResult.size.getHeight());
mOverlaySurface = new Surface(mOverlaySurfaceTexture);
mOverlayTransform = new float[16];
}
} }
@RendererThread @RendererThread
@Override @Override
public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, final float scaleX, final float scaleY) { public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, final float scaleX, final float scaleY) {
mPreview.removeRendererFrameCallback(this); mPreview.removeRendererFrameCallback(this);
SnapshotGlPictureRecorder.this.onRendererFrame(scaleX, scaleY);
}
});
}
// This kinda work but has drawbacks: @RendererThread
// - output is upside down due to coordinates in GL: need to flip the byte[] someway @TargetApi(Build.VERSION_CODES.KITKAT)
// - output is not rotated as we would like to: need to create a bitmap copy... private void onRendererTextureCreated(int textureId) {
// - works only in the renderer thread, where it allocates the buffer and reads pixels. Bad! mTextureId = textureId;
/* mViewport = new EglViewport();
ByteBuffer buffer = ByteBuffer.allocateDirect(width * height * 4); mSurfaceTexture = new SurfaceTexture(mTextureId, true);
buffer.order(ByteOrder.LITTLE_ENDIAN); // Need to crop the size.
GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buffer); Rect crop = CropHelper.computeCrop(mResult.size, mOutputRatio);
buffer.rewind(); mResult.size = new Size(crop.width(), crop.height());
ByteArrayOutputStream bos = new ByteArrayOutputStream(buffer.array().length); mSurfaceTexture.setDefaultBufferSize(mResult.size.getWidth(), mResult.size.getHeight());
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); mTransform = new float[16];
bitmap.copyPixelsFromBuffer(buffer);
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, bos); if (mHasOverlay) {
bitmap.recycle(); */ mOverlayTextureId = mViewport.createTexture();
mOverlaySurfaceTexture = new SurfaceTexture(mOverlayTextureId, true);
// For this reason it is better to create a new surface, mOverlaySurfaceTexture.setDefaultBufferSize(mResult.size.getWidth(), mResult.size.getHeight());
// and draw the last frame again there. mOverlaySurface = new Surface(mOverlaySurfaceTexture);
final EGLContext eglContext = EGL14.eglGetCurrentContext(); mOverlayTransform = new float[16];
final EglCore core = new EglCore(eglContext, EglCore.FLAG_RECORDABLE); }
// final EGLSurface oldSurface = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW); }
// final EGLDisplay oldDisplay = EGL14.eglGetCurrentDisplay();
WorkerHandler.execute(new Runnable() { /**
@Override * The tricky part here is the EGL surface creation.
public void run() { *
// 1. Get latest texture * We don't have a real output window for the EGL surface - we will use glReadPixels()
EglWindowSurface surface = new EglWindowSurface(core, mSurfaceTexture); * and never call swapBuffers(), so what we draw is never published.
surface.makeCurrent(); *
mSurfaceTexture.updateTexImage(); * 1. One option is to use a pbuffer EGL surface. This works, we just have to pass
mSurfaceTexture.getTransformMatrix(mTransform); * the correct width and height. However, it is significantly slower than the current
* solution.
// 2. Apply scale and crop: *
// scaleX and scaleY are in REF_VIEW, while our input appears to be in REF_SENSOR. * 2. Another option is to create the EGL surface out of a ImageReader.getSurface()
boolean flip = mEngine.getAngles().flip(Reference.VIEW, Reference.SENSOR); * and use the reader to create a JPEG. In this case, we would have to publish
float realScaleX = flip ? scaleY : scaleX; * the frame with swapBuffers(). However, currently ImageReader does not support
float realScaleY = flip ? scaleX : scaleY; * all formats, it's risky. This is an example error that we get:
float scaleTranslX = (1F - realScaleX) / 2F; * "RGBA override BLOB format buffer should have height == width"
float scaleTranslY = (1F - realScaleY) / 2F; *
Matrix.translateM(mTransform, 0, scaleTranslX, scaleTranslY, 0); * The third option, which we are using, is to create the EGL surface using whatever
Matrix.scaleM(mTransform, 0, realScaleX, realScaleY, 1); * {@link Surface} or {@link SurfaceTexture} we have at hand. Since we never call
* swapBuffers(), the frame will not actually be rendered. This is the fastest.
// 3. Go back to 0,0 so that rotate and flip work well. *
Matrix.translateM(mTransform, 0, 0.5F, 0.5F, 0); * @param scaleX frame scale x in {@link Reference#VIEW}
* @param scaleY frame scale y in {@link Reference#VIEW}
// 4. Apply rotation: */
// Not sure why we need the minus here. @RendererThread
Matrix.rotateM(mTransform, 0, -mResult.rotation, 0, 0, 1); @TargetApi(Build.VERSION_CODES.KITKAT)
mResult.rotation = 0; private void onRendererFrame(final float scaleX, final float scaleY) {
// Get egl context from the RendererThread, which is the one in which we have created
// 5. Flip horizontally for front camera: // the textureId and the overlayTextureId, managed by the GlSurfaceView.
if (mResult.facing == Facing.FRONT) { // Next operations can then be performed on different threads using this handle.
Matrix.scaleM(mTransform, 0, -1, 1, 1); final EGLContext eglContext = EGL14.eglGetCurrentContext();
} final EglCore core = new EglCore(eglContext, EglCore.FLAG_RECORDABLE);
WorkerHandler.execute(new Runnable() {
// 6. Go back to old position. @Override
Matrix.translateM(mTransform, 0, -0.5F, -0.5F, 0); public void run() {
// 0. Create an EGL surface
// 7. Do pretty much the same for overlays, though with EglBaseSurface eglSurface = new EglWindowSurface(core, mSurfaceTexture);
// some differences. eglSurface.makeCurrent();
if (mHasOverlay) {
// 1. First we must draw on the texture and get latest image. // 1. Get latest texture
try { mSurfaceTexture.updateTexImage();
final Canvas surfaceCanvas = mOverlaySurface.lockCanvas(null); mSurfaceTexture.getTransformMatrix(mTransform);
surfaceCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
mOverlay.drawOn(Overlay.Target.PICTURE_SNAPSHOT, surfaceCanvas); // 2. Apply scale and crop
mOverlaySurface.unlockCanvasAndPost(surfaceCanvas); boolean flip = mEngine.getAngles().flip(Reference.VIEW, Reference.SENSOR);
} catch (Surface.OutOfResourcesException e) { float realScaleX = flip ? scaleY : scaleX;
LOG.w("Got Surface.OutOfResourcesException while drawing picture overlays", e); float realScaleY = flip ? scaleX : scaleY;
} float scaleTranslX = (1F - realScaleX) / 2F;
mOverlaySurfaceTexture.updateTexImage(); float scaleTranslY = (1F - realScaleY) / 2F;
mOverlaySurfaceTexture.getTransformMatrix(mOverlayTransform); Matrix.translateM(mTransform, 0, scaleTranslX, scaleTranslY, 0);
Matrix.scaleM(mTransform, 0, realScaleX, realScaleY, 1);
// 2. Then we can apply the transformations.
int rotation = mEngine.getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE); // 3. Go back to 0,0 so that rotate and flip work well
Matrix.translateM(mOverlayTransform, 0, 0.5F, 0.5F, 0); Matrix.translateM(mTransform, 0, 0.5F, 0.5F, 0);
Matrix.rotateM(mOverlayTransform, 0, rotation, 0, 0, 1);
// No need to flip the x axis for front camera, but need to flip the y axis always. // 4. Apply rotation (not sure why we need the minus here)
Matrix.scaleM(mOverlayTransform, 0, 1, -1, 1); Matrix.rotateM(mTransform, 0, -mResult.rotation, 0, 0, 1);
Matrix.translateM(mOverlayTransform, 0, -0.5F, -0.5F, 0); mResult.rotation = 0;
}
// 5. Flip horizontally for front camera
// 8. Draw and save if (mResult.facing == Facing.FRONT) {
mViewport.drawFrame(mTextureId, mTransform); Matrix.scaleM(mTransform, 0, -1, 1, 1);
if (mHasOverlay) mViewport.drawFrame(mOverlayTextureId, mOverlayTransform); }
// don't - surface.swapBuffers();
mResult.data = surface.saveFrameTo(Bitmap.CompressFormat.JPEG); // 6. Go back to old position
mResult.format = PictureResult.FORMAT_JPEG; Matrix.translateM(mTransform, 0, -0.5F, -0.5F, 0);
// 9. Cleanup // 7. Do pretty much the same for overlays
mSurfaceTexture.releaseTexImage(); if (mHasOverlay) {
surface.release(); // 1. First we must draw on the texture and get latest image
mViewport.release(); try {
mSurfaceTexture.release(); final Canvas surfaceCanvas = mOverlaySurface.lockCanvas(null);
if (mHasOverlay) { surfaceCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
mOverlaySurface.release(); mOverlay.drawOn(Overlay.Target.PICTURE_SNAPSHOT, surfaceCanvas);
mOverlaySurfaceTexture.release(); mOverlaySurface.unlockCanvasAndPost(surfaceCanvas);
} } catch (Surface.OutOfResourcesException e) {
core.release(); LOG.w("Got Surface.OutOfResourcesException while drawing picture overlays", e);
dispatchResult();
} }
}); mOverlaySurfaceTexture.updateTexImage();
mOverlaySurfaceTexture.getTransformMatrix(mOverlayTransform);
// 2. Then we can apply the transformations
int rotation = mEngine.getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
Matrix.translateM(mOverlayTransform, 0, 0.5F, 0.5F, 0);
Matrix.rotateM(mOverlayTransform, 0, rotation, 0, 0, 1);
// No need to flip the x axis for front camera, but need to flip the y axis always.
Matrix.scaleM(mOverlayTransform, 0, 1, -1, 1);
Matrix.translateM(mOverlayTransform, 0, -0.5F, -0.5F, 0);
}
// 8. Draw and save
mViewport.drawFrame(mTextureId, mTransform);
if (mHasOverlay) mViewport.drawFrame(mOverlayTextureId, mOverlayTransform);
mResult.format = PictureResult.FORMAT_JPEG;
mResult.data = eglSurface.saveFrameTo(Bitmap.CompressFormat.JPEG);
// 9. Cleanup
mSurfaceTexture.releaseTexImage();
eglSurface.releaseEglSurface();
mViewport.release();
mSurfaceTexture.release();
if (mHasOverlay) {
mOverlaySurfaceTexture.releaseTexImage();
mOverlaySurface.release();
mOverlaySurfaceTexture.release();
}
core.release();
dispatchResult();
} }
}); });
} }

@ -19,7 +19,7 @@ public class AspectRatio implements Comparable<AspectRatio> {
* @return a (possibly cached) aspect ratio * @return a (possibly cached) aspect ratio
*/ */
@NonNull @NonNull
public static AspectRatio of(Size size) { public static AspectRatio of(@NonNull Size size) {
return AspectRatio.of(size.getWidth(), size.getHeight()); return AspectRatio.of(size.getWidth(), size.getHeight());
} }
@ -78,7 +78,6 @@ public class AspectRatio implements Comparable<AspectRatio> {
return mY; return mY;
} }
@SuppressWarnings("WeakerAccess")
public boolean matches(@NonNull Size size) { public boolean matches(@NonNull Size size) {
int gcd = gcd(size.getWidth(), size.getHeight()); int gcd = gcd(size.getWidth(), size.getHeight());
int x = size.getWidth() / gcd; int x = size.getWidth() / gcd;
@ -86,6 +85,10 @@ public class AspectRatio implements Comparable<AspectRatio> {
return mX == x && mY == y; return mX == x && mY == y;
} }
public boolean matches(@NonNull Size size, float tolerance) {
return Math.abs(toFloat() - (float) size.getWidth() / size.getHeight()) <= tolerance;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (o == null) { if (o == null) {
@ -107,7 +110,6 @@ public class AspectRatio implements Comparable<AspectRatio> {
return mX + ":" + mY; return mX + ":" + mY;
} }
@SuppressWarnings("WeakerAccess")
public float toFloat() { public float toFloat() {
return (float) mX / mY; return (float) mX / mY;
} }

@ -98,6 +98,7 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
mOverlaySurfaceTexture = new SurfaceTexture(mOverlayTextureId); mOverlaySurfaceTexture = new SurfaceTexture(mOverlayTextureId);
mOverlaySurfaceTexture.setDefaultBufferSize(mResult.size.getWidth(), mResult.size.getHeight()); mOverlaySurfaceTexture.setDefaultBufferSize(mResult.size.getWidth(), mResult.size.getHeight());
mOverlaySurface = new Surface(mOverlaySurfaceTexture); mOverlaySurface = new Surface(mOverlaySurfaceTexture);
temp.release(true);
} }
} }
@ -135,6 +136,9 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
videoConfig.textureId = mTextureId; videoConfig.textureId = mTextureId;
videoConfig.scaleX = scaleX; videoConfig.scaleX = scaleX;
videoConfig.scaleY = scaleY; videoConfig.scaleY = scaleY;
// Get egl context from the RendererThread, which is the one in which we have created
// the textureId and the overlayTextureId, managed by the GlSurfaceView.
// Next operations can then be performed on different threads using this handle.
videoConfig.eglContext = EGL14.eglGetCurrentContext(); videoConfig.eglContext = EGL14.eglGetCurrentContext();
if (mHasOverlay) { if (mHasOverlay) {
videoConfig.overlayTextureId = mOverlayTextureId; videoConfig.overlayTextureId = mOverlayTextureId;

@ -1,6 +1,7 @@
package com.otaliastudios.cameraview.video.encoding; package com.otaliastudios.cameraview.video.encoding;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.media.ImageReader;
import android.opengl.Matrix; import android.opengl.Matrix;
import android.os.Build; import android.os.Build;
@ -94,8 +95,7 @@ public class TextureMediaEncoder extends VideoMediaEncoder<TextureConfig> {
super.onPrepare(controller, maxLengthMillis); super.onPrepare(controller, maxLengthMillis);
mEglCore = new EglCore(mConfig.eglContext, EglCore.FLAG_RECORDABLE); mEglCore = new EglCore(mConfig.eglContext, EglCore.FLAG_RECORDABLE);
mWindow = new EglWindowSurface(mEglCore, mSurface, true); mWindow = new EglWindowSurface(mEglCore, mSurface, true);
mWindow.makeCurrent(); // drawing will happen on the InputWindowSurface, which mWindow.makeCurrent();
// is backed by mVideoEncoder.getInputSurface()
mViewport = new EglViewport(); mViewport = new EglViewport();
} }

@ -53,16 +53,16 @@ abstract class VideoMediaEncoder<C extends VideoConfig> extends MediaEncoder {
protected void onPrepare(@NonNull MediaEncoderEngine.Controller controller, long maxLengthMillis) { protected void onPrepare(@NonNull MediaEncoderEngine.Controller controller, long maxLengthMillis) {
MediaFormat format = MediaFormat.createVideoFormat(mConfig.mimeType, mConfig.width, mConfig.height); MediaFormat format = MediaFormat.createVideoFormat(mConfig.mimeType, mConfig.width, mConfig.height);
// Set some properties. Failing to specify some of these can cause the MediaCodec // Failing to specify some of these can cause the MediaCodec configure() call to throw an unhelpful exception.
// configure() call to throw an unhelpful exception. // About COLOR_FormatSurface, see https://stackoverflow.com/q/28027858/4288782
// This just means it is an opaque, implementation-specific format that the device GPU prefers.
// So as long as we use the GPU to draw, the format will match what the encoder expects.
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, mConfig.bitRate); format.setInteger(MediaFormat.KEY_BIT_RATE, mConfig.bitRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, mConfig.frameRate); format.setInteger(MediaFormat.KEY_FRAME_RATE, mConfig.frameRate);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
format.setInteger("rotation-degrees", mConfig.rotation); format.setInteger("rotation-degrees", mConfig.rotation);
// Create a MediaCodec encoder, and configure it with our format. Get a Surface
// we can use for input and wrap it with a class that handles the EGL work.
try { try {
mMediaCodec = MediaCodec.createEncoderByType(mConfig.mimeType); mMediaCodec = MediaCodec.createEncoderByType(mConfig.mimeType);
} catch (IOException e) { } catch (IOException e) {

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.otaliastudios.cameraview.demo"> package="com.otaliastudios.cameraview.demo">
<uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/>
@ -8,9 +9,9 @@
android:allowBackup="false" android:allowBackup="false"
android:icon="@mipmap/cameraview" android:icon="@mipmap/cameraview"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/cameraview"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<activity <activity
android:name=".CameraActivity" android:name=".CameraActivity"

@ -13,30 +13,30 @@ import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.BitmapCallback; import com.otaliastudios.cameraview.BitmapCallback;
import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.PictureResult;
import java.lang.ref.WeakReference;
public class PicturePreviewActivity extends Activity { public class PicturePreviewActivity extends Activity {
private static WeakReference<PictureResult> image; private static PictureResult picture;
public static void setPictureResult(@Nullable PictureResult im) { public static void setPictureResult(@Nullable PictureResult pictureResult) {
image = im != null ? new WeakReference<>(im) : null; picture = pictureResult;
} }
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_picture_preview); setContentView(R.layout.activity_picture_preview);
final ImageView imageView = findViewById(R.id.image); final PictureResult result = picture;
final MessageView captureResolution = findViewById(R.id.nativeCaptureResolution);
final MessageView captureLatency = findViewById(R.id.captureLatency);
final MessageView exifRotation = findViewById(R.id.exifRotation);
PictureResult result = image == null ? null : image.get();
if (result == null) { if (result == null) {
finish(); finish();
return; return;
} }
final ImageView imageView = findViewById(R.id.image);
final MessageView captureResolution = findViewById(R.id.nativeCaptureResolution);
final MessageView captureLatency = findViewById(R.id.captureLatency);
final MessageView exifRotation = findViewById(R.id.exifRotation);
final long delay = getIntent().getLongExtra("delay", 0); final long delay = getIntent().getLongExtra("delay", 0);
AspectRatio ratio = AspectRatio.of(result.getSize()); AspectRatio ratio = AspectRatio.of(result.getSize());
captureLatency.setTitleAndMessage("Approx. latency", delay + " milliseconds"); captureLatency.setTitleAndMessage("Approx. latency", delay + " milliseconds");

@ -13,23 +13,28 @@ import android.widget.MediaController;
import android.widget.VideoView; import android.widget.VideoView;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.size.AspectRatio;
import java.lang.ref.WeakReference;
public class VideoPreviewActivity extends Activity { public class VideoPreviewActivity extends Activity {
private VideoView videoView; private VideoView videoView;
private static WeakReference<VideoResult> videoResult; private static VideoResult videoResult;
public static void setVideoResult(@Nullable VideoResult result) { public static void setVideoResult(@Nullable VideoResult result) {
videoResult = result != null ? new WeakReference<>(result) : null; videoResult = result;
} }
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video_preview); setContentView(R.layout.activity_video_preview);
final VideoResult result = videoResult;
if (result == null) {
finish();
return;
}
videoView = findViewById(R.id.video); videoView = findViewById(R.id.video);
videoView.setOnClickListener(new View.OnClickListener() { videoView.setOnClickListener(new View.OnClickListener() {
@Override @Override
@ -46,13 +51,8 @@ public class VideoPreviewActivity extends Activity {
final MessageView videoBitRate = findViewById(R.id.videoBitRate); final MessageView videoBitRate = findViewById(R.id.videoBitRate);
final MessageView videoFrameRate = findViewById(R.id.videoFrameRate); final MessageView videoFrameRate = findViewById(R.id.videoFrameRate);
final VideoResult result = videoResult == null ? null : videoResult.get(); AspectRatio ratio = AspectRatio.of(result.getSize());
if (result == null) { actualResolution.setTitleAndMessage("Size", result.getSize() + " (" + ratio + ")");
finish();
return;
}
actualResolution.setTitleAndMessage("Size", result.getSize() + "");
isSnapshot.setTitleAndMessage("Snapshot", result.isSnapshot() + ""); isSnapshot.setTitleAndMessage("Snapshot", result.isSnapshot() + "");
rotation.setTitleAndMessage("Rotation", result.getRotation() + ""); rotation.setTitleAndMessage("Rotation", result.getRotation() + "");
audio.setTitleAndMessage("Audio", result.getAudio().name()); audio.setTitleAndMessage("Audio", result.getAudio().name());
@ -85,8 +85,9 @@ public class VideoPreviewActivity extends Activity {
} }
void playVideo() { void playVideo() {
if (videoView.isPlaying()) return; if (!videoView.isPlaying()) {
videoView.start(); videoView.start();
}
} }
@Override @Override

@ -23,7 +23,7 @@
app:cameraGrid="off" app:cameraGrid="off"
app:cameraFlash="off" app:cameraFlash="off"
app:cameraAudio="on" app:cameraAudio="on"
app:cameraFacing="front" app:cameraFacing="back"
app:cameraGestureTap="autoFocus" app:cameraGestureTap="autoFocus"
app:cameraGestureLongTap="none" app:cameraGestureLongTap="none"
app:cameraGesturePinch="zoom" app:cameraGesturePinch="zoom"

Loading…
Cancel
Save