「Android音视频编码那点破事」第二章,使用TextureV
本章仅对部分代码进行讲解,以帮助读者更好的理解章节内容。本系列文章涉及的项目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
把帧数据绘制(缓冲)到Camera
的SurfaceTexture
中。并且Render
接收TextureView
的SurfaceTexture
缓冲区,包括宽高,在内部初始化完成后开始渲染。
- Tip:这里会有两个
SurfaceTexture
,一个时我们自己初始化给Camera
的SurfaceTexture
,一个是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中定义三个事件INIT
、RENDER
、STOP
,分别对应初始化、有新的帧数据需要绘制、停止并释放资源。
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
。
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
}
从代码我们可以看到,在正式开始绘制到屏幕之前,还调用了drawCamera
和drawFilter
,这两个方法分别是
- drawCamera():上一章我们只是给
Camera
设置了一个缓冲区,如果不显式的通知SurfaceTexture
去缓冲数据,我们时拿不到Camera
数据的。所以这里时从Camera
中取出数据,存放在Camera
的SurfaceTexture
- drawFilter():把
Camera
的SurfaceTexture
数据经过处理后保存在自己的frameBuffer
中 - 接下来就可以让screen缓冲区去
filter
中取数据了。
这里需要注意的是,在代码上,我们并没有看到数据的流动,这一切都是通过frameBuffer
和frameBufferTexture
来进行传递了,上一章我们说到,这两个都只是一个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
是大同小异的,也是封装了SurfaceTexture
和EGL
,至不是一个是是Camera
缓冲区,一个是屏幕缓冲区。只不过ScreenTextureWrapper
的EGL
环境是在构造方法里面初始化的,因为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
实现,这是因为数据流到这里(屏幕)已经是终点了,没有别的地方需要屏幕纹理的数据。而CameraTexture
和filter
中的纹理数据还需要传递到别的地方,包括之后会讲到的硬编和软编码器中的纹理。
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!