GLSurfaceView显示Camera2的preview内容(支持前后摄像头切换和分辨率切换)

实现的基本功能

  1. 使用GLSurfaceView绘制camera的preview内容。
  2. 支持前后摄像头切换。
  3. 支持切换preview size。
  4. 通过手势可以缩放preview画面,移动previw画面。

初识OpenGl

GLSurfaceView为我们构建了一个OpenGl环境,如果我们想通过GLSurfaceView来渲染camera的Preview内容,那么我们必须掌握一些基础的OpenGl相关知识。如何使用OpenGL es接口绘制图形。重点需要学习下面的知识内容:

  1. 定义纹理显示位置,在没有使用mvp矩阵的情况下x轴、y轴、z轴的范围都是-1~1。
  2. 2d 纹理的栅格化的x轴、y轴范围是0~1
  3. 简单了解mvp矩阵的作用,因为opengl的位置信息与view系统的位置信息不统一,所以我们可以考虑使用mvp矩阵来把view系统的位置信息变换成opengl的位置信息。
  4. 如何使用纹理创建surface,因为camera的preview内容可以输出到surface上。

具体的代码实现

为了能够在GLSurfaceView上绘制内容,我定义了一个CustomRender类,这个类实现了GLSurfaceView.Renderer接口。我们可以在onDrawFrame方法中使用OPenGl来绘制camera preview内容。通常情况下我们除了需要绘制camera的preview内容,我们还需要绘制水印,sticker,filter等内容。所以我在这里把每一个绘制内容都抽象成一个node。NodeRender就是用于管理这些内容的。我们可以根据具体的需求来动态的添加绘制内容。frontBuffer是一个framebuffer的texture,NodeRender会将所有的node内容绘制到这个framebuffer上。

override fun onDrawFrame(gl: GL10?) {
        //绘制所以的node到frontBuffer上
        val frontBuffer = nodesRender.render()
        //清除屏幕内容
        GLES20.glViewport(0, 0, width, height)
        GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f)
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)
        //初识话显示用的Opengl程序
        if (displayProgram == null) {
            displayProgram = TextureProgram()
        }
        frontBuffer?:return
        //对显示位置进行变换,实现preview内容的缩放、移动操作。
        matrixHandler.applyVertexMatrix(OpenGlUtils.BufferUtils.SQUARE_VERTICES, displayPosition)
        //进行描画
        displayProgram!!.draw(
            frontBuffer.textureId,//描画的texture
            displayPosition,//描画的位置信息
            displayTextureCoordinate,//描画的纹理范围信息
            matrixValues//这里没有进行位置变换,所以使用的是单位矩阵
        )
    }

TextureProgram中的opengl程序是什么样的呢?首先来看下它定义的vertex shader和fragment shader。这两个shader非常简单,唯一需要注意的是位置坐标会经过mvp矩阵变换。我们这里传递的是单位矩阵,相当于无转换。如果我们的位置信息是参照view系统的而不是opengl系统定义的范围,那么我们需要根据具体的viewport大小来设置mvp矩阵。mvp矩阵设置方法。

"""
            attribute vec2 vPosition;
            attribute vec2 vInputTextureCoordinate;
            uniform mat4 mvpMatrix;
            varying vec2 vTextureCoordinate;
            void main(){
                gl_Position = mvpMatrix * vec4(vPosition,0.0,1.0);
                vTextureCoordinate = vInputTextureCoordinate; 
            }
        """,
    """
            precision mediump float;
            uniform sampler2D inputTexture;
            varying vec2 vTextureCoordinate;
             void main(){
                gl_FragColor = texture2D(inputTexture, vTextureCoordinate);
            }
        """

matrixHandler是如何实现的缩放和移动处理的呢?这里通过对显示矩形进行缩放和移动来实现的。显示矩形的计算主要通过updateVertexMatrix方法实现的。这个方法的输入参数包括屏幕的宽高和显示texture的宽高。通过输入的参数可以计算出fix center的显示矩形和fill center的显示矩形。我们可以根据实际需求来选择使用哪种显示矩形。详细的计算代码请参照下面的代码段。

 protected void updateVertexMatrix(int screenWidth, int screenHeight, int sourceWidth, int sourceHeight) {
        if (screenWidth <= 0 || screenHeight <= 0 || sourceWidth <= 0 || sourceHeight <= 0) {
            return;
        }

        boolean isLandscape = sourceRotate % 180 != 0;
        screenRectF = new RectF(0, 0, screenWidth, screenHeight);
        sourceRectF = new RectF(0, 0, isLandscape ? sourceHeight : sourceWidth, isLandscape ? sourceWidth : sourceHeight);
        if (displayRectF == null) {
            displayRectF = new RectF(0, 0, screenWidth, screenHeight);
        }
        minimumScaleSourceRectF = new RectF();
        maximumScaleSourceRectF = new RectF();

        float scaleH = displayRectF.height() / sourceRectF.height();
        float scaleW = displayRectF.width() / sourceRectF.width();
        minimumRealScale = scaleH < scaleW ? scaleH : scaleW;
        maximumRealScale = scaleH > scaleW ? scaleH : scaleW;

        Matrix matrix = new Matrix();
        matrix.setTranslate(displayRectF.centerX() - sourceRectF.centerX(), displayRectF.centerY() - sourceRectF.centerY());
        matrix.postScale(minimumRealScale, minimumRealScale, displayRectF.centerX(), displayRectF.centerY());
        matrix.mapRect(minimumScaleSourceRectF, sourceRectF);

        matrix.reset();
        matrix.setTranslate(displayRectF.centerX() - sourceRectF.centerX(), displayRectF.centerY() - sourceRectF.centerY());
        matrix.postScale(maximumRealScale, maximumRealScale, displayRectF.centerX(), displayRectF.centerY());
        matrix.mapRect(maximumScaleSourceRectF, sourceRectF);

        currentScaleRectF = new RectF(minimumScaleSourceRectF);
        currentScale = minimumRealScale;

        initRectFlag = true;
        updateMatrix();
    }

显示矩形已经计算好了,下面需要更新vertex的变换矩阵,使显示矩形的位置能够正确的映射到opengl的position进行显示位置的控制。这里通过updateMatrix()方法来更新变换矩阵。然后我们就可以对vertext坐标应用applyVertexMatrix来控制显示位置。在发生缩放或是移动手势的时候,我们对currentScaleRectF矩形实施缩放和移动操作,然后在调用updateMatrix()更新变换矩阵。

private void updateMatrix() {
        vertexMatrixLock.lock();
        try {
            float scaleX = currentScaleRectF.width() / screenRectF.width();
            float scaleY = currentScaleRectF.height() / screenRectF.height();

            applyVertexMatrix.reset();
            applyVertexMatrix.setScale(scaleX, scaleY);
            applyVertexMatrix.postTranslate((currentScaleRectF.centerX() - screenRectF.centerX()) * 2 / screenRectF.width(), -(currentScaleRectF.centerY() - screenRectF.centerY()) * 2 / screenRectF.height());
            needUpdateVertexMatrix = true;
        } finally {
            vertexMatrixLock.unlock();
        }
    }

由于camera需要设置surface来接收录制的内容,所以我们来看下如何通过textureId来生成surface。这里我定义了一个CombineSurfaceTexture类用于封装surface类型的texture。首先生成一个textureId,然后使用textureId生成一个SurfaceTexture对象,再用SurfaceTexture生成Surface对象。我这里使用GLSurfaceView.RENDERMODE_WHEN_DIRTY方式更新,所以在OnFrameAvailableListener中需要通知GLSurfaceView来更新。在我们绘制这个textureId的内容时,我们需要主动调用surfaceTexture.updateTexImage()来刷新到最新的数据。


class CombineSurfaceTexture(
    width: Int,
    height: Int,
    orientation: Int,
    flipX: Boolean = false,
    flipY: Boolean = false,
    notify: () -> Unit = {}
) :
    BasicTexture(width, height, orientation, flipX, flipY) {
    private val surfaceTexture: SurfaceTexture
    val surface: Surface

    init {
        textureId = OpenGlUtils.createTexture()
        surfaceTexture = SurfaceTexture(textureId)
        surfaceTexture.setDefaultBufferSize(width, height)
        surfaceTexture.setOnFrameAvailableListener { notify.invoke() }
        surface = Surface(surfaceTexture)
    }

    fun update() {
        if (surface.isValid) {
            surfaceTexture.updateTexImage()
        }
    }

    override fun release() {
        super.release()
        surface.release()
        surfaceTexture.release()
    }
}

在绘制Surface类型的texture的时候,我们需要声明输入texture的类型为samplerExternalOES。

"""
            attribute vec2 vPosition;
            attribute vec2 vInputTextureCoordinate;
            uniform mat4 mvpMatrix;
            varying vec2 vTextureCoordinate;
            void main(){
                gl_Position = mvpMatrix * vec4(vPosition,0.0,1.0);
                vTextureCoordinate = vInputTextureCoordinate; 
            }
        """,
    """
            #extension GL_OES_EGL_image_external : require
            precision mediump float;
            uniform samplerExternalOES inputTexture;
            varying vec2 vTextureCoordinate;
             void main(){
                gl_FragColor = texture2D(inputTexture, vTextureCoordinate);
            }
        """

这里没有直接把surface类型的texture绘制到GLSurfaceView上,而是先将它绘制到frameBuffer上然后再绘制到GLSurfaceView。这是因为为后面实现录制、添加filter、添加水印等工作做准备。
前后摄像头切换和更改分辨率的操作是什么样的处理呢?其实从下面的代码可以看到这两种情况下都把preview使用的texture释放了并生成新的texture和surface。由于我这里把绘制的程序封装到node里,所以这里进行了preview node替换。

switchCamera.setOnClickListener {
            nodesRender.runInRender {
                val cameraId = when (cameraHolder.cameraId) {
                    CAMERA_REAR -> CAMERA_FRONT
                    else -> CAMERA_REAR
                }
                cameraHolder.cameraId = cameraId
                updatePreviewNode(
                    cameraHolder.previewSizes.first().width,
                    cameraHolder.previewSizes.first().height
                )
                cameraHolder.setSurface(cameraPreviewNode!!.combineSurfaceTexture.surface)
                    .invalidate()
            }
        }
        previewSize.setOnClickListener {
            cameraHolder?.let {
                it.previewSizes?.let { sizes ->
                    val builder = AlertDialog.Builder(this@MainActivity)
                    val sizesString: Array = Array(sizes?.size ?: 0) { "" }
                    sizes?.forEachIndexed { index, item ->
                        sizesString[index] = item.width.toString() + "*" + item.height.toString()
                    }
                    builder.setItems(sizesString) { d, index ->
                        val size = sizesString[index].split("*")
                        val width = size[0].toInt()
                        val height = size[1].toInt()

                        nodesRender.runInRender {
                            updatePreviewNode(width, height)
                            cameraHolder.setSurface(cameraPreviewNode!!.combineSurfaceTexture.surface)
                                .invalidate()
                        }
                    }
                    builder.create().show()
                }
            }
        }

git地址

https://github.com/mjlong1231...

你可能感兴趣的