|
|
|
@ -14,6 +14,7 @@ import android.os.Build; |
|
|
|
|
|
|
|
|
|
import com.otaliastudios.cameraview.CameraLogger; |
|
|
|
|
import com.otaliastudios.cameraview.PictureResult; |
|
|
|
|
import com.otaliastudios.cameraview.internal.egl.EglBaseSurface; |
|
|
|
|
import com.otaliastudios.cameraview.overlay.Overlay; |
|
|
|
|
import com.otaliastudios.cameraview.controls.Facing; |
|
|
|
|
import com.otaliastudios.cameraview.engine.CameraEngine; |
|
|
|
@ -35,6 +36,23 @@ import androidx.annotation.Nullable; |
|
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
|
|
|
|
|
private static final String TAG = SnapshotGlPictureRecorder.class.getSimpleName(); |
|
|
|
@ -47,6 +65,17 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { |
|
|
|
|
private Overlay mOverlay; |
|
|
|
|
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( |
|
|
|
|
@NonNull PictureResult.Stub stub, |
|
|
|
|
@NonNull CameraEngine engine, |
|
|
|
@ -66,19 +95,23 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { |
|
|
|
|
public void take() { |
|
|
|
|
mPreview.addRendererFrameCallback(new RendererFrameCallback() { |
|
|
|
|
|
|
|
|
|
int mTextureId; |
|
|
|
|
SurfaceTexture mSurfaceTexture; |
|
|
|
|
float[] mTransform; |
|
|
|
|
|
|
|
|
|
int mOverlayTextureId = 0; |
|
|
|
|
SurfaceTexture mOverlaySurfaceTexture; |
|
|
|
|
Surface mOverlaySurface; |
|
|
|
|
float[] mOverlayTransform; |
|
|
|
|
@RendererThread |
|
|
|
|
public void onRendererTextureCreated(int textureId) { |
|
|
|
|
SnapshotGlPictureRecorder.this.onRendererTextureCreated(textureId); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
EglViewport mViewport; |
|
|
|
|
@RendererThread |
|
|
|
|
@Override |
|
|
|
|
public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, final float scaleX, final float scaleY) { |
|
|
|
|
mPreview.removeRendererFrameCallback(this); |
|
|
|
|
SnapshotGlPictureRecorder.this.onRendererFrame(scaleX, scaleY); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@RendererThread |
|
|
|
|
public void onRendererTextureCreated(int textureId) { |
|
|
|
|
@TargetApi(Build.VERSION_CODES.KITKAT) |
|
|
|
|
private void onRendererTextureCreated(int textureId) { |
|
|
|
|
mTextureId = textureId; |
|
|
|
|
mViewport = new EglViewport(); |
|
|
|
|
mSurfaceTexture = new SurfaceTexture(mTextureId, true); |
|
|
|
@ -97,43 +130,49 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* The tricky part here is the EGL surface creation. |
|
|
|
|
* |
|
|
|
|
* We don't have a real output window for the EGL surface - we will use glReadPixels() |
|
|
|
|
* and never call swapBuffers(), so what we draw is never published. |
|
|
|
|
* |
|
|
|
|
* 1. One option is to use a pbuffer EGL surface. This works, we just have to pass |
|
|
|
|
* the correct width and height. However, it is significantly slower than the current |
|
|
|
|
* solution. |
|
|
|
|
* |
|
|
|
|
* 2. Another option is to create the EGL surface out of a ImageReader.getSurface() |
|
|
|
|
* and use the reader to create a JPEG. In this case, we would have to publish |
|
|
|
|
* the frame with swapBuffers(). However, currently ImageReader does not support |
|
|
|
|
* all formats, it's risky. This is an example error that we get: |
|
|
|
|
* "RGBA override BLOB format buffer should have height == width" |
|
|
|
|
* |
|
|
|
|
* The third option, which we are using, is to create the EGL surface using whatever |
|
|
|
|
* {@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. |
|
|
|
|
* |
|
|
|
|
* @param scaleX frame scale x in {@link Reference#VIEW} |
|
|
|
|
* @param scaleY frame scale y in {@link Reference#VIEW} |
|
|
|
|
*/ |
|
|
|
|
@RendererThread |
|
|
|
|
@Override |
|
|
|
|
public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, final float scaleX, final float scaleY) { |
|
|
|
|
mPreview.removeRendererFrameCallback(this); |
|
|
|
|
|
|
|
|
|
// This kinda work but has drawbacks:
|
|
|
|
|
// - output is upside down due to coordinates in GL: need to flip the byte[] someway
|
|
|
|
|
// - output is not rotated as we would like to: need to create a bitmap copy...
|
|
|
|
|
// - works only in the renderer thread, where it allocates the buffer and reads pixels. Bad!
|
|
|
|
|
/* |
|
|
|
|
ByteBuffer buffer = ByteBuffer.allocateDirect(width * height * 4); |
|
|
|
|
buffer.order(ByteOrder.LITTLE_ENDIAN); |
|
|
|
|
GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buffer); |
|
|
|
|
buffer.rewind(); |
|
|
|
|
ByteArrayOutputStream bos = new ByteArrayOutputStream(buffer.array().length); |
|
|
|
|
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); |
|
|
|
|
bitmap.copyPixelsFromBuffer(buffer); |
|
|
|
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, bos); |
|
|
|
|
bitmap.recycle(); */ |
|
|
|
|
|
|
|
|
|
// For this reason it is better to create a new surface,
|
|
|
|
|
// and draw the last frame again there.
|
|
|
|
|
@TargetApi(Build.VERSION_CODES.KITKAT) |
|
|
|
|
private void onRendererFrame(final float scaleX, final float 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.
|
|
|
|
|
final EGLContext eglContext = EGL14.eglGetCurrentContext(); |
|
|
|
|
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 |
|
|
|
|
public void run() { |
|
|
|
|
// 0. Create an EGL surface
|
|
|
|
|
EglBaseSurface eglSurface = new EglWindowSurface(core, mSurfaceTexture); |
|
|
|
|
eglSurface.makeCurrent(); |
|
|
|
|
|
|
|
|
|
// 1. Get latest texture
|
|
|
|
|
EglWindowSurface surface = new EglWindowSurface(core, mSurfaceTexture); |
|
|
|
|
surface.makeCurrent(); |
|
|
|
|
mSurfaceTexture.updateTexImage(); |
|
|
|
|
mSurfaceTexture.getTransformMatrix(mTransform); |
|
|
|
|
|
|
|
|
|
// 2. Apply scale and crop:
|
|
|
|
|
// scaleX and scaleY are in REF_VIEW, while our input appears to be in REF_SENSOR.
|
|
|
|
|
// 2. Apply scale and crop
|
|
|
|
|
boolean flip = mEngine.getAngles().flip(Reference.VIEW, Reference.SENSOR); |
|
|
|
|
float realScaleX = flip ? scaleY : scaleX; |
|
|
|
|
float realScaleY = flip ? scaleX : scaleY; |
|
|
|
@ -142,26 +181,24 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { |
|
|
|
|
Matrix.translateM(mTransform, 0, scaleTranslX, scaleTranslY, 0); |
|
|
|
|
Matrix.scaleM(mTransform, 0, realScaleX, realScaleY, 1); |
|
|
|
|
|
|
|
|
|
// 3. 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); |
|
|
|
|
|
|
|
|
|
// 4. Apply rotation:
|
|
|
|
|
// Not sure why we need the minus here.
|
|
|
|
|
// 4. Apply rotation (not sure why we need the minus here)
|
|
|
|
|
Matrix.rotateM(mTransform, 0, -mResult.rotation, 0, 0, 1); |
|
|
|
|
mResult.rotation = 0; |
|
|
|
|
|
|
|
|
|
// 5. Flip horizontally for front camera:
|
|
|
|
|
// 5. Flip horizontally for front camera
|
|
|
|
|
if (mResult.facing == Facing.FRONT) { |
|
|
|
|
Matrix.scaleM(mTransform, 0, -1, 1, 1); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 6. Go back to old position.
|
|
|
|
|
// 6. Go back to old position
|
|
|
|
|
Matrix.translateM(mTransform, 0, -0.5F, -0.5F, 0); |
|
|
|
|
|
|
|
|
|
// 7. Do pretty much the same for overlays, though with
|
|
|
|
|
// some differences.
|
|
|
|
|
// 7. Do pretty much the same for overlays
|
|
|
|
|
if (mHasOverlay) { |
|
|
|
|
// 1. First we must draw on the texture and get latest image.
|
|
|
|
|
// 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); |
|
|
|
@ -173,7 +210,7 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { |
|
|
|
|
mOverlaySurfaceTexture.updateTexImage(); |
|
|
|
|
mOverlaySurfaceTexture.getTransformMatrix(mOverlayTransform); |
|
|
|
|
|
|
|
|
|
// 2. Then we can apply the transformations.
|
|
|
|
|
// 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); |
|
|
|
@ -185,16 +222,16 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { |
|
|
|
|
// 8. Draw and save
|
|
|
|
|
mViewport.drawFrame(mTextureId, mTransform); |
|
|
|
|
if (mHasOverlay) mViewport.drawFrame(mOverlayTextureId, mOverlayTransform); |
|
|
|
|
// don't - surface.swapBuffers();
|
|
|
|
|
mResult.data = surface.saveFrameTo(Bitmap.CompressFormat.JPEG); |
|
|
|
|
mResult.format = PictureResult.FORMAT_JPEG; |
|
|
|
|
mResult.data = eglSurface.saveFrameTo(Bitmap.CompressFormat.JPEG); |
|
|
|
|
|
|
|
|
|
// 9. Cleanup
|
|
|
|
|
mSurfaceTexture.releaseTexImage(); |
|
|
|
|
surface.release(); |
|
|
|
|
eglSurface.releaseEglSurface(); |
|
|
|
|
mViewport.release(); |
|
|
|
|
mSurfaceTexture.release(); |
|
|
|
|
if (mHasOverlay) { |
|
|
|
|
mOverlaySurfaceTexture.releaseTexImage(); |
|
|
|
|
mOverlaySurface.release(); |
|
|
|
|
mOverlaySurfaceTexture.release(); |
|
|
|
|
} |
|
|
|
@ -203,8 +240,6 @@ public class SnapshotGlPictureRecorder extends PictureRecorder { |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
protected void dispatchResult() { |
|
|
|
|