14、MediaMuxer实战:视频静音(Mute Video)——移除视频中的音频轨道。
好,我们直接进入正题。今天聊一个非常常见的需求——视频静音。
说白了,就是把视频里的声音去掉,只保留画面。你刷短视频时看到的那些「无声卡点视频」,很多就是这么做的。我在做直播回放剪辑工具时,经常需要把用户上传的带背景音乐的视频静音,再重新合成。嗯,今天就把这个流程拆开给你看。
14.1 核心思路:拆了再合
视频静音,本质上就两步:
- 拆:用 MediaExtractor 把原视频的音频轨道和视频轨道分开读取。
- 合:用 MediaMuxer 只写入视频轨道,音频轨道直接扔掉。
你想想看,MediaMuxer 本身不负责「删除」轨道,它只负责「写入」轨道。我们不把音频数据喂给它,它自然就不会写进去。这就是静音的原理。
核心结论:静音 = 复用(Mux)时跳过音频轨道。
14.2 准备工作:你需要知道的两件事
动手之前,有两个概念我得先讲清楚,不然代码写出来你可能会懵。
14.2.1 轨道索引(Track Index)
MediaExtractor 在解析视频文件时,会给每个轨道分配一个索引号。视频轨道通常是 0,音频轨道是 1。但这不是绝对的——有些视频只有音频没有画面,有些有多个音频轨道。我个人习惯是遍历所有轨道,根据 MIME 类型来判断。
14.2.2 轨道格式(MediaFormat)
每个轨道都有自己的格式信息,比如视频的宽高、帧率,音频的采样率、声道数。MediaMuxer 在添加轨道时,必须传入这个格式信息。所以我们在「拆」的阶段,要把视频轨道的 MediaFormat 保存下来,留给「合」的阶段用。
小提示:音频轨道的格式信息我们虽然不用,但最好也读一下——有时候可以用来判断原视频是否有声音。
14.3 代码实战:一步步来
好,我们直接上代码。我会把关键步骤拆开讲,你跟着走一遍就能跑通。
14.3.1 初始化提取器和复用器
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(inputPath);
MediaMuxer muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
这里要注意,MediaMuxer 的输出格式我用了 MUXER_OUTPUT_MPEG_4,也就是标准的 MP4 文件。如果你需要输出其他格式,可以换成对应的常量。
14.3.2 遍历轨道,找到视频轨道
int videoTrackIndex = -1;
MediaFormat videoFormat = null;
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime != null && mime.startsWith("video/")) {
videoTrackIndex = i;
videoFormat = format;
break;
}
}
if (videoTrackIndex == -1) {
// 没有视频轨道,直接报错
throw new RuntimeException("输入文件没有视频轨道");
}
我在项目中遇到过一种情况:有些视频文件虽然后缀是 .mp4,但里面只有音频轨道。所以这里一定要做判空处理,不然复用器会崩溃。
14.3.3 向复用器添加视频轨道
int muxerVideoTrack = muxer.addTrack(videoFormat);
muxer.start();
嗯,这里要注意顺序:必须先 addTrack,再 start。我曾经搞反过,结果 MediaMuxer 直接抛了个 IllegalStateException。你记住这个顺序就行。
14.3.4 读取并写入视频数据
extractor.selectTrack(videoTrackIndex);
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024); // 1MB 缓冲区
while (true) {
int sampleSize = extractor.readSampleData(buffer, 0);
if (sampleSize < 0) {
break; // 读取完毕
}
bufferInfo.offset = 0;
bufferInfo.size = sampleSize;
bufferInfo.presentationTimeUs = extractor.getSampleTime();
bufferInfo.flags = extractor.getSampleFlags();
muxer.writeSampleData(muxerVideoTrack, buffer, bufferInfo);
extractor.advance();
}
这段代码是核心。我解释几个关键点:
- 缓冲区大小:1MB 是经验值,对于 1080p 的视频基本够用。如果你处理 4K 视频,建议调到 4MB 以上。
- presentationTimeUs:这是时间戳,单位是微秒。MediaMuxer 靠它来排列数据顺序,所以必须原样传递。
- advance():读完一帧后,必须调用这个方法让提取器移动到下一帧。忘了调的话,你会陷入死循环。
注意:千万不要在循环里频繁创建 ByteBuffer。每次分配新缓冲区会导致内存抖动,严重时会引起 OOM。我建议在循环外创建一次,循环内复用。
14.3.5 收尾工作
extractor.release();
muxer.stop();
muxer.release();
顺序也很重要:先释放提取器,再停掉复用器,最后释放复用器。如果你先释放了复用器,再调用 stop() 会报错。
14.4 完整代码示例
我把上面的代码拼起来,给你一个可以直接用的函数:
public void muteVideo(String inputPath, String outputPath) throws IOException {
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(inputPath);
MediaMuxer muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
// 找到视频轨道
int videoTrackIndex = -1;
MediaFormat videoFormat = null;
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime != null && mime.startsWith("video/")) {
videoTrackIndex = i;
videoFormat = format;
break;
}
}
if (videoTrackIndex == -1) {
extractor.release();
muxer.release();
throw new RuntimeException("没有视频轨道");
}
// 添加视频轨道并启动复用器
int muxerVideoTrack = muxer.addTrack(videoFormat);
muxer.start();
// 读取并写入视频数据
extractor.selectTrack(videoTrackIndex);
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
while (true) {
int sampleSize = extractor.readSampleData(buffer, 0);
if (sampleSize < 0) break;
bufferInfo.offset = 0;
bufferInfo.size = sampleSize;
bufferInfo.presentationTimeUs = extractor.getSampleTime();
bufferInfo.flags = extractor.getSampleFlags();
muxer.writeSampleData(muxerVideoTrack, buffer, bufferInfo);
extractor.advance();
}
// 释放资源
extractor.release();
muxer.stop();
muxer.release();
}
14.5 避坑指南
代码写完了,但实际跑起来可能会遇到几个坑。我把自己踩过的列出来:
- 时间戳不连续:有些视频文件的时间戳不是从 0 开始的,或者中间有跳变。MediaMuxer 对时间戳顺序很敏感,如果乱序会输出花屏视频。解决办法是确保写入顺序和读取顺序一致。
- 音频轨道为空:有些视频虽然声明了音频轨道,但实际数据是空的。这种情况下,即使你跳过音频轨道,输出文件也是正常的。但如果你试图读取音频数据,会读到 -1。
- 输出路径覆盖:MediaMuxer 不会自动覆盖已有文件。如果 outputPath 对应的文件已存在,它会直接报错。我建议在调用前先删除旧文件。
我曾经踩过的坑:有一次我处理一个从网上下载的视频,静音后输出文件播放不了。排查了半天,发现原视频的 moov box 在文件末尾,MediaExtractor 解析时花了好几秒。后来我改用 MediaMetadataRetriever 先检查一下,才定位到问题。嗯,遇到奇怪的问题时,先检查原文件是否规范。
14.6 性能优化建议
如果你只是处理一两个视频,上面的代码完全够用。但如果你要做批量处理,有几个优化点:
| 优化点 | 说明 | 效果 |
|---|---|---|
| 增大缓冲区 | 从 1MB 增加到 4MB 或 8MB | 减少 I/O 次数,提升速度 |
| 复用提取器 | 处理多个文件时,不要每次都 new | 减少对象创建开销 |
| 异步写入 | 用线程池并行处理多个视频 | 充分利用多核 CPU |
14.7 本章小结
视频静音这个功能,说白了就是「拆了视频轨道,扔掉音频轨道,再重新打包」。MediaMuxer 在这里扮演的角色就是打包工,它不关心你给的数据是视频还是音频,你给它什么它就写什么。
我个人觉得,这个例子是理解 MediaMuxer 工作流程最好的入门练习。因为它只涉及一个轨道,逻辑简单,但把「提取-复用」的完整链路都走了一遍。你把这个跑通了,后面处理多轨道、混音、裁剪就水到渠成了。
上面这张图把整个流程画得很清楚了。你从左往右看,输入视频经过 MediaExtractor 拆成三条轨道,我们只保留视频轨道,音频和其他轨道直接扔掉,最后 MediaMuxer 把视频轨道重新打包成新的 MP4 文件。