10、MediaExtractor与MediaMuxer:音视频解封装、音视频封装、音视频合成
说到音视频处理,有个基础操作你肯定绕不开——解封装和封装。说白了,就是把视频文件拆开,再重新组合。我刚开始接触这块时,觉得不就是复制粘贴吗?后来踩了不少坑才明白,这里面的门道真不少。
Android 提供了两个核心工具:MediaExtractor 和 MediaMuxer。一个负责拆,一个负责装。今天我们就把它彻底讲透。
10.1 什么是解封装?什么是封装?
先理清概念。一个 MP4 文件,里面其实藏着好几样东西:视频流、音频流、字幕、元数据等等。这些数据按照一定规则打包在一起,这个规则就是「封装格式」。
解封装,就是把它们拆开,拿到原始的压缩数据包。比如从 MP4 里取出 H.264 视频流和 AAC 音频流。
封装,反过来,把独立的音视频流按某种格式打包成一个文件。比如把 H.264 和 AAC 合成一个新的 MP4。
你想想看,这两个操作在视频编辑、转码、剪辑、拼接等场景里,几乎是天天用。
核心要点:MediaExtractor 负责从文件中读取并分离音视频轨道数据;MediaMuxer 负责将多个轨道的数据写入到一个封装文件中。
10.2 MediaExtractor:拆箱高手
MediaExtractor 的使用,我个人习惯分三步走:
- 设置数据源——告诉它你要拆哪个文件
- 选择轨道——决定要拆哪一路数据(视频还是音频)
- 读取数据——循环读取压缩后的数据包
来看一段核心代码:
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 的使用同样清晰:
- 创建 Muxer——指定输出路径和封装格式(目前只支持 MP4 和 WebM)
- 添加轨道——传入 MediaFormat,拿到 track index
- 写入数据——按时间顺序写入每个轨道的压缩数据包
- 停止并释放——调用 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)
下面是一个完整的合成流程示意图:
你看,流程很清晰。但实际写代码时,有几个坑我不得不提。
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 的异步模式。但那是另一个话题了。