10、MediaExtractor与MediaMuxer:音视频解封装、音视频封装、音视频合成

说到音视频处理,有个基础操作你肯定绕不开——解封装和封装。说白了,就是把视频文件拆开,再重新组合。我刚开始接触这块时,觉得不就是复制粘贴吗?后来踩了不少坑才明白,这里面的门道真不少。

Android 提供了两个核心工具:MediaExtractorMediaMuxer。一个负责拆,一个负责装。今天我们就把它彻底讲透。

10.1 什么是解封装?什么是封装?

先理清概念。一个 MP4 文件,里面其实藏着好几样东西:视频流、音频流、字幕、元数据等等。这些数据按照一定规则打包在一起,这个规则就是「封装格式」。

解封装,就是把它们拆开,拿到原始的压缩数据包。比如从 MP4 里取出 H.264 视频流和 AAC 音频流。

封装,反过来,把独立的音视频流按某种格式打包成一个文件。比如把 H.264 和 AAC 合成一个新的 MP4。

你想想看,这两个操作在视频编辑、转码、剪辑、拼接等场景里,几乎是天天用。

核心要点:MediaExtractor 负责从文件中读取并分离音视频轨道数据;MediaMuxer 负责将多个轨道的数据写入到一个封装文件中。

10.2 MediaExtractor:拆箱高手

MediaExtractor 的使用,我个人习惯分三步走:

  1. 设置数据源——告诉它你要拆哪个文件
  2. 选择轨道——决定要拆哪一路数据(视频还是音频)
  3. 读取数据——循环读取压缩后的数据包

来看一段核心代码:

MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(inputFilePath);

// 获取轨道数量
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("video/")) {
        // 选中视频轨道
        extractor.selectTrack(i);
        break;
    }
}

// 读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
while (true) {
    int sampleSize = extractor.readSampleData(buffer, 0);
    if (sampleSize < 0) break;
    info.offset = 0;
    info.size = sampleSize;
    info.presentationTimeUs = extractor.getSampleTime();
    info.flags = extractor.getSampleFlags();
    // 处理这个数据包...
    extractor.advance();
}
extractor.release();

嗯,这里要注意:readSampleData 返回的是压缩后的数据,不是解码后的 YUV 或 PCM。你拿到的就是 H.264 NALU 或者 AAC 帧。

我的经验:我曾经在读取某些 MP4 文件时,发现 readSampleData 返回 -1 但文件还没读完。后来排查发现是文件索引损坏了。建议生产环境中对输入文件做校验,或者用 MediaMetadataRetriever 先检查一下。

10.3 MediaMuxer:打包大师

MediaMuxer 的使用同样清晰:

  1. 创建 Muxer——指定输出路径和封装格式(目前只支持 MP4 和 WebM)
  2. 添加轨道——传入 MediaFormat,拿到 track index
  3. 写入数据——按时间顺序写入每个轨道的压缩数据包
  4. 停止并释放——调用 stop 和 release

代码示例:

MediaMuxer muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

// 假设已经准备好了视频轨道的 MediaFormat
int videoTrackIndex = muxer.addTrack(videoFormat);
int audioTrackIndex = muxer.addTrack(audioFormat);

muxer.start();

// 写入视频数据
ByteBuffer videoBuffer = ...;
MediaCodec.BufferInfo videoInfo = ...;
muxer.writeSampleData(videoTrackIndex, videoBuffer, videoInfo);

// 写入音频数据
ByteBuffer audioBuffer = ...;
MediaCodec.BufferInfo audioInfo = ...;
muxer.writeSampleData(audioTrackIndex, audioBuffer, audioInfo);

muxer.stop();
muxer.release();

重要提醒:MediaMuxer 要求写入的数据包必须按时间戳递增的顺序。如果你同时写视频和音频,一定要保证交错写入,不能先把视频全写完再写音频。否则播放器会出问题。

10.4 音视频合成:把拆开的东西重新组合

合成,说白了就是解封装 + 封装。从源文件里拆出音视频流,再重新打包成一个新文件。这个过程可以做一些「手脚」,比如:

  • 替换音频:把视频里的原声换成另一段音乐
  • 裁剪拼接:只取某一段视频,再和另一段音频合并
  • 调整参数:改变视频的帧率、码率等(需要配合 MediaCodec)

下面是一个完整的合成流程示意图:

输入文件.mp4 MediaExtractor 解封装 视频轨道 (H.264) 音频轨道 (AAC) MediaMuxer 封装 输出文件.mp4 读取压缩数据包 按时间戳交错写入 selectTrack → readSampleData → advance addTrack → start → writeSampleData → stop

你看,流程很清晰。但实际写代码时,有几个坑我不得不提。

10.5 避坑指南

我曾经踩过的坑:

  • 时间戳问题:有一次合成出来的视频播放时画面乱跳,查了半天发现是音频和视频的时间戳单位不一致。视频是微秒,音频是采样率相关的 tick。一定要统一用微秒。
  • 关键帧缺失:如果你只截取视频中间一段,必须从关键帧开始。否则播放器解码会花屏。我建议用 seekTo 配合 SEEK_TO_PREVIOUS_SYNC 定位。
  • 内存泄漏:MediaExtractor 和 MediaMuxer 都是 native 资源,用完必须 release。我曾经在异常分支忘记释放,导致 app 崩溃。

我的建议:写合成逻辑时,先写一个「透传」版本——不修改任何数据,只是拆了再装。如果透传版本能正常播放,说明你的框架搭对了。然后再加入修改逻辑,这样排查问题会快很多。

10.6 实际应用场景

说几个我项目中用到的场景:

场景 做法 注意事项
视频裁剪 用 Extractor 读取指定时间范围的数据包,再用 Muxer 写入 必须从关键帧开始,否则开头花屏
音频替换 只保留视频轨道,丢弃原音频,加入新的音频轨道 新音频时长要和视频匹配,否则需要做变速或裁剪
视频拼接 依次读取多个文件的视频轨道,连续写入同一个 Muxer 时间戳要连续递增,不能重置
提取音频 只选择音频轨道,写入 Muxer(输出格式选 MP4) 有些播放器对纯音频 MP4 支持不好,建议输出 M4A

说白了,MediaExtractor 和 MediaMuxer 就是 Android 音视频处理的「搬运工」。你只要把数据流理清楚,它们就能帮你完成大部分封装相关的工作。

嗯,最后再啰嗦一句:如果你要做实时处理(比如一边录制一边合成),这两个类可能不够用,需要配合 MediaCodec 的异步模式。但那是另一个话题了。


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