4. MediaExtractor 与 MediaMuxer:音视频解封装与重新封装、轨道提取与合并

这一章咱们来聊聊 Android 多媒体框架里两个特别实用的工具——MediaExtractorMediaMuxer。说白了,它们就是音视频文件的「拆解工」和「组装师」。你想想看,有时候我们拿到一个 MP4 文件,只想把里面的音频单独拿出来,或者想把几段视频拼成一段,这时候就该它们上场了。

我个人习惯把 MediaExtractor 理解为「读文件的」,MediaMuxer 理解为「写文件的」。它们配合起来,能完成很多有意思的操作。我在项目中遇到过不少场景,比如从一段录屏视频里提取音频做语音识别,或者把多个短视频片段合并成一个长视频——嗯,这些活儿都离不开这两个类。

4.1 MediaExtractor:解封装与轨道提取

MediaExtractor 的作用,就是把一个封装好的多媒体文件「拆开」,让你能拿到里面的每一路数据。比如一个 MP4 文件,里面可能有一路视频、一路音频,甚至还有字幕轨道。Extractor 能帮你把这些轨道的数据分别读出来。

4.1.1 基本用法

先看一段最基础的代码,演示如何用 MediaExtractor 读取一个视频文件,并获取它的轨道信息:

MediaExtractor extractor = new MediaExtractor();
try {
    // 设置数据源,可以是文件路径、FD、URI 等
    extractor.setDataSource("/sdcard/input.mp4");

    // 获取轨道数量
    int trackCount = extractor.getTrackCount();
    Log.d("Extractor", "轨道数量: " + trackCount);

    for (int i = 0; i < trackCount; i++) {
        MediaFormat format = extractor.getTrackFormat(i);
        String mime = format.getString(MediaFormat.KEY_MIME);
        Log.d("Extractor", "轨道 " + i + " 的 MIME 类型: " + mime);

        if (mime.startsWith("video/")) {
            // 视频轨道
            int width = format.getInteger(MediaFormat.KEY_WIDTH);
            int height = format.getInteger(MediaFormat.KEY_HEIGHT);
            Log.d("Extractor", "视频尺寸: " + width + "x" + height);
        } else if (mime.startsWith("audio/")) {
            // 音频轨道
            int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
            int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
            Log.d("Extractor", "采样率: " + sampleRate + ", 声道数: " + channelCount);
        }
    }
} finally {
    extractor.release();
}

这段代码很简单,但有几个细节我想提醒你:

  • setDataSource 支持多种数据源,文件路径、FileDescriptor、Content URI 都可以。我建议优先用 FD,性能更好一些。
  • getTrackFormat 返回的是 MediaFormat 对象,里面包含了该轨道的所有编码参数。你可以通过 KEY_MIME 判断是视频还是音频。
  • 记得在 finally 里调用 release(),否则会泄漏资源。我曾经因为忘记 release,导致 App 在反复操作后崩溃——嗯,血的教训。

4.1.2 选择轨道并读取数据

拿到轨道信息后,下一步就是选择你要读取的轨道,然后开始读取数据。Extractor 是按「样本」来读取的,一个样本就是一帧视频或一段音频数据。

// 选择视频轨道(假设索引为 0)
extractor.selectTrack(0);

// 准备一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024); // 1MB 缓冲区
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();

while (true) {
    // 读取一个样本到缓冲区
    int sampleSize = extractor.readSampleData(buffer, 0);
    if (sampleSize < 0) {
        break; // 读取完毕
    }

    // 获取样本的时间戳和标志位
    long presentationTimeUs = extractor.getSampleTime();
    int flags = extractor.getSampleFlags();

    // 这里可以处理样本数据,比如传给 MediaCodec 解码
    // ...

    // 前进到下一个样本
    extractor.advance();
}

这里有个关键点:readSampleData 返回的是实际读取到的字节数,如果返回 -1 表示读取完毕。另外,advance() 必须调用,否则你会一直读到同一个样本——我刚开始用的时候犯过这个错,结果死循环了。

小技巧: 如果你只想提取某个时间段的音视频,可以用 seekTo(long timeUs, int mode) 方法跳转到指定位置。mode 参数可以是 SEEK_TO_PREVIOUS_SYNCSEEK_TO_NEXT_SYNCSEEK_TO_CLOSEST_SYNC。我个人习惯用 SEEK_TO_PREVIOUS_SYNC,因为它会跳到最近的 I 帧,保证解码不出问题。

4.2 MediaMuxer:重新封装与轨道合并

MediaMuxer 是 MediaExtractor 的「好搭档」。它负责把多路数据流合并成一个封装文件。比如你把提取出来的音频和视频重新组合,或者把几段视频拼接起来。

4.2.1 基本用法

先看一个简单的例子:把一段音频和一段视频合并成一个 MP4 文件。

MediaMuxer muxer = null;
try {
    muxer = new MediaMuxer("/sdcard/output.mp4", MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

    // 假设我们已经从 Extractor 获取了视频和音频的 MediaFormat
    int videoTrackIndex = muxer.addTrack(videoFormat);
    int audioTrackIndex = muxer.addTrack(audioFormat);

    // 开始写入
    muxer.start();

    // 写入视频数据(从 Extractor 读取的样本)
    // 写入音频数据(从 Extractor 读取的样本)
    // ...

    muxer.stop();
} catch (Exception e) {
    e.printStackTrace();
} finally {
    if (muxer != null) {
        muxer.release();
    }
}

这里有几个重要的点:

  • addTrack 必须在 start() 之前调用,一旦 start 之后就不能再添加轨道了。我建议先把所有轨道都 add 完,再调用 start。
  • writeSampleData 需要传入轨道索引、缓冲区、BufferInfo。BufferInfo 里要填好时间戳和标志位。
  • 目前 MediaMuxer 支持的输出格式有限,主要是 MP4 和 WebM。如果你需要其他格式,可能需要用第三方库。

4.2.2 完整示例:提取视频中的音频

下面是一个完整的例子,演示如何从一个 MP4 文件中提取音频轨道,保存成一个新的 AAC 文件(封装在 MP4 里)。

public void extractAudio(String inputPath, String outputPath) {
    MediaExtractor extractor = new MediaExtractor();
    MediaMuxer muxer = null;

    try {
        extractor.setDataSource(inputPath);
        int audioTrackIndex = -1;
        MediaFormat audioFormat = null;

        // 找到音频轨道
        for (int i = 0; i < extractor.getTrackCount(); i++) {
            MediaFormat format = extractor.getTrackFormat(i);
            String mime = format.getString(MediaFormat.KEY_MIME);
            if (mime.startsWith("audio/")) {
                audioTrackIndex = i;
                audioFormat = format;
                break;
            }
        }

        if (audioTrackIndex == -1) {
            Log.e("ExtractAudio", "没有找到音频轨道");
            return;
        }

        // 创建 Muxer
        muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
        int muxerTrackIndex = muxer.addTrack(audioFormat);
        muxer.start();

        // 选择音频轨道并开始读取
        extractor.selectTrack(audioTrackIndex);
        ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();

        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(muxerTrackIndex, buffer, bufferInfo);
            extractor.advance();
        }

        muxer.stop();
        Log.d("ExtractAudio", "音频提取完成: " + outputPath);

    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        extractor.release();
        if (muxer != null) {
            muxer.release();
        }
    }
}
注意: 提取出来的音频文件,它的编码格式和原始文件是一样的。如果原始视频里的音频是 AAC,那提取出来的就是 AAC。如果你需要转码(比如 AAC 转 MP3),那就需要用到 MediaCodec 了,这个我们后面会讲。

4.3 核心流程:解封装 + 重新封装

把 Extractor 和 Muxer 结合起来,就能实现「解封装 -> 处理 -> 重新封装」的完整流程。下面这张图展示了最核心的逻辑:

MediaExtractor + MediaMuxer 核心流程 输入文件 input.mp4 MediaExtractor 解封装 读取轨道、样本数据 视频轨道 H.264 / H.265 音频轨道 AAC / MP3 可选处理 裁剪/拼接/转码 MediaMuxer 重新封装 合并轨道、写入文件 输出文件 output.mp4 核心思路:Extractor 拆出轨道 → 可选处理 → Muxer 重新组装

这个流程其实很直观:先用 Extractor 把输入文件拆开,拿到各个轨道的数据,然后你可以选择性地处理这些数据(比如只保留音频、调整时间戳、拼接多个片段),最后用 Muxer 重新封装成一个新文件。

4.4 避坑指南

在实际开发中,有几个坑我踩过,分享给你:

1. 时间戳必须单调递增

MediaMuxer 要求写入的样本时间戳是严格递增的。如果你从多个 Extractor 读取数据然后合并,一定要确保时间戳不乱。我曾经在拼接视频时,因为两个片段的时间戳没有对齐,结果输出文件播放时画面乱跳。

2. 关键帧对齐

如果你要裁剪视频,最好从关键帧(I 帧)开始。否则解码器会因为没有参考帧而出现花屏。Extractor 的 seekTo 方法默认会跳到最近的 I 帧,这个设计很贴心。

3. 缓冲区大小要合理

我建议缓冲区大小设为 1MB 左右,太小会导致频繁的 I/O 操作,太大又浪费内存。对于 4K 视频,可能需要更大的缓冲区,但一般 2MB 以内就够了。

个人经验: 如果你只是做简单的轨道提取或合并,用 MediaExtractor + MediaMuxer 完全够用。但如果你需要做复杂的编辑(比如加滤镜、转场效果),那还是得用 MediaCodec 做解码-处理-编码的完整链路。不过那是后面章节的内容了。

4.5 总结

MediaExtractor 和 MediaMuxer 是 Android 多媒体处理的基础工具。它们不涉及编解码,只负责「拆」和「装」。理解它们的用法,你就能实现很多实用的功能:

  • 从视频中提取音频
  • 合并多个视频片段
  • 替换视频中的音频轨道
  • 裁剪视频的某一段

嗯,这些操作在实际项目中非常常见。我个人建议你把本章的示例代码跑一遍,亲手感受一下数据是怎么流动的。下一章我们会深入 MediaCodec,看看如何对音视频数据进行真正的编解码处理。


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