共计 2963 个字符,预计需要花费 8 分钟才能阅读完成。
背景痛点
在移动应用开发中,将多张图片与背景音乐合成为轮播视频是一个常见需求,但实现起来却面临诸多挑战。开发者通常需要处理以下问题:

- 内存管理:高分辨率图片加载容易导致 OOM(内存溢出)
- 性能瓶颈:视频编码过程可能造成 UI 卡顿
- 同步难题:音频与视频轨道的时间戳对齐
- 兼容性问题:不同 Android 版本的 MediaCodec API 差异
技术选型对比
主流方案主要有两种:
- FFmpeg 方案
- 优点:功能全面,社区资源丰富
-
缺点:包体积增加明显(约增加 8 -10MB),需要处理 so 库兼容性
-
原生 MediaCodec 方案
- 优点:系统级支持,无需额外依赖
- 缺点:API 较为底层,开发复杂度高
考虑到 APK 体积和性能要求,我们选择原生 MediaCodec+MediaMuxer 方案。
核心实现步骤
1. 图片解码与尺寸处理
关键点在于统一所有输入图片的尺寸和格式:
fun decodeImage(assetManager: AssetManager, path: String): Bitmap {return BitmapFactory.decodeStream(assetManager.open(path))
?.let { sourceBitmap ->
// 统一缩放至目标尺寸
Bitmap.createScaledBitmap(
sourceBitmap,
TARGET_WIDTH,
TARGET_HEIGHT,
true
).apply {sourceBitmap.recycle()
}
} ?: throw IllegalArgumentException("Invalid image path: $path")
}
2. 音频提取与参数配置
需要从 MP3 文件中提取 PCM 数据并配置编码器:
val audioExtractor = MediaExtractor().apply {setDataSource(audioPath)
selectTrack(getAudioTrackIndex())
}
val audioFormat = MediaFormat().apply {setString(MediaFormat.KEY_MIME, "audio/mp4a-latm")
setInteger(MediaFormat.KEY_SAMPLE_RATE, 44100)
setInteger(MediaFormat.KEY_CHANNEL_COUNT, 2)
setInteger(MediaFormat.KEY_BIT_RATE, 128000)
setInteger(MediaFormat.KEY_AAC_PROFILE,
MediaCodecInfo.CodecProfileLevel.AACObjectLC)
}
3. 视频帧生成与编码
使用 Surface 配合 MediaCodec 进行高效编码:
val codec = MediaCodec.createEncoderByType("video/avc")
val format = MediaFormat.createVideoFormat("video/avc", width, height).apply {
// 配置编码参数
setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
setInteger(MediaFormat.KEY_BIT_RATE, bitrate)
setInteger(MediaFormat.KEY_FRAME_RATE, 30)
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
}
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
val surface = codec.createInputSurface()
codec.start()
4. 使用 MediaMuxer 合成最终视频
关键时序控制代码:
val muxer = MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
// 添加轨道后启动混合器
var videoTrackIndex = -1
var audioTrackIndex = -1
var muxerStarted = false
fun onFormatChanged(codec: MediaCodec, format: MediaFormat) {if (videoTrackIndex == -1) {videoTrackIndex = muxer.addTrack(format)
}
if (audioTrackIndex != -1 && videoTrackIndex != -1 && !muxerStarted) {muxer.start()
muxerStarted = true
}
}
完整代码结构
推荐的项目结构:
├── VideoComposer.kt // 主控制器
├── ImageProcessor.kt // 图片处理
├── AudioDecoder.kt // 音频解码
├── VideoEncoder.kt // 视频编码
└── MediaMuxerController.kt // 混合器管理
性能优化策略
内存管理
- 使用 Bitmap 复用池
- 及时回收解码后的 Bitmap
- 限制并发处理图片数量
线程模型
private val encoderThread = HandlerThread("VideoEncoder").apply {start() }
private val decoderThread = HandlerThread("ImageDecoder").apply {start() }
// 使用协程管理异步任务
viewModelScope.launch(Dispatchers.IO) {val videoJob = launch { encodeVideoFrames() }
val audioJob = launch {encodeAudioData() }
joinAll(videoJob, audioJob)
releaseResources()}
常见问题解决方案
1. 视频音频不同步
- 确保所有时间戳基于同一时钟源
- 音频采样率必须与编码器配置一致
2. 编码器初始化失败
- 检查设备支持的 color format:
val codecInfo = MediaCodecList(MediaCodecList.REGULAR_CODECS) .findEncoderForFormat(format)
3. 输出视频花屏
- 确保关键帧间隔 (KEY_I_FRAME_INTERVAL) 设置合理
- 检查 YUV 格式转换是否正确
扩展功能思考
- 转场效果:通过 OpenGL ES 实现图片间过渡动画
- 动态调速:使用 AudioTrack 调整播放速率
- 文字叠加:利用 Canvas 在编码前绘制文字
- 滤镜支持:集成 RenderScript 实现实时滤镜
结语
通过 MediaCodec 和 MediaMuxer 的组合,我们实现了高性能的视频合成方案。虽然初期学习曲线较陡峭,但原生方案带来的性能和体积优势非常明显。建议在实际项目中:
- 做好设备兼容性测试
- 添加进度回调接口
- 实现合成任务队列管理
完整示例代码已放在 GitHub 仓库(伪链接),欢迎交流改进建议。
正文完
