封面出自:板栗懒得很
本章仅对部分代码进行讲解,以帮助读者更好的理解章节内容。本系列文章涉及的项目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。
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改变视图大小位置即可。
至此,你已经学会了
- OpenGL的基本使用
- FBO(Frame Buffer Object)
- EGL
- 离屏缓冲
- 摄像头预览
- 画面裁剪 Enjoy it!
分类:
多媒体系列文章