package com.lassi.presentation.cameraview.preview; import android.content.Context; import android.graphics.SurfaceTexture; import android.opengl.GLSurfaceView; import android.opengl.Matrix; import android.view.LayoutInflater; import android.view.SurfaceHolder; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.lassi.R; import com.lassi.presentation.cameraview.controls.AspectRatio; import com.lassi.presentation.cameraview.video.EglViewport; import java.util.Collections; import java.util.HashSet; import java.util.Set; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; /** * - The android camera will stream image to the given {@link SurfaceTexture}. * <p> * - in the SurfaceTexture constructor we pass the GL texture handle that we have created. * <p> * - The SurfaceTexture is linked to the Camera1 object. The camera will pass down buffers of data with * a specified size (that is, the Camera1 preview size). For this reason we don't have to specify * surfaceTexture.setDefaultBufferSize() (like we do, for example, in SnapshotPictureRecorder). * <p> * - When SurfaceTexture.updateTexImage() is called, it will fetch the latest texture image from the * camera stream and assign it to the GL texture that was passed. * Now the GL texture must be drawn using draw* APIs. The SurfaceTexture will also give us * the transformation matrix to be applied. * <p> * - The easy way to render an OpenGL texture is using the {@link GLSurfaceView} class. * It manages the GL context, hosts a surface and runs a separated rendering thread that will perform * the rendering. * <p> * - As per docs, we ask the GLSurfaceView to delegate rendering to us, using * {@link GLSurfaceView#setRenderer(GLSurfaceView.Renderer)}. We request a render on the SurfaceView * anytime the SurfaceTexture notifies that it has new data available (see OnFrameAvailableListener below). * <p> * - So in short: * - The SurfaceTexture has buffers of data of mInputStreamSize * - The SurfaceView hosts a view (and a surface) of size mOutputSurfaceSize. * These are determined by the CameraView.onMeasure method. * - We have a GL rich texture to be drawn (in the given method & thread). * <p> * This class will provide rendering callbacks to anyone who registers a {@link RendererFrameCallback}. * Callbacks are guaranteed to be called on the renderer thread, which means that we can fetch * the GL context that was created and is managed by the {@link GLSurfaceView}. */ public class GlCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture> implements GLSurfaceView.Renderer { private final float[] mTransformMatrix = new float[16]; /* for tests */ float mScaleX = 1F; /* for tests */ float mScaleY = 1F; private boolean mDispatched; private int mOutputTextureId = 0; private SurfaceTexture mInputSurfaceTexture; private EglViewport mOutputViewport; private Set<RendererFrameCallback> mRendererFrameCallbacks = Collections.synchronizedSet(new HashSet<RendererFrameCallback>()); private View mRootView; public GlCameraPreview(@NonNull Context context, @NonNull ViewGroup parent, @Nullable SurfaceCallback callback) { super(context, parent, callback); } @NonNull @Override protected GLSurfaceView onCreateView(@NonNull Context context, @NonNull ViewGroup parent) { ViewGroup root = (ViewGroup) LayoutInflater.from(context).inflate(R.layout.cameraview_gl_view, parent, false); GLSurfaceView glView = root.findViewById(R.id.gl_surface_view); glView.setEGLContextClientVersion(2); glView.setRenderer(this); glView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); // Tried these 2 to remove the black background, does not work. // glView.getHolder().setFormat(PixelFormat.TRANSLUCENT); // glView.setZOrderMediaOverlay(true); glView.getHolder().addCallback(new SurfaceHolder.Callback() { public void surfaceCreated(SurfaceHolder holder) { } public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { dispatchOnSurfaceDestroyed(); mDispatched = false; } }); parent.addView(root, 0); mRootView = root; return glView; } @NonNull @Override public View getRootView() { return mRootView; } @Override public void onResume() { super.onResume(); getView().onResume(); } @Override public void onPause() { super.onPause(); getView().onPause(); } @Override public void onDestroy() { super.onDestroy(); // View is gone, so EGL context is gone: callbacks make no sense anymore. mRendererFrameCallbacks.clear(); if (mInputSurfaceTexture != null) { mInputSurfaceTexture.setOnFrameAvailableListener(null); mInputSurfaceTexture.release(); mInputSurfaceTexture = null; } mOutputTextureId = 0; if (mOutputViewport != null) { mOutputViewport.release(); mOutputViewport = null; } } @RendererThread @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { mOutputViewport = new EglViewport(); mOutputTextureId = mOutputViewport.createTexture(); mInputSurfaceTexture = new SurfaceTexture(mOutputTextureId); getView().queueEvent(new Runnable() { @Override public void run() { for (RendererFrameCallback callback : mRendererFrameCallbacks) { callback.onRendererTextureCreated(mOutputTextureId); } } }); // Since we are using GLSurfaceView.RENDERMODE_WHEN_DIRTY, we must notify the SurfaceView // of dirtyness, so that it draws again. This is how it's done. mInputSurfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() { @Override public void onFrameAvailable(SurfaceTexture surfaceTexture) { getView().requestRender(); // requestRender is thread-safe. } }); } @RendererThread @SuppressWarnings("StatementWithEmptyBody") @Override public void onSurfaceChanged(GL10 gl, final int width, final int height) { gl.glViewport(0, 0, width, height); if (!mDispatched) { dispatchOnSurfaceAvailable(width, height); mDispatched = true; } else if (width != mOutputSurfaceWidth || height != mOutputSurfaceHeight) { dispatchOnSurfaceSizeChanged(width, height); } } @RendererThread @Override public void onDrawFrame(GL10 gl) { // Latch the latest frame. If there isn't anything new, // we'll just re-use whatever was there before. mInputSurfaceTexture.updateTexImage(); if (mInputStreamWidth <= 0 || mInputStreamHeight <= 0) { // Skip drawing. Camera was not opened. return; } mInputSurfaceTexture.getTransformMatrix(mTransformMatrix); if (isCropping()) { // Scaling is easy. However: // If the view is 10x1000 (very tall), it will show only the left strip of the preview (not the center one). // If the view is 1000x10 (very large), it will show only the bottom strip of the preview (not the center one). // So we must use Matrix.translateM, and it must happen before the crop. float translX = (1F - mScaleX) / 2F; float translY = (1F - mScaleY) / 2F; Matrix.translateM(mTransformMatrix, 0, translX, translY, 0); Matrix.scaleM(mTransformMatrix, 0, mScaleX, mScaleY, 1); } // Future note: passing scale to the viewport? // They are scaleX an scaleY, but flipped based on mInputFlipped. mOutputViewport.drawFrame(mOutputTextureId, mTransformMatrix); for (RendererFrameCallback callback : mRendererFrameCallbacks) { callback.onRendererFrame(mInputSurfaceTexture, mScaleX, mScaleY); } } @NonNull @Override public Class<SurfaceTexture> getOutputClass() { return SurfaceTexture.class; } @NonNull @Override public SurfaceTexture getOutput() { return mInputSurfaceTexture; } @Override public boolean supportsCropping() { return true; } /** * To crop in GL, we could actually use view.setScaleX and setScaleY, but only from Android N onward. * See documentation: https://developer.android.com/reference/android/view/SurfaceView * <p> * Note: Starting in platform version Build.VERSION_CODES.N, SurfaceView's window position is updated * synchronously with other View rendering. This means that translating and scaling a SurfaceView on * screen will not cause rendering artifacts. Such artifacts may occur on previous versions of the * platform when its window is positioned asynchronously. * <p> * But to support older platforms, this seem to work - computing scale values and requesting a new frame, * then drawing it with a scaled transformation matrix. See {@link #onDrawFrame(GL10)}. */ @Override protected void crop() { mCropTask.start(); if (mInputStreamWidth > 0 && mInputStreamHeight > 0 && mOutputSurfaceWidth > 0 && mOutputSurfaceHeight > 0) { float scaleX = 1f, scaleY = 1f; AspectRatio current = AspectRatio.Companion.of(mOutputSurfaceWidth, mOutputSurfaceHeight); AspectRatio target = AspectRatio.Companion.of(mInputStreamWidth, mInputStreamHeight); if (current.toFloat() >= target.toFloat()) { // We are too short. Must increase height. scaleY = current.toFloat() / target.toFloat(); } else { // We must increase width. scaleX = target.toFloat() / current.toFloat(); } mCropping = scaleX > 1.02f || scaleY > 1.02f; mScaleX = 1F / scaleX; mScaleY = 1F / scaleY; getView().requestRender(); } mCropTask.end(null); } public void addRendererFrameCallback(@NonNull final RendererFrameCallback callback) { getView().queueEvent(new Runnable() { @Override public void run() { mRendererFrameCallbacks.add(callback); if (mOutputTextureId != 0) callback.onRendererTextureCreated(mOutputTextureId); } }); } public void removeRendererFrameCallback(@NonNull final RendererFrameCallback callback) { mRendererFrameCallbacks.remove(callback); } public interface RendererFrameCallback { /** * Called on the renderer thread, hopefully only once, to notify that * the texture was created (or to inform a new callback of the old texture). * * @param textureId the GL texture linked to the image stream */ @RendererThread void onRendererTextureCreated(int textureId); /** * Called on the renderer thread after each frame was drawn. * You are not supposed to hold for too long onto this thread, because * well, it is the rendering thread. * * @param surfaceTexture the texture to get transformation * @param scaleX the scaleX (in REF_VIEW) value * @param scaleY the scaleY (in REF_VIEW) value */ @RendererThread void onRendererFrame(SurfaceTexture surfaceTexture, float scaleX, float scaleY); } }