「Android音视频编码那点破事」第二章,使用TextureView渲染Camera画面

2022-05-16 12:39:36 浏览数 (1)

封面出自:板栗懒得很

 本章仅对部分代码进行讲解,以帮助读者更好的理解章节内容。本系列文章涉及的项目HardwareVideoCodec已经开源到Github,支持软编和硬编。使用它你可以很容易的实现任何分辨率的视频编码,无需关心摄像头预览大小。一切都如此简单。目前已迭代多个稳定版本,欢迎查阅学习和使用,如有BUG或建议,欢迎Issue。

  上一章我们讲到了使用SurfaceTexture作为Camera数据的缓冲区,这仅仅是把帧数据缓冲到了纹理上,并没有把它绘制出来,所以这一章我们来实现这个功能。

  按照惯例,还是先来个脑图,以便很好的了解这部分的结构。

  首先来看看Render接口,其中定义了一系列方法:

  • onFrameAvailable: 在SurfaceTexture.OnFrameAvailableListener的同名回调方法中调用,通知Render摄像头的SurfaceTexture有新的数据生成,可以准备进行处理了,这里是绘制到屏幕。
  • draw:把帧数据绘制到屏幕上。
  • start:接收一个SurfaceTexture,把它绑定到OpenGL环境上即可进行屏幕绘制。
  • stop:停止渲染。
  • release:并且释放资源。
  • setFilter:设置一个渲染滤镜(待实现)
  • getFrameBuffer: Int:获取屏幕纹理的frameBuffer,设置了滤镜后会返回滤镜的frameBuffer。
  • getFrameBufferTexture: Int:获取屏幕纹理的id,设置了滤镜后会返回滤镜的纹理id。
代码语言:javascript复制
interface Render {
    fun onFrameAvailable(): Render
    fun draw()
    fun start(texture: SurfaceTexture, width: Int, height: Int)
    fun start(texture: SurfaceTexture, width: Int, height: Int, runnable: Runnable?)
    fun stop()
    fun release()
    /**
     * After render completed
     */
    fun afterRender(runnable: Runnable)

    fun setFilter(filter: BaseFrameBufferTexture)
    fun getFrameBuffer(): Int
    fun getFrameBufferTexture(): Int
}

  由于Render需要知道Camera的纹理中是否有数据,所以需要接收Camera SurfaceTexture的回调。在这个项目中,Render是被CameraPreviewPresenter管理的,所以我们对上一章讲到的CameraPreviewPresenter进行扩展。

  我们可以看到,这里实现了OnFrameAvailableListener接口,并且在CameraWrapper.open的时候传给了CameraWrapper,在这个类内部又会把接口设置给SurfaceTexture,当这个缓冲区中有数据时,就会回调这个接口中的方法来通知我们进行处理。

  于此同时,我么也初始化了一个DefaultRenderImpl对象,这个对象接收上面我们讲到的回调通知,用来把SurfaceTexture缓冲区中的数据绘制到屏幕。

  数据入口我们有了,合适开始预览呢。当TextureView初始化完成时,我们调用startPreview(screenTexture: SurfaceTexture, width: Int, height: Int)方法,来通知CameraWrapper把帧数据绘制(缓冲)到Camera的SurfaceTexture中。并且Render接收TextureView的SurfaceTexture缓冲区,包括宽高,在内部初始化完成后开始渲染。

  Tip:这里会有两个SurfaceTexture,一个时我们自己初始化给Camera的SurfaceTexture,一个是TextureView提供的SurfaceTexture。前者是Camera缓冲区,后者是屏幕缓冲区。

代码语言:javascript复制
class CameraPreviewPresenter(var parameter: Parameter,
                             private var cameraWrapper: CameraWrapper? = null,
                             private var render: Render? = null,)
    : SurfaceTexture.OnFrameAvailableListener {
    init {
        cameraWrapper = CameraWrapper.open(parameter, this)
        render = DefaultRenderImpl(parameter, cameraWrapper!!.textureWrapper as CameraTextureWrapper)
    }
    /**
     * Camera有数据生成时回调
     * For CameraWrapper
     */
    override fun onFrameAvailable(cameraTexture: SurfaceTexture?) {
        render?.onFrameAvailable()
    }
    fun startPreview(screenTexture: SurfaceTexture, width: Int, height: Int) {
        synchronized(syncOp) {
            cameraWrapper!!.startPreview()
            render?.start(screenTexture, width, height)
        }
    }
}

  接下来时本章的重。我们首先实现一个Render,名字就叫做DefaultRenderImpl,它包含一系列必要的属性。

  • Parameter:用来初始化Render的参数
  • CameraTextureWrapper:上一章初始化的Camera纹理环境,它的EGL会跟Render环境在同一线程中初始化,注意,必须时同一个线程。
  • SurfaceTexture:前面讲到的由TextureView提供的屏幕纹理缓冲区。
  • ScreenTextureWrapper:屏幕纹理缓冲区的环境。
  • width:TextureView的宽度。
  • height:TextureView的高度。
  • viewportX和viewportY:绘制到OpenGL坐标中的位置(左上角)

  和Camera环境一样,我们先在主线程初始化一组HandlerThread/Handler,在Handler中定义三个事件INIT、RENDER、STOP,分别对应初始化、有新的帧数据需要绘制、停止并释放资源。

代码语言:javascript复制
class DefaultRenderImpl(var parameter: Parameter,
                        var cameraWrapper: CameraTextureWrapper,
                        var transformMatrix: FloatArray = FloatArray(16),
                        var screenTexture: SurfaceTexture? = null,
                        var screenWrapper: ScreenTextureWrapper? = null,
                        var width: Int = 1,
                        var height: Int = 1,
                        private var viewportX: Int = 0,
                        private var viewportY: Int = 0
    : Render {
    init {
        mHandlerThread.start()
        mHandler = object : Handler(mHandlerThread.looper) {
            override fun handleMessage(msg: Message) {
                when (msg.what) {
                    INIT -> {
                        init()
                        if (null != msg.obj) {
                            (msg.obj as Runnable).run()
                        }
                    }
                    RENDER -> {
                        draw()
                    }
                    STOP -> {
                        mHandlerThread.quitSafely()
                        screenWrapper?.release()
                    }
                }
            }
        }
    }
}

  按照顺序,INIT事件会在start方法中发送出去,这时候Handler接收到INIT事件,开始在mHandlerThread中调用init方法初始化环境

  摄像头的缓冲区EGL环境在这里正式开始初始化cameraWrapper.initEGL(parameter.video.width, parameter.video.height),之后解这初始化一组没有任何特效的滤镜NormalTextureFilter,最后再初始化ScreenTextureWrapper,这个是屏幕缓冲区的环境。

  Tip:在上一章我们说到了,frameBuffer和frameBufferTexture是一对孪生兄弟,前者用于缓冲数据,后者用于读取数据。所以这里的数据流时这样的:CameraFrameBufferTexture-> NormalTextureFilterFrameBufferTexture-> ScreenTexture。

代码语言:javascript复制
    override fun start(texture: SurfaceTexture, width: Int, height: Int, runnable: Runnable?) {
        updateScreenTexture(texture)
        initViewport(width, height)
        if (mHandlerThread.isAlive)
            mHandler?.sendMessage(mHandler!!.obtainMessage(INIT, runnable))
    }

    fun init() {
        cameraWrapper.initEGL(parameter.video.width, parameter.video.height)
        filter = NormalTextureFilter(parameter.video.width, parameter.video.height)
        filter.textureId = cameraWrapper.getFrameBufferTexture()
        screenWrapper = ScreenTextureWrapper(screenTexture, cameraWrapper.egl!!.eglContext!!)
    }

  环境初始化完成之后就可以开始渲染了,RENDER事件理所当然要在

代码语言:javascript复制
onFrameAvailable中发送出去。接下来会在子线程中调用draw方法。
    override fun onFrameAvailable(): Render {
        try {
            if (mHandlerThread.isAlive)
                mHandler?.sendEmptyMessage(RENDER)
        } catch (e: Exception) {
        }
        return this
    }

  从代码我们可以看到,在正式开始绘制到屏幕之前,还调用了drawCamera和drawFilter,这两个方法分别是

  • drawCamera():上一章我们只是给Camera设置了一个缓冲区,如果不显式的通知SurfaceTexture去缓冲数据,我们时拿不到Camera数据的。所以这里时从Camera中取出数据,存放在Camera的SurfaceTexture
  • drawFilter():把Camera的SurfaceTexture数据经过处理后保存在自己的frameBuffer中
  • 接下来就可以让screen缓冲区去filter中取数据了。

  这里需要注意的是,在代码上,我们并没有看到数据的流动,这一切都是通过frameBuffer和frameBufferTexture来进行传递了,上一章我们说到,这两个都只是一个ID,这就是OpenGL的特点。

代码语言:javascript复制
    override fun draw() {
        drawCamera()
        drawFilter()
        screenWrapper?.egl?.makeCurrent()
        GLES20.glViewport(viewportX, viewportY, width, height)
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
        GLES20.glClearColor(0.3f, 0.3f, 0.3f, 0f)
        screenWrapper?.drawTexture(transformMatrix)
        screenWrapper?.egl?.swapBuffers()
        runnable?.run()
    }
    private fun drawCamera() {
        if (null != cameraWrapper.surfaceTexture) {
            cameraWrapper.surfaceTexture?.updateTexImage()
            cameraWrapper.surfaceTexture?.getTransformMatrix(transformMatrix)
        }
        cameraWrapper.egl?.makeCurrent("cameraWrapper")
        GLES20.glViewport(0, 0, parameter.previewHeight, parameter.previewWidth)
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
        GLES20.glClearColor(0.3f, 0.3f, 0.3f, 0f)
        cameraWrapper.drawTexture(transformMatrix)
    }
    private fun drawFilter() {
        GLES20.glViewport(0, 0, parameter.video.width, parameter.video.height)
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
        filter.drawTexture(null)
    }

  Render的逻辑我们已经比较清晰了,那ScreenTextureWrapper里面干了上面呢,其实跟CameraTextureWrapper是大同小异的,也是封装了SurfaceTexture和EGL,至不是一个是是Camera缓冲区,一个是屏幕缓冲区。只不过ScreenTextureWrapper的EGL环境是在构造方法里面初始化的,因为ScreenTextureWrapper是在子线程中新建的,所以没有跨线程的问题。

代码语言:javascript复制
class ScreenTextureWrapper(override var surfaceTexture: SurfaceTexture? = null,
                           var eglContext: EGLContext? = null) : TextureWrapper() {
    init {
        if (null != surfaceTexture) {
            egl = Egl()
            egl!!.initEGL(surfaceTexture!!, eglContext)
            egl!!.makeCurrent()
            texture = NormalTexture(textureId!!)
        } else {
            debug_e("Egl create failed")
        }
    }

    fun setFilter(texture: BaseTexture) {
        this.texture = texture
    }

    override fun drawTexture(transformMatrix: FloatArray?) {
        if (null == texture) {
            debug_e("Render failed. Texture is null")
            return
        }
        texture?.drawTexture(transformMatrix)
    }
}

  在ScreenTextureWrapper构造方法里面还新建了一个纹理NormalTexture,和CameraTexture不同,NormalTexture是继承自BaseTexture的,他没有FBO实现,这是因为数据流到这里(屏幕)已经是终点了,没有别的地方需要屏幕纹理的数据。而CameraTexture和filter中的纹理数据还需要传递到别的地方,包括之后会讲到的硬编和软编码器中的纹理。

代码语言:javascript复制
class NormalTexture(textureId: Int) : BaseTexture(textureId) {
    override fun drawTexture(transformMatrix: FloatArray?) {
        GLES20.glUseProgram(shaderProgram!!)
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
        GLES20.glUniform1i(uTextureLocation, 0)
        enableVertex(aPositionLocation, aTextureCoordinateLocation, buffer!!, verticesBuffer!!)

        drawer.draw()

        GLES20.glFinish()
        GLES20.glDisableVertexAttribArray(aPositionLocation)
        GLES20.glDisableVertexAttribArray(aTextureCoordinateLocation)
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, GLES20.GL_NONE)
        GLES20.glUseProgram(GLES20.GL_NONE)
    }
}

  不出意外的话,现在你已经可以在屏幕上看到画面了。这里需要注意的是glViewport的处理。

  在这个项目中,默认的录像分辨率是720X480,所以会选择一个1280X720的分辨率进行预览(如果有的话),所以在drawCamera中Viewport的大小应该是预览分辨率的大小。

  由于我们需要的分辨率是720X480,所以要进行裁剪,这一步由filter完成。filter纹理的大小我们设置720X480就好,这时候就需要注意Viewport大小和位置了,因为这个分辨率跟Camera纹理的分辨率不一样,所以要进行定位裁剪,使用glViewport改变视图大小位置即可。

  至此,你已经学会了

  1. OpenGL的基本使用
  2. FBO(Frame Buffer Object)
  3. EGL
  4. 离屏缓冲
  5. 摄像头预览
  6. 画面裁剪 Enjoy it!

分类:

多媒体系列文章

0 人点赞