一、什么是SurfaceTexture?
SurfaceTexture是Android中用于捕获图像流作为OpenGL ES纹理的类。它可以接收来自相机、视频解码器等的图像数据,并将其转换为OpenGL纹理供渲染使用。
核心特点
1.充当生产者-消费者模式中的消费者
2.将图像数据转换为OpenGL纹理
3.支持异步更新,不阻塞主线程
4.常用于相机预览、视频播放、屏幕录制等场景
二、SurfaceTexture的工作原理
SurfaceTexture内部维护一个BufferQueue(缓冲队列) :
- 生产者(相机,视频解码器)将图像写入队列
- 消费者(SurfaceTexture)从队列读取
- 采用多缓冲机制,避免卡顿
纹理类型:GL_TEXTURE_EXTERNAL_OES
SurfaceTexture使用的纹理是外部纹理:
js
#extension GL_OES_EGL_image_external : require
uniform samplerExternalOES uTexture; // 注意:不是sampler2D
三、SurfaceTexture的核心API
| 方法 | 功能 | 调用时机 |
|---|---|---|
updateTexImage() |
更新纹理内容 | 收到新帧回调后 |
getTransformMatrix(float[]) |
获取纹理变换矩阵 | 每次渲染前 |
setOnFrameAvailableListener() |
设置新帧回调 | 初始化时 |
attachToGLContext(int) |
附加到GL上下文 | 切换上下文时 |
detachFromGLContext() |
从GL上下文分离 | 切换上下文时 |
release() |
释放资源 | 不再使用时 |
setDefaultBufferSize(int, int) |
设置缓冲区大小 | 初始化时 |
四、实战案例
实现相机预览并添加滤镜效果
对CameraX使用不了解的可以看这篇文章: CameraX:Android相机开发的现代化解决方案
核心代码:
相机设置:
js
/**
* 启动相机
*/
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
// 获取相机提供者
val cameraProvider = cameraProviderFuture.get()
// 创建预览用例
val preview = Preview.Builder().build()
// 选择后置相机
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// 解绑所有用例
cameraProvider.unbindAll()
// 将相机与生命周期绑定
val camera = cameraProvider.bindToLifecycle(
this, cameraSelector, preview)
// 获取CameraView的SurfaceTexture
val surfaceTexture = binding.cameraView.getSurfaceTexture()
// 设置预览的SurfaceProvider
surfaceTexture?.let { texture ->
val surface = Preview.SurfaceProvider { request ->
texture.setDefaultBufferSize(
request.resolution.width,
request.resolution.height
)
val surface = android.view.Surface(texture)
request.provideSurface(surface, cameraExecutor) { }
}
preview.setSurfaceProvider(surface)
}
} catch (e: Exception) {
Log.e(TAG, "相机绑定失败", e)
Toast.makeText(this, "相机启动失败: ${e.message}", Toast.LENGTH_SHORT).show()
}
}, ContextCompat.getMainExecutor(this)) // 因为要预览画面,所以要设置为主线程
}
相机预览:
js
/**
* 相机预览视图
* 使用OpenGL ES渲染相机预览,并支持滤镜切换
*/
class CameraView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
): GLSurfaceView(context, attrs), GLSurfaceView.Renderer, SurfaceTexture.OnFrameAvailableListener {
private var mCameraTexture: SurfaceTexture? = null
private val filterManager = FilterManager(context)
private var currentFilter: ScreenFilter? = null
// 纹理ID
private var textureId = 0
// 是否初始化完成
private var isInitialized = false
init {
// 设置OpenGL ES版本号
setEGLContextClientVersion(2)
setRenderer(this)
//设置手动渲染
renderMode = RENDERMODE_WHEN_DIRTY
}
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
// 向OpenGL申请创建一个新的、空的纹理对象,并获取这个纹理对象的唯一ID
val textures = IntArray(1)
GLES20.glGenTextures(1, textures, 0)
textureId = textures[0]
// 创建SurfaceTexture
mCameraTexture = SurfaceTexture(textureId)
mCameraTexture?.setOnFrameAvailableListener(this)
// 获取当前滤镜
currentFilter = filterManager.getFilter()
isInitialized = true
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
}
override fun onDrawFrame(gl: GL10?) {
//这行代码本身并不执行任何绘制或清除操作,它只是告诉OpenGL:"下一次当你被命令去清除颜色缓冲区时,请使用这个我现在指定的颜色
GLES20.glClearColor(0f, 0f, 0f, 1f)
// 执行清除操作。这行代码是一个执行命令。它命令OpenGL:现在,请使用你当前存储的'清除颜色'(也就是我们上一步设置的黑色),去填充(擦除)指定的缓冲区
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) // 这指定了颜色缓冲区。颜色缓冲区是最终显示在屏幕上的像素颜色信息的存储位置。所以清除它,就等于擦除了屏幕上的画面
mCameraTexture?.let {
// 捕获数据流中最新到达的一帧图像,并将其更新到 SurfaceTexture 所管理的那个OpenGL纹理上
// 将 SurfaceTexture 缓冲区(共享内存)中最新的那帧图像数据,正式绑定并更新到它所管理的OpenGL纹理(即 textureId)上。
// 只有在这之后,GPU才能在着色器中通过采样器访问到这帧新图像。
// SurfaceTexture的缓冲区巧妙地避开了CPU,实现了数据从生产者到GPU的直接通路
it.updateTexImage()
val mtx = FloatArray(16)
// 根据从相机那里接收到的画面的实际情况,计算出一个修正矩阵,并把结果填满准备好的mtx里
it.getTransformMatrix(mtx)
// 使用更新后的纹理ID和变换矩阵将图像绘制到屏幕上
currentFilter?.onDraw(width, height, mtx, textureId)
}
}
// SurfaceTexture 在接收到新的一帧数据后,会立即在其指定的监听器上回调 onFrameAvailable() 方法
// onFrameAvailable() 方法并不在你的主UI线程或OpenGL渲染线程上执行。它通常是在一个由Android系统框架管理的特定线程(例如Binder线程)上被调用的。
override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
// 通知 GLSurfaceView 渲染器(Renderer)的数据已经"变脏"(dirty),需要进行一次重绘
// requestRender() 不会立即调用 onDrawFrame()。它只是向 GLSurfaceView 的渲染线程发送一个请求,告诉它在下一个合适的时机执行一次渲染。
requestRender()
}
/**
* 切换滤镜
*/
fun switchFilter() {
queueEvent {
filterManager.switchToNextFilter()
currentFilter = filterManager.getFilter()
}
}
/**
* 获取SurfaceTexture,用于CameraX预览
*/
fun getSurfaceTexture(): SurfaceTexture? {
return mCameraTexture
}
/**
* 释放资源
*/
fun release() {
filterManager.release()
mCameraTexture?.release()
mCameraTexture = null
}
}
着色器核心代码:
js
init {
val vert = readRawTextFile(context, R.raw.camera_vert)
// 创建顶点程序
val vShader = createShader(SHADER_TYPE_VERTEX, vert)
val frag = readRawTextFile(context, R.raw.camera_frag)
// 创建片段程序
val fragShader = createShader(SHADER_TYPE_FRAGMENT, frag)
// 创建并链接程序
program = createAndLinkProgram(vShader, fragShader)
// 实际分配的是主内存 它是一种特殊的内存,称为DMA内存,GPU可以直接从该内存中读取数据,不需要CPU参与 设置字节序order(ByteOrder.nativeOrder())
// 这个内存与VBO内存不同,VBO内存才是GPU中的内存
vertexBuffer = ByteBuffer.allocateDirect(4 * 2 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer()
vertexBuffer.clear()
vertexBuffer.put(VERTEX)
textureBuffer = ByteBuffer.allocateDirect(4 * 2 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer()
textureBuffer.clear()
textureBuffer.put(TEXTURE)
}
js
fun onDraw(width: Int, height: Int,mtx:FloatArray,textures: Int) {
GLES20.glViewport(0,0,width,height)
GLES20.glUseProgram(program)
// 读取指针拨回到最开始的位置
vertexBuffer.position(0)
textureBuffer.position(0)
//定位到GPU的变量地址
vPosition = GLES20.glGetAttribLocation(program,"vPosition")
vCoord = GLES20.glGetAttribLocation(program,"vCoord")
vTexture = GLES20.glGetUniformLocation(program, "vTexture")
vMatrix = GLES20.glGetUniformLocation(program, "vMatrix")
// 这两行代码必须成对出现。只有先详细地描述了数据,然后才能启用它
GLES20.glVertexAttribPointer(vPosition,2,GLES20.GL_FLOAT,false,0,vertexBuffer)// 这行代码是描述性的
GLES20.glEnableVertexAttribArray(vPosition) // 这行代码是执行性的
GLES20.glVertexAttribPointer(vCoord,2,GLES20.GL_FLOAT,false,0,textureBuffer)
GLES20.glEnableVertexAttribArray(vCoord)
// 激活0号纹理单元
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
// 把摄像头画面(身份证号为textures的那个纹理)绑定到当前激活的0号纹理单元上。"
GLES20.glBindTexture(GLES20.GL_TEXTURE0,textures)
// 告诉片元着色器里的vTexture这个采样器(sampler2D),让它去0号纹理单元采样颜色
GLES20.glUniform1i(vTexture,0)
// 把CPU内存里的这个mtx矩阵的值,传递给你顶点着色器里的vMatrix这个uniform变量
GLES20.glUniformMatrix4fv(vMatrix,1,false,mtx,0)
// 通知GPU渲染
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,4)
// 释放资源
GLES20.glDisableVertexAttribArray(vPosition)
GLES20.glDisableVertexAttribArray(vCoord)
}
完整代码以及详细解释都在下面这个链接里[github.com/zhaohaiyiti...]
五、总结
SurfaceTexture核心特点:
-
SurfaceTexture是桥梁:连接图像生产者和OpenGL消费者
-
必须在GL线程操作:线程安全至关重要
-
变换矩阵不可少:处理旋转、镜像、裁剪
-
及时释放资源:避免内存泄漏
-
使用OES纹理:性能更好,支持更多格式
使用场景:
-
相机实时滤镜
-
视频播放特效
-
多路视频合成
-
屏幕录制
-
直播推流