12、MediaExtractor与MediaMuxer结合:重新封装为新的MP4文件
好,咱们今天聊点实战的。
前面几章我们把MediaExtractor和MediaMuxer拆开讲了一遍。一个负责读,一个负责写。那问题来了——能不能让它们俩配合,把视频重新封装一下?
说白了,就是从一个MP4里把音视频轨道读出来,再写进一个新的MP4文件。中间不做编解码,只做“搬运”。这种操作在业内叫重新封装(Remux)。
我在项目里遇到过好几次这种需求。比如用户录了个视频,但原始文件里音频是AAC、视频是H.264,我想把它转成另一种封装格式,或者单纯想去掉某个轨道。这时候MediaExtractor + MediaMuxer就是最佳拍档。
核心要点:重新封装不涉及编解码,性能开销极低。你不需要关心视频具体是什么编码格式,只要轨道类型匹配,MediaMuxer就能直接写入。
12.1 整体流程
先看一张流程图,帮你快速建立整体认知:
流程其实很简单:
- 用MediaExtractor打开源文件
- 遍历所有轨道,找到你想要的音视频轨道
- 把轨道信息传给MediaMuxer,让它添加对应的轨道
- 循环从Extractor读取数据,写入Muxer
- 释放资源
嗯,这里要注意:MediaMuxer在添加完所有轨道后,必须调用start()方法,然后才能开始写数据。顺序搞反了会直接崩溃。
12.2 核心代码实现
直接上代码。我个人习惯把重新封装写成一个工具方法,方便复用:
public class Remuxer {
private static final String TAG = "Remuxer";
private static final int BUFFER_SIZE = 1024 * 1024; // 1MB缓冲区
public static boolean remux(String srcPath, String dstPath) {
MediaExtractor extractor = null;
MediaMuxer muxer = null;
boolean success = false;
try {
// 1. 初始化Extractor
extractor = new MediaExtractor();
extractor.setDataSource(srcPath);
// 2. 初始化Muxer
muxer = new MediaMuxer(dstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
// 3. 遍历轨道,添加到Muxer
int videoTrackIndex = -1;
int audioTrackIndex = -1;
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/") && videoTrackIndex == -1) {
videoTrackIndex = muxer.addTrack(format);
extractor.selectTrack(i);
} else if (mime.startsWith("audio/") && audioTrackIndex == -1) {
audioTrackIndex = muxer.addTrack(format);
extractor.selectTrack(i);
}
}
// 4. 开始封装
muxer.start();
// 5. 读取并写入数据
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
// 先处理视频轨道
if (videoTrackIndex != -1) {
extractor.unselectTrack(videoTrackIndex);
extractor.selectTrack(videoTrackIndex);
while (true) {
bufferInfo.set(0, 0, 0, 0);
int sampleSize = extractor.readSampleData(buffer, 0);
if (sampleSize < 0) break;
bufferInfo.size = sampleSize;
bufferInfo.presentationTimeUs = extractor.getSampleTime();
bufferInfo.flags = extractor.getSampleFlags();
muxer.writeSampleData(videoTrackIndex, buffer, bufferInfo);
extractor.advance();
}
}
// 再处理音频轨道
if (audioTrackIndex != -1) {
extractor.unselectTrack(audioTrackIndex);
extractor.selectTrack(audioTrackIndex);
while (true) {
bufferInfo.set(0, 0, 0, 0);
int sampleSize = extractor.readSampleData(buffer, 0);
if (sampleSize < 0) break;
bufferInfo.size = sampleSize;
bufferInfo.presentationTimeUs = extractor.getSampleTime();
bufferInfo.flags = extractor.getSampleFlags();
muxer.writeSampleData(audioTrackIndex, buffer, bufferInfo);
extractor.advance();
}
}
success = true;
} catch (IOException e) {
Log.e(TAG, "remux failed", e);
} finally {
// 6. 释放资源
if (muxer != null) {
try {
muxer.stop();
muxer.release();
} catch (Exception e) {
Log.e(TAG, "release muxer error", e);
}
}
if (extractor != null) {
extractor.release();
}
}
return success;
}
}
我的经验:缓冲区大小建议设为1MB左右。太小了会导致频繁的I/O操作,太大了又浪费内存。1MB是个比较平衡的值。另外,记得在finally块里释放资源,否则文件句柄泄漏了很难排查。
12.3 关键细节
代码看起来简单,但有几个坑我得提醒你:
- 轨道选择顺序:MediaExtractor同一时间只能选择一个轨道。所以我在代码里先处理视频,再处理音频。每次切换轨道前先
unselectTrack,再selectTrack。 - 时间戳连续性:重新封装时,时间戳必须保持原样。不要自作聪明去修改
presentationTimeUs,否则播放器会乱跳。 - 关键帧问题:如果你只提取部分片段,记得从关键帧开始。否则新文件开头几秒可能花屏。
我曾经踩过的坑:有一次我忘了调用muxer.start(),结果写数据时直接抛了IllegalStateException。查了半天才发现是顺序问题。还有一次,我在循环里重复创建了ByteBuffer,导致内存暴涨。记住:一个缓冲区复用即可。
12.4 进阶:只提取视频或音频
有时候我们只需要视频轨道,或者只需要音频轨道。比如用户想从视频里提取背景音乐。那代码就更简单了:
// 只提取音频
public static boolean extractAudio(String srcPath, String dstPath) {
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(srcPath);
int audioTrackIndex = -1;
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat format = extractor.getTrackFormat(i);
if (format.getString(MediaFormat.KEY_MIME).startsWith("audio/")) {
audioTrackIndex = i;
break;
}
}
if (audioTrackIndex == -1) return false;
extractor.selectTrack(audioTrackIndex);
MediaFormat audioFormat = extractor.getTrackFormat(audioTrackIndex);
MediaMuxer muxer = new MediaMuxer(dstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
int muxerTrackIndex = muxer.addTrack(audioFormat);
muxer.start();
// ... 读取写入逻辑同上
}
你想想看,这个场景是不是很常见?抖音上那些“提取视频中的音乐”功能,底层就是这么干的。
12.5 性能对比
重新封装和重新编码的性能差距有多大?我拿一个1080P、30秒的视频做过测试:
| 操作方式 | 耗时 | CPU占用 | 文件大小 |
|---|---|---|---|
| 重新封装(Remux) | 约0.5秒 | 5%以下 | 与原文件几乎一致 |
| 重新编码(Transcode) | 约15秒 | 80%以上 | 可能变大或变小 |
看到了吧?重新封装几乎是瞬间完成。所以如果你的需求只是改变封装格式、提取轨道、或者拼接视频片段,优先考虑Remux。
12.6 总结
MediaExtractor和MediaMuxer的组合,是Android音视频开发里最实用的“搬运工”。
我个人建议你把今天这个remux()方法保存下来,以后做视频编辑、格式转换、轨道提取时直接拿来用。嗯,代码量不大,但能解决很多实际问题。
最后提醒一句:别忘了在AndroidManifest里加上文件读写权限。我刚开始学的时候,调试了半天发现是权限没开,尴尬得很。
核心收获:
- MediaExtractor负责读取,MediaMuxer负责写入
- 重新封装不涉及编解码,性能极高
- 注意轨道选择顺序和资源释放
- 缓冲区复用,避免频繁创建对象
公众号:蓝海资料掘金营,微信deep3321