如何在不阻塞UI线程的情况下录制Jetpack Compose @Composable组件视频?
非阻塞录制Jetpack Compose指定组件(含相机预览+Canvas)
针对你遇到的问题,这里提供一套不阻塞主线程、仅录制目标Compose组件的方案,核心是分离相机预览与Compose内容的捕获,在后台线程完成绘制、合成与编码:
核心思路
- 相机预览帧捕获:通过CameraX的ImageAnalysis在后台线程获取相机原始帧,避免占用主线程
- Compose内容后台绘制:使用
RenderNode创建离线渲染环境,在后台线程绘制Compose组件内容,完全不阻塞UI - 帧合成与视频编码:将相机帧与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
相关产品推荐
相关产品推荐

