美文网首页Android音视频系列Android开发经验谈Android开发
「Android音视频编码那点破事」第二章,使用TextureV

「Android音视频编码那点破事」第二章,使用TextureV

作者: Alimin利民 | 来源:发表于2018-04-26 22:19 被阅读93次

      本章仅对部分代码进行讲解,以帮助读者更好的理解章节内容。本系列文章涉及的项目HardwareVideoCodec已经开源到Github。目前已迭代多个稳定版本,欢迎查阅学习和使用,如有BUG或建议,欢迎Issue。

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

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


    Render

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

    • onFrameAvailable(): 在SurfaceTexture.OnFrameAvailableListener的同名回调方法中调用,通知Render摄像头的SurfaceTexture有新的数据生成,可以准备进行处理了,这里是绘制到屏幕。
    • draw():把帧数据绘制到屏幕上。
    • start(texture: SurfaceTexture, width: Int, height: Int):接收一个SurfaceTexture,把它绑定到OpenGL环境上即可进行屏幕绘制。
    • stop():停止渲染。
    • release():并且释放资源。
    • setFilter(filter: BaseFrameBufferTexture):设置一个渲染滤镜(待实现)
    • getFrameBuffer(): Int:获取屏幕纹理的frameBuffer,设置了滤镜后会返回滤镜的frameBuffer
    • getFrameBufferTexture(): Int:获取屏幕纹理的id,设置了滤镜后会返回滤镜的纹理id。
    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进行扩展。

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

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

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

    • Tip:这里会有两个SurfaceTexture,一个时我们自己初始化给CameraSurfaceTexture,一个是TextureView提供的SurfaceTexture。前者是Camera缓冲区,后者是屏幕缓冲区。
    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中定义三个事件INITRENDERSTOP,分别对应初始化、有新的帧数据需要绘制、停止并释放资源。

    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:在上一章我们说到了,frameBufferframeBufferTexture是一对孪生兄弟,前者用于缓冲数据,后者用于读取数据。所以这里的数据流时这样的:
      CameraFrameBufferTexture-> NormalTextureFilterFrameBufferTexture-> ScreenTexture
        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事件理所当然要在onFrameAvailable中发送出去。接下来会在子线程中调用draw方法。

        override fun onFrameAvailable(): Render {
            try {
                if (mHandlerThread.isAlive)
                    mHandler?.sendEmptyMessage(RENDER)
            } catch (e: Exception) {
            }
            return this
        }
    

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

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

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

        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是大同小异的,也是封装了SurfaceTextureEGL,至不是一个是是Camera缓冲区,一个是屏幕缓冲区。只不过ScreenTextureWrapperEGL环境是在构造方法里面初始化的,因为ScreenTextureWrapper是在子线程中新建的,所以没有跨线程的问题。

    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实现,这是因为数据流到这里(屏幕)已经是终点了,没有别的地方需要屏幕纹理的数据。而CameraTexturefilter中的纹理数据还需要传递到别的地方,包括之后会讲到的硬编和软编码器中的纹理。

    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改变视图大小位置即可。

      至此,你已经学会了

    • OpenGL的基本使用
    • FBO(Frame Buffer Object)
    • EGL
    • 离屏缓冲
    • 摄像头预览
    • 画面裁剪
      Enjoy it!

    相关文章

      网友评论

        本文标题:「Android音视频编码那点破事」第二章,使用TextureV

        本文链接:https://www.haomeiwen.com/subject/nptvlftx.html