3. 核心流程拆解:MediaExtractor的初始化、数据源设置、轨道选择
好,咱们直接进入正题。MediaExtractor 这个类,说白了就是 Android 系统给我们准备的一个「音视频拆解工具」。你给它一个文件,它就能把里面的音频流、视频流、字幕流统统拆出来,一根一根摆在你面前。
我个人习惯把它的使用流程分成三步:初始化 → 挂载数据源 → 挑选轨道。这三步走完,你才能拿到最原始的压缩数据包(也就是 MediaCodec 要喂的东西)。
核心流程一览:
- 第一步:new MediaExtractor() —— 创建实例
- 第二步:setDataSource() —— 告诉它文件在哪
- 第三步:getTrackCount() + selectTrack() —— 挑你要的轨道
3.1 初始化:new MediaExtractor()
这一步没什么花头。就是 new 一个对象。但我想提醒你一点:MediaExtractor 本身不占用太多资源,真正的「重活」在 setDataSource 之后才开始。
MediaExtractor extractor = new MediaExtractor();
嗯,就这一行。你可能会想:「这也太简单了吧?」 没错,但简单不代表可以忽略。我记得有一次在项目里,同事在循环中反复 new 和 release 这个对象,结果导致内存抖动。其实复用同一个实例,调用 reset() 方法就能重新使用。
小技巧: 如果你需要处理多个文件,不要每次都 new。用完之后调 extractor.reset(),然后重新 setDataSource 即可。省去反复创建的开销。
3.2 数据源设置:setDataSource() 的三种姿势
这一步才是真正的「入口」。MediaExtractor 提供了三种设置数据源的方式,我按推荐程度排个序:
| 方法签名 | 适用场景 | 注意事项 |
|---|---|---|
setDataSource(String path) |
本地文件路径 | 需要文件读权限(Android 10+ 推荐用 SAF) |
setDataSource(FileDescriptor fd) |
从 Asset、ContentProvider 读取 | 需要自己管理 fd 的打开和关闭 |
setDataSource(Context, Uri) |
通过 Content URI 访问媒体文件 | 自动处理权限,推荐用于 Android 10+ |
我个人最常用的是第三种——传 Uri 的方式。为什么呢?因为 Android 10 开始,直接传文件路径越来越受限,而 Uri 方式配合 MediaStore 是最稳妥的。
// 方式一:直接传路径(简单粗暴,但注意权限)
extractor.setDataSource("/sdcard/DCIM/video.mp4");
// 方式二:传 FileDescriptor(适合从 Asset 读取)
AssetFileDescriptor afd = context.getAssets().openFd("demo.mp4");
extractor.setDataSource(afd.getFileDescriptor());
// 方式三:传 Uri(推荐,自动处理权限)
extractor.setDataSource(context, Uri.parse("content://media/external/video/media/123"));
我曾经踩过的坑: 用方式一传路径时,如果文件路径包含中文或特殊字符,setDataSource 会静默失败,不抛异常但返回 false。后来我统一改用 Uri 方式,再也没出过这种问题。
3.3 轨道选择:getTrackCount() + selectTrack()
数据源挂载成功后,MediaExtractor 内部已经解析了文件的容器格式(比如 MP4、MKV)。这时候我们可以问它:「你这里面有几条轨道?」
int trackCount = extractor.getTrackCount();
Log.d("Extractor", "总共有 " + trackCount + " 条轨道");
拿到轨道数量后,我们需要遍历每一条轨道,看看它的媒体类型。这里要用到 getTrackFormat() 方法,它返回一个 MediaFormat 对象,里面包含了轨道的详细信息。
int audioTrackIndex = -1;
int videoTrackIndex = -1;
for (int i = 0; i < trackCount; i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/")) {
videoTrackIndex = i;
Log.d("Extractor", "找到视频轨道,索引: " + i + ",MIME: " + mime);
} else if (mime.startsWith("audio/")) {
audioTrackIndex = i;
Log.d("Extractor", "找到音频轨道,索引: " + i + ",MIME: " + mime);
}
}
找到你想要的轨道索引后,调用 selectTrack() 告诉提取器:「我就处理这一条,其他的先不管。」
// 假设我们只处理视频轨道
if (videoTrackIndex != -1) {
extractor.selectTrack(videoTrackIndex);
}
关键点: selectTrack() 之后,extractor 的 readSampleData() 就只会返回选中轨道的数据包。如果你同时需要音视频,需要分别创建两个 MediaExtractor 实例,或者用 MediaMuxer 重新混合。
你想想看,为什么不能一个 extractor 同时选中多条轨道?因为 readSampleData() 是按时间顺序返回数据包的,如果同时选中音视频,返回的数据包就会交错在一起,你根本分不清哪个是音频哪个是视频。所以 Android 的设计就是「一次只处理一条轨道」。
3.4 完整代码骨架
把上面三步串起来,就是一个完整的初始化流程:
MediaExtractor extractor = new MediaExtractor();
try {
// 1. 设置数据源
extractor.setDataSource(context, videoUri);
// 2. 遍历轨道,找到视频轨道
int videoTrackIndex = -1;
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/")) {
videoTrackIndex = i;
break;
}
}
// 3. 选中视频轨道
if (videoTrackIndex != -1) {
extractor.selectTrack(videoTrackIndex);
} else {
throw new IllegalStateException("没有找到视频轨道");
}
// 接下来就可以用 extractor 读取数据包了...
} catch (IOException e) {
e.printStackTrace();
} finally {
extractor.release();
}
我的习惯: 在 finally 块里一定要调 release()。虽然 MediaExtractor 实现了 Closeable,但显式释放更保险。我曾经见过因为忘记 release 导致文件句柄泄漏的案例,排查了半天才发现是 extractor 没关。
3.5 避坑指南
- setDataSource 不抛异常不代表成功 —— 它返回 boolean 值,记得检查返回值。如果返回 false,说明文件格式不被支持或文件已损坏。
- getTrackFormat 返回的 MediaFormat 不要直接修改 —— 它是内部对象的引用,修改它会影响 extractor 的状态。如果需要修改,请用 MediaFormat 的副本。
- selectTrack 可以多次调用 —— 但每次调用会取消之前选中的轨道。如果你想切换轨道,直接调 selectTrack 传新的索引即可。
- 某些文件可能只有音频没有视频 —— 比如录音文件、音乐文件。遍历轨道时一定要做好判空处理。
好了,核心流程就这些。说白了就是三步:创建、挂载、选择。每一步都有它的细节,但整体逻辑非常清晰。你只要记住「先挂载再选择」这个顺序,基本不会出大问题。