Add GLCameraPreview based on GLSurfaceView, preview appears to be working with some bugs

pull/360/head
Mattia Iavarone 7 years ago
parent c1e3cced4b
commit 4f6271d670
  1. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraPreviewTest.java
  2. 24
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  3. 19
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/GLCameraPreviewTest.java
  4. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/SurfaceCameraPreviewTest.java
  5. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureCameraPreviewTest.java
  6. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  7. 14
      cameraview/src/main/res/layout/cameraview_gl_view.xml
  8. 2
      cameraview/src/main/res/values/attrs.xml
  9. 6
      cameraview/src/main/views/com/otaliastudios/cameraview/CameraPreview.java
  10. 134
      cameraview/src/main/views/com/otaliastudios/cameraview/GLCameraPreview.java
  11. 92
      cameraview/src/main/views/com/otaliastudios/cameraview/GLElement.java
  12. 179
      cameraview/src/main/views/com/otaliastudios/cameraview/GLViewport.java
  13. 5
      demo/src/main/res/layout/activity_camera.xml

@ -17,7 +17,7 @@ import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
public abstract class PreviewTest extends BaseTest {
public abstract class CameraPreviewTest extends BaseTest {
protected abstract CameraPreview createPreview(Context context, ViewGroup parent, CameraPreview.SurfaceCallback callback);

@ -68,6 +68,30 @@ public class CameraViewTest extends BaseTest {
hasPermissions = false;
}
//region testLifecycle
@Test
public void testStart() {
cameraView.start();
verify(mockPreview, times(1)).onResume();
// Can't verify controller, depends on permissions.
// See to-do at the end.
}
@Test
public void testStop() {
cameraView.stop();
verify(mockPreview, times(1)).onPause();
verify(mockController, times(1)).stop();
}
@Test
public void testDestroy() {
cameraView.destroy();
verify(mockPreview, times(1)).onDestroy();
verify(mockController, times(1)).destroy();
}
//region testDefaults
@Test

@ -0,0 +1,19 @@
package com.otaliastudios.cameraview;
import android.content.Context;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.view.ViewGroup;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class GLCameraPreviewTest extends CameraPreviewTest {
@Override
protected CameraPreview createPreview(Context context, ViewGroup parent, CameraPreview.SurfaceCallback callback) {
return new GLCameraPreview(context, parent, callback);
}
}

@ -10,7 +10,7 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class SurfaceCameraPreviewTest extends PreviewTest {
public class SurfaceCameraPreviewTest extends CameraPreviewTest {
@Override
protected CameraPreview createPreview(Context context, ViewGroup parent, CameraPreview.SurfaceCallback callback) {

@ -10,7 +10,7 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class TextureCameraPreviewTest extends PreviewTest {
public class TextureCameraPreviewTest extends CameraPreviewTest {
@Override
protected CameraPreview createPreview(Context context, ViewGroup parent, CameraPreview.SurfaceCallback callback) {

@ -70,6 +70,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
TapGestureLayout mTapGestureLayout;
ScrollGestureLayout mScrollGestureLayout;
private boolean mKeepScreenOn;
private boolean mExperimental;
// Threading
private Handler mUiHandler;
@ -94,6 +95,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
// Self managed
boolean playSounds = a.getBoolean(R.styleable.CameraView_cameraPlaySounds, DEFAULT_PLAY_SOUNDS);
mExperimental = a.getBoolean(R.styleable.CameraView_cameraExperimental, false);
// Camera controller params
Facing facing = Facing.fromValue(a.getInteger(R.styleable.CameraView_cameraFacing, Facing.DEFAULT.value()));
@ -229,6 +231,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
protected CameraPreview instantiatePreview(Context context, ViewGroup container) {
// TextureView is not supported without hardware acceleration.
LOG.w("preview:", "isHardwareAccelerated:", isHardwareAccelerated());
if (mExperimental) return new GLCameraPreview(context, container, null);
return isHardwareAccelerated() ?
new TextureCameraPreview(context, container, null) :
new SurfaceCameraPreview(context, container, null);
@ -580,6 +583,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
public void start() {
if (!isEnabled()) return;
if (mCameraPreview != null) mCameraPreview.onResume();
if (checkPermissions(getMode(), getAudio())) {
// Update display orientation for current CameraController
@ -650,6 +654,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void stop() {
mCameraController.stop();
if (mCameraPreview != null) mCameraPreview.onPause();
}
@ -662,6 +667,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
clearCameraListeners();
clearFrameProcessors();
mCameraController.destroy();
if (mCameraPreview != null) mCameraPreview.onDestroy();
}
//endregion

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center">
<android.opengl.GLSurfaceView
android:id="@+id/gl_surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>

@ -118,5 +118,7 @@
<enum name="h264" value="2" />
</attr>
<attr name="cameraExperimental" format="boolean" />
</declare-styleable>
</resources>

@ -102,6 +102,12 @@ abstract class CameraPreview<T extends View, Output> {
mSurfaceHeight = 0;
}
void onResume() {}
void onPause() {}
void onDestroy() {}
final boolean isReady() {
return mSurfaceWidth > 0 && mSurfaceHeight > 0;
}

@ -0,0 +1,134 @@
package com.otaliastudios.cameraview;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.opengl.GLSurfaceView;
import android.support.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.SurfaceHolder;
import android.view.View;
import android.view.ViewGroup;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
class GLCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture> implements GLSurfaceView.Renderer {
private boolean mDispatched;
private final float[] mTransformMatrix = new float[16];
private int mTextureId = -1;
private SurfaceTexture mSurfaceTexture;
private GLViewport mViewport;
GLCameraPreview(Context context, ViewGroup parent, SurfaceCallback callback) {
super(context, parent, callback);
}
@NonNull
@Override
protected GLSurfaceView onCreateView(Context context, ViewGroup parent) {
View root = LayoutInflater.from(context).inflate(R.layout.cameraview_gl_view, parent, false);
parent.addView(root, 0);
GLSurfaceView glView = root.findViewById(R.id.gl_surface_view);
glView.setEGLContextClientVersion(2);
glView.setRenderer(this);
glView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
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) {
onSurfaceDestroyed();
}
});
return glView;
}
@Override
void onResume() {
super.onResume();
getView().onResume();
}
@Override
void onPause() {
super.onPause();
getView().onPause();
}
@Override
void onDestroy() {
super.onDestroy();
if (mSurfaceTexture != null) {
mSurfaceTexture.release();
mSurfaceTexture = null;
}
if (mViewport != null) {
mViewport.release();
mViewport = null;
}
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
mViewport = new GLViewport();
mTextureId = mViewport.createTexture();
mSurfaceTexture = new SurfaceTexture(mTextureId);
mSurfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
// requestRender is thread-safe.
getView().requestRender();
}
});
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
if (!mDispatched) {
onSurfaceAvailable(width, height);
mDispatched = true;
} else {
onSurfaceSizeChanged(width, height);
}
}
@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.
mSurfaceTexture.updateTexImage();
if (mDesiredWidth <= 0 || mDesiredHeight <= 0) {
// Skip drawing. Camera was not opened.
return;
}
// Draw the video frame.
mSurfaceTexture.getTransformMatrix(mTransformMatrix);
mViewport.drawFrame(mTextureId, mTransformMatrix);
}
@Override
Class<SurfaceTexture> getOutputClass() {
return SurfaceTexture.class;
}
@Override
SurfaceTexture getOutput() {
return mSurfaceTexture;
}
@Override
boolean supportsCropping() {
return true;
}
@Override
protected void crop() {
mCropTask.start();
mCropTask.end(null);
}
}

@ -0,0 +1,92 @@
package com.otaliastudios.cameraview;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.opengl.Matrix;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
class GLElement {
private final static CameraLogger LOG = CameraLogger.create(GLElement.class.getSimpleName());
// Identity matrix for general use.
protected static final float[] IDENTITY_MATRIX = new float[16];
static {
Matrix.setIdentityM(IDENTITY_MATRIX, 0);
}
// Check for GLES errors.
protected static void check(String opName) {
int error = GLES20.glGetError();
if (error != GLES20.GL_NO_ERROR) {
LOG.e("Error during", opName, "glError 0x", Integer.toHexString(error));
throw new RuntimeException(CameraLogger.lastMessage);
}
}
// Check for valid location.
protected static void checkLocation(int location, String label) {
if (location < 0) {
LOG.e("Unable to locate", label, "in program");
throw new RuntimeException(CameraLogger.lastMessage);
}
}
// Compiles the given shader, returns a handle.
protected static int loadShader(int shaderType, String source) {
int shader = GLES20.glCreateShader(shaderType);
check("glCreateShader type=" + shaderType);
GLES20.glShaderSource(shader, source);
GLES20.glCompileShader(shader);
int[] compiled = new int[1];
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
if (compiled[0] == 0) {
LOG.e("Could not compile shader", shaderType, ":", GLES20.glGetShaderInfoLog(shader));
GLES20.glDeleteShader(shader);
shader = 0;
}
return shader;
}
// Creates a program with given vertex shader and pixel shader.
protected static int createProgram(String vertexSource, String fragmentSource) {
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
if (vertexShader == 0) return 0;
int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
if (pixelShader == 0) return 0;
int program = GLES20.glCreateProgram();
check("glCreateProgram");
if (program == 0) {
LOG.e("Could not create program");
}
GLES20.glAttachShader(program, vertexShader);
check("glAttachShader");
GLES20.glAttachShader(program, pixelShader);
check("glAttachShader");
GLES20.glLinkProgram(program);
int[] linkStatus = new int[1];
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0] != GLES20.GL_TRUE) {
LOG.e("Could not link program:", GLES20.glGetProgramInfoLog(program));
GLES20.glDeleteProgram(program);
program = 0;
}
return program;
}
// Allocates a direct float buffer, and populates it with the float array data.
protected static FloatBuffer floatBuffer(float[] coords) {
// Allocate a direct ByteBuffer, using 4 bytes per float, and copy coords into it.
ByteBuffer bb = ByteBuffer.allocateDirect(coords.length * 4);
bb.order(ByteOrder.nativeOrder());
FloatBuffer fb = bb.asFloatBuffer();
fb.put(coords);
fb.position(0);
return fb;
}
}

@ -0,0 +1,179 @@
package com.otaliastudios.cameraview;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.opengl.Matrix;
import android.util.Log;
import java.nio.FloatBuffer;
class GLViewport extends GLElement {
private final static CameraLogger LOG = CameraLogger.create(GLViewport.class.getSimpleName());
// Simple vertex shader.
private static final String SIMPLE_VERTEX_SHADER =
"uniform mat4 uMVPMatrix;\n" +
"uniform mat4 uTexMatrix;\n" +
"attribute vec4 aPosition;\n" +
"attribute vec4 aTextureCoord;\n" +
"varying vec2 vTextureCoord;\n" +
"void main() {\n" +
" gl_Position = uMVPMatrix * aPosition;\n" +
" vTextureCoord = (uTexMatrix * aTextureCoord).xy;\n" +
"}\n";
// Simple fragment shader for use with external 2D textures
// (e.g. what we get from SurfaceTexture).
private static final String SIMPLE_FRAGMENT_SHADER =
"#extension GL_OES_EGL_image_external : require\n" +
"precision mediump float;\n" +
"varying vec2 vTextureCoord;\n" +
"uniform samplerExternalOES sTexture;\n" +
"void main() {\n" +
" gl_FragColor = texture2D(sTexture, vTextureCoord);\n" +
"}\n";
// Stuff from Drawable2d.FULL_RECTANGLE
// A full square, extending from -1 to +1 in both dimensions.
// When the model/view/projection matrix is identity, this will exactly cover the viewport.
private static final float FULL_RECTANGLE_COORDS[] = {
-1.0f, -1.0f, // 0 bottom left
1.0f, -1.0f, // 1 bottom right
-1.0f, 1.0f, // 2 top left
1.0f, 1.0f, // 3 top right
};
// Stuff from Drawable2d.FULL_RECTANGLE
// A full square, extending from -1 to +1 in both dimensions.
private static final float FULL_RECTANGLE_TEX_COORDS[] = {
0.0f, 0.0f, // 0 bottom left
1.0f, 0.0f, // 1 bottom right
0.0f, 1.0f, // 2 top left
1.0f, 1.0f // 3 top right
};
// Stuff from Drawable2d.FULL_RECTANGLE
private FloatBuffer mVertexArray = floatBuffer(FULL_RECTANGLE_COORDS);
private FloatBuffer mTexCoordArray = floatBuffer(FULL_RECTANGLE_TEX_COORDS);
private int mVertexCount = FULL_RECTANGLE_COORDS.length / 2;
private final int mCoordsPerVertex = 2;
private final int mVertexStride = 8;
private final int mTexCoordStride = 8;
// Stuff from Texture2dProgram
private int mProgramHandle;
private int mTextureTarget;
private int muMVPMatrixLoc;
private int muTexMatrixLoc;
private int maPositionLoc;
private int maTextureCoordLoc;
// private int muKernelLoc; // Used for filtering
// private int muTexOffsetLoc; // Used for filtering
// private int muColorAdjustLoc; // Used for filtering
GLViewport() {
mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES;
mProgramHandle = createProgram(SIMPLE_VERTEX_SHADER, SIMPLE_FRAGMENT_SHADER);
maPositionLoc = GLES20.glGetAttribLocation(mProgramHandle, "aPosition");
checkLocation(maPositionLoc, "aPosition");
maTextureCoordLoc = GLES20.glGetAttribLocation(mProgramHandle, "aTextureCoord");
checkLocation(maTextureCoordLoc, "aTextureCoord");
muMVPMatrixLoc = GLES20.glGetUniformLocation(mProgramHandle, "uMVPMatrix");
checkLocation(muMVPMatrixLoc, "uMVPMatrix");
muTexMatrixLoc = GLES20.glGetUniformLocation(mProgramHandle, "uTexMatrix");
checkLocation(muTexMatrixLoc, "uTexMatrix");
// Stuff from Drawable2d.FULL_RECTANGLE
}
void release() {
GLES20.glDeleteProgram(mProgramHandle);
mProgramHandle = -1;
}
int createTexture() {
int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0);
check("glGenTextures");
int texId = textures[0];
GLES20.glBindTexture(mTextureTarget, texId);
check("glBindTexture " + texId);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
check("glTexParameter");
return texId;
}
void drawFrame(int textureId, float[] texMatrix) {
drawFrame(textureId, texMatrix,
IDENTITY_MATRIX, mVertexArray, 0,
mVertexCount, mCoordsPerVertex,
mVertexStride, mTexCoordArray,
mTexCoordStride);
}
private void drawFrame(int textureId, float[] texMatrix,
float[] mvpMatrix, FloatBuffer vertexBuffer, int firstVertex,
int vertexCount, int coordsPerVertex, int vertexStride,
FloatBuffer texBuffer, int texStride) {
check("draw start");
// Select the program.
GLES20.glUseProgram(mProgramHandle);
check("glUseProgram");
// Set the texture.
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(mTextureTarget, textureId);
// Copy the model / view / projection matrix over.
GLES20.glUniformMatrix4fv(muMVPMatrixLoc, 1, false, mvpMatrix, 0);
check("glUniformMatrix4fv");
// Copy the texture transformation matrix over.
GLES20.glUniformMatrix4fv(muTexMatrixLoc, 1, false, texMatrix, 0);
check("glUniformMatrix4fv");
// Enable the "aPosition" vertex attribute.
GLES20.glEnableVertexAttribArray(maPositionLoc);
check("glEnableVertexAttribArray");
// Connect vertexBuffer to "aPosition".
GLES20.glVertexAttribPointer(maPositionLoc, coordsPerVertex,
GLES20.GL_FLOAT, false, vertexStride, vertexBuffer);
check("glVertexAttribPointer");
// Enable the "aTextureCoord" vertex attribute.
GLES20.glEnableVertexAttribArray(maTextureCoordLoc);
check("glEnableVertexAttribArray");
// Connect texBuffer to "aTextureCoord".
GLES20.glVertexAttribPointer(maTextureCoordLoc, 2,
GLES20.GL_FLOAT, false, texStride, texBuffer);
check("glVertexAttribPointer");
// Draw the rect.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, firstVertex, vertexCount);
check("glDrawArrays");
// Done -- disable vertex array, texture, and program.
GLES20.glDisableVertexAttribArray(maPositionLoc);
GLES20.glDisableVertexAttribArray(maTextureCoordLoc);
GLES20.glBindTexture(mTextureTarget, 0);
GLES20.glUseProgram(0);
}
}

@ -11,10 +11,11 @@
<com.otaliastudios.cameraview.CameraView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/camera"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:keepScreenOn="true"
app:cameraExperimental="true"
app:cameraPlaySounds="true"
app:cameraGrid="off"
app:cameraFacing="back"

Loading…
Cancel
Save