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

如何将ImageReader图像传入MediaRecorder?Camera2多输出兼容方案

兼容API23+的Camera2帧流转方案

要解决Camera2无法同时挂载四个输出Surface的问题,同时实现帧处理与视频录制的管线合并,你可以通过OpenGL手动将YUV_420_888格式的Image数据转换为纹理,再渲染到MediaRecorder的输入Surface,无需依赖API28+的HardwareBuffer。以下是具体实现步骤和代码:


1. 核心思路

通过OpenGL创建三个纹理分别对应YUV_420_888的Y、U、V平面,使用着色器将YUV数据转换为RGB(或直接适配MediaRecorder的输入格式),最终渲染到MediaRecorder的Surface上。同时在帧处理回调中完成自定义逻辑。


2. OpenGL纹理渲染工具类

这个类负责初始化EGL环境、创建YUV纹理、编写着色器以及将Image数据渲染到输出Surface:

class GLRenderer(private val outputSurface: Surface) {
    private var eglDisplay: EGLDisplay = EGL14.EGL_NO_DISPLAY
    private var eglContext: EGLContext = EGL14.EGL_NO_CONTEXT
    private var eglSurface: EGLSurface = EGL14.EGL_NO_SURFACE
    private var yTextureId = 0
    private var uTextureId = 0
    private var vTextureId = 0
    private var program = 0

    fun init() {
        // 初始化EGL显示
        eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
        val version = IntArray(2)
        EGL14.eglInitialize(eglDisplay, version, 0, version, 1)

        // 选择EGL配置
        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 valid EGL config found")

        // 创建EGL上下文
        val contextAttribs = intArrayOf(
            EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
            EGL14.EGL_NONE
        )
        eglContext = EGL14.eglCreateContext(eglDisplay, config, EGL14.EGL_NO_CONTEXT, contextAttribs, 0)

        // 创建输出Surface
        eglSurface = EGL14.eglCreateWindowSurface(eglDisplay, config, outputSurface, null, 0)
        EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)

        // 初始化着色器和纹理
        initShaders()
        initTextures()
    }

    private fun initShaders() {
        // 顶点着色器:处理顶点坐标映射
        val vertexShader = """
            attribute vec4 aPosition;
            attribute vec2 aTexCoord;
            varying vec2 vTexCoord;
            void main() {
                gl_Position = aPosition;
                vTexCoord = aTexCoord;
            }
        """.trimIndent()

        // 片段着色器:YUV转RGB
        val fragmentShader = """
            precision mediump float;
            varying vec2 vTexCoord;
            uniform sampler2D yTexture;
            uniform sampler2D uTexture;
            uniform sampler2D vTexture;
            
            void main() {
                float y = texture2D(yTexture, vTexCoord).r;
                float u = texture2D(uTexture, vTexCoord).r - 0.5;
                float v = texture2D(vTexture, vTexCoord).r - 0.5;
                
                float r = y + 1.402 * v;
                float g = y - 0.344136 * u - 0.714136 * v;
                float b = y + 1.772 * u;
                
                gl_FragColor = vec4(r, g, b, 1.0);
            }
        """.trimIndent()

        program = createProgram(vertexShader, fragmentShader)
    }

    private fun createProgram(vertexSource: String, fragmentSource: String): Int {
        val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource)
        val fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource)
        val program = GLES20.glCreateProgram()
        GLES20.glAttachShader(program, vertexShader)
        GLES20.glAttachShader(program, fragmentShader)
        GLES20.glLinkProgram(program)
        return program
    }

    private fun loadShader(type: Int, source: String): Int {
        val shader = GLES20.glCreateShader(type)
        GLES20.glShaderSource(shader, source)
        GLES20.glCompileShader(shader)
        return shader
    }

    private fun initTextures() {
        val textures = IntArray(3)
        GLES20.glGenTextures(3, textures, 0)
        yTextureId = textures[0]
        uTextureId = textures[1]
        vTextureId = textures[2]

        // 配置Y纹理(单通道亮度)
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yTextureId)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)

        // 配置U纹理(单通道色度)
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, uTextureId)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)

        // 配置V纹理(单通道色度)
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, vTextureId)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
    }

    fun renderImage(image: Image) {
        EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)
        val planes = image.planes

        // 上传Y平面数据(处理rowStride对齐问题)
        val yPlane = planes[0]
        val yRowStride = yPlane.rowStride
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yTextureId)
        for (y in 0 until image.height) {
            yPlane.buffer.position(y * yRowStride)
            val rowBuffer = yPlane.buffer.slice()
            GLES20.glTexSubImage2D(
                GLES20.GL_TEXTURE_2D, 0,
                0, y, image.width, 1,
                GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE,
                rowBuffer
            )
        }

        // 上传U平面数据(宽高为原图像的1/2)
        val uPlane = planes[1]
        val uWidth = image.width / 2
        val uHeight = image.height / 2
        val uRowStride = uPlane.rowStride
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, uTextureId)
        for (y in 0 until uHeight) {
            uPlane.buffer.position(y * uRowStride)
            val rowBuffer = uPlane.buffer.slice()
            GLES20.glTexSubImage2D(
                GLES20.GL_TEXTURE_2D, 0,
                0, y, uWidth, 1,
                GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE,
                rowBuffer
            )
        }

        // 上传V平面数据
        val vPlane = planes[2]
        val vRowStride = vPlane.rowStride
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, vTextureId)
        for (y in 0 until uHeight) {
            vPlane.buffer.position(y * vRowStride)
            val rowBuffer = vPlane.buffer.slice()
            GLES20.glTexSubImage2D(
                GLES20.GL_TEXTURE_2D, 0,
                0, y, uWidth, 1,
                GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE,
                rowBuffer
            )
        }

        // 执行渲染
        GLES20.glUseProgram(program)

        val positionHandle = GLES20.glGetAttribLocation(program, "aPosition")
        val texCoordHandle = GLES20.glGetAttribLocation(program, "aTexCoord")
        val yTextureHandle = GLES20.glGetUniformLocation(program, "yTexture")
        val uTextureHandle = GLES20.glGetUniformLocation(program, "uTexture")
        val vTextureHandle = GLES20.glGetUniformLocation(program, "vTexture")

        // 顶点坐标和纹理坐标
        val vertices = floatArrayOf(-1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f)
        val texCoords = floatArrayOf(0f, 1f, 1f, 1f, 0f, 0f, 1f, 0f)

        val vertexBuffer = ByteBuffer.allocateDirect(vertices.size * 4)
            .order(ByteOrder.nativeOrder())
            .asFloatBuffer()
            .put(vertices)
            .position(0)

        val texCoordBuffer = ByteBuffer.allocateDirect(texCoords.size * 4)
            .order(ByteOrder.nativeOrder())
            .asFloatBuffer()
            .put(texCoords)
            .position(0)

        GLES20.glEnableVertexAttribArray(positionHandle)
        GLES20.glVertexAttribPointer(positionHandle, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer)

        GLES20.glEnableVertexAttribArray(texCoordHandle)
        GLES20.glVertexAttribPointer(texCoordHandle, 2, GLES20.GL_FLOAT, false, 0, texCoordBuffer)

        // 绑定纹理到着色器
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yTextureId)
        GLES20.glUniform1i(yTextureHandle, 0)

        GLES20.glActiveTexture(GLES20.GL_TEXTURE1)
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, uTextureId)
        GLES20.glUniform1i(uTextureHandle, 1)

        GLES20.glActiveTexture(GLES20.GL_TEXTURE2)
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, vTextureId)
        GLES20.glUniform1i(vTextureHandle, 2)

        // 绘制画面
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)

        // 交换缓冲区,输出到MediaRecorder
        EGL14.eglSwapBuffers(eglDisplay, eglSurface)

        // 必须释放Image,避免Camera2阻塞
        image.close()
    }

    fun release() {
        // 清理EGL资源
        EGL14.eglMakeCurrent(eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT)
        EGL14.eglDestroySurface(eglDisplay, eglSurface)
        EGL14.eglDestroyContext(eglDisplay, eglContext)
        EGL14.eglTerminate(eglDisplay)

        // 清理OpenGL资源
        GLES20.glDeleteTextures(3, intArrayOf(yTextureId, uTextureId, vTextureId), 0)
        GLES20.glDeleteProgram(program)
    }
}

3. 整合Camera2与MediaRecorder流程

将GLRenderer与你的Camera2、MediaRecorder逻辑整合,只需要挂载三个Surface到CaptureSession:

// 初始化MediaRecorder
val recorderSurface = MediaCodec.createPersistentInputSurface()
val mediaRecorder = MediaRecorder(context).apply {
    setAudioSource(MediaRecorder.AudioSource.MIC)
    setVideoSource(MediaRecorder.VideoSource.SURFACE)
    setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
    setVideoEncoder(MediaRecorder.VideoEncoder.H264)
    setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
    setVideoSize(3840, 2160) // 4K分辨率
    setVideoFrameRate(30)
    setInputSurface(recorderSurface)
    setOutputFile(/* 你的视频输出路径 */)
    prepare()
}

// 初始化GLRenderer(必须在GL线程执行,这里用主线程)
val glRenderer = GLRenderer(recorderSurface)
Handler(Looper.getMainLooper()).post {
    glRenderer.init()
}

// 初始化帧处理用的ImageReader
val frameReader = ImageReader.newInstance(4000, 2256, ImageFormat.YUV_420_888, 3)
frameReader.setOnImageAvailableListener({ reader ->
    val image = reader.acquireNextImage() ?: return@setOnImageAvailableListener
    // 在GL线程执行渲染和帧处理
    Handler(Looper.getMainLooper()).post {
        // 先执行自定义帧处理逻辑
        callback.onVideoFrameCaptured(image)
        // 再渲染到MediaRecorder的Surface
        glRenderer.renderImage(image)
    }
}, handler)

// 创建CameraCaptureSession,只挂载三个Surface:预览、照片、帧处理
val surfaces = listOf(
    surfaceView.holder.surface,
    photoImageReader.surface,
    frameReader.surface
)
cameraDevice.createCaptureSession(surfaces, object : CameraCaptureSession.StateCallback() {
    override fun onConfigured(session: CameraCaptureSession) {
        // 配置预览请求、拍照请求等
        val previewRequest = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
            addTarget(surfaceView.holder.surface)
            addTarget(frameReader.surface)
        }.build()
        session.setRepeatingRequest(previewRequest, null, handler)
    }

    override fun onConfigureFailed(session: CameraCaptureSession) {
        // 处理配置失败逻辑
    }
}, handler)

4. 关键注意事项

  • 线程安全:所有OpenGL操作必须在同一个GL线程执行(示例用主线程,也可以创建专门的GL线程)。
  • Image释放:必须调用
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 02:00:03