5、MediaMuxer与MediaExtractor的协同工作:解封装后的数据如何重新封装
上一章我们把MediaMuxer的写入流程走了一遍。但有个问题一直悬着——喂给Muxer的原始数据从哪来?
你当然可以自己构造ByteBuffer,但真实场景下,我们更多是从一个已有的视频文件里把数据“拆”出来,再重新“装”进去。这就是MediaExtractor和MediaMuxer的协同工作。
说白了,一个负责拆,一个负责装。拆封装、重新封装,中间你可以做各种手脚——比如去掉某个音轨、替换背景音乐、调整视频分辨率。我最早做短视频剪辑SDK时,这个组合就是整个Pipeline的基石。
核心概念:MediaExtractor负责从源文件中读取并分离音视频数据,MediaMuxer负责将数据重新混合写入新文件。两者通过MediaCodec或直接传递ByteBuffer来交换数据。
5.1 协同工作的基本流程
先看一张整体流程图,帮你建立直观印象:
流程其实很清晰:
- 初始化Extractor:打开源文件,选择要读取的Track
- 初始化Muxer:根据Extractor的Track信息,添加对应Track
- 循环读取+写入:从Extractor读取Sample,直接写入Muxer
- 收尾:释放资源
嗯,这里要注意——Muxer的addTrack必须在start()之前调用,而addTrack需要的MediaFormat信息,正好可以从Extractor的getTrackFormat()拿到。这就是它们配合的第一个关键点。
5.2 核心代码实现
直接上代码。我习惯把复用逻辑封装成一个方法,这样在多个场景下都能调用:
private void remux(String srcPath, String dstPath) {
MediaExtractor extractor = null;
MediaMuxer muxer = null;
try {
// 1. 初始化Extractor
extractor = new MediaExtractor();
extractor.setDataSource(srcPath);
// 2. 遍历Track,收集信息
int audioTrackIndex = -1;
int videoTrackIndex = -1;
MediaFormat audioFormat = null;
MediaFormat videoFormat = null;
int trackCount = extractor.getTrackCount();
for (int i = 0; i < trackCount; i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("audio/")) {
audioTrackIndex = i;
audioFormat = format;
} else if (mime.startsWith("video/")) {
videoTrackIndex = i;
videoFormat = format;
}
}
// 3. 初始化Muxer
muxer = new MediaMuxer(dstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
int muxerAudioTrack = -1;
int muxerVideoTrack = -1;
if (audioFormat != null) {
muxerAudioTrack = muxer.addTrack(audioFormat);
}
if (videoFormat != null) {
muxerVideoTrack = muxer.addTrack(videoFormat);
}
// 4. 启动Muxer
muxer.start();
// 5. 读取并写入音频数据
if (audioTrackIndex != -1) {
extractor.selectTrack(audioTrackIndex);
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();
muxer.writeSampleData(muxerAudioTrack, buffer, info);
extractor.advance();
}
extractor.unselectTrack(audioTrackIndex);
}
// 6. 读取并写入视频数据(同上逻辑)
// ... 省略,与音频类似
} catch (IOException e) {
e.printStackTrace();
} finally {
// 7. 释放资源
if (muxer != null) {
muxer.stop();
muxer.release();
}
if (extractor != null) {
extractor.release();
}
}
}
个人经验:缓冲区大小我一般分配1MB。太小会导致频繁的readSampleData调用,太大又浪费内存。1MB在大多数1080P视频上都能覆盖一个关键帧的大小,是个比较稳妥的选择。
5.3 关键细节与避坑指南
代码看起来简单,但实际跑起来坑不少。我一个个说。
5.3.1 Track的选择与取消
Extractor同一时间只能读取一个Track。你要先selectTrack,读完再unselectTrack。我曾经犯过一个错误——读完音频后忘了unselect,直接select视频,结果视频数据读出来全是乱的。
正确的做法是:
- 读完一个Track后,立即unselectTrack
- 再select下一个Track
- 或者更稳妥:每次只处理一个Track,处理完再处理下一个
5.3.2 时间戳的连续性
Muxer要求写入的Sample时间戳必须是单调递增的。但Extractor读出来的数据,尤其是B帧存在时,时间戳可能不是严格递增的。怎么办?
我个人的做法是:在写入前做一个简单的排序,或者直接用MediaCodec解码后再编码,这样时间戳就规整了。如果只是纯复用(remux),不涉及编解码,那Extractor返回的时间戳本身就是递增的,可以放心用。
5.3.3 CSD信息的处理
视频Track的MediaFormat里,会包含csd-0、csd-1等关键信息(SPS/PPS)。这些信息在Extractor读取第一个Sample时就已经带上了。你不需要手动处理,Muxer在addTrack时会自动解析。
但有一点要注意——如果你在读取过程中跳过了某些Sample,可能导致CSD信息丢失。所以第一个Sample一定要完整读取。
我曾经踩过的坑:有一次做视频裁剪功能,我想跳过前几帧来达到“裁剪开头”的效果。结果跳过了包含SPS/PPS的第一个Sample,导致输出文件无法播放。后来我改成先读取第一个Sample写入Muxer,再根据时间戳决定是否写入后续Sample,问题就解决了。
5.4 实际应用场景
这个协同模式能做什么?我列几个常见场景:
| 场景 | 实现方式 | 注意事项 |
|---|---|---|
| 视频格式转换(如MKV转MP4) | Extractor读取源文件,Muxer写入新格式 | 确保Muxer支持目标格式的OutputFormat |
| 去除某个音轨 | 只select需要的Track,忽略不需要的 | 注意主音轨的选择,不要误删 |
| 替换背景音乐 | Extractor只读视频Track,音频Track用其他来源 | 时间戳要对齐,否则音画不同步 |
| 视频拼接 | 多个Extractor依次读取,写入同一个Muxer | 时间戳需要做偏移处理 |
你想想看,这些场景本质上都是“拆了再装”,只不过在中间环节做了不同的处理。理解了Extractor和Muxer的协同原理,这些功能实现起来就是排列组合的问题。
5.5 性能优化建议
最后聊点性能相关的。纯remux操作不涉及编解码,CPU消耗其实不高。但如果你处理的是4K视频,或者需要同时处理多个文件,还是要注意:
- 复用ByteBuffer:不要每次循环都new一个,提前分配好,反复使用
- 控制缓冲区大小:根据视频分辨率动态调整,别用固定值
- 异步处理:如果UI线程需要显示进度,把remux操作放到子线程
- 及时释放:Extractor和Muxer都持有文件句柄,不释放会导致文件占用
我记得有一次线上反馈说App处理完视频后,源文件删不掉。查了半天,发现是Extractor的release()没调用。从那以后,我所有资源释放都写在finally块里,养成习惯了。
好了,这一章就到这里。Extractor和Muxer的配合是音视频编辑的基础操作,掌握了它,后面讲裁剪、拼接、变速这些功能时,你就能更快上手。
核心要点回顾:
- Extractor负责解封装,Muxer负责重新封装
- 通过getTrackFormat()传递MediaFormat信息
- 注意Track的选择/取消、时间戳连续性、CSD信息
- 纯remux性能开销小,但资源释放一定要做好