Android SurfaceTexture 深度解析

一、什么是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纹理:性能更好,支持更多格式

使用场景:

  • 相机实时滤镜

  • 视频播放特效

  • 多路视频合成

  • 屏幕录制

  • 直播推流

相关推荐
xiangpanf8 小时前
Laravel 10.x重磅升级:五大核心特性解析
android
robotx11 小时前
安卓线程相关
android
消失的旧时光-194311 小时前
Android 面试高频:JSON 文件、大数据存储与断电安全(从原理到工程实践)
android·面试·json
dalancon12 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon13 小时前
VSYNC 信号完整流程2
android
dalancon13 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户693717500138414 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android14 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才15 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶15 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle