以下是使用 Android MediaCodec 实现多张图片与短音频文件合成视频的完整代码流程,结合 Mermaid 流程图说明核心步骤:
📽️ 完整代码流程 (Kotlin)
scss
// 1. 初始化视频编码器 & 音频编码器
val videoCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC).apply {
val format = MediaFormat.createVideoFormat(...).apply {
setInteger(MediaFormat.KEY_BIT_RATE, 5000000)
setInteger(MediaFormat.KEY_FRAME_RATE, 30)
setInteger(KEY_COLOR_FORMAT, COLOR_FormatSurface) // 使用 Surface 渲染
}
configure(format, null, null, CONFIGURE_FLAG_ENCODE)
val surface = createInputSurface() // 获取渲染 Surface
start()
}
val audioCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC).apply {
val format = MediaFormat.createAudioFormat(...).apply {
setInteger(KEY_BIT_RATE, 128000)
setInteger(KEY_SAMPLE_RATE, 44100)
}
configure(format, null, null, CONFIGURE_FLAG_ENCODE)
start()
}
// 2. 配置 MediaMuxer (封装视频+音频)
val muxer = MediaMuxer(outputPath, MUXER_OUTPUT_MPEG_4)
var videoTrackIndex = -1
var audioTrackIndex = -1
// 3. 处理视频帧 (图片→Surface)
imagePaths.forEachIndexed { index, path ->
val bitmap = BitmapFactory.decodeFile(path)
val canvas = surface.lockCanvas(null)
canvas.drawBitmap(bitmap, null, Rect(0, 0, width, height), null)
surface.unlockCanvasAndPost(canvas)
// 设置时间戳 (单位微秒)
val pts = index * 1_000_000L / frameRate
// 提取编码后的视频数据
drainEncoder(videoCodec, muxer, videoTrackIndex, pts, isVideo = true)
}
// 4. 处理音频数据 (示例:短音频 PCM 输入)
val audioBuffer = readAudioFile(audioPath) // 读取音频 PCM 数据
val audioInfo = MediaCodec.BufferInfo().apply {
presentationTimeUs = 0 // 音频起始时间戳
size = audioBuffer.size
}
drainEncoder(audioCodec, muxer, audioTrackIndex, audioInfo, isVideo = false)
// 5. 资源释放
videoCodec.stop()
audioCodec.stop()
muxer.stop()
videoCodec.release()
audioCodec.release()
muxer.release()
// 辅助函数:提取编码数据并写入 Muxer
fun drainEncoder(
codec: MediaCodec,
muxer: MediaMuxer,
trackIndex: Int,
info: BufferInfo,
isVideo: Boolean
) {
while (true) {
val outputIndex = codec.dequeueOutputBuffer(info, TIMEOUT_US)
if (outputIndex >= 0) {
val buffer = codec.getOutputBuffer(outputIndex)
muxer.writeSampleData(trackIndex, buffer, info) // 写入封装器
codec.releaseOutputBuffer(outputIndex, false)
} else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 首次获取轨道格式
if (isVideo) videoTrackIndex = muxer.addTrack(codec.outputFormat)
else audioTrackIndex = muxer.addTrack(codec.outputFormat)
if (videoTrackIndex >= 0 && audioTrackIndex >= 0) muxer.start()
}
}
}
🔄 Mermaid 流程图
flowchart TD
S((开始)) --> I["初始化编码器"]
I --> V["配置视频 Surface"]
I --> A["配置音频 PCM"]
V --> R["图片渲染到 Surface"]
R --> E1["编码视频帧"]
A --> E2["编码音频数据"]
E1 --> M["MediaMuxer 封装"]
E2 --> M
M --> D{"全部帧完成?"}
D -->|是| C["释放资源"]
D -->|否| R
C --> F["输出视频文件"]
⚙️ 关键实现细节
-
视频编码初始化
- 必须使用
COLOR_FormatSurface
避免兼容性问题 - 时间戳计算:
pts = 帧序号 × (1_000_000 / 帧率)
- 必须使用
-
音频处理
- 音频需转换为 PCM 格式输入编码器
- 音频时间戳从 0 开始,与视频同步需对齐起始点
-
同步机制
- 视频同步:按帧率均匀分配时间戳
- 音视频对齐:确保音频总时长 ≥ 视频时长,不足则循环填充
-
MediaMuxer 启动时机
- 需等待视频和音频轨道格式均获取后(
INFO_OUTPUT_FORMAT_CHANGED
)再启动
- 需等待视频和音频轨道格式均获取后(
⚠️ 注意事项
-
线程管理
- 编码操作需在后台线程执行
- 使用
HandlerThread
避免阻塞 UI
-
资源释放
- 严格按顺序释放:先停止编码器 → 停止 Muxer → 释放资源
-
兼容性处理
-
检测设备支持的编码格式:
luaMediaCodecList.findEncoderForFormat(format)
-
-
音频截断
- 若音频短于视频,需在
drainEncoder
中补静音帧或循环音频
- 若音频短于视频,需在