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

如何录制包含VideoView/SurfaceView的指定ViewGroup(RelativeLayout)

解决方案:支持SurfaceView的ViewGroup录制实现

核心问题在于SurfaceView的渲染缓冲区独立于View的绘制层级,普通的drawToBitmap或getDrawingCache无法捕获其内容。下面是一套分图层捕获+帧合成的实现方案,能覆盖普通View和SurfaceView的录制需求:

实现思路

  1. 拆分捕获逻辑:普通View用常规的绘制捕获,SurfaceView通过SurfaceTexture监听帧回调获取画面
  2. 帧合成:将SurfaceView的帧绘制到普通View的帧画布对应位置,得到完整的ViewGroup画面
  3. 编码输出:用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 18:53:10