14、MediaMuxer实战:视频静音(Mute Video)——移除视频中的音频轨道。

好,我们直接进入正题。今天聊一个非常常见的需求——视频静音。

说白了,就是把视频里的声音去掉,只保留画面。你刷短视频时看到的那些「无声卡点视频」,很多就是这么做的。我在做直播回放剪辑工具时,经常需要把用户上传的带背景音乐的视频静音,再重新合成。嗯,今天就把这个流程拆开给你看。

14.1 核心思路:拆了再合

视频静音,本质上就两步:

  1. :用 MediaExtractor 把原视频的音频轨道和视频轨道分开读取。
  2. :用 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 只写入视频轨道 输出视频(无音频)

上面这张图把整个流程画得很清楚了。你从左往右看,输入视频经过 MediaExtractor 拆成三条轨道,我们只保留视频轨道,音频和其他轨道直接扔掉,最后 MediaMuxer 把视频轨道重新打包成新的 MP4 文件。

公众号:蓝海资料掘金营,微信 deep3321