4. MediaExtractor 与 MediaMuxer:音视频解封装与重新封装、轨道提取与合并
这一章咱们来聊聊 Android 多媒体框架里两个特别实用的工具——MediaExtractor 和 MediaMuxer。说白了,它们就是音视频文件的「拆解工」和「组装师」。你想想看,有时候我们拿到一个 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_SYNC、SEEK_TO_NEXT_SYNC 或 SEEK_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();
}
}
}
4.3 核心流程:解封装 + 重新封装
把 Extractor 和 Muxer 结合起来,就能实现「解封装 -> 处理 -> 重新封装」的完整流程。下面这张图展示了最核心的逻辑:
这个流程其实很直观:先用 Extractor 把输入文件拆开,拿到各个轨道的数据,然后你可以选择性地处理这些数据(比如只保留音频、调整时间戳、拼接多个片段),最后用 Muxer 重新封装成一个新文件。
4.4 避坑指南
在实际开发中,有几个坑我踩过,分享给你:
1. 时间戳必须单调递增
MediaMuxer 要求写入的样本时间戳是严格递增的。如果你从多个 Extractor 读取数据然后合并,一定要确保时间戳不乱。我曾经在拼接视频时,因为两个片段的时间戳没有对齐,结果输出文件播放时画面乱跳。
2. 关键帧对齐
如果你要裁剪视频,最好从关键帧(I 帧)开始。否则解码器会因为没有参考帧而出现花屏。Extractor 的 seekTo 方法默认会跳到最近的 I 帧,这个设计很贴心。
3. 缓冲区大小要合理
我建议缓冲区大小设为 1MB 左右,太小会导致频繁的 I/O 操作,太大又浪费内存。对于 4K 视频,可能需要更大的缓冲区,但一般 2MB 以内就够了。
4.5 总结
MediaExtractor 和 MediaMuxer 是 Android 多媒体处理的基础工具。它们不涉及编解码,只负责「拆」和「装」。理解它们的用法,你就能实现很多实用的功能:
- 从视频中提取音频
- 合并多个视频片段
- 替换视频中的音频轨道
- 裁剪视频的某一段
嗯,这些操作在实际项目中非常常见。我个人建议你把本章的示例代码跑一遍,亲手感受一下数据是怎么流动的。下一章我们会深入 MediaCodec,看看如何对音视频数据进行真正的编解码处理。