You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

咨询Android端Snapchat/Instagram视频捕获实现及滤镜录制问题

Snapchat/Instagram 风格的视频捕获+滤镜实现方案(Android Kotlin)

问题根源

你当前的方案(MediaRecorder直接录摄像头、PixelCopy录屏幕)失效的核心原因:

  • MediaRecorder直接录制摄像头时,只能捕获原始摄像头帧,完全看不到UI层叠加的滤镜——因为滤镜是绘制在独立的View层级,和摄像头的Surface输出是分离的。
  • PixelCopy无法捕获SurfaceView/TextureView的硬件加速内容:摄像头预览通常用这两个控件,它们的渲染在硬件层完成,受限于设备硬件隔离机制,PixelCopy只能拿到UI元素,拿不到摄像头画面。

Snapchat、Instagram这类APP的核心思路是摄像头帧数据 → GPU实时滤镜渲染 → 同时输出到预览和编码器,全程在GPU链路完成,不依赖UI层叠加。

正确实现步骤

1. 用CameraX替代原生Camera API

CameraX是Google官方推荐的摄像头框架,比Camera2简洁,能轻松获取摄像头帧数据或Surface输出,方便后续滤镜处理。

2. 基于OpenGL ES实现实时滤镜+录制(核心方案)

这是大厂的标准实现,性能最优,支持复杂滤镜。核心逻辑:

  • 让CameraX把预览帧输出到OpenGL的SurfaceTexture
  • 在OpenGL中创建两个输出Surface:一个绑定到屏幕预览的TextureView,另一个绑定到MediaRecorder的输入Surface
  • 对每一帧摄像头数据应用滤镜(通过自定义Shader实现)
  • 将处理后的帧同时绘制到两个Surface,实现“预览带滤镜+录制带滤镜”

关键代码片段

初始化CameraX与渲染器
class MainActivity : AppCompatActivity() {
    private lateinit var textureView: TextureView
    private lateinit var filterRenderer: FilterRenderer
    private lateinit var preview: Preview

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        textureView = findViewById(R.id.preview_texture)
        
        // 配置CameraX预览参数
        preview = Preview.Builder()
            .setTargetAspectRatio(AspectRatio.RATIO_16_9)
            .build()

        // 初始化滤镜渲染器
        filterRenderer = FilterRenderer(textureView)

        // TextureView就绪后启动渲染与摄像头
        textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
            override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
                filterRenderer.init(width, height)
                preview.setSurfaceProvider(textureView.surfaceProvider)
                CameraX.bindToLifecycle(this@MainActivity, preview)
            }

            override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
                filterRenderer.updateSize(width, height)
            }

            override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
                filterRenderer.release()
                return true
            }

            override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
                filterRenderer.renderFrame(surface)
            }
        }
    }
}
滤镜渲染器核心逻辑
class FilterRenderer(private val textureView: TextureView) {
    private lateinit var eglHelper: EglHelper
    private lateinit var mediaRecorder: MediaRecorder
    private var encoderSurface: Surface? = null
    private var shaderProgram: Int = 0
    private var cameraTextureId: Int = 0

    fun init(width: Int, height: Int) {
        // 初始化EGL环境,用于OpenGL渲染
        eglHelper = EglHelper()
        eglHelper.init(textureView.surfaceTexture, width, height)

        // 加载灰度滤镜Shader(可替换为自定义滤镜)
        shaderProgram = createShaderProgram(
            """attribute vec4 aPosition; attribute vec2 aTexCoord; varying vec2 vTexCoord; void main() { gl_Position = aPosition; vTexCoord = aTexCoord; }""",
            """precision mediump float; varying vec2 vTexCoord; uniform sampler2D uTexture; void main() { vec4 color = texture2D(uTexture, vTexCoord); float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114)); gl_FragColor = vec4(gray, gray, gray, color.a); }"""
        )

        // 初始化MediaRecorder
        mediaRecorder = MediaRecorder().apply {
            setAudioSource(MediaRecorder.AudioSource.MIC)
            setVideoSource(MediaRecorder.VideoSource.SURFACE)
            setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
            setVideoEncoder(MediaRecorder.VideoEncoder.H264)
            setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
            setVideoSize(width, height)
            setVideoFrameRate(30)
            setOutputFile(getOutputFilePath())
            prepare()
        }
        encoderSurface = mediaRecorder.surface
    }

    fun renderFrame(surfaceTexture: SurfaceTexture) {
        // 初始化摄像头纹理ID
        if (cameraTextureId == 0) {
            cameraTextureId = createTextureFromSurfaceTexture(surfaceTexture)
        }

        // 渲染到预览Surface
        eglHelper.makeCurrent()
        drawFrame(cameraTextureId)
        eglHelper.swapBuffers()

        // 渲染到编码器Surface,实现录制
        encoderSurface?.let {
            eglHelper.makeCurrent(it)
            drawFrame(cameraTextureId)
            eglHelper.swapBuffers()
        }
    }

    private fun drawFrame(textureId: Int) {
        GLES20.glClearColor(0f, 0f, 0f, 1f)
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)

        GLES20.glUseProgram(shaderProgram)
        // 绑定顶点坐标、纹理坐标、纹理ID(省略顶点缓冲区配置代码)
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
    }

    // 辅助方法:创建Shader程序
    private fun createShaderProgram(vertexShader: String, fragmentShader: String): Int {
        val vertex = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER)
        GLES20.glShaderSource(vertex, vertexShader)
        GLES20.glCompileShader(vertex)

        val fragment = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER)
        GLES20.glShaderSource(fragment, fragmentShader)
        GLES20.glCompileShader(fragment)

        val program = GLES20.glCreateProgram()
        GLES20.glAttachShader(program, vertex)
        GLES20.glAttachShader(program, fragment)
        GLES20.glLinkProgram(program)
        return program
    }

    // 辅助方法:从SurfaceTexture创建纹理
    private fun createTextureFromSurfaceTexture(surfaceTexture: SurfaceTexture): Int {
        val textureIds = IntArray(1)
        GLES20.glGenTextures(1, textureIds, 0)
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureIds[0])
        GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
        GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
        surfaceTexture.attachToGLContext(textureIds[0])
        return textureIds[0]
    }

    // 获取视频输出路径
    private fun getOutputFilePath(): String {
        return externalMediaDirs.first().absolutePath + "/filtered_video.mp4"
    }

    fun updateSize(width: Int, height: Int) {
        eglHelper.updateSize(width, height)
    }

    fun release() {
        mediaRecorder.stop()
        mediaRecorder.release()
        eglHelper.release()
        GLES20.glDeleteTextures(1, intArrayOf(cameraTextureId), 0)
    }
}

// EGL操作工具类(封装EGL初始化、上下文切换等)
class EglHelper {
    private var eglDisplay: EGLDisplay = EGL14.EGL_NO_DISPLAY
    private var eglContext: EGLContext = EGL14.EGL_NO_CONTEXT
    private var eglSurface: EGLSurface = EGL14.EGL_NO_SURFACE

    fun init(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
        eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
        val version = IntArray(2)
        EGL14.eglInitialize(eglDisplay, version, 0, version, 1)

        val configAttribs = intArrayOf(
            EGL14.EGL_RED_SIZE, 8,
            EGL14.EGL_GREEN_SIZE, 8,
            EGL14.EGL_BLUE_SIZE, 8,
            EGL14.EGL_ALPHA_SIZE, 8,
            EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
            EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT,
            EGL14.EGL_NONE
        )
        val configs = arrayOfNulls<EGLConfig>(1)
        val numConfigs = IntArray(1)
        EGL14.eglChooseConfig(eglDisplay, configAttribs, 0, configs, 0, 1, numConfigs, 0)
        val config = configs[0] ?: throw RuntimeException("No EGL config found")

        val contextAttribs = intArrayOf(
            EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
            EGL14.EGL_NONE
        )
        eglContext = EGL14.eglCreateContext(eglDisplay, config, EGL14.EGL_NO_CONTEXT, contextAttribs, 0)

        val surfaceAttribs = intArrayOf(EGL14.EGL_NONE)
        eglSurface = EGL14.eglCreateWindowSurface(eglDisplay, config, surfaceTexture, surfaceAttribs, 0)
    }

    fun makeCurrent(surface: Surface? = null) {
        val targetSurface = surface?.let {
            EGL14.eglCreateWindowSurface(eglDisplay, null, it, intArrayOf(EGL14.EGL_NONE), 0)
        } ?: eglSurface
        EGL14.eglMakeCurrent(eglDisplay, targetSurface, targetSurface, eglContext)
    }

    fun swapBuffers() {
        EGL14.eglSwapBuffers(eglDisplay, eglSurface)
    }

    fun updateSize(width: Int, height: Int) {
        EGL14.eglDestroySurface(eglDisplay, eglSurface)
        val surfaceAttribs = intArrayOf(EGL14.EGL_NONE)
        eglSurface = EGL14.eglCreateWindowSurface(eglDisplay, null, textureView.surfaceTexture, surfaceAttribs, 0)
    }

    fun release() {
        EGL14.eglDestroySurface(eglDisplay, eglSurface)
        EGL14.eglDestroyContext(eglDisplay, eglContext)
        EGL14.eglTerminate(eglDisplay)
    }
}

3. 简化方案:用RenderScript处理滤镜

如果不想写复杂的OpenGL代码,可采用RenderScript处理摄像头帧数据,再将处理后的帧喂给MediaRecorder(性能略低,适合简单滤镜):

  • 用CameraX的ImageAnalysis获取摄像头的Image数据
  • 用RenderScript的ScriptIntrinsicColorMatrix或自定义Script实现滤镜
  • 将处理后的Bitmap转成YUV数据,写入MediaRecorder(注意格式转换的性能损耗)

关键注意事项

  • 权限:必须申请CAMERA、RECORD_AUDIO、WRITE_EXTERNAL_STORAGE(Android 10+可改用MediaStore)权限
  • 性能:实时渲染+录制对GPU要求较高,建议测试中低端设备,必要时降低分辨率或帧率
  • 方向:处理摄像头预览与录制的方向一致性,可通过CameraX的setTargetRotation或手动旋转渲染矩阵解决

内容的提问来源于stack exchange,提问作者Edu Polo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 16:31:01