共计 4016 个字符,预计需要花费 11 分钟才能阅读完成。
背景与痛点
在移动开发中,将图片序列合成为视频的需求非常常见,比如生成用户相册的动态回顾、制作短视频内容或实现游戏录屏等功能。这类需求看似简单,但实际操作中会遇到几个核心挑战:

- 性能问题 :移动设备资源有限,处理大量高分辨率图片时容易引发 OOM 或卡顿
- 格式兼容性 :不同 Android 版本对视频编码器的支持存在差异(如 H.264/HEVC)
- 时间精度控制 :需要精确管理帧间隔以实现稳定帧率
- 色彩空间转换 :Bitmap 的 ARGB_8888 格式与视频编码器要求的 YUV 格式需要正确转换
技术选型对比
方案一:MediaMuxer + MediaCodec(官方推荐)
- 优点:
- 系统级 API,兼容性好(API 18+)
- 直接硬件加速编码,性能优异
- 无需引入第三方库
- 缺点:
- API 较底层,需要手动管理编码器状态机
- 颜色空间转换需要自行处理
方案二:FFmpeg(第三方库)
- 优点:
- 功能全面,参数配置灵活
- 社区支持丰富
- 缺点:
- 库体积较大(通常需要 7 -10MB)
- 软件编码性能不如硬件加速
- 需要处理 JNI 调用
对于大多数应用场景,特别是需要轻量级集成的项目,MediaMuxer 方案更具优势。
核心实现详解
1. MediaMuxer 初始化
关键配置参数包括:
val muxer = MediaMuxer(outputPath,
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
// 视频参数配置(以 1080P 30fps 为例)val format = MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_VIDEO_AVC,
1920,
1080).apply {
setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)
setInteger(MediaFormat.KEY_BIT_RATE, 8_000_000) // 8Mbps
setInteger(MediaFormat.KEY_FRAME_RATE, 30)
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
}
2. 图片帧处理流程
完整转换步骤:
- Bitmap 解码与尺寸调整
- ARGB 到 YUV420SP 的颜色空间转换
- 配置正确的 PresentationTimeUs
- 送入编码器队列
关键转换代码示例:
fun bitmapToYuv420(bitmap: Bitmap): ByteArray {
val inputWidth = bitmap.width
val inputHeight = bitmap.height
val yuvSize = (inputWidth * inputHeight * 1.5f).toInt()
// 实际转换实现应使用 RenderScript 或高效算法
// 此处为示意简化版
return ByteArray(yuvSize).apply {// ... 具体转换逻辑}
}
3. 时间戳同步机制
确保视频流畅的关键点:
- 计算每帧的显示时间戳:
val frameDurationUs = 1_000_000L / targetFrameRate var presentationTimeUs = 0L // 每帧处理时递增 presentationTimeUs += frameDurationUs - 处理编码器的输出缓冲区时携带正确时间戳
- 遇到编码延迟时需要适当等待
完整代码实现
以下是核心流程的 Kotlin 实现(精简版):
class VideoEncoder(
private val outputPath: String,
private val width: Int,
private val height: Int,
private val frameRate: Int
) {
private lateinit var muxer: MediaMuxer
private lateinit var codec: MediaCodec
private var trackIndex = -1
private var isMuxerStarted = false
fun prepare() {
// 初始化编码器
codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
val format = createVideoFormat()
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
// 启动组件
muxer = MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
codec.start()}
fun encodeFrame(bitmap: Bitmap, frameIndex: Int) {
val inputBuffers = codec.inputBuffers
val outputBuffers = codec.outputBuffers
// 转换并提交输入
val yuvData = bitmapToYuv420(bitmap)
val inputBufferIndex = codec.dequeueInputBuffer(TIMEOUT_USEC)
if (inputBufferIndex >= 0) {val inputBuffer = inputBuffers[inputBufferIndex]
inputBuffer.clear()
inputBuffer.put(yuvData)
val pts = computePresentationTime(frameIndex)
codec.queueInputBuffer(inputBufferIndex, 0, yuvData.size, pts, 0)
}
// 处理输出
val bufferInfo = MediaCodec.BufferInfo()
var encoderStatus = codec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC)
while (encoderStatus >= 0) {val outputBuffer = outputBuffers[encoderStatus]
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {bufferInfo.size = 0}
if (bufferInfo.size > 0) {if (!isMuxerStarted) {
// 确保在收到第一个有效帧后再启动混合器
trackIndex = muxer.addTrack(codec.outputFormat)
muxer.start()
isMuxerStarted = true
}
outputBuffer.position(bufferInfo.offset)
outputBuffer.limit(bufferInfo.offset + bufferInfo.size)
muxer.writeSampleData(trackIndex, outputBuffer, bufferInfo)
}
codec.releaseOutputBuffer(encoderStatus, false)
encoderStatus = codec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC)
}
}
fun release() {
try {codec.stop()
codec.release()
muxer.stop()
muxer.release()} catch (e: Exception) {Log.e(TAG, "Release error", e)
}
}
private fun computePresentationTime(frameIndex: Int): Long {return frameIndex * 1_000_000L / frameRate}
}
性能优化建议
内存优化
- 分块处理大图集 :
- 避免同时加载所有图片到内存
-
采用生产者 - 消费者模式逐个处理
-
复用 Bitmap 对象 :
val reuseBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) -
使用 inSampleSize 加载图片 :
val options = BitmapFactory.Options().apply {inSampleSize = calculateInSampleSize(srcWidth, targetWidth) }
编码优化
- 根据设备性能动态调整比特率
- 使用 Surface 输入模式(API 18+)可避免 YUV 转换
- 合理设置 KEY_I_FRAME_INTERVAL(建议 1 - 2 秒)
常见问题解决
问题 1:颜色失真
- 现象 :视频出现绿色 / 紫色色偏
- 原因 :YUV 转换算法错误
- 解决 :
- 使用正确的转换矩阵
- 推荐采用 RenderScript 执行转换
问题 2:帧率不稳定
- 现象 :视频播放时快时慢
- 原因 :时间戳计算不连续
- 解决 :
- 确保每帧 presentationTimeUs 等差递增
- 使用 System.nanoTime() 作为基准
问题 3:混合器启动失败
- 现象 :IllegalStateException when starting muxer
- 原因 :未收到编码器配置帧
- 解决 :
- 等待 BUFFER_FLAG_CODEC_CONFIG 标志
- 延迟 muxer.start() 调用
结语
通过 MediaMuxer 实现图片转视频功能,虽然需要处理较多底层细节,但能获得最佳的性能和兼容性平衡。实际开发中建议:
- 在非 UI 线程执行编码操作
- 添加进度回调机制
- 对输入图片进行预处理(尺寸校验、旋转校正)
- 测试不同厂商设备的兼容性
完整的示例项目已托管在 GitHub(示例链接),包含更多高级功能实现如音频混合、实时预览等。欢迎交流改进建议。
正文完
发表至: 移动开发
近一天内
