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

如何在不阻塞UI线程的情况下录制Jetpack Compose @Composable组件视频?

非阻塞录制Jetpack Compose指定组件(含相机预览+Canvas)

针对你遇到的问题,这里提供一套不阻塞主线程、仅录制目标Compose组件的方案,核心是分离相机预览与Compose内容的捕获,在后台线程完成绘制、合成与编码:

核心思路

  1. 相机预览帧捕获:通过CameraX的ImageAnalysis在后台线程获取相机原始帧,避免占用主线程
  2. Compose内容后台绘制:使用RenderNode创建离线渲染环境,在后台线程绘制Compose组件内容,完全不阻塞UI
  3. 帧合成与视频编码:将相机帧与Compose绘制内容合成后,用MediaCodec在后台线程完成视频编码,输出MP4文件

具体实现步骤

1. 配置CameraX捕获相机帧

使用CameraX的ImageAnalysis模块,在后台线程获取相机的YUV帧并转为Bitmap:

// 初始化CameraX的ImageAnalysis
val imageAnalysis = ImageAnalysis.Builder()
    .setTargetResolution(Size(1920, 1080)) // 匹配组件尺寸
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    .build()

// 用后台线程池处理帧数据
val executor = Executors.newSingleThreadExecutor()
imageAnalysis.setAnalyzer(executor) { imageProxy ->
    val mediaImage = imageProxy.image ?: run {
        imageProxy.close()
        return@setAnalyzer
    }
    // 将YUV格式的相机帧转为Bitmap
    val cameraBitmap = mediaImage.toBitmap()
    // 捕获Compose组件内容并合成编码
    captureComposeAndEncode(cameraBitmap)
    imageProxy.close()
}

// 将ImageAnalysis绑定到相机生命周期
CameraX.bindToLifecycle(this as LifecycleOwner, imageAnalysis)

2. 后台绘制Compose组件内容

用RenderNode创建离线渲染节点,在后台线程绘制目标Compose组件:

class ComposeOffscreenRenderer(private val context: Context) {
    fun render(width: Int, height: Int, content: @Composable () -> Unit): Bitmap {
        val renderNode = RenderNode("ComposeOffscreen").apply {
            setPosition(0, 0, width, height)
            setClipToBounds(true)
        }
        // 在后台线程创建ComposeView并渲染
        val composeView = ComposeView(context).apply {
            setContent(content)
            measure(
                MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
            )
            layout(0, 0, width, height)
        }
        // 将ComposeView绘制到离线RenderNode
        composeView.draw(renderNode.startRecording())
        renderNode.endRecording()

        // 将RenderNode内容转为Bitmap
        val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
        Canvas(bitmap).apply { renderNode.draw(this) }
        return bitmap
    }
}

// 初始化渲染器(在后台线程使用)
val composeRenderer = ComposeOffscreenRenderer(context)

3. 帧合成与视频编码

用MediaCodec在后台线程完成视频编码,将相机帧与Compose内容合成后写入文件:

class VideoEncoder(private val width: Int, private val height: Int, outputFile: File) {
    private val mediaCodec = MediaCodec.createEncoderByType("video/avc")
    private val outputStream = FileOutputStream(outputFile)
    private val bufferInfo = MediaCodec.BufferInfo()

    init {
        val format = MediaFormat.createVideoFormat("video/avc", width, height).apply {
            setInteger(MediaFormat.KEY_BIT_RATE, 10_000_000) // 码率,可调整
            setInteger(MediaFormat.KEY_FRAME_RATE, 30) // 帧率
            setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1) // I帧间隔
            setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)
        }
        mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
        mediaCodec.start()
    }

    fun encodeFrame(combinedBitmap: Bitmap) {
        // 写入输入缓冲
        val inputBufferIndex = mediaCodec.dequeueInputBuffer(1000)
        if (inputBufferIndex >= 0) {
            mediaCodec.getInputBuffer(inputBufferIndex)?.let { buffer ->
                buffer.rewind()
                // 将Bitmap转为YUV数据写入缓冲
                combinedBitmap.copyPixelsToBuffer(buffer)
                val timestamp = System.nanoTime() / 1000 // 微秒时间戳
                mediaCodec.queueInputBuffer(inputBufferIndex, 0, combinedBitmap.byteCount, timestamp, 0)
            }
        }
        // 读取输出缓冲并写入文件
        var outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 1000)
        while (outputBufferIndex >= 0) {
            mediaCodec.getOutputBuffer(outputBufferIndex)?.let { buffer ->
                val data = ByteArray(bufferInfo.size)
                buffer.get(data)
                outputStream.write(data)
            }
            mediaCodec.releaseOutputBuffer(outputBufferIndex, false)
            outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 1000)
        }
    }

    fun stop() {
        mediaCodec.stop()
        mediaCodec.release()
        outputStream.flush()
        outputStream.close()
    }
}

// 初始化编码器(指定输出文件)
val outputFile = File(context.externalMediaDirs.first(), "video.mp4")
val videoEncoder = VideoEncoder(1920, 1080, outputFile)

// 合成并编码帧的函数
fun captureComposeAndEncode(cameraBitmap: Bitmap) {
    // 后台绘制Compose组件内容
    val composeBitmap = composeRenderer.render(1920, 1080) {
        ExerciseCanvasView(...) // 传入你的组件参数
    }
    // 合成相机帧与Compose内容
    val combinedBitmap = Bitmap.createBitmap(cameraBitmap.width, cameraBitmap.height, Bitmap.Config.ARGB_8888)
    Canvas(combinedBitmap).apply {
        drawBitmap(cameraBitmap, 0f, 0f, null)
        drawBitmap(composeBitmap, 0f, 0f, null)
    }
    // 编码帧
    videoEncoder.encodeFrame(combinedBitmap)
    // 回收Bitmap避免内存泄漏
    cameraBitmap.recycle()
    composeBitmap.recycle()
    combinedBitmap.recycle()
}

关键注意事项

  • 所有绘制、编码、相机帧处理操作必须放在后台线程,可通过CoroutineScope(Dispatchers.IO)或ExecutorService实现,绝对不能占用主线程
  • 注意Bitmap的及时回收,避免OOM
  • 调整视频编码参数(码率、帧率)可平衡文件大小与画质
  • 若Compose组件包含动画,需确保后台绘制频率与动画帧率同步(比如30fps)
  • 需申请相机权限、存储权限,Android 13+还需申请媒体权限

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 03:25:00