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

Android双Bitmap列表转视频:FFmpeg与MediaCodec问题求助

如何高效将两个Bitmap列表合成为带叠加层的视频?

我需要将两个长度一致的Bitmap列表转换为视频——第一个列表是视频的主帧,第二个列表是逐帧叠加在主帧上的图层。

现有FFmpeg方案的问题

我用FFmpeg实现了需求,但存在两个明显问题:

  • APK体积增大:引入FFmpeg库会显著增加安装包大小
  • 转换耗时过长:当前代码需要先把所有Bitmap保存为缓存目录下的PNG文件,再调用FFmpeg处理,IO操作拖慢了速度

FFmpeg实现代码:

fun convertToVideo(context: Context, bitmapList: List<Bitmap?>, effectedBitmaps:  List<Bitmap?>, executed: (Boolean) -> Unit) {
    val outputDirCap = File(context.cacheDir, "temp_cap")
    val outputDirEff = File(context.cacheDir, "temp_effect")
    outputDirCap.mkdirs()
    outputDirEff.mkdirs()
    val outputFilePath = File(
        context.cacheDir,
        "output.mp4"
    ).absolutePath

    for (i in bitmapList.indices) {
        val bitmap = bitmapList[i]
        val outputFile = File(outputDirCap, "frame$i.png")
        try {
            val fileOutputStream = FileOutputStream(outputFile)
            bitmap?.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream)
            fileOutputStream.flush()
            fileOutputStream.close()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }

    for (i in effectedBitmaps.indices) {
        val bitmap = effectedBitmaps[i]
        val outputFile = File(outputDirEff, "frame$i.png")

        try {
            val fileOutputStream = FileOutputStream(outputFile)
            bitmap?.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream)
            fileOutputStream.flush()
            fileOutputStream.close()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }

    val command = arrayOf(
        "-i", "${outputDirCap.absolutePath}/frame%d.png",
        "-i", "${outputDirEff.absolutePath}/frame%d.png",
        "-filter_complex",
        "[0:v][1:v]overlay",
        outputFilePath
    )

    try {
        FFmpeg.executeAsync(command
        ) { executionId, returnCode ->
            if (returnCode == RETURN_CODE_SUCCESS) {
                Log.d(TAG, "Async command execution completed successfully.");
                outputDirCap.deleteRecursively()
                outputDirEff.deleteRecursively()
                executed(true)
            } else if (returnCode == RETURN_CODE_CANCEL) {
                Log.d(TAG, "Async command execution cancelled by user.");
                outputDirCap.deleteRecursively()
                outputDirEff.deleteRecursively()
                executed(false)
            } else {
                outputDirCap.deleteRecursively()
                outputDirEff.deleteRecursively()
                executed(false)
                Log.d(
                    TAG,
                    String.format(
                        "Async command execution failed with returnCode=%d.",
                        returnCode
                    )
                );
            }
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

MediaCodec方案的问题

我尝试用MediaCodec实现,但代码卡在var inputBufferIndex = encoder.dequeueInputBuffer(-1)这一行,无法继续执行。

MediaCodec实现代码:

private fun convertBitmapsToVideo(list1: List<Bitmap?>, list2: List<Bitmap?>) {
    val OUTPUT_VIDEO_PATH =
        requireActivity().cacheDir.absolutePath + File.separator + "output.mp4"
    try {
        val encoder = MediaCodec.createEncoderByType(OUTPUT_VIDEO_MIME_TYPE)
        val format = MediaFormat.createVideoFormat(
            OUTPUT_VIDEO_MIME_TYPE,
            OUTPUT_VIDEO_WIDTH,
            OUTPUT_VIDEO_HEIGHT
        )
        format.setInteger(MediaFormat.KEY_BIT_RATE, OUTPUT_VIDEO_BIT_RATE)
        format.setInteger(MediaFormat.KEY_FRAME_RATE, OUTPUT_VIDEO_FRAME_RATE)
        format.setInteger(
            MediaFormat.KEY_COLOR_FORMAT,
            MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
        )
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
        encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
        val muxer = MediaMuxer(OUTPUT_VIDEO_PATH, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
        encoder.start()
        val inputBuffers = encoder.inputBuffers
        val outputBuffers = encoder.outputBuffers
        val bufferInfo = BufferInfo()
        var trackIndex = -1
        var isMuxerStarted = false
        var presentationTimeUs: Long = 0
        val maxFrames = Math.max(list1.size, list2.size)
        Log.d(TAG, "convertBitmapsToVideo: Max Frames $maxFrames")
        for (i in 0 until maxFrames) {
            val bitmap1 = if (i < list1.size) list1[i] else null
            val bitmap2 = if (i < list2.size) list2[i] else null
            var inputBufferIndex = encoder.dequeueInputBuffer(-1)
            if (inputBufferIndex >= 0) {
                var inputBuffer = inputBuffers[inputBufferIndex]
                inputBuffer.clear()

                // Convert bitmaps to YUV420 format
                val frameData1: ByteArray? = bitmap1?.let { convertBitmapToYUV(it) }
                val frameData2: ByteArray? = bitmap2?.let { convertBitmapToYUV(it) }
                if (frameData1 != null) {
                    inputBuffer.put(frameData1)
                    encoder.queueInputBuffer(
                        inputBufferIndex,
                        0,
                        frameData1.size,
                        presentationTimeUs,
                        0
                    )
                }
                if (frameData2 != null) {
                    inputBufferIndex = encoder.dequeueInputBuffer(-1)
                    if (inputBufferIndex >= 0) {
                        inputBuffer = inputBuffers[inputBufferIndex]
                        inputBuffer.clear()
                        inputBuffer.put(frameData2)
                        encoder.queueInputBuffer(
                            inputBufferIndex,
                            0,
                            frameData2.size,
                            presentationTimeUs,
                            0
                        )
                    }
                }
                presentationTimeUs += 1000000 / OUTPUT_VIDEO_FRAME_RATE
                var outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 0)
                while (outputBufferIndex >= 0) {
                    Log.d(TAG, "convertBitmapsToVideo: while")
                    val outputBuffer = outputBuffers[outputBufferIndex]
                    if (!isMuxerStarted) {
                        trackIndex = muxer.addTrack(encoder.outputFormat)
                        muxer.start()
                        isMuxerStarted = true
                    }
                    outputBuffer.position(bufferInfo.offset)
                    outputBuffer.limit(bufferInfo.offset + bufferInfo.size)
                    muxer.writeSampleData(trackIndex, outputBuffer, bufferInfo)
                    encoder.releaseOutputBuffer(outputBufferIndex, false)
                    outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 0)
                }
            }
        }
        encoder.signalEndOfInputStream()
        var outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 0)
        while (outputBufferIndex >= 0) {
            Log.d(TAG, "convertBitmapsToVideo: ")
            val outputBuffer = outputBuffers[outputBufferIndex]
            if (!isMuxerStarted) {
                trackIndex = muxer.addTrack(encoder.outputFormat)
                muxer.start()
                isMuxerStarted = true
            }
            outputBuffer.position(bufferInfo.offset)
            outputBuffer.limit(bufferInfo.offset + bufferInfo.size)
            muxer.writeSampleData(trackIndex, outputBuffer, bufferInfo)
            encoder.releaseOutputBuffer(outputBufferIndex, false)
            outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 0)
        }
        encoder.stop()
        encoder.release()
        muxer.stop()
        muxer.release()
        Log.d(TAG, "Video conversion completed. Output file path: $OUTPUT_VIDEO_PATH")
    } catch (e: IOException) {
        Log.d(TAG, "Error converting bitmaps to video: " + e.message)
    }
}

private fun convertBitmapToYUV(bitmap: Bitmap): ByteArray? {
    val width = bitmap.width
    val height = bitmap.height
    val argb = IntArray(width * height)
    val yuv = ByteArray(width * height * 3 / 2) // YUV420 format
    bitmap.getPixels(argb, 0, width, 0, 0, width, height)
    var inputOffset = 0
    var outputOffset = 0

    // Convert ARGB to YUV420 format
    for (i in 0 until height) {
        for (j in 0 until width) {
            val rgb = argb[inputOffset++]
            val r = rgb shr 16 and 0xFF
            val g = rgb shr 8 and 0xFF
            val b = rgb and 0xFF
            var y = (0.299 * r + 0.587 * g + 0.114 * b).toInt()
            var u = (-0.14713 * r - 0.28886 * g + 0.436 * b).toInt()
            var v = (0.615 * r - 0.51498 * g - 0.10001 * b).toInt()

            // Clamp values
            y = Math.max(0, Math.min(y, 255))
            u = Math.max(0, Math.min(u, 255))
            v = Math.max(0, Math.min(v, 255))
            yuv[outputOffset++] = y.toByte()
            if (i % 2 == 0 && j % 2 == 0) {
                yuv[outputOffset++] = u.toByte()
                yuv[outputOffset++] = v.toByte()
            }
        }
        inputOffset += width - width
    }
    return yuv
}

请求帮助

希望得到以下任一解决方案:

  • 修复上述MediaCodec代码中的阻塞问题,实现双Bitmap列表的视频合成
  • 优化FFmpeg方案,减少APK体积或降低转换耗时
  • 提供更高效的替代实现方案

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 12:56:59