MediaMuxer实战:视频拼接——把多个片段合成一个完整视频
视频拼接,说白了就是把几段视频首尾相连,合成一个完整的文件。你刷抖音看到的那些卡点视频、Vlog合集,背后基本都是这个操作。我在项目中做过不少拼接功能,踩过的坑比走过的路还多。今天咱们就把它彻底搞明白。
拼接的核心思路
MediaMuxer本身不提供拼接API,但我们可以换个思路:
- 把每个视频片段分别解封装,拿到音视频轨道数据
- 按顺序把数据写入同一个MediaMuxer
- 注意时间戳要连续,不能断层
嗯,听起来简单,但实际做起来有几个关键点要处理好。
核心原则:拼接的本质是「时间戳重映射」。每个片段的起始时间必须接上上一个片段的结束时间。
准备工作:你需要什么
- Android 4.3+(API 18+),MediaMuxer最低要求
- 几个MP4文件,最好编码格式一致(H.264 + AAC)
- MediaExtractor用来读取源文件
- MediaMuxer用来写入目标文件
我的习惯:拼接前先检查所有片段的编码参数。如果一个是H.264,另一个是H.265,MediaMuxer会直接罢工。我曾经因为这个排查了半天,最后发现是素材格式不统一。
代码实现:一步一步来
先看一个完整的拼接函数。我会把关键步骤拆开讲。
private void concatenateVideos(String[] inputPaths, String outputPath) {
MediaMuxer muxer = null;
int videoTrackIndex = -1;
int audioTrackIndex = -1;
long baseTimeUs = 0; // 基础时间戳偏移
try {
muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
for (int i = 0; i < inputPaths.length; i++) {
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(inputPaths[i]);
// 第一次循环:添加轨道
if (i == 0) {
for (int j = 0; j < extractor.getTrackCount(); j++) {
MediaFormat format = extractor.getTrackFormat(j);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/")) {
videoTrackIndex = muxer.addTrack(format);
} else if (mime.startsWith("audio/")) {
audioTrackIndex = muxer.addTrack(format);
}
}
muxer.start();
}
// 选择轨道并读取数据
for (int j = 0; j < extractor.getTrackCount(); j++) {
String mime = extractor.getTrackFormat(j).getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/")) {
extractor.selectTrack(j);
} else if (mime.startsWith("audio/")) {
extractor.selectTrack(j);
}
}
// 读取并写入数据
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024); // 1MB缓冲区
while (true) {
int sampleSize = extractor.readSampleData(buffer, 0);
if (sampleSize < 0) break;
info.offset = 0;
info.size = sampleSize;
info.flags = extractor.getSampleFlags();
info.presentationTimeUs = extractor.getSampleTime() + baseTimeUs;
int trackIndex = -1;
String currentMime = extractor.getTrackFormat(extractor.getSampleTrackIndex())
.getString(MediaFormat.KEY_MIME);
if (currentMime.startsWith("video/")) {
trackIndex = videoTrackIndex;
} else if (currentMime.startsWith("audio/")) {
trackIndex = audioTrackIndex;
}
if (trackIndex >= 0) {
muxer.writeSampleData(trackIndex, buffer, info);
}
extractor.advance();
}
// 更新基础时间戳
long durationUs = getDuration(inputPaths[i]);
baseTimeUs += durationUs;
extractor.release();
}
muxer.stop();
muxer.release();
} catch (Exception e) {
e.printStackTrace();
}
}
关键点解析
1. 时间戳偏移
这是最容易出错的地方。每个片段的时间戳都是从0开始的,如果不做偏移,第二个片段的数据会覆盖第一个片段的时间点。我用的baseTimeUs就是干这个的——每处理完一个片段,就把它的时长累加上去。
我曾经踩过的坑:直接用System.nanoTime()当时间戳。结果拼接出来的视频播放时直接跳帧,因为时间戳不是从0开始的连续值。记住,时间戳必须是单调递增的,而且要从0开始。
2. 轨道添加时机
MediaMuxer有个限制:addTrack()必须在start()之前调用,而且一旦start了就不能再加轨道。所以第一个片段负责添加所有轨道,后面的片段只读数据不添加。
3. 缓冲区大小
我习惯用1MB的缓冲区。太小了频繁IO,太大了内存吃不消。如果是4K视频,建议调到2MB以上。
获取视频时长的小工具
private long getDuration(String path) {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(path);
String duration = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_DURATION);
retriever.release();
return Long.parseLong(duration) * 1000; // 转成微秒
}
拼接流程图
常见问题与避坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 拼接后视频卡顿 | 时间戳不连续或跳变 | 检查baseTimeUs累加是否正确 |
| 只有画面没有声音 | 音频轨道未正确添加 | 确认音频轨道索引不为-1 |
| 拼接后文件损坏 | MediaMuxer未正确stop | 确保在finally中调用release |
| 编码格式不兼容 | 片段使用了不同编码器 | 统一转码后再拼接 |
我的经验:如果遇到「MediaMuxer failed」这种笼统的错误,先检查输出路径是否有写入权限。Android 10以上还要处理Scoped Storage。我有个项目上线后突然崩溃,就是因为没适配分区存储。
性能优化建议
- 使用异步处理:拼接操作很耗时,一定要放在子线程。我习惯用AsyncTask或者Coroutine。
- 控制内存:不要一次性把所有片段加载到内存。逐片段处理,用完就释放。
- 预检查:拼接前先检查所有片段的时长、编码格式、分辨率。不匹配的提前报错,别等到写了一半才崩溃。
总结
视频拼接用MediaMuxer实现,核心就三点:时间戳偏移、轨道只加一次、逐片段读写。你想想看,其实跟用胶带粘纸片是一个道理——对齐了才能粘得牢。
我在实际项目中还遇到过更复杂的情况,比如不同帧率的片段拼接、带旋转信息的视频处理。这些咱们后面章节再聊。先把基础的跑通,后面就好办了。