一、为什么需要给无声视频加BGM?
说实话,我刚开始做音视频编辑时,遇到最多的需求就是「给视频加个背景音乐」。你想想看,用户拍的视频往往没有声音,或者原声太嘈杂。加一段合适的BGM,整个视频的质感立马就上来了。
MediaMuxer这个组件,说白了就是个「音视频打包器」。它能把视频轨道和音频轨道合并成一个文件。嗯,这里要注意——它只管混合,不管编解码。所以我们需要先准备好解码后的数据。
核心思路:用MediaExtractor读取视频,用MediaCodec解码视频帧,同时用另一个MediaExtractor读取音频文件,再用MediaMuxer把两路数据合成输出。
二、整体流程设计
我个人习惯把流程分成三步走:
- 准备阶段——打开视频文件、音频文件,配置MediaMuxer的输出格式
- 处理阶段——逐帧读取视频数据,逐段读取音频数据,写入同一个输出文件
- 收尾阶段——释放资源,检查输出文件是否完整
这里我画了一张流程图,帮你理清整体脉络:
三、核心代码实现
好,我们直接上代码。我会把关键部分拆开讲,你跟着走一遍就能跑通。
3.1 初始化MediaMuxer
// 设置输出文件路径
String outputPath = Environment.getExternalStorageDirectory() + "/output_with_bgm.mp4";
MediaMuxer muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
// 注意:先添加轨道,再start()
int videoTrackIndex = muxer.addTrack(videoMediaFormat);
int audioTrackIndex = muxer.addTrack(audioMediaFormat);
muxer.start();
我曾经踩过的坑:MediaMuxer的addTrack()必须在start()之前调用。如果你先调了start()再addTrack(),会直接抛IllegalStateException。嗯,这个错误信息我看了不下十次。
3.2 读取视频数据并写入
MediaExtractor videoExtractor = new MediaExtractor();
videoExtractor.setDataSource(videoPath);
int videoTrack = selectTrack(videoExtractor, "video/");
videoExtractor.selectTrack(videoTrack);
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024); // 1MB缓冲区
while (true) {
int sampleSize = videoExtractor.readSampleData(buffer, 0);
if (sampleSize < 0) break;
info.offset = 0;
info.size = sampleSize;
info.flags = videoExtractor.getSampleFlags();
info.presentationTimeUs = videoExtractor.getSampleTime();
muxer.writeSampleData(videoTrackIndex, buffer, info);
videoExtractor.advance();
}
这里有个细节——presentationTimeUs必须严格递增。我遇到过视频画面跳帧的情况,排查了半天,发现是时间戳没有对齐。你想想看,MediaMuxer内部是按时间戳排序的,乱序就会出问题。
3.3 读取音频数据并写入
MediaExtractor audioExtractor = new MediaExtractor();
audioExtractor.setDataSource(audioPath);
int audioTrack = selectTrack(audioExtractor, "audio/");
audioExtractor.selectTrack(audioTrack);
while (true) {
int sampleSize = audioExtractor.readSampleData(buffer, 0);
if (sampleSize < 0) break;
info.offset = 0;
info.size = sampleSize;
info.flags = audioExtractor.getSampleFlags();
info.presentationTimeUs = audioExtractor.getSampleTime();
muxer.writeSampleData(audioTrackIndex, buffer, info);
audioExtractor.advance();
}
小技巧:如果BGM比视频长,你可以在读取音频时加一个判断——当音频时间戳超过视频总时长时,直接break。这样BGM就不会「拖尾」了。我个人习惯在视频读取完后记录一下总时长,传给音频循环用。
3.4 收尾工作
// 先释放提取器
videoExtractor.release();
audioExtractor.release();
// 最后停止并释放muxer
muxer.stop();
muxer.release();
顺序很重要。一定要先release提取器,再stop muxer。我曾经反过来写过,结果输出文件损坏了,播放器直接报「无法播放此视频」。
四、避坑指南
做这个功能,有几个坑我不得不提:
| 常见问题 | 原因 | 解决方案 |
|---|---|---|
| 输出文件无法播放 | muxer.stop()前没有写入所有数据 | 确保两个while循环都完整执行 |
| 音视频不同步 | 时间戳没有对齐 | 检查presentationTimeUs是否连续递增 |
| BGM音量太大盖过原声 | 没有做音量混音处理 | 用AudioMixer或MediaCodec调整音量 |
| 内存溢出 | ByteBuffer分配过大 | 建议1MB以内,根据实际帧大小动态调整 |
五、完整代码示例
我把上面所有片段拼成一个完整的方法,你直接复制就能用:
public void mergeVideoAndAudio(String videoPath, String audioPath, String outputPath) {
MediaMuxer muxer = null;
MediaExtractor videoExtractor = null;
MediaExtractor audioExtractor = null;
try {
// 1. 初始化视频提取器
videoExtractor = new MediaExtractor();
videoExtractor.setDataSource(videoPath);
int videoTrackIndex = selectTrack(videoExtractor, "video/");
videoExtractor.selectTrack(videoTrackIndex);
MediaFormat videoFormat = videoExtractor.getTrackFormat(videoTrackIndex);
// 2. 初始化音频提取器
audioExtractor = new MediaExtractor();
audioExtractor.setDataSource(audioPath);
int audioTrackIndex = selectTrack(audioExtractor, "audio/");
audioExtractor.selectTrack(audioTrackIndex);
MediaFormat audioFormat = audioExtractor.getTrackFormat(audioTrackIndex);
// 3. 初始化Muxer
muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
int videoMuxerIndex = muxer.addTrack(videoFormat);
int audioMuxerIndex = muxer.addTrack(audioFormat);
muxer.start();
// 4. 写入视频数据
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
while (true) {
int sampleSize = videoExtractor.readSampleData(buffer, 0);
if (sampleSize < 0) break;
info.offset = 0;
info.size = sampleSize;
info.flags = videoExtractor.getSampleFlags();
info.presentationTimeUs = videoExtractor.getSampleTime();
muxer.writeSampleData(videoMuxerIndex, buffer, info);
videoExtractor.advance();
}
// 5. 写入音频数据
while (true) {
int sampleSize = audioExtractor.readSampleData(buffer, 0);
if (sampleSize < 0) break;
info.offset = 0;
info.size = sampleSize;
info.flags = audioExtractor.getSampleFlags();
info.presentationTimeUs = audioExtractor.getSampleTime();
muxer.writeSampleData(audioMuxerIndex, buffer, info);
audioExtractor.advance();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (videoExtractor != null) videoExtractor.release();
if (audioExtractor != null) audioExtractor.release();
if (muxer != null) {
muxer.stop();
muxer.release();
}
}
}
private int selectTrack(MediaExtractor extractor, String mimeType) {
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith(mimeType)) {
return i;
}
}
return -1;
}
六、总结
MediaMuxer做视频加BGM,说白了就是「拆开-混合-打包」三个动作。你只要记住:
- 视频和音频分别用两个MediaExtractor读取
- 时间戳必须严格递增
- 释放顺序不能乱
做到这三点,基本不会出大问题。我刚开始做这个功能时,也踩过不少坑,但跑通一次之后,后面就顺了。嗯,代码量不大,但细节确实多。
最后提醒一句:如果你要处理的是带原声的视频,那就不能直接用MediaMuxer了——它只能混合,不能混音。那时候你需要用AudioMixer或者MediaCodec做PCM级别的混音。这个我们后面章节会讲到。