如何录制包含VideoView/SurfaceView的指定ViewGroup(RelativeLayout)
解决方案:支持SurfaceView的ViewGroup录制实现
核心问题在于SurfaceView的渲染缓冲区独立于View的绘制层级,普通的drawToBitmap或getDrawingCache无法捕获其内容。下面是一套分图层捕获+帧合成的实现方案,能覆盖普通View和SurfaceView的录制需求:
实现思路
- 拆分捕获逻辑:普通View用常规的绘制捕获,SurfaceView通过
SurfaceTexture监听帧回调获取画面 - 帧合成:将SurfaceView的帧绘制到普通View的帧画布对应位置,得到完整的ViewGroup画面
- 编码输出:用
MediaCodec将合成后的帧编码为MP4,通过MediaMuxer写入文件
关键代码实现
1. 初始化录制参数与编码工具
class ViewRecorder(private val targetViewGroup: ViewGroup) { private var mediaCodec: MediaCodec? = null private var mediaMuxer: MediaMuxer? = null private var videoTrackIndex = -1 private val viewWidth = targetViewGroup.width private val viewHeight = targetViewGroup.height private val frameRate = 30 // 录制帧率 private val bitRate = viewWidth * viewHeight * 3 * frameRate // 码率估算 fun startRecording(outputPath: String) { // 初始化MediaCodec val format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, viewWidth, viewHeight) format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible) format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate) format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate) format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10) // 每10帧一个关键帧 mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC).apply { configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) start() } // 初始化MediaMuxer mediaMuxer = MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) } }
2. 捕获SurfaceView帧
给目标ViewGroup中的所有SurfaceView绑定SurfaceTexture,监听帧可用事件:
private val surfaceFrameCache = mutableMapOf<SurfaceView, Pair<IntArray, Bitmap>>() private fun setupSurfaceViewCapture(surfaceViews: List<SurfaceView>) { surfaceViews.forEach { surfaceView -> val surfaceTexture = SurfaceTexture(0) surfaceTexture.setDefaultBufferSize(surfaceView.width, surfaceView.height) // 替换SurfaceView的Surface surfaceView.holder.surface = Surface(surfaceTexture) surfaceTexture.setOnFrameAvailableListener { st -> st.updateTexImage() // 将纹理转成Bitmap(需依赖OpenGL环境,可封装独立GL工具类) val bitmap = textureToBitmap(st, surfaceView.width, surfaceView.height) // 获取SurfaceView在目标ViewGroup中的相对坐标 val location = IntArray(2) surfaceView.getLocationInWindow(location) targetViewGroup.getLocationInWindow(intArrayOf(0, 0)).let { parentLoc -> location[0] -= parentLoc[0] location[1] -= parentLoc[1] } surfaceFrameCache[surfaceView] = Pair(location, bitmap) } } } // 纹理转Bitmap工具方法(简化实现,实际建议用ImageReader配合GL线程) private fun textureToBitmap(surfaceTexture: SurfaceTexture, width: Int, height: Int): Bitmap { val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) val matrix = Matrix().apply { setScale(1f, -1f) // 反转纹理坐标适配Canvas坐标系 postTranslate(0f, height.toFloat()) } // 此处需通过OpenGL将纹理绘制到Bitmap,可封装GLRenderer类处理 // 若不想手写GL,可使用ImageReader监听SurfaceTexture的帧数据 return bitmap }
3. 帧合成与编码
通过Choreographer同步帧率,每帧捕获并合成后送入编码器:
private fun startFrameCapture() { Choreographer.getInstance().postFrameCallback(object : Choreographer.FrameCallback { override fun doFrame(frameTimeNanos: Long) { // 1. 捕获普通View的基础帧 val baseBitmap = targetViewGroup.drawToBitmap() val canvas = Canvas(baseBitmap) // 2. 合成所有SurfaceView的帧到对应位置 surfaceFrameCache.forEach { (_, frameData) -> val (location, frameBitmap) = frameData canvas.drawBitmap(frameBitmap, location[0].toFloat(), location[1].toFloat(), null) } // 3. 将Bitmap转成YUV格式,送入MediaCodec编码 val yuvData = bitmapToYUV(baseBitmap) val inputBuffer = mediaCodec?.dequeueInputBuffer(10000) inputBuffer?.let { mediaCodec?.getInputBuffer(it)?.put(yuvData) mediaCodec?.queueInputBuffer(it, 0, yuvData.size, System.nanoTime() / 1000, 0) } // 4. 处理编码输出,写入MP4文件 val bufferInfo = MediaCodec.BufferInfo() var outputBufferId = mediaCodec?.dequeueOutputBuffer(bufferInfo, 10000) while (outputBufferId != null && outputBufferId >= 0) { val outputBuffer = mediaCodec?.getOutputBuffer(outputBufferId) if (videoTrackIndex == -1) { val format = mediaCodec?.outputFormat videoTrackIndex = mediaMuxer?.addTrack(format!!) ?: -1 mediaMuxer?.start() } mediaMuxer?.writeSampleData(videoTrackIndex, outputBuffer!!, bufferInfo) mediaCodec?.releaseOutputBuffer(outputBufferId, false) outputBufferId = mediaCodec?.dequeueOutputBuffer(bufferInfo, 10000) } // 继续捕获下一帧 Choreographer.getInstance().postFrameCallback(this) } }) } // Bitmap转YUV420格式工具方法 private fun bitmapToYUV(bitmap: Bitmap): ByteArray { val intArray = IntArray(bitmap.width * bitmap.height) bitmap.getPixels(intArray, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height) val yuvSize = bitmap.width * bitmap.height * 3 / 2 val yuvData = ByteArray(yuvSize) // 手动转换为YUV420格式,或使用YuvImage简化处理 val yuvImage = YuvImage(yuvData, ImageFormat.NV21, bitmap.width, bitmap.height, null) val outputStream = ByteArrayOutputStream() yuvImage.compressToJpeg(Rect(0,0,bitmap.width,bitmap.height), 100, outputStream) return outputStream.toByteArray() }
4. 停止录制与资源释放
fun stopRecording() { // 发送编码结束信号 mediaCodec?.signalEndOfInputStream() val bufferInfo = MediaCodec.BufferInfo() var outputBufferId = mediaCodec?.dequeueOutputBuffer(bufferInfo, 10000) while (outputBufferId != null && outputBufferId >= 0) { mediaMuxer?.writeSampleData(videoTrackIndex, mediaCodec?.getOutputBuffer(outputBufferId)!!, bufferInfo) mediaCodec?.releaseOutputBuffer(outputBufferId, false) outputBufferId = mediaCodec?.dequeueOutputBuffer(bufferInfo, 10000) } // 释放所有资源 mediaCodec?.stop() mediaCodec?.release() mediaMuxer?.stop() mediaMuxer?.release() surfaceFrameCache.clear() }
关键注意事项
- 坐标转换:必须计算SurfaceView在目标ViewGroup中的相对坐标,确保合成位置准确
- GL线程:SurfaceTexture转Bitmap需要独立的OpenGL线程,避免阻塞UI线程
- 性能优化:复用Bitmap和YUV缓冲区,减少内存分配;帧率过高时可跳过部分帧
- 权限适配:Android 10+ 使用
MediaStore写入文件,无需申请WRITE_EXTERNAL_STORAGE权限 - 简化替代:若业务允许,将
SurfaceView替换为TextureView,其内容可直接用getBitmap()捕获,实现更简单
内容的提问来源于stack exchange,提问作者Akshay Kumar Both
相关产品推荐
相关产品推荐

