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

使用场景:

  • 相机实时滤镜

  • 视频播放特效

  • 多路视频合成

  • 屏幕录制

  • 直播推流

相关推荐
茄子凉心3 小时前
Android Bluetooth 蓝牙通信
android·蓝牙通信·bluetooth通信
00后程序员张4 小时前
iOS 26 App 运行状况全面解析 多工具协同监控与调试实战指南
android·ios·小程序·https·uni-app·iphone·webview
2501_916007475 小时前
iOS 混淆实战,多工具组合完成 IPA 混淆、加固与发布治理(iOS混淆|IPA加固|无源码混淆|App 防反编译)
android·ios·小程序·https·uni-app·iphone·webview
2501_915918415 小时前
怎么上架 App?iOS 应用上架完整流程详解与跨平台发布实战指南
android·ios·小程序·https·uni-app·iphone·webview
2501_929157685 小时前
【安卓+PC+IOS】psp全中文游戏+高清纹理包+金手指
android·游戏·ios
2501_916008896 小时前
iOS 混淆工具链实战 多工具组合完成 IPA 混淆与加固(iOS混淆|IPA加固|无源码加固|App 防反编译)
android·ios·小程序·https·uni-app·iphone·webview
yinghuaqipao6 小时前
面向对象——设计模式(创建型)
android·java·设计模式
用户41659673693556 小时前
Android 性能调优与故障排查:ADB 诊断命令终极指南
android
沐怡旸6 小时前
【底层机制】【Android】本地Socket 对比 Binder 以及在 Android系统中的应用
android·面试